aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Dag <me@dvikan.no> 2022-07-01 15:10:30 +0200
committerGravatar GitHub <noreply@github.com> 2022-07-01 15:10:30 +0200
commit4f75591060d95208a301bc6bf460d875631b29cc (patch)
tree4e37d86840e8d990a563ba75d3de6f84a53cc2de
parent66568e3a39c61546c09a47a5688914a0bdf3c60c (diff)
downloadrss-bridge-4f75591060d95208a301bc6bf460d875631b29cc.tar.gz
rss-bridge-4f75591060d95208a301bc6bf460d875631b29cc.tar.zst
rss-bridge-4f75591060d95208a301bc6bf460d875631b29cc.zip
Reformat codebase v4 (#2872)
Reformat code base to PSR12 Co-authored-by: rssbridge <noreply@github.com>
-rw-r--r--actions/ConnectivityAction.php166
-rw-r--r--actions/DetectAction.php62
-rw-r--r--actions/DisplayAction.php421
-rw-r--r--actions/ListAction.php86
-rw-r--r--bridges/ABCNewsBridge.php79
-rw-r--r--bridges/AO3Bridge.php242
-rw-r--r--bridges/ARDMediathekBridge.php175
-rw-r--r--bridges/ASRockNewsBridge.php75
-rw-r--r--bridges/AcrimedBridge.php61
-rw-r--r--bridges/AirBreizhBridge.php97
-rw-r--r--bridges/AlbionOnlineBridge.php133
-rw-r--r--bridges/AlfaBankByBridge.php146
-rw-r--r--bridges/AllocineFRBridge.php198
-rw-r--r--bridges/AmazonBridge.php201
-rw-r--r--bridges/AmazonPriceTrackerBridge.php490
-rw-r--r--bridges/AnidexBridge.php409
-rw-r--r--bridges/AnimeUltimeBridge.php278
-rw-r--r--bridges/AppleAppStoreBridge.php304
-rw-r--r--bridges/AppleMusicBridge.php98
-rw-r--r--bridges/ArtStationBridge.php189
-rw-r--r--bridges/Arte7Bridge.php289
-rw-r--r--bridges/AsahiShimbunAJWBridge.php134
-rw-r--r--bridges/AskfmBridge.php119
-rw-r--r--bridges/AssociatedPressNewsBridge.php496
-rw-r--r--bridges/AstrophysicsDataSystemBridge.php89
-rw-r--r--bridges/AtmoNouvelleAquitaineBridge.php9209
-rw-r--r--bridges/AtmoOccitanieBridge.php114
-rw-r--r--bridges/AutoJMBridge.php264
-rw-r--r--bridges/AwwwardsBridge.php113
-rw-r--r--bridges/BAEBridge.php504
-rw-r--r--bridges/BadDragonBridge.php868
-rw-r--r--bridges/BakaUpdatesMangaReleasesBridge.php393
-rw-r--r--bridges/BandcampBridge.php824
-rw-r--r--bridges/BandcampDailyBridge.php319
-rw-r--r--bridges/BastaBridge.php48
-rw-r--r--bridges/BinanceBridge.php82
-rw-r--r--bridges/BlaguesDeMerdeBridge.php65
-rw-r--r--bridges/BleepingComputerBridge.php45
-rw-r--r--bridges/BlizzardNewsBridge.php110
-rw-r--r--bridges/BookMyShowBridge.php2888
-rw-r--r--bridges/BooruprojectBridge.php124
-rw-r--r--bridges/BrutBridge.php216
-rw-r--r--bridges/BugzillaBridge.php364
-rw-r--r--bridges/BukowskisBridge.php406
-rw-r--r--bridges/BundesbankBridge.php166
-rw-r--r--bridges/BundestagParteispendenBridge.php161
-rw-r--r--bridges/CBCEditorsBlogBridge.php60
-rw-r--r--bridges/CNETBridge.php196
-rw-r--r--bridges/CNETFranceBridge.php119
-rw-r--r--bridges/CVEDetailsBridge.php265
-rw-r--r--bridges/CachetBridge.php240
-rw-r--r--bridges/CarThrottleBridge.php61
-rw-r--r--bridges/CastorusBridge.php249
-rw-r--r--bridges/CdactionBridge.php108
-rw-r--r--bridges/CeskaTelevizeBridge.php165
-rw-r--r--bridges/CodebergBridge.php774
-rw-r--r--bridges/CollegeDeFranceBridge.php146
-rw-r--r--bridges/ComboiosDePortugalBridge.php41
-rw-r--r--bridges/ComicsKingdomBridge.php105
-rw-r--r--bridges/CommonDreamsBridge.php44
-rw-r--r--bridges/CopieDoubleBridge.php56
-rw-r--r--bridges/CourrierInternationalBridge.php41
-rw-r--r--bridges/CraigslistBridge.php184
-rw-r--r--bridges/CrewbayBridge.php456
-rw-r--r--bridges/CryptomeBridge.php77
-rw-r--r--bridges/CubariBridge.php182
-rw-r--r--bridges/CuriousCatBridge.php158
-rw-r--r--bridges/CyanideAndHappinessBridge.php65
-rw-r--r--bridges/DailymotionBridge.php406
-rw-r--r--bridges/DanbooruBridge.php117
-rw-r--r--bridges/DansTonChatBridge.php45
-rw-r--r--bridges/DarkReadingBridge.php157
-rw-r--r--bridges/DauphineLibereBridge.php100
-rw-r--r--bridges/DavesTrailerPageBridge.php57
-rw-r--r--bridges/DealabsBridge.php3908
-rw-r--r--bridges/DemoBridge.php81
-rw-r--r--bridges/DerpibooruBridge.php208
-rw-r--r--bridges/DesoutterBridge.php473
-rw-r--r--bridges/DevToBridge.php172
-rw-r--r--bridges/DeveloppezDotComBridge.php743
-rw-r--r--bridges/DiarioDeNoticiasBridge.php147
-rw-r--r--bridges/DiarioDoAlentejoBridge.php123
-rw-r--r--bridges/DiceBridge.php236
-rw-r--r--bridges/DilbertBridge.php59
-rw-r--r--bridges/DiscogsBridge.php228
-rw-r--r--bridges/DockerHubBridge.php299
-rw-r--r--bridges/DonnonsBridge.php224
-rw-r--r--bridges/DribbbleBridge.php205
-rw-r--r--bridges/Drive2ruBridge.php411
-rw-r--r--bridges/DuckDuckGoBridge.php72
-rw-r--r--bridges/EZTVBridge.php195
-rw-r--r--bridges/EconomistBridge.php270
-rw-r--r--bridges/EconomistWorldInBriefBridge.php260
-rw-r--r--bridges/EliteDangerousGalnetBridge.php104
-rw-r--r--bridges/ElloBridge.php285
-rw-r--r--bridges/ElsevierBridge.php71
-rw-r--r--bridges/EngadgetBridge.php46
-rw-r--r--bridges/EpicgamesBridge.php162
-rw-r--r--bridges/EsquerdaNetBridge.php132
-rw-r--r--bridges/EstCeQuonMetEnProdBridge.php42
-rw-r--r--bridges/EtsyBridge.php130
-rw-r--r--bridges/EuronewsBridge.php392
-rw-r--r--bridges/ExecuteProgramBridge.php54
-rw-r--r--bridges/ExplosmBridge.php110
-rw-r--r--bridges/ExtremeDownloadBridge.php195
-rw-r--r--bridges/FB2Bridge.php620
-rw-r--r--bridges/FDroidBridge.php143
-rw-r--r--bridges/FDroidRepoBridge.php370
-rw-r--r--bridges/FM4Bridge.php105
-rw-r--r--bridges/FSecureBlogBridge.php212
-rw-r--r--bridges/FabriceBellardBridge.php69
-rw-r--r--bridges/FacebookBridge.php1475
-rw-r--r--bridges/FeedExpanderExampleBridge.php115
-rw-r--r--bridges/FeedMergeBridge.php106
-rw-r--r--bridges/FeedReducerBridge.php120
-rw-r--r--bridges/FicbookBridge.php375
-rw-r--r--bridges/FilterBridge.php255
-rw-r--r--bridges/FindACrewBridge.php158
-rw-r--r--bridges/FirefoxAddonsBridge.php188
-rw-r--r--bridges/FirstLookMediaTechBridge.php97
-rw-r--r--bridges/FlashbackBridge.php350
-rw-r--r--bridges/FlickrBridge.php558
-rw-r--r--bridges/FolhaDeSaoPauloBridge.php130
-rw-r--r--bridges/ForGifsBridge.php75
-rw-r--r--bridges/Formula1Bridge.php119
-rw-r--r--bridges/FourchanBridge.php133
-rw-r--r--bridges/FreeCodeCampBridge.php48
-rw-r--r--bridges/FunkBridge.php152
-rw-r--r--bridges/FurAffinityBridge.php1857
-rw-r--r--bridges/FurAffinityUserBridge.php117
-rw-r--r--bridges/FuturaSciencesBridge.php302
-rw-r--r--bridges/GBAtempBridge.php274
-rw-r--r--bridges/GOGBridge.php119
-rw-r--r--bridges/GQMagazineBridge.php260
-rw-r--r--bridges/GatesNotesBridge.php105
-rw-r--r--bridges/GelbooruBridge.php153
-rw-r--r--bridges/GenshinImpactBridge.php118
-rw-r--r--bridges/GettrBridge.php186
-rw-r--r--bridges/GiphyBridge.php172
-rw-r--r--bridges/GitHubGistBridge.php258
-rw-r--r--bridges/GiteaBridge.php613
-rw-r--r--bridges/GithubIssueBridge.php589
-rw-r--r--bridges/GithubPullRequestBridge.php79
-rw-r--r--bridges/GithubSearchBridge.php112
-rw-r--r--bridges/GithubTrendingBridge.php1234
-rw-r--r--bridges/GitlabIssueBridge.php415
-rw-r--r--bridges/GizmodoBridge.php152
-rw-r--r--bridges/GlassdoorBridge.php378
-rw-r--r--bridges/GlowficBridge.php172
-rw-r--r--bridges/GoComicsBridge.php119
-rw-r--r--bridges/GogsBridge.php417
-rw-r--r--bridges/GolemBridge.php250
-rw-r--r--bridges/GoodreadsBridge.php184
-rw-r--r--bridges/GoogleGroupsBridge.php118
-rw-r--r--bridges/GooglePlayStoreBridge.php120
-rw-r--r--bridges/GoogleSearchBridge.php175
-rw-r--r--bridges/GrandComicsDatabaseBridge.php118
-rw-r--r--bridges/GroupBundNaturschutzBridge.php199
-rw-r--r--bridges/HDWallpapersBridge.php145
-rw-r--r--bridges/HackerNewsUserThreadsBridge.php80
-rw-r--r--bridges/HardwareInfoBridge.php101
-rw-r--r--bridges/HashnodeBridge.php82
-rw-r--r--bridges/HaveIBeenPwnedBridge.php278
-rw-r--r--bridges/HeiseBridge.php136
-rw-r--r--bridges/HotUKDealsBridge.php6657
-rw-r--r--bridges/IGNBridge.php107
-rw-r--r--bridges/IKWYDBridge.php212
-rw-r--r--bridges/IPBBridge.php612
-rw-r--r--bridges/IdenticaBridge.php104
-rw-r--r--bridges/IndeedBridge.php443
-rw-r--r--bridges/IndiegogoBridge.php265
-rw-r--r--bridges/InstagramBridge.php638
-rw-r--r--bridges/InstructablesBridge.php655
-rw-r--r--bridges/InternetArchiveBridge.php519
-rw-r--r--bridges/ItchioBridge.php78
-rw-r--r--bridges/IvooxBridge.php246
-rw-r--r--bridges/JapanExpoBridge.php186
-rw-r--r--bridges/JornalDeNoticiasBridge.php109
-rw-r--r--bridges/JustETFBridge.php614
-rw-r--r--bridges/Kanali6Bridge.php32
-rw-r--r--bridges/KernelBugTrackerBridge.php297
-rw-r--r--bridges/KhinsiderBridge.php66
-rw-r--r--bridges/KilledbyGoogleBridge.php147
-rw-r--r--bridges/KonachanBridge.php13
-rw-r--r--bridges/KoreusBridge.php33
-rw-r--r--bridges/KununuBridge.php285
-rw-r--r--bridges/LWNprevBridge.php534
-rw-r--r--bridges/LaCentraleBridge.php927
-rw-r--r--bridges/LaTeX3ProjectNewslettersBridge.php52
-rw-r--r--bridges/LeBonCoinBridge.php1064
-rw-r--r--bridges/LeMondeInformatiqueBridge.php64
-rw-r--r--bridges/LegifranceJOBridge.php126
-rw-r--r--bridges/LegoIdeasBridge.php169
-rw-r--r--bridges/LesJoiesDuCodeBridge.php56
-rw-r--r--bridges/ListverseBridge.php37
-rw-r--r--bridges/LolibooruBridge.php13
-rw-r--r--bridges/MallTvBridge.php141
-rw-r--r--bridges/MangaDexBridge.php473
-rw-r--r--bridges/MarktplaatsBridge.php253
-rw-r--r--bridges/MastodonBridge.php364
-rw-r--r--bridges/MediapartBlogsBridge.php97
-rw-r--r--bridges/MediapartBridge.php114
-rw-r--r--bridges/MilbooruBridge.php13
-rw-r--r--bridges/MixCloudBridge.php119
-rw-r--r--bridges/ModelKarteiBridge.php216
-rw-r--r--bridges/MoebooruBridge.php96
-rw-r--r--bridges/MoinMoinBridge.php669
-rw-r--r--bridges/MondeDiploBridge.php47
-rw-r--r--bridges/MozillaBugTrackerBridge.php297
-rw-r--r--bridges/MozillaSecurityBridge.php52
-rw-r--r--bridges/MsnMondeBridge.php70
-rw-r--r--bridges/MspabooruBridge.php21
-rw-r--r--bridges/MydealsBridge.php4151
-rw-r--r--bridges/N26Bridge.php58
-rw-r--r--bridges/NFLRUSBridge.php39
-rw-r--r--bridges/NYTBridge.php72
-rw-r--r--bridges/NasaApodBridge.php89
-rw-r--r--bridges/NationalGeographicBridge.php692
-rw-r--r--bridges/NewOnNetflixBridge.php94
-rw-r--r--bridges/NewgroundsBridge.php105
-rw-r--r--bridges/NextInpactBridge.php376
-rw-r--r--bridges/NextgovBridge.php115
-rw-r--r--bridges/NiceMatinBridge.php54
-rw-r--r--bridges/NikonDownloadCenterBridge.php63
-rw-r--r--bridges/NineGagBridge.php754
-rw-r--r--bridges/NordbayernBridge.php308
-rw-r--r--bridges/NotAlwaysBridge.php119
-rw-r--r--bridges/NovayaGazetaEuropeBridge.php264
-rw-r--r--bridges/NovelUpdatesBridge.php118
-rw-r--r--bridges/NpciBridge.php162
-rw-r--r--bridges/NyaaTorrentsBridge.php197
-rw-r--r--bridges/OnVaSortirBridge.php256
-rw-r--r--bridges/OneFortuneADayBridge.php142
-rw-r--r--bridges/OpenlyBridge.php498
-rw-r--r--bridges/OpenwhydBridge.php124
-rw-r--r--bridges/OpenwrtSecurityBridge.php72
-rw-r--r--bridges/OtrkeyFinderBridge.php371
-rw-r--r--bridges/PCGWNewsBridge.php56
-rw-r--r--bridges/PanacheDigitalGamesBridge.php91
-rw-r--r--bridges/ParksOnTheAirBridge.php53
-rw-r--r--bridges/ParlerBridge.php132
-rw-r--r--bridges/ParuVenduImmoBridge.php223
-rw-r--r--bridges/PatreonBridge.php415
-rw-r--r--bridges/PcGamerBridge.php67
-rw-r--r--bridges/PepperBridgeAbstract.php1345
-rw-r--r--bridges/PhoronixBridge.php118
-rw-r--r--bridges/PicalaBridge.php120
-rw-r--r--bridges/PickyWallpapersBridge.php175
-rw-r--r--bridges/PicukiBridge.php187
-rw-r--r--bridges/PikabuBridge.php304
-rw-r--r--bridges/PillowfortBridge.php377
-rw-r--r--bridges/PinterestBridge.php123
-rw-r--r--bridges/PirateCommunityBridge.php186
-rw-r--r--bridges/PixivBridge.php440
-rw-r--r--bridges/PlantUMLReleasesBridge.php71
-rw-r--r--bridges/PokemonTVBridge.php272
-rw-r--r--bridges/PornhubBridge.php195
-rw-r--r--bridges/PresidenciaPTBridge.php145
-rw-r--r--bridges/RaceDepartmentBridge.php79
-rw-r--r--bridges/RadioMelodieBridge.php389
-rw-r--r--bridges/RainbowSixSiegeBridge.php95
-rw-r--r--bridges/RedditBridge.php573
-rw-r--r--bridges/Releases3DSBridge.php196
-rw-r--r--bridges/ReleasesSwitchBridge.php19
-rw-r--r--bridges/ReporterreBridge.php65
-rw-r--r--bridges/ReutersBridge.php1229
-rw-r--r--bridges/RoadAndTrackBridge.php137
-rw-r--r--bridges/RobinhoodSnacksBridge.php221
-rw-r--r--bridges/RoosterTeethBridge.php183
-rw-r--r--bridges/RtsBridge.php123
-rw-r--r--bridges/Rue89Bridge.php87
-rw-r--r--bridges/Rule34Bridge.php13
-rw-r--r--bridges/Rule34pahealBridge.php47
-rw-r--r--bridges/RutubeBridge.php165
-rw-r--r--bridges/SIMARBridge.php123
-rw-r--r--bridges/SafebooruBridge.php23
-rw-r--r--bridges/SchweinfurtBuergerinformationenBridge.php249
-rw-r--r--bridges/ScmbBridge.php66
-rw-r--r--bridges/ScoopItBridge.php68
-rw-r--r--bridges/ScribdBridge.php115
-rw-r--r--bridges/SensCritiqueBridge.php174
-rw-r--r--bridges/SeznamZpravyBridge.php250
-rw-r--r--bridges/ShanaprojectBridge.php371
-rw-r--r--bridges/Shimmie2Bridge.php62
-rw-r--r--bridges/SkimfeedBridge.php1471
-rw-r--r--bridges/SlusheBridge.php323
-rw-r--r--bridges/SoundcloudBridge.php461
-rw-r--r--bridges/SplCenterBridge.php123
-rw-r--r--bridges/SpotifyBridge.php498
-rw-r--r--bridges/SpottschauBridge.php59
-rw-r--r--bridges/StanfordSIRbookreviewBridge.php76
-rw-r--r--bridges/SteamBridge.php226
-rw-r--r--bridges/SteamCommunityBridge.php394
-rw-r--r--bridges/StockFilingsBridge.php136
-rw-r--r--bridges/StripeAPIChangeLogBridge.php39
-rw-r--r--bridges/SummitsOnTheAirBridge.php81
-rw-r--r--bridges/SuperSmashBlogBridge.php85
-rw-r--r--bridges/SymfonyCastsBridge.php55
-rw-r--r--bridges/TbibBridge.php23
-rw-r--r--bridges/TebeoBridge.php74
-rw-r--r--bridges/TelegramBridge.php599
-rw-r--r--bridges/TheFarSideBridge.php63
-rw-r--r--bridges/TheGuardianBridge.php193
-rw-r--r--bridges/TheHackerNewsBridge.php136
-rw-r--r--bridges/ThePirateBayBridge.php554
-rw-r--r--bridges/TheWhiteboardBridge.php33
-rw-r--r--bridges/TheYeteeBridge.php72
-rw-r--r--bridges/TikTokBridge.php170
-rw-r--r--bridges/TinyLetterBridge.php108
-rw-r--r--bridges/TorrentGalaxyBridge.php213
-rw-r--r--bridges/TrelloBridge.php1348
-rw-r--r--bridges/TwitScoopBridge.php280
-rw-r--r--bridges/TwitchBridge.php399
-rw-r--r--bridges/TwitterBridge.php1181
-rw-r--r--bridges/TwitterEngineeringBridge.php125
-rw-r--r--bridges/TwitterV2Bridge.php1260
-rw-r--r--bridges/UberNewsroomBridge.php360
-rw-r--r--bridges/UnogsBridge.php371
-rw-r--r--bridges/UnraidCommunityApplicationsBridge.php121
-rw-r--r--bridges/UnsplashBridge.php202
-rw-r--r--bridges/UrlebirdBridge.php131
-rw-r--r--bridges/UsbekEtRicaBridge.php222
-rw-r--r--bridges/UsenixBridge.php109
-rw-r--r--bridges/VarietyBridge.php47
-rw-r--r--bridges/ViadeoCompanyBridge.php65
-rw-r--r--bridges/ViceBridge.php70
-rw-r--r--bridges/VieDeMerdeBridge.php110
-rw-r--r--bridges/VimeoBridge.php350
-rw-r--r--bridges/VixenBridge.php180
-rw-r--r--bridges/VkBridge.php929
-rw-r--r--bridges/WallmineNewsBridge.php64
-rw-r--r--bridges/WallpaperflareBridge.php77
-rw-r--r--bridges/WeLiveSecurityBridge.php63
-rw-r--r--bridges/WebfailBridge.php321
-rw-r--r--bridges/WikiLeaksBridge.php257
-rw-r--r--bridges/WikipediaBridge.php663
-rw-r--r--bridges/WiredBridge.php209
-rw-r--r--bridges/WordPressBridge.php194
-rw-r--r--bridges/WordPressMadaraBridge.php261
-rw-r--r--bridges/WordPressPluginUpdateBridge.php120
-rw-r--r--bridges/WorldCosplayBridge.php266
-rw-r--r--bridges/WorldOfTanksBridge.php116
-rw-r--r--bridges/XPathBridge.php401
-rw-r--r--bridges/XbooruBridge.php21
-rw-r--r--bridges/XenForoBridge.php865
-rw-r--r--bridges/YGGTorrentBridge.php278
-rw-r--r--bridges/YandereBridge.php13
-rw-r--r--bridges/YeggiBridge.php168
-rw-r--r--bridges/YouTubeCommunityTabBridge.php513
-rw-r--r--bridges/YoutubeBridge.php853
-rw-r--r--bridges/ZDNetBridge.php395
-rw-r--r--bridges/ZenodoBridge.php104
-rw-r--r--caches/FileCache.php277
-rw-r--r--caches/MemcachedCache.php237
-rw-r--r--caches/SQLiteCache.php256
-rw-r--r--contrib/prepare_release/fetch_contributors.php64
-rw-r--r--formats/AtomFormat.php356
-rw-r--r--formats/HtmlFormat.php217
-rw-r--r--formats/JsonFormat.php241
-rw-r--r--formats/MrssFormat.php288
-rw-r--r--formats/PlaintextFormat.php31
-rw-r--r--index.php38
-rw-r--r--lib/ActionFactory.php43
-rw-r--r--lib/ActionInterface.php26
-rw-r--r--lib/Authentication.php108
-rw-r--r--lib/BridgeAbstract.php802
-rw-r--r--lib/BridgeCard.php682
-rw-r--r--lib/BridgeFactory.php144
-rw-r--r--lib/BridgeInterface.php162
-rw-r--r--lib/BridgeList.php275
-rw-r--r--lib/CacheFactory.php95
-rw-r--r--lib/CacheInterface.php96
-rw-r--r--lib/Configuration.php610
-rw-r--r--lib/Debug.php184
-rw-r--r--lib/Exceptions.php162
-rw-r--r--lib/FactoryAbstract.php99
-rw-r--r--lib/FeedExpander.php857
-rw-r--r--lib/FeedItem.php1046
-rw-r--r--lib/FormatAbstract.php260
-rw-r--r--lib/FormatFactory.php102
-rw-r--r--lib/FormatInterface.php118
-rw-r--r--lib/ParameterValidator.php476
-rw-r--r--lib/XPathAbstract.php1162
-rw-r--r--lib/contents.php570
-rw-r--r--lib/error.php61
-rw-r--r--lib/html.php202
-rw-r--r--lib/php8backports.php28
-rw-r--r--lib/rssbridge.php33
-rw-r--r--phpcs.xml72
-rw-r--r--tests/Actions/ActionImplementationTest.php106
-rw-r--r--tests/Actions/ListActionTest.php165
-rw-r--r--tests/Bridges/BridgeImplementationTest.php451
-rw-r--r--tests/Caches/CacheImplementationTest.php75
-rw-r--r--tests/Formats/AtomFormatTest.php27
-rw-r--r--tests/Formats/BaseFormatTest.php116
-rw-r--r--tests/Formats/FormatImplementationTest.php67
-rw-r--r--tests/Formats/JsonFormatTest.php27
-rw-r--r--tests/Formats/MrssFormatTest.php27
398 files changed, 62578 insertions, 60413 deletions
diff --git a/actions/ConnectivityAction.php b/actions/ConnectivityAction.php
index 1018c4a2..c657f21b 100644
--- a/actions/ConnectivityAction.php
+++ b/actions/ConnectivityAction.php
@@ -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
*/
/**
@@ -23,85 +24,84 @@
*/
class ConnectivityAction implements ActionInterface
{
- public $userData = [];
-
- public function execute() {
-
- if(!Debug::isEnabled()) {
- returnError('This action is only available in debug mode!', 400);
- }
-
- if(!isset($this->userData['bridge'])) {
- $this->returnEntryPage();
- return;
- }
-
- $bridgeName = $this->userData['bridge'];
-
- $this->reportBridgeConnectivity($bridgeName);
-
- }
-
- /**
- * Generates a report about the bridge connectivity status and sends it back
- * to the user.
- *
- * The report is generated as Json-formatted string in the format
- * {
- * "bridge": "<bridge-name>",
- * "successful": true/false
- * }
- *
- * @param string $bridgeName Name of the bridge to generate the report for
- * @return void
- */
- private function reportBridgeConnectivity($bridgeName) {
-
- $bridgeFac = new \BridgeFactory();
-
- if(!$bridgeFac->isWhitelisted($bridgeName)) {
- header('Content-Type: text/html');
- returnServerError('Bridge is not whitelisted!');
- }
-
- header('Content-Type: text/json');
-
- $retVal = array(
- 'bridge' => $bridgeName,
- 'successful' => false,
- 'http_code' => 200,
- );
-
- $bridge = $bridgeFac->create($bridgeName);
-
- if($bridge === false) {
- echo json_encode($retVal);
- return;
- }
-
- $curl_opts = array(
- CURLOPT_CONNECTTIMEOUT => 5
- );
-
- try {
- $reply = getContents($bridge::URI, array(), $curl_opts, true);
-
- if($reply['code'] === 200) {
- $retVal['successful'] = true;
- if (strpos(implode('', $reply['status_lines']), '301 Moved Permanently')) {
- $retVal['http_code'] = 301;
- }
- }
- } catch(Exception $e) {
- $retVal['successful'] = false;
- }
-
- echo json_encode($retVal);
-
- }
-
- private function returnEntryPage() {
- echo <<<EOD
+ public $userData = [];
+
+ public function execute()
+ {
+ if (!Debug::isEnabled()) {
+ returnError('This action is only available in debug mode!', 400);
+ }
+
+ if (!isset($this->userData['bridge'])) {
+ $this->returnEntryPage();
+ return;
+ }
+
+ $bridgeName = $this->userData['bridge'];
+
+ $this->reportBridgeConnectivity($bridgeName);
+ }
+
+ /**
+ * Generates a report about the bridge connectivity status and sends it back
+ * to the user.
+ *
+ * The report is generated as Json-formatted string in the format
+ * {
+ * "bridge": "<bridge-name>",
+ * "successful": true/false
+ * }
+ *
+ * @param string $bridgeName Name of the bridge to generate the report for
+ * @return void
+ */
+ private function reportBridgeConnectivity($bridgeName)
+ {
+ $bridgeFac = new \BridgeFactory();
+
+ if (!$bridgeFac->isWhitelisted($bridgeName)) {
+ header('Content-Type: text/html');
+ returnServerError('Bridge is not whitelisted!');
+ }
+
+ header('Content-Type: text/json');
+
+ $retVal = [
+ 'bridge' => $bridgeName,
+ 'successful' => false,
+ 'http_code' => 200,
+ ];
+
+ $bridge = $bridgeFac->create($bridgeName);
+
+ if ($bridge === false) {
+ echo json_encode($retVal);
+ return;
+ }
+
+ $curl_opts = [
+ CURLOPT_CONNECTTIMEOUT => 5
+ ];
+
+ try {
+ $reply = getContents($bridge::URI, [], $curl_opts, true);
+
+ 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;
+ }
+
+ echo json_encode($retVal);
+ }
+
+ private function returnEntryPage()
+ {
+ echo <<<EOD
<!DOCTYPE html>
<html>
@@ -132,5 +132,5 @@ class ConnectivityAction implements ActionInterface
</body>
</html>
EOD;
- }
+ }
}
diff --git a/actions/DetectAction.php b/actions/DetectAction.php
index d662d7aa..149b239d 100644
--- a/actions/DetectAction.php
+++ b/actions/DetectAction.php
@@ -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,50 +7,49 @@
* 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 implements ActionInterface
{
- public $userData = [];
-
- public function execute() {
- $targetURL = $this->userData['url']
- or returnClientError('You must specify a url!');
-
- $format = $this->userData['format']
- or returnClientError('You must specify a format!');
+ public $userData = [];
- $bridgeFac = new \BridgeFactory();
+ public function execute()
+ {
+ $targetURL = $this->userData['url']
+ or returnClientError('You must specify a url!');
- foreach($bridgeFac->getBridgeNames() as $bridgeName) {
+ $format = $this->userData['format']
+ or returnClientError('You must specify a format!');
- if(!$bridgeFac->isWhitelisted($bridgeName)) {
- continue;
- }
+ $bridgeFac = new \BridgeFactory();
- $bridge = $bridgeFac->create($bridgeName);
+ foreach ($bridgeFac->getBridgeNames() as $bridgeName) {
+ if (!$bridgeFac->isWhitelisted($bridgeName)) {
+ continue;
+ }
- if($bridge === false) {
- continue;
- }
+ $bridge = $bridgeFac->create($bridgeName);
- $bridgeParams = $bridge->detectParameters($targetURL);
+ if ($bridge === false) {
+ continue;
+ }
- if(is_null($bridgeParams)) {
- continue;
- }
+ $bridgeParams = $bridge->detectParameters($targetURL);
- $bridgeParams['bridge'] = $bridgeName;
- $bridgeParams['format'] = $format;
+ if (is_null($bridgeParams)) {
+ continue;
+ }
- header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
- die();
+ $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);
- }
+ returnClientError('No bridge found for given URL: ' . $targetURL);
+ }
}
diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php
index e7031dab..721e9446 100644
--- a/actions/DisplayAction.php
+++ b/actions/DisplayAction.php
@@ -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,216 +7,220 @@
* 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 implements ActionInterface
{
- public $userData = [];
-
- private function getReturnCode($error) {
- $returnCode = $error->getCode();
- if ($returnCode === 301 || $returnCode === 302) {
- # Don't pass redirect codes to the exterior
- $returnCode = 508;
- }
- return $returnCode;
- }
-
- public function execute() {
- $bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null;
-
- $format = $this->userData['format']
- or returnClientError('You must specify a format!');
-
- $bridgeFac = new \BridgeFactory();
-
- // whitelist control
- if(!$bridgeFac->isWhitelisted($bridge)) {
- throw new \Exception('This bridge is not whitelisted', 401);
- die;
- }
-
- // Data retrieval
- $bridge = $bridgeFac->create($bridge);
- $bridge->loadConfiguration();
-
- $noproxy = array_key_exists('_noproxy', $this->userData)
- && filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN);
-
- if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
- define('NOPROXY', true);
- }
-
- // Cache timeout
- $cache_timeout = -1;
- if(array_key_exists('_cache_timeout', $this->userData)) {
-
- 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();
- }
-
- $cache_timeout = filter_var($this->userData['_cache_timeout'], FILTER_VALIDATE_INT);
-
- } else {
- $cache_timeout = $bridge->getCacheTimeout();
- }
-
- // 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'
- ), '')
- );
-
- // 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'
- ), '')
- );
-
- // Initialize cache
- $cacheFac = new CacheFactory();
-
- $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $cache->setScope('');
- $cache->purgeCache(86400); // 24 hours
- $cache->setKey($cache_params);
-
- $items = array();
- $infos = array();
- $mtime = $cache->getTime();
-
- if($mtime !== false
- && (time() - $cache_timeout < $mtime)
- && !Debug::isEnabled()) { // Load cached data
-
- // 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']);
-
- 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();
- }
- }
-
- $cached = $cache->loadData();
-
- if(isset($cached['items']) && isset($cached['extraInfos'])) {
- foreach($cached['items'] as $item) {
- $items[] = new \FeedItem($item);
- }
-
- $infos = $cached['extraInfos'];
- }
-
- } else { // Collect new data
-
- try {
- $bridge->setDatas($bridge_params);
- $bridge->collectData();
-
- $items = $bridge->getItems();
-
- // 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();
-
- foreach($items as $item) {
- $feedItems[] = new \FeedItem($item);
- }
-
- $items = $feedItems;
- }
-
- $infos = array(
- 'name' => $bridge->getName(),
- 'uri' => $bridge->getURI(),
- 'donationUri' => $bridge->getDonationURI(),
- 'icon' => $bridge->getIcon()
- );
- } catch(\Throwable $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));
-
- $message = sprintf(
- 'Bridge returned error %s! (%s)',
- $e->getCode(),
- $this->userData['_error_time']
- );
- $item->setTitle($message);
-
- $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->getReturnCode($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();
- $format = $formatFac->create($format);
- $format->setItems($items);
- $format->setExtraInfos($infos);
- $lastModified = $cache->getTime();
- $format->setLastModified($lastModified);
- if ($lastModified) {
- header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $lastModified) . 'GMT');
- }
- header('Content-Type: ' . $format->getMimeType() . '; charset=' . $format->getCharset());
-
- echo $format->stringify();
- } catch(\Throwable $e) {
- error_log($e);
- header('Content-Type: text/html', true, $e->getCode());
- die(buildTransformException($e, $bridge));
- }
- }
+ public $userData = [];
+
+ private function getReturnCode($error)
+ {
+ $returnCode = $error->getCode();
+ if ($returnCode === 301 || $returnCode === 302) {
+ # Don't pass redirect codes to the exterior
+ $returnCode = 508;
+ }
+ return $returnCode;
+ }
+
+ public function execute()
+ {
+ $bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null;
+
+ $format = $this->userData['format']
+ or returnClientError('You must specify a format!');
+
+ $bridgeFac = new \BridgeFactory();
+
+ // whitelist control
+ if (!$bridgeFac->isWhitelisted($bridge)) {
+ throw new \Exception('This bridge is not whitelisted', 401);
+ die;
+ }
+
+ // Data retrieval
+ $bridge = $bridgeFac->create($bridge);
+ $bridge->loadConfiguration();
+
+ $noproxy = array_key_exists('_noproxy', $this->userData)
+ && filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN);
+
+ if (defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
+ define('NOPROXY', true);
+ }
+
+ // Cache timeout
+ $cache_timeout = -1;
+ if (array_key_exists('_cache_timeout', $this->userData)) {
+ 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();
+ }
+
+ $cache_timeout = filter_var($this->userData['_cache_timeout'], FILTER_VALIDATE_INT);
+ } else {
+ $cache_timeout = $bridge->getCacheTimeout();
+ }
+
+ // Remove parameters that don't concern bridges
+ $bridge_params = array_diff_key(
+ $this->userData,
+ array_fill_keys(
+ [
+ 'action',
+ 'bridge',
+ 'format',
+ '_noproxy',
+ '_cache_timeout',
+ '_error_time'
+ ],
+ ''
+ )
+ );
+
+ // Remove parameters that don't concern caches
+ $cache_params = array_diff_key(
+ $this->userData,
+ array_fill_keys(
+ [
+ 'action',
+ 'format',
+ '_noproxy',
+ '_cache_timeout',
+ '_error_time'
+ ],
+ ''
+ )
+ );
+
+ // Initialize cache
+ $cacheFac = new CacheFactory();
+
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope('');
+ $cache->purgeCache(86400); // 24 hours
+ $cache->setKey($cache_params);
+
+ $items = [];
+ $infos = [];
+ $mtime = $cache->getTime();
+
+ if (
+ $mtime !== false
+ && (time() - $cache_timeout < $mtime)
+ && !Debug::isEnabled()
+ ) { // Load cached data
+ // 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']);
+
+ 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();
+ }
+ }
+
+ $cached = $cache->loadData();
+
+ if (isset($cached['items']) && isset($cached['extraInfos'])) {
+ foreach ($cached['items'] as $item) {
+ $items[] = new \FeedItem($item);
+ }
+
+ $infos = $cached['extraInfos'];
+ }
+ } else { // Collect new data
+ try {
+ $bridge->setDatas($bridge_params);
+ $bridge->collectData();
+
+ $items = $bridge->getItems();
+
+ // 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 = [];
+
+ 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) {
+ 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));
+
+ $message = sprintf(
+ 'Bridge returned error %s! (%s)',
+ $e->getCode(),
+ $this->userData['_error_time']
+ );
+ $item->setTitle($message);
+
+ $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->getReturnCode($e));
+ die(buildTransformException($e, $bridge));
+ }
+ }
+ }
+
+ // Store data in cache
+ $cache->saveData([
+ 'items' => array_map(function ($i) {
+ return $i->toArray();
+ }, $items),
+ 'extraInfos' => $infos
+ ]);
+ }
+
+ // Data transformation
+ try {
+ $formatFac = new FormatFactory();
+ $format = $formatFac->create($format);
+ $format->setItems($items);
+ $format->setExtraInfos($infos);
+ $lastModified = $cache->getTime();
+ $format->setLastModified($lastModified);
+ if ($lastModified) {
+ header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $lastModified) . 'GMT');
+ }
+ header('Content-Type: ' . $format->getMimeType() . '; charset=' . $format->getCharset());
+
+ echo $format->stringify();
+ } catch (\Throwable $e) {
+ error_log($e);
+ header('Content-Type: text/html', true, $e->getCode());
+ die(buildTransformException($e, $bridge));
+ }
+ }
}
diff --git a/actions/ListAction.php b/actions/ListAction.php
index a778d846..7ddc42cb 100644
--- a/actions/ListAction.php
+++ b/actions/ListAction.php
@@ -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,49 @@
* 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 implements ActionInterface
{
- public function execute() {
- $list = new StdClass();
- $list->bridges = array();
- $list->total = 0;
-
- $bridgeFac = new \BridgeFactory();
-
- foreach($bridgeFac->getBridgeNames() as $bridgeName) {
-
- $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);
- }
+ public function execute()
+ {
+ $list = new StdClass();
+ $list->bridges = [];
+ $list->total = 0;
+
+ $bridgeFac = new \BridgeFactory();
+
+ foreach ($bridgeFac->getBridgeNames() as $bridgeName) {
+ $bridge = $bridgeFac->create($bridgeName);
+
+ if ($bridge === false) { // Broken bridge, show as inactive
+ $list->bridges[$bridgeName] = [
+ 'status' => 'inactive'
+ ];
+
+ continue;
+ }
+
+ $status = $bridgeFac->isWhitelisted($bridgeName) ? 'active' : 'inactive';
+
+ $list->bridges[$bridgeName] = [
+ '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);
+ }
}
diff --git a/bridges/ABCNewsBridge.php b/bridges/ABCNewsBridge.php
index 44208de1..94cd1fb3 100644
--- a/bridges/ABCNewsBridge.php
+++ b/bridges/ABCNewsBridge.php
@@ -1,45 +1,48 @@
<?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();
+ 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());
- $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);
+ foreach ($html->find('._2H7Su') as $article) {
+ $item = [];
- $this->items[] = $item;
- }
- }
+ $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;
+ }
+ }
}
diff --git a/bridges/AO3Bridge.php b/bridges/AO3Bridge.php
index 4a6f2ccf..f55c0d45 100644
--- a/bridges/AO3Bridge.php
+++ b/bridges/AO3Bridge.php
@@ -1,118 +1,130 @@
<?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',
- ),
- )
- );
-
- // 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);
-
- foreach($html->find('.index.group > li') as $element) {
- $item = array();
-
- $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;
-
- $strdate = $element->find('div p.datetime', 0)->plaintext;
- $item['timestamp'] = strtotime($strdate);
-
- $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";
-
- $this->items[] = $item;
- }
- }
-
- // 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->title = $html->find('h2 a', 0)->plaintext;
-
- foreach($html->find('ol.index.group > li') as $element) {
- $item = array();
-
- $item['title'] = $element->find('a', 0)->plaintext;
- $item['content'] = $element;
- $item['uri'] = $element->find('a', 0)->href;
-
- $strdate = $element->find('span.datetime', 0)->plaintext;
- $strdate = str_replace('(', '', $strdate);
- $strdate = str_replace(')', '', $strdate);
- $item['timestamp'] = strtotime($strdate);
-
- $item['uid'] = $item['uri'] . "/$strdate";
-
- $this->items[] = $item;
- }
-
- $this->items = array_reverse($this->items);
- }
-
- 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')
- );
- }
- }
-
- 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';
- }
+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);
+
+ foreach ($html->find('.index.group > li') as $element) {
+ $item = [];
+
+ $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;
+
+ $strdate = $element->find('div p.datetime', 0)->plaintext;
+ $item['timestamp'] = strtotime($strdate);
+
+ $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";
+
+ $this->items[] = $item;
+ }
+ }
+
+ // 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->title = $html->find('h2 a', 0)->plaintext;
+
+ foreach ($html->find('ol.index.group > li') as $element) {
+ $item = [];
+
+ $item['title'] = $element->find('a', 0)->plaintext;
+ $item['content'] = $element;
+ $item['uri'] = $element->find('a', 0)->href;
+
+ $strdate = $element->find('span.datetime', 0)->plaintext;
+ $strdate = str_replace('(', '', $strdate);
+ $strdate = str_replace(')', '', $strdate);
+ $item['timestamp'] = strtotime($strdate);
+
+ $item['uid'] = $item['uri'] . "/$strdate";
+
+ $this->items[] = $item;
+ }
+
+ $this->items = array_reverse($this->items);
+ }
+
+ 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')
+ );
+ }
+ }
+
+ 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';
+ }
}
diff --git a/bridges/ARDMediathekBridge.php b/bridges/ARDMediathekBridge.php
index 97250272..6de8dad7 100644
--- a/bridges/ARDMediathekBridge.php
+++ b/bridges/ARDMediathekBridge.php
@@ -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);
+ }
}
diff --git a/bridges/ASRockNewsBridge.php b/bridges/ASRockNewsBridge.php
index 6c93798f..1b516377 100644
--- a/bridges/ASRockNewsBridge.php
+++ b/bridges/ASRockNewsBridge.php
@@ -1,55 +1,58 @@
<?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);
+ $item['enclosures'][] = $a->find('img', 0)->src;
+ $this->items[] = $item;
- if (count($this->items) >= 10) {
- break;
- }
- }
- }
+ if (count($this->items) >= 10) {
+ break;
+ }
+ }
+ }
- private function extractDate($text) {
- $dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/';
+ private function extractDate($text)
+ {
+ $dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/';
- $text = trim($text);
+ $text = trim($text);
- if (preg_match($dateRegex, $text, $matches)) {
- return $matches[1];
- }
+ if (preg_match($dateRegex, $text, $matches)) {
+ return $matches[1];
+ }
- return '';
- }
+ return '';
+ }
}
diff --git a/bridges/AcrimedBridge.php b/bridges/AcrimedBridge.php
index 7bc73176..d37f3ce4 100644
--- a/bridges/AcrimedBridge.php
+++ b/bridges/AcrimedBridge.php
@@ -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;
+ }
}
diff --git a/bridges/AirBreizhBridge.php b/bridges/AirBreizhBridge.php
index 2d852da5..a822625f 100644
--- a/bridges/AirBreizhBridge.php
+++ b/bridges/AirBreizhBridge.php
@@ -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 l’air' => '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 l’air' => '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;
+ }
+ }
}
diff --git a/bridges/AlbionOnlineBridge.php b/bridges/AlbionOnlineBridge.php
index f51b815b..4b191b18 100644
--- a/bridges/AlbionOnlineBridge.php
+++ b/bridges/AlbionOnlineBridge.php
@@ -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;
+ }
}
diff --git a/bridges/AlfaBankByBridge.php b/bridges/AlfaBankByBridge.php
index 4b1ed48e..7c13c14d 100644
--- a/bridges/AlfaBankByBridge.php
+++ b/bridges/AlfaBankByBridge.php
@@ -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);
+ }
}
diff --git a/bridges/AllocineFRBridge.php b/bridges/AllocineFRBridge.php
index 07d031d8..b93bccd2 100644
--- a/bridges/AllocineFRBridge.php
+++ b/bridges/AllocineFRBridge.php
@@ -1,113 +1,115 @@
<?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/',
- 'fanzone' => '/video/programme-12298/',
- 'game-in-cine' => '/video/programme-12288/',
- 'pour-la-faire-courte' => '/video/programme-20960/',
- 'home-cinema' => '/video/programme-12287/',
- 'pils-par-ici-les-sorties' => '/video/programme-25789/',
- 'allocine-lemission-sur-lestream' => '/video/programme-25123/',
- 'give-me-five' => '/video/programme-21919/saison-34518/',
- 'aviez-vous-remarque' => '/video/programme-19518/',
- 'et-paf-il-est-mort' => '/video/programme-25113/',
- 'the-big-fan-theory' => '/video/programme-20403/',
- 'cliches' => '/video/programme-24834/',
- 'completement' => '/video/programme-23859/',
- 'fun-facts' => '/video/programme-23040/',
- 'origin-story' => '/video/programme-25667/'
- );
+ $category = $this->getInput('category');
+ if (array_key_exists($category, $categories)) {
+ return static::URI . $this->getLastSeasonURI($categories[$category]);
+ } else {
+ returnClientError('Emission inconnue');
+ }
+ }
- $category = $this->getInput('category');
- if(array_key_exists($category, $categories)) {
- return static::URI . $this->getLastSeasonURI($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;
+ }
- 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 . ' : '
- . array_search(
- $this->getInput('category'),
- self::PARAMETERS[$this->queriedContext]['category']['values']
- );
- }
+ return parent::getName();
+ }
- return parent::getName();
- }
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
- public function collectData(){
+ $category = array_search(
+ $this->getInput('category'),
+ self::PARAMETERS[$this->queriedContext]['category']['values']
+ );
+ foreach ($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) {
+ $item = [];
- $html = getSimpleHTMLDOM($this->getURI());
+ $title = $element->find('a[class*=meta-title-link]', 0);
+ $content = trim(defaultLinkTo($element->outertext, static::URI));
- $category = array_search(
- $this->getInput('category'),
- self::PARAMETERS[$this->queriedContext]['category']['values']
- );
- foreach($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) {
- $item = array();
+ // 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(defaultLinkTo($element->outertext, static::URI));
+ // 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;
+ }
+ }
}
diff --git a/bridges/AmazonBridge.php b/bridges/AmazonBridge.php
index ba440a59..40855a15 100644
--- a/bridges/AmazonBridge.php
+++ b/bridges/AmazonBridge.php
@@ -1,103 +1,104 @@
<?php
-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 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',
- 'Sweden' => 'se',
- 'Turkey' => 'com.tr',
- 'United Kingdom' => 'co.uk',
- 'United States' => 'com',
- ),
- 'defaultValue' => 'com',
- ),
- ));
-
- public function collectData() {
-
- $baseUrl = sprintf('https://www.amazon.%s', $this->getInput('tld'));
-
- $url = sprintf(
- '%s/s/?field-keywords=%s&sort=%s',
- $baseUrl,
- urlencode($this->getInput('q')),
- $this->getInput('sort')
- );
-
- $dom = getSimpleHTMLDOM($url);
-
- $elements = $dom->find('div.s-result-item');
-
- foreach($elements as $element) {
- $item = [];
-
- $title = $element->find('h2', 0);
- if (!$title) {
- continue;
- }
-
- $item['title'] = $title->innertext;
-
- $itemUrl = $element->find('a', 0)->href;
- $item['uri'] = urljoin($baseUrl, $itemUrl);
-
- $image = $element->find('img', 0);
- if ($image) {
- $item['content'] = '<img src="' . $image->getAttribute('src') . '" /><br />';
- }
-
- $price = $element->find('span.a-price > .a-offscreen', 0);
- if ($price) {
- $item['content'] .= $price->innertext;
- }
-
- $this->items[] = $item;
- }
- }
-
- public function getName(){
- if(!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) {
- return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('q');
- }
-
- return parent::getName();
- }
+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 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',
+ 'Spain' => 'es',
+ 'Sweden' => 'se',
+ 'Turkey' => 'com.tr',
+ 'United Kingdom' => 'co.uk',
+ 'United States' => 'com',
+ ],
+ 'defaultValue' => 'com',
+ ],
+ ]];
+
+ public function collectData()
+ {
+ $baseUrl = sprintf('https://www.amazon.%s', $this->getInput('tld'));
+
+ $url = sprintf(
+ '%s/s/?field-keywords=%s&sort=%s',
+ $baseUrl,
+ urlencode($this->getInput('q')),
+ $this->getInput('sort')
+ );
+
+ $dom = getSimpleHTMLDOM($url);
+
+ $elements = $dom->find('div.s-result-item');
+
+ foreach ($elements as $element) {
+ $item = [];
+
+ $title = $element->find('h2', 0);
+ if (!$title) {
+ continue;
+ }
+
+ $item['title'] = $title->innertext;
+
+ $itemUrl = $element->find('a', 0)->href;
+ $item['uri'] = urljoin($baseUrl, $itemUrl);
+
+ $image = $element->find('img', 0);
+ if ($image) {
+ $item['content'] = '<img src="' . $image->getAttribute('src') . '" /><br />';
+ }
+
+ $price = $element->find('span.a-price > .a-offscreen', 0);
+ if ($price) {
+ $item['content'] .= $price->innertext;
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) {
+ return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('q');
+ }
+
+ return parent::getName();
+ }
}
diff --git a/bridges/AmazonPriceTrackerBridge.php b/bridges/AmazonPriceTrackerBridge.php
index 3824c939..af8f4459 100644
--- a/bridges/AmazonPriceTrackerBridge.php
+++ b/bridges/AmazonPriceTrackerBridge.php
@@ -1,243 +1,257 @@
<?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';
-
- 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',
- '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 WHITESPACE = " \t\n\r\0\x0B\xC2\xA0";
-
- protected $title;
-
- /**
- * 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();
- }
-
- /**
- * 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));
- }
- }
-
- /**
- * 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();
- }
- }
-
- private function parseDynamicImage($attribute) {
- $json = json_decode(html_entity_decode($attribute), true);
-
- 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);
-
- 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
+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 = [
+ [
+ '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',
+ 'Spain' => 'es',
+ 'Sweden' => 'se',
+ 'Turkey' => 'com.tr',
+ 'United Kingdom' => 'co.uk',
+ 'United States' => 'com',
+ ],
+ 'defaultValue' => 'com',
+ ],
+ ]];
+
+ 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";
+
+ protected $title;
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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));
+ }
+ }
+
+ /**
+ * 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();
+ }
+ }
+
+ private function parseDynamicImage($attribute)
+ {
+ $json = json_decode(html_entity_decode($attribute), true);
+
+ 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);
+
+ 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
<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 getSimpleHTMLDOM($uri) ?: returnServerError('Could not request Amazon.');
- }
-
- 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')
- );
- }
-
- return false;
- }
-
- 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',
- );
- }
-
- return false;
- }
-
- private function scrapePriceGeneric($html) {
- $priceDiv = null;
-
- foreach(self::PRICE_SELECTORS as $sel) {
- $priceDiv = $html->find($sel, 0);
- if ($priceDiv) {
- break;
- }
- }
-
- if (!$priceDiv) {
- return false;
- }
-
- $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);
-
- if ($price != null && $currency != null) {
- return array(
- 'price' => $price,
- 'currency' => $currency,
- 'shipping' => '0'
- );
- }
-
- return false;
- }
-
- private function renderContent($image, $data) {
- $price = $data['displayPrice'];
- if (!$price) {
- $price = "{$data['price']} {$data['currency']}";
- }
-
- $html = "$image<br>Price: $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;
- }
+ }
+
+ /**
+ * 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.');
+ }
+
+ 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 [
+ 'price' => $asinData->getAttribute('data-asin-price'),
+ 'currency' => $asinData->getAttribute('data-asin-currency-code'),
+ 'shipping' => $asinData->getAttribute('data-asin-shipping')
+ ];
+ }
+
+ return false;
+ }
+
+ 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 [
+ 'displayPrice' => $data['displayPrice'],
+ 'currency' => $data['currency'],
+ 'shipping' => '0',
+ ];
+ }
+
+ return false;
+ }
+
+ private function scrapePriceGeneric($html)
+ {
+ $priceDiv = null;
+
+ foreach (self::PRICE_SELECTORS as $sel) {
+ $priceDiv = $html->find($sel, 0);
+ if ($priceDiv) {
+ break;
+ }
+ }
+
+ if (!$priceDiv) {
+ return false;
+ }
+
+ $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);
+
+ if ($price != null && $currency != null) {
+ return [
+ 'price' => $price,
+ 'currency' => $currency,
+ 'shipping' => '0'
+ ];
+ }
+
+ return false;
+ }
+
+ private function renderContent($image, $data)
+ {
+ $price = $data['displayPrice'];
+ if (!$price) {
+ $price = "{$data['price']} {$data['currency']}";
+ }
+
+ $html = "$image<br>Price: $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 = [
+ '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;
+ }
}
diff --git a/bridges/AnidexBridge.php b/bridges/AnidexBridge.php
index a97e434c..6d41365b 100644
--- a/bridges/AnidexBridge.php
+++ b/bridges/AnidexBridge.php
@@ -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;
+ }
}
diff --git a/bridges/AnimeUltimeBridge.php b/bridges/AnimeUltimeBridge.php
index c18821aa..23d093a6 100644
--- a/bridges/AnimeUltimeBridge.php
+++ b/bridges/AnimeUltimeBridge.php
@@ -1,141 +1,141 @@
<?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'
- )
- )
- ));
-
- private $filter = 'Releases';
-
- public function collectData(){
-
- //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;
-
- //Process each HTML page until having 10 releases
- $processedOK = 0;
- foreach (array($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);
-
- //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));
-
- //<h3>day</h3><br /><table><tr> <-- useful data in table rows
- $release = $daySection->next_sibling()->next_sibling()->first_child();
-
- //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);
-
- $item_image = self::URI . substr(
- $item_link_element->onmouseover,
- 37,
- strpos($item_link_element->onmouseover, ' ', 37) - 37
- );
-
- $item_episode = html_entity_decode(
- str_pad(
- $release->find('td', 1)->plaintext,
- 2,
- '0',
- STR_PAD_LEFT
- )
- );
-
- $item_fansub = $release->find('td', 2)->plaintext;
- $item_type = $release->find('td', 4)->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">')
- );
-
- // 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();
- }
+
+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';
+
+ public function collectData()
+ {
+ //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;
+
+ //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);
+
+ //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));
+
+ //<h3>day</h3><br /><table><tr> <-- useful data in table rows
+ $release = $daySection->next_sibling()->next_sibling()->first_child();
+
+ //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);
+
+ $item_image = self::URI . substr(
+ $item_link_element->onmouseover,
+ 37,
+ strpos($item_link_element->onmouseover, ' ', 37) - 37
+ );
+
+ $item_episode = html_entity_decode(
+ str_pad(
+ $release->find('td', 1)->plaintext,
+ 2,
+ '0',
+ STR_PAD_LEFT
+ )
+ );
+
+ $item_fansub = $release->find('td', 2)->plaintext;
+ $item_type = $release->find('td', 4)->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">')
+ );
+
+ // 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 = [];
+ $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++;
+
+ //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();
+ }
}
diff --git a/bridges/AppleAppStoreBridge.php b/bridges/AppleAppStoreBridge.php
index 8655a891..607581e8 100644
--- a/bridges/AppleAppStoreBridge.php
+++ b/bridges/AppleAppStoreBridge.php
@@ -1,151 +1,159 @@
<?php
-class AppleAppStoreBridge extends BridgeAbstract {
-
- const MAINTAINER = 'captn3m0';
- const NAME = 'Apple App Store';
- const URI = 'https://apps.apple.com/';
- const CACHE_TIMEOUT = 3600; // 1h
- const DESCRIPTION = 'Returns version updates for a specific application';
-
- const PARAMETERS = array(array(
- 'id' => array(
- 'name' => 'Application ID',
- 'required' => true,
- 'exampleValue' => '310633997'
- ),
- 'p' => array(
- 'name' => 'Platform',
- 'type' => 'list',
- 'values' => array(
- 'iPad' => 'ipad',
- 'iPhone' => 'iphone',
- 'Mac' => 'mac',
-
- // The following 2 are present in responses
- // but not yet tested
- 'Web' => 'web',
- 'Apple TV' => 'appletv',
- ),
- 'defaultValue' => 'iphone',
- ),
- 'country' => array(
- 'name' => 'Store Country',
- 'type' => 'list',
- 'values' => array(
- 'US' => 'US',
- 'India' => 'IN',
- 'Canada' => 'CA',
- 'Germany' => 'DE',
- ),
- 'defaultValue' => 'US',
- ),
- ));
-
- const PLATFORM_MAPPING = array(
- 'iphone' => 'ios',
- 'ipad' => 'ios',
- );
-
- private function makeHtmlUrl($id, $country){
- return 'https://apps.apple.com/' . $country . '/app/id' . $id;
- }
-
- private function makeJsonUrl($id, $platform, $country){
- return "https://amp-api.apps.apple.com/v1/catalog/$country/apps/$id?platform=$platform&extend=versionHistory";
- }
-
- public function getName(){
- if (isset($this->name)) {
- return $this->name . ' - AppStore Updates';
- }
-
- return parent::getName();
- }
-
- /**
- * In case of some platforms, the data is present in the initial response
- */
- private function getDataFromShoebox($id, $platform, $country){
- $uri = $this->makeHtmlUrl($id, $country);
- $html = getSimpleHTMLDOMCached($uri, 3600);
- $script = $html->find('script[id="shoebox-ember-data-store"]', 0);
-
- $json = json_decode($script->innertext, true);
- return $json['data'];
- }
-
- private function getJWTToken($id, $platform, $country){
- $uri = $this->makeHtmlUrl($id, $country);
-
- $html = getSimpleHTMLDOMCached($uri, 3600);
-
- $meta = $html->find('meta[name="web-experience-app/config/environment"]', 0);
-
- $json = urldecode($meta->content);
-
- $json = json_decode($json);
-
- return $json->MEDIA_API->token;
- }
-
- private function getAppData($id, $platform, $country, $token){
- $uri = $this->makeJsonUrl($id, $platform, $country);
-
- $headers = array(
- "Authorization: Bearer $token",
- 'Origin: https://apps.apple.com',
- );
-
- $json = json_decode(getContents($uri, $headers), true);
-
- return $json['data'][0];
- }
-
- /**
- * Parses the version history from the data received
- * @return array list of versions with details on each element
- */
- private function getVersionHistory($data, $platform){
- switch($platform) {
- case 'mac':
- return $data['relationships']['platforms']['data'][0]['attributes']['versionHistory'];
- default:
- $os = self::PLATFORM_MAPPING[$platform];
- return $data['attributes']['platformAttributes'][$os]['versionHistory'];
- }
- }
-
- public function collectData() {
- $id = $this->getInput('id');
- $country = $this->getInput('country');
- $platform = $this->getInput('p');
-
- switch ($platform) {
- case 'mac':
- $data = $this->getDataFromShoebox($id, $platform, $country);
- break;
-
- default:
- $token = $this->getJWTToken($id, $platform, $country);
- $data = $this->getAppData($id, $platform, $country, $token);
- }
-
- $versionHistory = $this->getVersionHistory($data, $platform);
- $name = $this->name = $data['attributes']['name'];
- $author = $data['attributes']['artistName'];
-
- foreach ($versionHistory as $row) {
- $item = array();
-
- $item['content'] = nl2br($row['releaseNotes']);
- $item['title'] = $name . ' - ' . $row['versionDisplay'];
- $item['timestamp'] = $row['releaseDate'];
- $item['author'] = $author;
-
- $item['uri'] = $this->makeHtmlUrl($id, $country);
-
- $this->items[] = $item;
- }
- }
+class AppleAppStoreBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'captn3m0';
+ const NAME = 'Apple App Store';
+ const URI = 'https://apps.apple.com/';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Returns version updates for a specific application';
+
+ const PARAMETERS = [[
+ 'id' => [
+ 'name' => 'Application ID',
+ 'required' => true,
+ 'exampleValue' => '310633997'
+ ],
+ 'p' => [
+ 'name' => 'Platform',
+ 'type' => 'list',
+ 'values' => [
+ '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',
+ ],
+ 'defaultValue' => 'US',
+ ],
+ ]];
+
+ const PLATFORM_MAPPING = [
+ 'iphone' => 'ios',
+ 'ipad' => 'ios',
+ ];
+
+ private function makeHtmlUrl($id, $country)
+ {
+ return 'https://apps.apple.com/' . $country . '/app/id' . $id;
+ }
+
+ private function makeJsonUrl($id, $platform, $country)
+ {
+ return "https://amp-api.apps.apple.com/v1/catalog/$country/apps/$id?platform=$platform&extend=versionHistory";
+ }
+
+ public function getName()
+ {
+ if (isset($this->name)) {
+ return $this->name . ' - AppStore Updates';
+ }
+
+ return parent::getName();
+ }
+
+ /**
+ * In case of some platforms, the data is present in the initial response
+ */
+ private function getDataFromShoebox($id, $platform, $country)
+ {
+ $uri = $this->makeHtmlUrl($id, $country);
+ $html = getSimpleHTMLDOMCached($uri, 3600);
+ $script = $html->find('script[id="shoebox-ember-data-store"]', 0);
+
+ $json = json_decode($script->innertext, true);
+ return $json['data'];
+ }
+
+ private function getJWTToken($id, $platform, $country)
+ {
+ $uri = $this->makeHtmlUrl($id, $country);
+
+ $html = getSimpleHTMLDOMCached($uri, 3600);
+
+ $meta = $html->find('meta[name="web-experience-app/config/environment"]', 0);
+
+ $json = urldecode($meta->content);
+
+ $json = json_decode($json);
+
+ return $json->MEDIA_API->token;
+ }
+
+ private function getAppData($id, $platform, $country, $token)
+ {
+ $uri = $this->makeJsonUrl($id, $platform, $country);
+
+ $headers = [
+ "Authorization: Bearer $token",
+ 'Origin: https://apps.apple.com',
+ ];
+
+ $json = json_decode(getContents($uri, $headers), true);
+
+ return $json['data'][0];
+ }
+
+ /**
+ * Parses the version history from the data received
+ * @return array list of versions with details on each element
+ */
+ private function getVersionHistory($data, $platform)
+ {
+ switch ($platform) {
+ case 'mac':
+ return $data['relationships']['platforms']['data'][0]['attributes']['versionHistory'];
+ default:
+ $os = self::PLATFORM_MAPPING[$platform];
+ return $data['attributes']['platformAttributes'][$os]['versionHistory'];
+ }
+ }
+
+ public function collectData()
+ {
+ $id = $this->getInput('id');
+ $country = $this->getInput('country');
+ $platform = $this->getInput('p');
+
+ switch ($platform) {
+ case 'mac':
+ $data = $this->getDataFromShoebox($id, $platform, $country);
+ break;
+
+ default:
+ $token = $this->getJWTToken($id, $platform, $country);
+ $data = $this->getAppData($id, $platform, $country, $token);
+ }
+
+ $versionHistory = $this->getVersionHistory($data, $platform);
+ $name = $this->name = $data['attributes']['name'];
+ $author = $data['attributes']['artistName'];
+
+ foreach ($versionHistory as $row) {
+ $item = [];
+
+ $item['content'] = nl2br($row['releaseNotes']);
+ $item['title'] = $name . ' - ' . $row['versionDisplay'];
+ $item['timestamp'] = $row['releaseDate'];
+ $item['author'] = $author;
+
+ $item['uri'] = $this->makeHtmlUrl($id, $country);
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/AppleMusicBridge.php b/bridges/AppleMusicBridge.php
index 26efe204..4c3e0e2f 100644
--- a/bridges/AppleMusicBridge.php
+++ b/bridges/AppleMusicBridge.php
@@ -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,
+ ];
+ }
+ }
+ }
}
diff --git a/bridges/ArtStationBridge.php b/bridges/ArtStationBridge.php
index 55bf87a8..5a2be59d 100644
--- a/bridges/ArtStationBridge.php
+++ b/bridges/ArtStationBridge.php
@@ -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'
- )
- )
- );
-
- public function getIcon() {
- return 'https://www.artstation.com/assets/favicon-58653022bc38c1905ac7aa1b10bffa6b.ico';
- }
-
- public function getName() {
- return self::NAME . ': ' . $this->getInput('q');
- }
-
- private function fetchSearch($searchQuery) {
- $data = '{"query":"' . $searchQuery . '","page":1,"per_page":50,"sorting":"date",';
- $data .= '"pro_first":"1","filters":[],"additional_fields":[]}';
-
- $header = array(
- 'Content-Type: application/json',
- 'Accept: application/json'
- );
-
- $opts = array(
- CURLOPT_POST => true,
- CURLOPT_POSTFIELDS => $data,
- CURLOPT_RETURNTRANSFER => true
- );
-
- $jsonSearchURL = self::URI . '/api/v2/search/projects.json';
- $jsonSearchStr = getContents($jsonSearchURL, $header, $opts);
- return json_decode($jsonSearchStr);
- }
-
- private function fetchProject($hashID) {
- $jsonProjectURL = self::URI . '/projects/' . $hashID . '.json';
- $jsonProjectStr = getContents($jsonProjectURL);
- return json_decode($jsonProjectStr);
- }
-
- public function collectData() {
- $searchTerm = $this->getInput('q');
- $jsonQuery = $this->fetchSearch($searchTerm);
-
- foreach($jsonQuery->data as $media) {
- // get detailed info about media item
- $jsonProject = $this->fetchProject($media->hash_id);
-
- // 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);
-
- $item['content'] = '<a href="'
- . $media->url
- . '"><img style="max-width: 100%" src="'
- . $jsonProject->cover_url
- . '"></a><p>'
- . $jsonProject->description
- . '</p>';
-
- $numAssets = count($jsonProject->assets);
-
- if ($numAssets > 1)
- $item['content'] .= '<p><a href="'
- . $media->url
- . '">Project contains '
- . ($numAssets - 1)
- . ' more item(s).</a></p>';
-
- $this->items[] = $item;
-
- if (count($this->items) >= 10)
- break;
- }
- }
+
+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 = [
+ 'Search Query' => [
+ 'q' => [
+ 'name' => 'Search term',
+ 'required' => true,
+ 'exampleValue' => 'bird'
+ ]
+ ]
+ ];
+
+ public function getIcon()
+ {
+ return 'https://www.artstation.com/assets/favicon-58653022bc38c1905ac7aa1b10bffa6b.ico';
+ }
+
+ public function getName()
+ {
+ return self::NAME . ': ' . $this->getInput('q');
+ }
+
+ private function fetchSearch($searchQuery)
+ {
+ $data = '{"query":"' . $searchQuery . '","page":1,"per_page":50,"sorting":"date",';
+ $data .= '"pro_first":"1","filters":[],"additional_fields":[]}';
+
+ $header = [
+ 'Content-Type: application/json',
+ 'Accept: application/json'
+ ];
+
+ $opts = [
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $data,
+ CURLOPT_RETURNTRANSFER => true
+ ];
+
+ $jsonSearchURL = self::URI . '/api/v2/search/projects.json';
+ $jsonSearchStr = getContents($jsonSearchURL, $header, $opts);
+ return json_decode($jsonSearchStr);
+ }
+
+ private function fetchProject($hashID)
+ {
+ $jsonProjectURL = self::URI . '/projects/' . $hashID . '.json';
+ $jsonProjectStr = getContents($jsonProjectURL);
+ return json_decode($jsonProjectStr);
+ }
+
+ public function collectData()
+ {
+ $searchTerm = $this->getInput('q');
+ $jsonQuery = $this->fetchSearch($searchTerm);
+
+ foreach ($jsonQuery->data as $media) {
+ // get detailed info about media item
+ $jsonProject = $this->fetchProject($media->hash_id);
+
+ // 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);
+
+ $item['content'] = '<a href="'
+ . $media->url
+ . '"><img style="max-width: 100%" src="'
+ . $jsonProject->cover_url
+ . '"></a><p>'
+ . $jsonProject->description
+ . '</p>';
+
+ $numAssets = count($jsonProject->assets);
+
+ if ($numAssets > 1) {
+ $item['content'] .= '<p><a href="'
+ . $media->url
+ . '">Project contains '
+ . ($numAssets - 1)
+ . ' more item(s).</a></p>';
+ }
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10) {
+ break;
+ }
+ }
+ }
}
diff --git a/bridges/Arte7Bridge.php b/bridges/Arte7Bridge.php
index 26296104..ae092c0e 100644
--- a/bridges/Arte7Bridge.php
+++ b/bridges/Arte7Bridge.php
@@ -1,160 +1,163 @@
<?php
-class Arte7Bridge extends BridgeAbstract {
- const NAME = 'Arte +7';
- const URI = 'https://www.arte.tv/';
- const MAINTAINER = 'imagoiq';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Returns newest videos from ARTE +7';
+class Arte7Bridge extends BridgeAbstract
+{
+ const NAME = 'Arte +7';
+ const URI = 'https://www.arte.tv/';
+ const MAINTAINER = 'imagoiq';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns newest videos from ARTE +7';
- const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA';
+ const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA';
- const PARAMETERS = array(
- 'global' => [
- 'sort_by' => array(
- 'type' => 'list',
- 'name' => 'Sort by',
- 'required' => false,
- 'defaultValue' => null,
- 'values' => array(
- 'Default' => null,
- 'Video rights start date' => 'videoRightsBegin',
- 'Video rights end date' => 'videoRightsEnd',
- 'Brodcast date' => 'broadcastBegin',
- 'Creation date' => 'creationDate',
- 'Last modified' => 'lastModified',
- 'Number of views' => 'views',
- 'Number of views per period' => 'viewsPeriod',
- 'Available screens' => 'availableScreens',
- 'Episode' => 'episode'
- ),
- ),
- 'sort_direction' => array(
- 'type' => 'list',
- 'name' => 'Sort direction',
- 'required' => false,
- 'defaultValue' => 'DESC',
- 'values' => array(
- 'Ascending' => 'ASC',
- 'Descending' => 'DESC'
- ),
- ),
- 'exclude_trailers' => [
- 'name' => 'Exclude trailers',
- 'type' => 'checkbox',
- 'required' => false,
- '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'
- ),
- ),
- '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(){
- 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;
+ }
- $lang = $this->getInput('lang');
- $sort_by = $this->getInput('sort_by');
- $sort_direction = $this->getInput('sort_direction') == 'ASC' ? '' : '-';
+ $lang = $this->getInput('lang');
+ $sort_by = $this->getInput('sort_by');
+ $sort_direction = $this->getInput('sort_direction') == 'ASC' ? '' : '-';
- $url = 'https://api.arte.tv/api/opa/v3/videos?limit=15&language='
- . $lang
- . ($sort_by != null ? '&sort=' . $sort_direction . $sort_by : '')
- . ($category != null ? '&category.code=' . $category : '')
- . ($collectionId != null ? '&collections.collectionId=' . $collectionId : '');
+ $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 : '');
- $header = array(
- 'Authorization: Bearer ' . self::API_TOKEN
- );
+ $header = [
+ 'Authorization: Bearer ' . self::API_TOKEN
+ ];
- $input = getContents($url, $header);
- $input_json = json_decode($input, true);
+ $input = getContents($url, $header);
+ $input_json = json_decode($input, true);
- foreach($input_json['videos'] as $element) {
- if($this->getInput('exclude_trailers') && $element['platform'] == 'EXTRAIT') {
- continue;
- }
+ foreach ($input_json['videos'] as $element) {
+ if ($this->getInput('exclude_trailers') && $element['platform'] == 'EXTRAIT') {
+ continue;
+ }
- $durationSeconds = $element['durationSeconds'];
+ $durationSeconds = $element['durationSeconds'];
- $item = array();
- $item['uri'] = $element['url'];
- $item['id'] = $element['id'];
+ $item = [];
+ $item['uri'] = $element['url'];
+ $item['id'] = $element['id'];
- $item['timestamp'] = strtotime($element['videoRightsBegin']);
- $item['title'] = $element['title'];
+ $item['timestamp'] = strtotime($element['videoRightsBegin']);
+ $item['title'] = $element['title'];
- if(!empty($element['subtitle']))
- $item['title'] = $element['title'] . ' | ' . $element['subtitle'];
+ if (!empty($element['subtitle'])) {
+ $item['title'] = $element['title'] . ' | ' . $element['subtitle'];
+ }
- $durationMinutes = round((int)$durationSeconds / 60);
- $item['content'] = $element['teaserText']
- . '<br><br>'
- . $durationMinutes
- . 'min<br><a href="'
- . $item['uri']
- . '"><img src="'
- . $element['mainImage']['url']
- . '" /></a>';
+ $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;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/AsahiShimbunAJWBridge.php b/bridges/AsahiShimbunAJWBridge.php
index 2e7c5dc2..226196d8 100644
--- a/bridges/AsahiShimbunAJWBridge.php
+++ b/bridges/AsahiShimbunAJWBridge.php
@@ -1,72 +1,76 @@
<?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_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',
- )
- )
- );
- 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 self::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";
+ }
+ $item['title'] = $element->innertext;
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/AskfmBridge.php b/bridges/AskfmBridge.php
index cf92ed6a..0a326417 100644
--- a/bridges/AskfmBridge.php
+++ b/bridges/AskfmBridge.php
@@ -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();
+ }
}
diff --git a/bridges/AssociatedPressNewsBridge.php b/bridges/AssociatedPressNewsBridge.php
index 80fa98fc..303168a0 100644
--- a/bridges/AssociatedPressNewsBridge.php
+++ b/bridges/AssociatedPressNewsBridge.php
@@ -1,270 +1,278 @@
<?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
-
- private $detectParamRegex = '/^https?:\/\/(?:www\.)?apnews\.com\/(?:[tag|hub]+\/)?([\w-]+)$/';
- private $tagEndpoint = 'https://afs-prod.appspot.com/api/v2/feed/tag?tags=';
- private $feedName = '';
-
- public function detectParameters($url) {
- $params = array();
-
- if(preg_match($this->detectParamRegex, $url, $matches) > 0) {
- $params['topic'] = $matches[1];
- $params['context'] = 'Custom Topic';
- return $params;
- }
-
- return null;
- }
-
- 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();
- }
- }
-
- public function getURI() {
- if (!is_null($this->getInput('topic'))) {
- return self::URI . $this->getInput('topic');
- }
-
- return parent::getURI();
- }
-
- public function getName() {
- if (!empty($this->feedName)) {
- return $this->feedName . ' - Associated Press';
- }
-
- return parent::getName();
- }
-
- private function getTagURI() {
- if (!is_null($this->getInput('topic'))) {
- return $this->tagEndpoint . $this->getInput('topic');
- }
-
- return parent::getURI();
- }
-
- private function collectCardData() {
- $json = getContents($this->getTagURI())
- or returnServerError('Could not request: ' . $this->getTagURI());
-
- $tagContents = json_decode($json, true);
-
- if (empty($tagContents['tagObjs'])) {
- returnClientError('Topic not found: ' . $this->getInput('topic'));
- }
-
- $this->feedName = $tagContents['tagObjs'][0]['name'];
-
- foreach ($tagContents['cards'] as $card) {
- $item = array();
-
- // skip hub peeks & Notifications
- if ($card['cardType'] == 'Hub Peek' || $card['cardType'] == 'Notification') {
- continue;
- }
-
- $storyContent = $card['contents'][0];
-
- switch($storyContent['contentType']) {
- case 'web': // Skip link only content
- continue 2;
-
- case 'video':
- $html = $this->processVideo($storyContent);
-
- $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;
- }
-
- $html = defaultLinkTo($storyContent['storyHTML'], self::URI);
- $html = str_get_html($html);
-
- $this->processMediaPlaceholders($html, $storyContent['id']);
- $this->processHubLinks($html, $storyContent);
- $this->processIframes($html);
- if (!is_null($storyContent['leadPhotoId'])) {
- $item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/'
- . $storyContent['leadPhotoId'] . '/800.jpeg';
- }
- }
+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'
+ ],
+ ]
+ ];
+
+ const CACHE_TIMEOUT = 900; // 15 mins
+
+ private $detectParamRegex = '/^https?:\/\/(?:www\.)?apnews\.com\/(?:[tag|hub]+\/)?([\w-]+)$/';
+ private $tagEndpoint = 'https://afs-prod.appspot.com/api/v2/feed/tag?tags=';
+ private $feedName = '';
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ if (preg_match($this->detectParamRegex, $url, $matches) > 0) {
+ $params['topic'] = $matches[1];
+ $params['context'] = 'Custom Topic';
+ return $params;
+ }
+
+ return null;
+ }
+
+ 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();
+ }
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('topic'))) {
+ return self::URI . $this->getInput('topic');
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if (!empty($this->feedName)) {
+ return $this->feedName . ' - Associated Press';
+ }
+
+ return parent::getName();
+ }
+
+ private function getTagURI()
+ {
+ if (!is_null($this->getInput('topic'))) {
+ return $this->tagEndpoint . $this->getInput('topic');
+ }
+
+ return parent::getURI();
+ }
+
+ private function collectCardData()
+ {
+ $json = getContents($this->getTagURI())
+ or returnServerError('Could not request: ' . $this->getTagURI());
+
+ $tagContents = json_decode($json, true);
+
+ if (empty($tagContents['tagObjs'])) {
+ returnClientError('Topic not found: ' . $this->getInput('topic'));
+ }
+
+ $this->feedName = $tagContents['tagObjs'][0]['name'];
+
+ foreach ($tagContents['cards'] as $card) {
+ $item = [];
+
+ // skip hub peeks & Notifications
+ if ($card['cardType'] == 'Hub Peek' || $card['cardType'] == 'Notification') {
+ continue;
+ }
+
+ $storyContent = $card['contents'][0];
+
+ switch ($storyContent['contentType']) {
+ case 'web': // Skip link only content
+ continue 2;
+
+ case 'video':
+ $html = $this->processVideo($storyContent);
+
+ $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;
+ }
+
+ $html = defaultLinkTo($storyContent['storyHTML'], self::URI);
+ $html = str_get_html($html);
+
+ $this->processMediaPlaceholders($html, $storyContent['id']);
+ $this->processHubLinks($html, $storyContent);
+ $this->processIframes($html);
+
+ if (!is_null($storyContent['leadPhotoId'])) {
+ $item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/'
+ . $storyContent['leadPhotoId'] . '/800.jpeg';
+ }
+ }
+
+ $item['title'] = $card['contents'][0]['headline'];
+ $item['uri'] = self::URI . $card['shortId'];
+
+ if ($card['contents'][0]['localLinkUrl']) {
+ $item['uri'] = $card['contents'][0]['localLinkUrl'];
+ }
+
+ $item['timestamp'] = $storyContent['published'];
+
+ 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['content'] = $html;
- $item['title'] = $card['contents'][0]['headline'];
- $item['uri'] = self::URI . $card['shortId'];
+ foreach ($storyContent['tagObjs'] as $tag) {
+ $item['categories'][] = $tag['name'];
+ }
- if ($card['contents'][0]['localLinkUrl']) {
- $item['uri'] = $card['contents'][0]['localLinkUrl'];
- }
+ $this->items[] = $item;
- $item['timestamp'] = $storyContent['published'];
+ if (count($this->items) >= 15) {
+ break;
+ }
+ }
+ }
- 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'];
- }
- }
+ 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);
- $item['content'] = $html;
+ foreach ($html->find('div.media-placeholder') as $div) {
+ $key = array_search($div->id, $storyContent['mediumIds']);
- foreach ($storyContent['tagObjs'] as $tag) {
- $item['categories'][] = $tag['name'];
- }
+ if (!isset($storyContent['media'][$key])) {
+ continue;
+ }
- $this->items[] = $item;
+ $media = $storyContent['media'][$key];
- if (count($this->items) >= 15) {
- break;
- }
- }
- }
+ if ($media['type'] === 'Photo') {
+ $mediaUrl = $media['gcsBaseUrl'] . $media['imageRenderedSizes'][0] . $media['imageFileExtension'];
+ $mediaCaption = $media['caption'];
- 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']);
-
- if (!isset($storyContent['media'][$key])) {
- continue;
- }
-
- $media = $storyContent['media'][$key];
-
- 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) {
-
- 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
+ }
+ }
+ }
+ }
+
+ /*
+ 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 ($div) {
+ $div->outertext = <<<EOD
<p><a href="{$url}">{$embed['calloutText']} {$embed['displayName']}</a></p>
EOD;
- }
- }
- }
- }
- }
-
- private function processVideo($storyContent) {
- $video = $storyContent['media'][0];
-
- if ($video['type'] === 'YouTube') {
- $url = 'https://www.youtube.com/embed/' . $video['externalId'];
- $html = <<<EOD
+ }
+ }
+ }
+ }
+ }
+
+ private function processVideo($storyContent)
+ {
+ $video = $storyContent['media'][0];
+
+ 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;
- }
-
- // 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 = '';
-
- if ($html->find('script', $index)) {
- $html->find('script', $index)->outertext = '';
- }
- }
- }
- }
+ }
+
+ return $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 = '';
+
+ if ($html->find('script', $index)) {
+ $html->find('script', $index)->outertext = '';
+ }
+ }
+ }
+ }
}
diff --git a/bridges/AstrophysicsDataSystemBridge.php b/bridges/AstrophysicsDataSystemBridge.php
index b287b52b..e971cad4 100644
--- a/bridges/AstrophysicsDataSystemBridge.php
+++ b/bridges/AstrophysicsDataSystemBridge.php
@@ -1,48 +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 = array(
- 'Publications' => array(
- 'query' => array(
- 'name' => 'query',
- 'title' => 'Same format as the search bar on the website',
- 'exampleValue' => 'author:"huchra, john"',
- 'required' => true
- )
- ));
- private $feedTitle;
+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
+ ]
+ ]];
- public function getName() {
- if ($this->queriedContext === 'Publications') {
- return $this->feedTitle;
- }
- return parent::getName();
- }
+ private $feedTitle;
- public function getURI() {
- if ($this->queriedContext === 'Publications') {
- return self::URI . '/search/?q=' . urlencode($this->getInput('query'));
- }
- return parent::getURI();
- }
+ public function getName()
+ {
+ if ($this->queriedContext === 'Publications') {
+ return $this->feedTitle;
+ }
+ return parent::getName();
+ }
- public function collectData() {
- $headers = array (
- '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 = array();
- $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;
- }
- }
+ 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;
+ }
+ }
}
diff --git a/bridges/AtmoNouvelleAquitaineBridge.php b/bridges/AtmoNouvelleAquitaineBridge.php
index f84d1120..d4244fa9 100644
--- a/bridges/AtmoNouvelleAquitaineBridge.php
+++ b/bridges/AtmoNouvelleAquitaineBridge.php
@@ -1,4637 +1,4650 @@
<?php
-class AtmoNouvelleAquitaineBridge extends BridgeAbstract {
- const NAME = 'Atmo Nouvelle Aquitaine';
- const URI = 'https://www.atmo-nouvelleaquitaine.org';
- const DESCRIPTION = 'Fetches the latest air polution of cities in Nouvelle Aquitaine from Atmo';
- const MAINTAINER = 'floviolleau';
- const PARAMETERS = array(array(
- 'cities' => array(
- 'name' => 'Choisir une ville',
- 'type' => 'list',
- 'values' => self::CITIES
- )
- ));
- const CACHE_TIMEOUT = 7200;
+class AtmoNouvelleAquitaineBridge extends BridgeAbstract
+{
+ const NAME = 'Atmo Nouvelle Aquitaine';
+ const URI = 'https://www.atmo-nouvelleaquitaine.org';
+ const DESCRIPTION = 'Fetches the latest air polution of cities in Nouvelle Aquitaine from Atmo';
+ const MAINTAINER = 'floviolleau';
+ const PARAMETERS = [[
+ 'cities' => [
+ 'name' => 'Choisir une ville',
+ 'type' => 'list',
+ 'values' => self::CITIES
+ ]
+ ]];
+ const CACHE_TIMEOUT = 7200;
- private $dom;
+ private $dom;
- private function getClosest($search, $arr) {
- $closest = null;
- foreach ($arr as $key => $value) {
- if ($closest === null || abs((int)$search - $closest) > abs((int)$key - (int)$search)) {
- $closest = (int)$key;
- }
- }
- return $arr[$closest];
- }
+ private function getClosest($search, $arr)
+ {
+ $closest = null;
+ foreach ($arr as $key => $value) {
+ if ($closest === null || abs((int)$search - $closest) > abs((int)$key - (int)$search)) {
+ $closest = (int)$key;
+ }
+ }
+ return $arr[$closest];
+ }
- public function collectData() {
- $uri = self::URI . '/monair/commune/' . $this->getInput('cities');
+ public function collectData()
+ {
+ $uri = self::URI . '/monair/commune/' . $this->getInput('cities');
- $html = getSimpleHTMLDOM($uri);
+ $html = getSimpleHTMLDOM($uri);
- $this->dom = $html->find('#block-system-main .city-prevision-map', 0);
+ $this->dom = $html->find('#block-system-main .city-prevision-map', 0);
- $message = $this->getIndexMessage() . ' ' . $this->getQualityMessage();
- $message .= ' ' . $this->getTomorrowTrendIndexMessage() . ' ' . $this->getTomorrowTrendQualityMessage();
+ $message = $this->getIndexMessage() . ' ' . $this->getQualityMessage();
+ $message .= ' ' . $this->getTomorrowTrendIndexMessage() . ' ' . $this->getTomorrowTrendQualityMessage();
- $item['uri'] = $uri;
- $today = date('d/m/Y');
- $item['title'] = "Bulletin de l'air du $today pour la région Nouvelle Aquitaine.";
- $item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-nouvelleaquitaine.org #QualiteAir.';
- $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 région Nouvelle Aquitaine.";
+ $item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-nouvelleaquitaine.org #QualiteAir.';
+ $item['author'] = 'floviolleau';
+ $item['content'] = $message;
+ $item['uid'] = hash('sha256', $item['title']);
- $this->items[] = $item;
- }
+ $this->items[] = $item;
+ }
- private function getIndex() {
- $index = $this->dom->find('.indice', 0)->innertext;
+ private function getIndex()
+ {
+ $index = $this->dom->find('.indice', 0)->innertext;
- if ($index == 'XX') {
- return -1;
- }
+ if ($index == 'XX') {
+ return -1;
+ }
- return $index;
- }
+ return $index;
+ }
- private function getMaxIndexText() {
- // will return '/100'
- return $this->dom->find('.pourcent', 0)->innertext;
- }
+ private function getMaxIndexText()
+ {
+ // will return '/100'
+ return $this->dom->find('.pourcent', 0)->innertext;
+ }
- private function getQualityText($index, $indexes) {
- if ($index == -1) {
- if (array_key_exists('no-available', $indexes)) {
- return $indexes['no-available'];
- }
+ private function getQualityText($index, $indexes)
+ {
+ if ($index == -1) {
+ if (array_key_exists('no-available', $indexes)) {
+ return $indexes['no-available'];
+ }
- return 'Aucune donnée';
- }
+ return 'Aucune donnée';
+ }
- return $this->getClosest($index, $indexes);
- }
+ return $this->getClosest($index, $indexes);
+ }
- private function getLegendIndexes() {
- $rawIndexes = $this->dom->find('.prevision-legend .prevision-legend-label');
- $indexes = array();
- for ($i = 0; $i < count($rawIndexes); $i++) {
- if ($rawIndexes[$i]->hasAttribute('data-color')) {
- $indexes[$rawIndexes[$i]->getAttribute('data-color')] = $rawIndexes[$i]->innertext;
- }
- }
+ private function getLegendIndexes()
+ {
+ $rawIndexes = $this->dom->find('.prevision-legend .prevision-legend-label');
+ $indexes = [];
+ for ($i = 0; $i < count($rawIndexes); $i++) {
+ if ($rawIndexes[$i]->hasAttribute('data-color')) {
+ $indexes[$rawIndexes[$i]->getAttribute('data-color')] = $rawIndexes[$i]->innertext;
+ }
+ }
- return $indexes;
- }
+ return $indexes;
+ }
- private function getTomorrowTrendIndex() {
- $tomorrowTrendDomNode = $this->dom
- ->find('.day-controls.raster-controls .list-raster-controls .raster-control', 2);
- $tomorrowTrendIndexNode = null;
+ private function getTomorrowTrendIndex()
+ {
+ $tomorrowTrendDomNode = $this->dom
+ ->find('.day-controls.raster-controls .list-raster-controls .raster-control', 2);
+ $tomorrowTrendIndexNode = null;
- if ($tomorrowTrendDomNode) {
- $tomorrowTrendIndexNode = $tomorrowTrendDomNode->find('.raster-control-link', 0);
- }
+ if ($tomorrowTrendDomNode) {
+ $tomorrowTrendIndexNode = $tomorrowTrendDomNode->find('.raster-control-link', 0);
+ }
- if ($tomorrowTrendIndexNode && $tomorrowTrendIndexNode->hasAttribute('data-index')) {
- $tomorrowTrendIndex = $tomorrowTrendIndexNode->getAttribute('data-index');
- } else {
- return -1;
- }
+ if ($tomorrowTrendIndexNode && $tomorrowTrendIndexNode->hasAttribute('data-index')) {
+ $tomorrowTrendIndex = $tomorrowTrendIndexNode->getAttribute('data-index');
+ } else {
+ return -1;
+ }
- return $tomorrowTrendIndex;
- }
+ return $tomorrowTrendIndex;
+ }
- private function getTomorrowTrendQualityText($trendIndex, $indexes) {
- if ($trendIndex == -1) {
- if (array_key_exists('no-available', $indexes)) {
- return $indexes['no-available'];
- }
+ private function getTomorrowTrendQualityText($trendIndex, $indexes)
+ {
+ if ($trendIndex == -1) {
+ if (array_key_exists('no-available', $indexes)) {
+ return $indexes['no-available'];
+ }
- return 'Aucune donnée';
- }
+ return 'Aucune donnée';
+ }
- return $this->getClosest($trendIndex, $indexes);
- }
+ return $this->getClosest($trendIndex, $indexes);
+ }
- private function getIndexMessage() {
- $index = $this->getIndex();
- $maxIndexText = $this->getMaxIndexText();
+ private function getIndexMessage()
+ {
+ $index = $this->getIndex();
+ $maxIndexText = $this->getMaxIndexText();
- if ($index == -1) {
- return 'Aucune donnée pour l\'indice.';
- }
+ if ($index == -1) {
+ return 'Aucune donnée pour l\'indice.';
+ }
- return "L'indice d'aujourd'hui est $index$maxIndexText.";
- }
+ return "L'indice d'aujourd'hui est $index$maxIndexText.";
+ }
- private function getQualityMessage() {
- $index = $index = $this->getIndex();
- $indexes = $this->getLegendIndexes();
- $quality = $this->getQualityText($index, $indexes);
+ private function getQualityMessage()
+ {
+ $index = $index = $this->getIndex();
+ $indexes = $this->getLegendIndexes();
+ $quality = $this->getQualityText($index, $indexes);
- if ($index == -1) {
- return 'Aucune donnée pour la qualité de l\'air.';
- }
+ if ($index == -1) {
+ return 'Aucune donnée pour la qualité de l\'air.';
+ }
- return "La qualité de l'air est $quality.";
- }
+ return "La qualité de l'air est $quality.";
+ }
- private function getTomorrowTrendIndexMessage() {
- $trendIndex = $this->getTomorrowTrendIndex();
- $maxIndexText = $this->getMaxIndexText();
+ private function getTomorrowTrendIndexMessage()
+ {
+ $trendIndex = $this->getTomorrowTrendIndex();
+ $maxIndexText = $this->getMaxIndexText();
- if ($trendIndex == -1) {
- return 'Aucune donnée pour l\'indice prévu demain.';
- }
+ if ($trendIndex == -1) {
+ return 'Aucune donnée pour l\'indice prévu demain.';
+ }
- return "L'indice prévu pour demain est $trendIndex$maxIndexText.";
- }
+ return "L'indice prévu pour demain est $trendIndex$maxIndexText.";
+ }
- private function getTomorrowTrendQualityMessage() {
- $trendIndex = $this->getTomorrowTrendIndex();
- $indexes = $this->getLegendIndexes();
- $trendQuality = $this->getTomorrowTrendQualityText($trendIndex, $indexes);
+ private function getTomorrowTrendQualityMessage()
+ {
+ $trendIndex = $this->getTomorrowTrendIndex();
+ $indexes = $this->getLegendIndexes();
+ $trendQuality = $this->getTomorrowTrendQualityText($trendIndex, $indexes);
- if ($trendIndex == -1) {
- return 'Aucune donnée pour la qualité de l\'air de demain.';
- }
- return "La qualite de l'air pour demain sera $trendQuality.";
- }
+ if ($trendIndex == -1) {
+ return 'Aucune donnée pour la qualité de l\'air de demain.';
+ }
+ return "La qualite de l'air pour demain sera $trendQuality.";
+ }
- const CITIES = array(
- 'Aast (64460)' => '64001',
- 'Abère (64160)' => '64002',
- 'Abidos (64150)' => '64003',
- 'Abitain (64390)' => '64004',
- 'Abjat-sur-Bandiat (24300)' => '24001',
- 'Abos (64360)' => '64005',
- 'Abzac (16500)' => '16001',
- 'Abzac (33230)' => '33001',
- 'Accous (64490)' => '64006',
- 'Adilly (79200)' => '79002',
- 'Adriers (86430)' => '86001',
- 'Affieux (19260)' => '19001',
- 'Agen (47000)' => '47001',
- 'Agmé (47350)' => '47002',
- 'Agnac (47800)' => '47003',
- 'Agnos (64400)' => '64007',
- 'Agonac (24460)' => '24002',
- 'Agris (16110)' => '16003',
- 'Agudelle (17500)' => '17002',
- 'Ahaxe-Alciette-Bascassan (64220)' => '64008',
- 'Ahetze (64210)' => '64009',
- 'Ahun (23150)' => '23001',
- 'Aïcirits-Camou-Suhast (64120)' => '64010',
- 'Aiffres (79230)' => '79003',
- 'Aignes-et-Puypéroux (16190)' => '16004',
- 'Aigonnay (79370)' => '79004',
- 'Aigre (16140)' => '16005',
- 'Aigrefeuille-d\'Aunis (17290)' => '17003',
- 'Aiguillon (47190)' => '47004',
- 'Aillas (33124)' => '33002',
- 'Aincille (64220)' => '64011',
- 'Ainharp (64130)' => '64012',
- 'Ainhice-Mongelos (64220)' => '64013',
- 'Ainhoa (64250)' => '64014',
- 'Aire-sur-l\'Adour (40800)' => '40001',
- 'Airvault (79600)' => '79005',
- 'Aix (19200)' => '19002',
- 'Aixe-sur-Vienne (87700)' => '87001',
- 'Ajain (23380)' => '23002',
- 'Ajat (24210)' => '24004',
- 'Albignac (19190)' => '19003',
- 'Albussac (19380)' => '19004',
- 'Alçay-Alçabéhéty-Sunharette (64470)' => '64015',
- 'Aldudes (64430)' => '64016',
- 'Allas-Bocage (17150)' => '17005',
- 'Allas-Champagne (17500)' => '17006',
- 'Allas-les-Mines (24220)' => '24006',
- 'Allassac (19240)' => '19005',
- 'Allemans (24600)' => '24007',
- 'Allemans-du-Dropt (47800)' => '47005',
- 'Alles-sur-Dordogne (24480)' => '24005',
- 'Alleyrat (19200)' => '19006',
- 'Alleyrat (23200)' => '23003',
- 'Allez-et-Cazeneuve (47110)' => '47006',
- 'Allonne (79130)' => '79007',
- 'Allons (47420)' => '47007',
- 'Alloue (16490)' => '16007',
- 'Alos-Sibas-Abense (64470)' => '64017',
- 'Altillac (19120)' => '19007',
- 'Amailloux (79350)' => '79008',
- 'Ambarès-et-Lagrave (33440)' => '33003',
- 'Ambazac (87240)' => '87002',
- 'Ambérac (16140)' => '16008',
- 'Ambernac (16490)' => '16009',
- 'Amberre (86110)' => '86002',
- 'Ambès (33810)' => '33004',
- 'Ambleville (16300)' => '16010',
- 'Ambrugeat (19250)' => '19008',
- 'Ambrus (47160)' => '47008',
- 'Amendeuix-Oneix (64120)' => '64018',
- 'Amorots-Succos (64120)' => '64019',
- 'Amou (40330)' => '40002',
- 'Amuré (79210)' => '79009',
- 'Anais (16560)' => '16011',
- 'Anais (17540)' => '17007',
- 'Ance (64570)' => '64020',
- 'Anché (86700)' => '86003',
- 'Andernos-les-Bains (33510)' => '33005',
- 'Andilly (17230)' => '17008',
- 'Andiran (47170)' => '47009',
- 'Andoins (64420)' => '64021',
- 'Andrein (64390)' => '64022',
- 'Angaïs (64510)' => '64023',
- 'Angeac-Champagne (16130)' => '16012',
- 'Angeac-Charente (16120)' => '16013',
- 'Angeduc (16300)' => '16014',
- 'Anglade (33390)' => '33006',
- 'Angles-sur-l\'Anglin (86260)' => '86004',
- 'Anglet (64600)' => '64024',
- 'Angliers (17540)' => '17009',
- 'Angliers (86330)' => '86005',
- 'Angoisse (24270)' => '24008',
- 'Angoulême (16000)' => '16015',
- 'Angoulins (17690)' => '17010',
- 'Angoumé (40990)' => '40003',
- 'Angous (64190)' => '64025',
- 'Angresse (40150)' => '40004',
- 'Anhaux (64220)' => '64026',
- 'Anlhiac (24160)' => '24009',
- 'Annepont (17350)' => '17011',
- 'Annesse-et-Beaulieu (24430)' => '24010',
- 'Annezay (17380)' => '17012',
- 'Anos (64160)' => '64027',
- 'Anoye (64350)' => '64028',
- 'Ansac-sur-Vienne (16500)' => '16016',
- 'Antagnac (47700)' => '47010',
- 'Antezant-la-Chapelle (17400)' => '17013',
- 'Anthé (47370)' => '47011',
- 'Antigny (86310)' => '86006',
- 'Antonne-et-Trigonant (24420)' => '24011',
- 'Antran (86100)' => '86007',
- 'Anville (16170)' => '16017',
- 'Anzême (23000)' => '23004',
- 'Anzex (47700)' => '47012',
- 'Aramits (64570)' => '64029',
- 'Arancou (64270)' => '64031',
- 'Araujuzon (64190)' => '64032',
- 'Araux (64190)' => '64033',
- 'Arbanats (33640)' => '33007',
- 'Arbérats-Sillègue (64120)' => '64034',
- 'Arbis (33760)' => '33008',
- 'Arbonne (64210)' => '64035',
- 'Arboucave (40320)' => '40005',
- 'Arbouet-Sussaute (64120)' => '64036',
- 'Arbus (64230)' => '64037',
- 'Arcachon (33120)' => '33009',
- 'Arçais (79210)' => '79010',
- 'Arcangues (64200)' => '64038',
- 'Arçay (86200)' => '86008',
- 'Arces (17120)' => '17015',
- 'Archiac (17520)' => '17016',
- 'Archignac (24590)' => '24012',
- 'Archigny (86210)' => '86009',
- 'Archingeay (17380)' => '17017',
- 'Arcins (33460)' => '33010',
- 'Ardilleux (79110)' => '79011',
- 'Ardillières (17290)' => '17018',
- 'Ardin (79160)' => '79012',
- 'Aren (64400)' => '64039',
- 'Arengosse (40110)' => '40006',
- 'Arès (33740)' => '33011',
- 'Aressy (64320)' => '64041',
- 'Arette (64570)' => '64040',
- 'Arfeuille-Châtain (23700)' => '23005',
- 'Argagnon (64300)' => '64042',
- 'Argelos (40700)' => '40007',
- 'Argelos (64450)' => '64043',
- 'Argelouse (40430)' => '40008',
- 'Argentat (19400)' => '19010',
- 'Argenton (47250)' => '47013',
- 'Argenton-l\'Église (79290)' => '79014',
- 'Argentonnay (79150)' => '79013',
- 'Arget (64410)' => '64044',
- 'Arhansus (64120)' => '64045',
- 'Arjuzanx (40110)' => '40009',
- 'Armendarits (64640)' => '64046',
- 'Armillac (47800)' => '47014',
- 'Arnac-la-Poste (87160)' => '87003',
- 'Arnac-Pompadour (19230)' => '19011',
- 'Arnéguy (64220)' => '64047',
- 'Arnos (64370)' => '64048',
- 'Aroue-Ithorots-Olhaïby (64120)' => '64049',
- 'Arrast-Larrebieu (64130)' => '64050',
- 'Arraute-Charritte (64120)' => '64051',
- 'Arrènes (23210)' => '23006',
- 'Arricau-Bordes (64350)' => '64052',
- 'Arrien (64420)' => '64053',
- 'Arros-de-Nay (64800)' => '64054',
- 'Arrosès (64350)' => '64056',
- 'Ars (16130)' => '16018',
- 'Ars (23480)' => '23007',
- 'Ars-en-Ré (17590)' => '17019',
- 'Arsac (33460)' => '33012',
- 'Arsague (40330)' => '40011',
- 'Artassenx (40090)' => '40012',
- 'Arthenac (17520)' => '17020',
- 'Arthez-d\'Armagnac (40190)' => '40013',
- 'Arthez-d\'Asson (64800)' => '64058',
- 'Arthez-de-Béarn (64370)' => '64057',
- 'Artigueloutan (64420)' => '64059',
- 'Artiguelouve (64230)' => '64060',
- 'Artigues-près-Bordeaux (33370)' => '33013',
- 'Artix (64170)' => '64061',
- 'Arudy (64260)' => '64062',
- 'Arue (40120)' => '40014',
- 'Arvert (17530)' => '17021',
- 'Arveyres (33500)' => '33015',
- 'Arx (40310)' => '40015',
- 'Arzacq-Arraziguet (64410)' => '64063',
- 'Asasp-Arros (64660)' => '64064',
- 'Ascain (64310)' => '64065',
- 'Ascarat (64220)' => '64066',
- 'Aslonnes (86340)' => '86010',
- 'Asnières-en-Poitou (79170)' => '79015',
- 'Asnières-la-Giraud (17400)' => '17022',
- 'Asnières-sur-Blour (86430)' => '86011',
- 'Asnières-sur-Nouère (16290)' => '16019',
- 'Asnois (86250)' => '86012',
- 'Asques (33240)' => '33016',
- 'Assais-les-Jumeaux (79600)' => '79016',
- 'Assat (64510)' => '64067',
- 'Asson (64800)' => '64068',
- 'Astaffort (47220)' => '47015',
- 'Astaillac (19120)' => '19012',
- 'Aste-Béon (64260)' => '64069',
- 'Astis (64450)' => '64070',
- 'Athos-Aspis (64390)' => '64071',
- 'Aubagnan (40700)' => '40016',
- 'Aubas (24290)' => '24014',
- 'Aubazines (19190)' => '19013',
- 'Aubertin (64290)' => '64072',
- 'Aubeterre-sur-Dronne (16390)' => '16020',
- 'Aubiac (33430)' => '33017',
- 'Aubiac (47310)' => '47016',
- 'Aubigné (79110)' => '79018',
- 'Aubigny (79390)' => '79019',
- 'Aubin (64230)' => '64073',
- 'Aubous (64330)' => '64074',
- 'Aubusson (23200)' => '23008',
- 'Audaux (64190)' => '64075',
- 'Audenge (33980)' => '33019',
- 'Audignon (40500)' => '40017',
- 'Audon (40400)' => '40018',
- 'Audrix (24260)' => '24015',
- 'Auga (64450)' => '64077',
- 'Auge (23170)' => '23009',
- 'Augé (79400)' => '79020',
- 'Auge-Saint-Médard (16170)' => '16339',
- 'Augères (23210)' => '23010',
- 'Augignac (24300)' => '24016',
- 'Augne (87120)' => '87004',
- 'Aujac (17770)' => '17023',
- 'Aulnay (17470)' => '17024',
- 'Aulnay (86330)' => '86013',
- 'Aulon (23210)' => '23011',
- 'Aumagne (17770)' => '17025',
- 'Aunac (16460)' => '16023',
- 'Auradou (47140)' => '47017',
- 'Aureil (87220)' => '87005',
- 'Aureilhan (40200)' => '40019',
- 'Auriac (19220)' => '19014',
- 'Auriac (64450)' => '64078',
- 'Auriac-du-Périgord (24290)' => '24018',
- 'Auriac-sur-Dropt (47120)' => '47018',
- 'Auriat (23400)' => '23012',
- 'Aurice (40500)' => '40020',
- 'Auriolles (33790)' => '33020',
- 'Aurions-Idernes (64350)' => '64079',
- 'Auros (33124)' => '33021',
- 'Aussac-Vadalle (16560)' => '16024',
- 'Aussevielle (64230)' => '64080',
- 'Aussurucq (64130)' => '64081',
- 'Auterrive (64270)' => '64082',
- 'Autevielle-Saint-Martin-Bideren (64390)' => '64083',
- 'Authon-Ébéon (17770)' => '17026',
- 'Auzances (23700)' => '23013',
- 'Availles-en-Châtellerault (86530)' => '86014',
- 'Availles-Limouzine (86460)' => '86015',
- 'Availles-Thouarsais (79600)' => '79022',
- 'Avanton (86170)' => '86016',
- 'Avensan (33480)' => '33022',
- 'Avon (79800)' => '79023',
- 'Avy (17800)' => '17027',
- 'Aydie (64330)' => '64084',
- 'Aydius (64490)' => '64085',
- 'Ayen (19310)' => '19015',
- 'Ayguemorte-les-Graves (33640)' => '33023',
- 'Ayherre (64240)' => '64086',
- 'Ayron (86190)' => '86017',
- 'Aytré (17440)' => '17028',
- 'Azat-Châtenet (23210)' => '23014',
- 'Azat-le-Ris (87360)' => '87006',
- 'Azay-le-Brûlé (79400)' => '79024',
- 'Azay-sur-Thouet (79130)' => '79025',
- 'Azerables (23160)' => '23015',
- 'Azerat (24210)' => '24019',
- 'Azur (40140)' => '40021',
- 'Badefols-d\'Ans (24390)' => '24021',
- 'Badefols-sur-Dordogne (24150)' => '24022',
- 'Bagas (33190)' => '33024',
- 'Bagnizeau (17160)' => '17029',
- 'Bahus-Soubiran (40320)' => '40022',
- 'Baigneaux (33760)' => '33025',
- 'Baignes-Sainte-Radegonde (16360)' => '16025',
- 'Baigts (40380)' => '40023',
- 'Baigts-de-Béarn (64300)' => '64087',
- 'Bajamont (47480)' => '47019',
- 'Balansun (64300)' => '64088',
- 'Balanzac (17600)' => '17030',
- 'Baleix (64460)' => '64089',
- 'Baleyssagues (47120)' => '47020',
- 'Baliracq-Maumusson (64330)' => '64090',
- 'Baliros (64510)' => '64091',
- 'Balizac (33730)' => '33026',
- 'Ballans (17160)' => '17031',
- 'Balledent (87290)' => '87007',
- 'Ballon (17290)' => '17032',
- 'Balzac (16430)' => '16026',
- 'Banca (64430)' => '64092',
- 'Baneuil (24150)' => '24023',
- 'Banize (23120)' => '23016',
- 'Banos (40500)' => '40024',
- 'Bar (19800)' => '19016',
- 'Barbaste (47230)' => '47021',
- 'Barbezières (16140)' => '16027',
- 'Barbezieux-Saint-Hilaire (16300)' => '16028',
- 'Barcus (64130)' => '64093',
- 'Bardenac (16210)' => '16029',
- 'Bardos (64520)' => '64094',
- 'Bardou (24560)' => '24024',
- 'Barie (33190)' => '33027',
- 'Barinque (64160)' => '64095',
- 'Baron (33750)' => '33028',
- 'Barraute-Camu (64390)' => '64096',
- 'Barret (16300)' => '16030',
- 'Barro (16700)' => '16031',
- 'Bars (24210)' => '24025',
- 'Barsac (33720)' => '33030',
- 'Barzan (17120)' => '17034',
- 'Barzun (64530)' => '64097',
- 'Bas-Mauco (40500)' => '40026',
- 'Bascons (40090)' => '40025',
- 'Bassac (16120)' => '16032',
- 'Bassanne (33190)' => '33031',
- 'Bassens (33530)' => '33032',
- 'Bassercles (40700)' => '40027',
- 'Basses (86200)' => '86018',
- 'Bassignac-le-Bas (19430)' => '19017',
- 'Bassignac-le-Haut (19220)' => '19018',
- 'Bassillac (24330)' => '24026',
- 'Bassillon-Vauzé (64350)' => '64098',
- 'Bassussarry (64200)' => '64100',
- 'Bastanès (64190)' => '64099',
- 'Bastennes (40360)' => '40028',
- 'Basville (23260)' => '23017',
- 'Bats (40320)' => '40029',
- 'Baudignan (40310)' => '40030',
- 'Baudreix (64800)' => '64101',
- 'Baurech (33880)' => '33033',
- 'Bayac (24150)' => '24027',
- 'Bayas (33230)' => '33034',
- 'Bayers (16460)' => '16033',
- 'Bayon-sur-Gironde (33710)' => '33035',
- 'Bayonne (64100)' => '64102',
- 'Bazac (16210)' => '16034',
- 'Bazas (33430)' => '33036',
- 'Bazauges (17490)' => '17035',
- 'Bazelat (23160)' => '23018',
- 'Bazens (47130)' => '47022',
- 'Beaugas (47290)' => '47023',
- 'Beaugeay (17620)' => '17036',
- 'Beaulieu-sous-Parthenay (79420)' => '79029',
- 'Beaulieu-sur-Dordogne (19120)' => '19019',
- 'Beaulieu-sur-Sonnette (16450)' => '16035',
- 'Beaumont (19390)' => '19020',
- 'Beaumont (86490)' => '86019',
- 'Beaumont-du-Lac (87120)' => '87009',
- 'Beaumontois en Périgord (24440)' => '24028',
- 'Beaupouyet (24400)' => '24029',
- 'Beaupuy (47200)' => '47024',
- 'Beauregard-de-Terrasson (24120)' => '24030',
- 'Beauregard-et-Bassac (24140)' => '24031',
- 'Beauronne (24400)' => '24032',
- 'Beaussac (24340)' => '24033',
- 'Beaussais-Vitré (79370)' => '79030',
- 'Beautiran (33640)' => '33037',
- 'Beauvais-sur-Matha (17490)' => '17037',
- 'Beauville (47470)' => '47025',
- 'Beauvoir-sur-Niort (79360)' => '79031',
- 'Beauziac (47700)' => '47026',
- 'Béceleuf (79160)' => '79032',
- 'Bécheresse (16250)' => '16036',
- 'Bédeille (64460)' => '64103',
- 'Bedenac (17210)' => '17038',
- 'Bedous (64490)' => '64104',
- 'Bégaar (40400)' => '40031',
- 'Bégadan (33340)' => '33038',
- 'Bègles (33130)' => '33039',
- 'Béguey (33410)' => '33040',
- 'Béguios (64120)' => '64105',
- 'Béhasque-Lapiste (64120)' => '64106',
- 'Béhorléguy (64220)' => '64107',
- 'Beissat (23260)' => '23019',
- 'Beleymas (24140)' => '24034',
- 'Belhade (40410)' => '40032',
- 'Belin-Béliet (33830)' => '33042',
- 'Bélis (40120)' => '40033',
- 'Bellac (87300)' => '87011',
- 'Bellebat (33760)' => '33043',
- 'Bellechassagne (19290)' => '19021',
- 'Bellefond (33760)' => '33044',
- 'Bellefonds (86210)' => '86020',
- 'Bellegarde-en-Marche (23190)' => '23020',
- 'Belleville (79360)' => '79033',
- 'Bellocq (64270)' => '64108',
- 'Bellon (16210)' => '16037',
- 'Belluire (17800)' => '17039',
- 'Bélus (40300)' => '40034',
- 'Belvès-de-Castillon (33350)' => '33045',
- 'Benassay (86470)' => '86021',
- 'Benayes (19510)' => '19022',
- 'Bénéjacq (64800)' => '64109',
- 'Bénesse-lès-Dax (40180)' => '40035',
- 'Bénesse-Maremne (40230)' => '40036',
- 'Benest (16350)' => '16038',
- 'Bénévent-l\'Abbaye (23210)' => '23021',
- 'Benon (17170)' => '17041',
- 'Benquet (40280)' => '40037',
- 'Bentayou-Sérée (64460)' => '64111',
- 'Béost (64440)' => '64110',
- 'Berbiguières (24220)' => '24036',
- 'Bercloux (17770)' => '17042',
- 'Bérenx (64300)' => '64112',
- 'Bergerac (24100)' => '24037',
- 'Bergouey (40250)' => '40038',
- 'Bergouey-Viellenave (64270)' => '64113',
- 'Bernac (16700)' => '16039',
- 'Bernadets (64160)' => '64114',
- 'Bernay-Saint-Martin (17330)' => '17043',
- 'Berneuil (16480)' => '16040',
- 'Berneuil (17460)' => '17044',
- 'Berneuil (87300)' => '87012',
- 'Bernos-Beaulac (33430)' => '33046',
- 'Berrie (86120)' => '86022',
- 'Berrogain-Laruns (64130)' => '64115',
- 'Bersac-sur-Rivalier (87370)' => '87013',
- 'Berson (33390)' => '33047',
- 'Berthegon (86420)' => '86023',
- 'Berthez (33124)' => '33048',
- 'Bertric-Burée (24320)' => '24038',
- 'Béruges (86190)' => '86024',
- 'Bescat (64260)' => '64116',
- 'Bésingrand (64150)' => '64117',
- 'Bessac (16250)' => '16041',
- 'Bessé (16140)' => '16042',
- 'Besse (24550)' => '24039',
- 'Bessines (79000)' => '79034',
- 'Bessines-sur-Gartempe (87250)' => '87014',
- 'Betbezer-d\'Armagnac (40240)' => '40039',
- 'Bétête (23270)' => '23022',
- 'Béthines (86310)' => '86025',
- 'Bétracq (64350)' => '64118',
- 'Beurlay (17250)' => '17045',
- 'Beuste (64800)' => '64119',
- 'Beuxes (86120)' => '86026',
- 'Beychac-et-Caillau (33750)' => '33049',
- 'Beylongue (40370)' => '40040',
- 'Beynac (87700)' => '87015',
- 'Beynac-et-Cazenac (24220)' => '24040',
- 'Beynat (19190)' => '19023',
- 'Beyrie-en-Béarn (64230)' => '64121',
- 'Beyrie-sur-Joyeuse (64120)' => '64120',
- 'Beyries (40700)' => '40041',
- 'Beyssac (19230)' => '19024',
- 'Beyssenac (19230)' => '19025',
- 'Bézenac (24220)' => '24041',
- 'Biard (86580)' => '86027',
- 'Biarritz (64200)' => '64122',
- 'Biarrotte (40390)' => '40042',
- 'Bias (40170)' => '40043',
- 'Bias (47300)' => '47027',
- 'Biaudos (40390)' => '40044',
- 'Bidache (64520)' => '64123',
- 'Bidarray (64780)' => '64124',
- 'Bidart (64210)' => '64125',
- 'Bidos (64400)' => '64126',
- 'Bielle (64260)' => '64127',
- 'Bieujac (33210)' => '33050',
- 'Biganos (33380)' => '33051',
- 'Bignay (17400)' => '17046',
- 'Bignoux (86800)' => '86028',
- 'Bilhac (19120)' => '19026',
- 'Bilhères (64260)' => '64128',
- 'Billère (64140)' => '64129',
- 'Bioussac (16700)' => '16044',
- 'Birac (16120)' => '16045',
- 'Birac (33430)' => '33053',
- 'Birac-sur-Trec (47200)' => '47028',
- 'Biras (24310)' => '24042',
- 'Biriatou (64700)' => '64130',
- 'Biron (17800)' => '17047',
- 'Biron (24540)' => '24043',
- 'Biron (64300)' => '64131',
- 'Biscarrosse (40600)' => '40046',
- 'Bizanos (64320)' => '64132',
- 'Blaignac (33190)' => '33054',
- 'Blaignan (33340)' => '33055',
- 'Blanquefort (33290)' => '33056',
- 'Blanquefort-sur-Briolance (47500)' => '47029',
- 'Blanzac (87300)' => '87017',
- 'Blanzac-lès-Matha (17160)' => '17048',
- 'Blanzac-Porcheresse (16250)' => '16046',
- 'Blanzaguet-Saint-Cybard (16320)' => '16047',
- 'Blanzay (86400)' => '86029',
- 'Blanzay-sur-Boutonne (17470)' => '17049',
- 'Blasimon (33540)' => '33057',
- 'Blaslay (86170)' => '86030',
- 'Blaudeix (23140)' => '23023',
- 'Blaye (33390)' => '33058',
- 'Blaymont (47470)' => '47030',
- 'Blésignac (33670)' => '33059',
- 'Blessac (23200)' => '23024',
- 'Blis-et-Born (24330)' => '24044',
- 'Blond (87300)' => '87018',
- 'Boé (47550)' => '47031',
- 'Boeil-Bezing (64510)' => '64133',
- 'Bois (17240)' => '17050',
- 'Boisbreteau (16480)' => '16048',
- 'Boismé (79300)' => '79038',
- 'Boisné-La Tude (16320)' => '16082',
- 'Boisredon (17150)' => '17052',
- 'Boisse (24560)' => '24045',
- 'Boisserolles (79360)' => '79039',
- 'Boisseuil (87220)' => '87019',
- 'Boisseuilh (24390)' => '24046',
- 'Bommes (33210)' => '33060',
- 'Bon-Encontre (47240)' => '47032',
- 'Bonloc (64240)' => '64134',
- 'Bonnac-la-Côte (87270)' => '87020',
- 'Bonnat (23220)' => '23025',
- 'Bonnefond (19170)' => '19027',
- 'Bonnegarde (40330)' => '40047',
- 'Bonnes (16390)' => '16049',
- 'Bonnes (86300)' => '86031',
- 'Bonnetan (33370)' => '33061',
- 'Bonneuil (16120)' => '16050',
- 'Bonneuil-Matours (86210)' => '86032',
- 'Bonneville (16170)' => '16051',
- 'Bonneville-et-Saint-Avit-de-Fumadières (24230)' => '24048',
- 'Bonnut (64300)' => '64135',
- 'Bonzac (33910)' => '33062',
- 'Boos (40370)' => '40048',
- 'Borce (64490)' => '64136',
- 'Bord-Saint-Georges (23230)' => '23026',
- 'Bordeaux (33000)' => '33063',
- 'Bordères (64800)' => '64137',
- 'Bordères-et-Lamensans (40270)' => '40049',
- 'Bordes (64510)' => '64138',
- 'Bords (17430)' => '17053',
- 'Boresse-et-Martron (17270)' => '17054',
- 'Borrèze (24590)' => '24050',
- 'Bors (Canton de Baignes-Sainte-Radegonde) (16360)' => '16053',
- 'Bors (Canton de Montmoreau-Saint-Cybard) (16190)' => '16052',
- 'Bort-les-Orgues (19110)' => '19028',
- 'Boscamnant (17360)' => '17055',
- 'Bosdarros (64290)' => '64139',
- 'Bosmie-l\'Aiguille (87110)' => '87021',
- 'Bosmoreau-les-Mines (23400)' => '23027',
- 'Bosroger (23200)' => '23028',
- 'Bosset (24130)' => '24051',
- 'Bossugan (33350)' => '33064',
- 'Bostens (40090)' => '40050',
- 'Boucau (64340)' => '64140',
- 'Boudy-de-Beauregard (47290)' => '47033',
- 'Boueilh-Boueilho-Lasque (64330)' => '64141',
- 'Bouëx (16410)' => '16055',
- 'Bougarber (64230)' => '64142',
- 'Bouglon (47250)' => '47034',
- 'Bougneau (17800)' => '17056',
- 'Bougon (79800)' => '79042',
- 'Bougue (40090)' => '40051',
- 'Bouhet (17540)' => '17057',
- 'Bouillac (24480)' => '24052',
- 'Bouillé-Loretz (79290)' => '79043',
- 'Bouillé-Saint-Paul (79290)' => '79044',
- 'Bouillon (64410)' => '64143',
- 'Bouin (79110)' => '79045',
- 'Boulazac Isle Manoire (24750)' => '24053',
- 'Bouliac (33270)' => '33065',
- 'Boumourt (64370)' => '64144',
- 'Bouniagues (24560)' => '24054',
- 'Bourcefranc-le-Chapus (17560)' => '17058',
- 'Bourdalat (40190)' => '40052',
- 'Bourdeilles (24310)' => '24055',
- 'Bourdelles (33190)' => '33066',
- 'Bourdettes (64800)' => '64145',
- 'Bouresse (86410)' => '86034',
- 'Bourg (33710)' => '33067',
- 'Bourg-Archambault (86390)' => '86035',
- 'Bourg-Charente (16200)' => '16056',
- 'Bourg-des-Maisons (24320)' => '24057',
- 'Bourg-du-Bost (24600)' => '24058',
- 'Bourganeuf (23400)' => '23030',
- 'Bourgnac (24400)' => '24059',
- 'Bourgneuf (17220)' => '17059',
- 'Bourgougnague (47410)' => '47035',
- 'Bourideys (33113)' => '33068',
- 'Bourlens (47370)' => '47036',
- 'Bournand (86120)' => '86036',
- 'Bournel (47210)' => '47037',
- 'Bourniquel (24150)' => '24060',
- 'Bournos (64450)' => '64146',
- 'Bourran (47320)' => '47038',
- 'Bourriot-Bergonce (40120)' => '40053',
- 'Bourrou (24110)' => '24061',
- 'Boussac (23600)' => '23031',
- 'Boussac-Bourg (23600)' => '23032',
- 'Boussais (79600)' => '79047',
- 'Boussès (47420)' => '47039',
- 'Bouteilles-Saint-Sébastien (24320)' => '24062',
- 'Boutenac-Touvent (17120)' => '17060',
- 'Bouteville (16120)' => '16057',
- 'Boutiers-Saint-Trojan (16100)' => '16058',
- 'Bouzic (24250)' => '24063',
- 'Brach (33480)' => '33070',
- 'Bran (17210)' => '17061',
- 'Branceilles (19500)' => '19029',
- 'Branne (33420)' => '33071',
- 'Brannens (33124)' => '33072',
- 'Brantôme en Périgord (24310)' => '24064',
- 'Brassempouy (40330)' => '40054',
- 'Braud-et-Saint-Louis (33820)' => '33073',
- 'Brax (47310)' => '47040',
- 'Bresdon (17490)' => '17062',
- 'Bressuire (79300)' => '79049',
- 'Bretagne-de-Marsan (40280)' => '40055',
- 'Bretignolles (79140)' => '79050',
- 'Brettes (16240)' => '16059',
- 'Breuil-la-Réorte (17700)' => '17063',
- 'Breuil-Magné (17870)' => '17065',
- 'Breuilaufa (87300)' => '87022',
- 'Breuilh (24380)' => '24065',
- 'Breuillet (17920)' => '17064',
- 'Bréville (16370)' => '16060',
- 'Brie (16590)' => '16061',
- 'Brie (79100)' => '79054',
- 'Brie-sous-Archiac (17520)' => '17066',
- 'Brie-sous-Barbezieux (16300)' => '16062',
- 'Brie-sous-Chalais (16210)' => '16063',
- 'Brie-sous-Matha (17160)' => '17067',
- 'Brie-sous-Mortagne (17120)' => '17068',
- 'Brieuil-sur-Chizé (79170)' => '79055',
- 'Brignac-la-Plaine (19310)' => '19030',
- 'Brigueil-le-Chantre (86290)' => '86037',
- 'Brigueuil (16420)' => '16064',
- 'Brillac (16500)' => '16065',
- 'Brion (86160)' => '86038',
- 'Brion-près-Thouet (79290)' => '79056',
- 'Brioux-sur-Boutonne (79170)' => '79057',
- 'Briscous (64240)' => '64147',
- 'Brive-la-Gaillarde (19100)' => '19031',
- 'Brives-sur-Charente (17800)' => '17069',
- 'Brivezac (19120)' => '19032',
- 'Brizambourg (17770)' => '17070',
- 'Brocas (40420)' => '40056',
- 'Brossac (16480)' => '16066',
- 'Brouchaud (24210)' => '24066',
- 'Brouqueyran (33124)' => '33074',
- 'Brousse (23700)' => '23034',
- 'Bruch (47130)' => '47041',
- 'Bruges (33520)' => '33075',
- 'Bruges-Capbis-Mifaget (64800)' => '64148',
- 'Brugnac (47260)' => '47042',
- 'Brûlain (79230)' => '79058',
- 'Brux (86510)' => '86039',
- 'Buanes (40320)' => '40057',
- 'Budelière (23170)' => '23035',
- 'Budos (33720)' => '33076',
- 'Bugeat (19170)' => '19033',
- 'Bugnein (64190)' => '64149',
- 'Bujaleuf (87460)' => '87024',
- 'Bunus (64120)' => '64150',
- 'Bunzac (16110)' => '16067',
- 'Burgaronne (64390)' => '64151',
- 'Burgnac (87800)' => '87025',
- 'Burie (17770)' => '17072',
- 'Buros (64160)' => '64152',
- 'Burosse-Mendousse (64330)' => '64153',
- 'Bussac (24350)' => '24069',
- 'Bussac-Forêt (17210)' => '17074',
- 'Bussac-sur-Charente (17100)' => '17073',
- 'Busserolles (24360)' => '24070',
- 'Bussière-Badil (24360)' => '24071',
- 'Bussière-Dunoise (23320)' => '23036',
- 'Bussière-Galant (87230)' => '87027',
- 'Bussière-Nouvelle (23700)' => '23037',
- 'Bussière-Poitevine (87320)' => '87028',
- 'Bussière-Saint-Georges (23600)' => '23038',
- 'Bussunarits-Sarrasquette (64220)' => '64154',
- 'Bustince-Iriberry (64220)' => '64155',
- 'Buxerolles (86180)' => '86041',
- 'Buxeuil (37160)' => '86042',
- 'Buzet-sur-Baïse (47160)' => '47043',
- 'Buziet (64680)' => '64156',
- 'Buzy (64260)' => '64157',
- 'Cabanac-et-Villagrains (33650)' => '33077',
- 'Cabara (33420)' => '33078',
- 'Cabariot (17430)' => '17075',
- 'Cabidos (64410)' => '64158',
- 'Cachen (40120)' => '40058',
- 'Cadarsac (33750)' => '33079',
- 'Cadaujac (33140)' => '33080',
- 'Cadillac (33410)' => '33081',
- 'Cadillac-en-Fronsadais (33240)' => '33082',
- 'Cadillon (64330)' => '64159',
- 'Cagnotte (40300)' => '40059',
- 'Cahuzac (47330)' => '47044',
- 'Calès (24150)' => '24073',
- 'Calignac (47600)' => '47045',
- 'Callen (40430)' => '40060',
- 'Calonges (47430)' => '47046',
- 'Calviac-en-Périgord (24370)' => '24074',
- 'Camarsac (33750)' => '33083',
- 'Cambes (33880)' => '33084',
- 'Cambes (47350)' => '47047',
- 'Camblanes-et-Meynac (33360)' => '33085',
- 'Cambo-les-Bains (64250)' => '64160',
- 'Came (64520)' => '64161',
- 'Camiac-et-Saint-Denis (33420)' => '33086',
- 'Camiran (33190)' => '33087',
- 'Camou-Cihigue (64470)' => '64162',
- 'Campagnac-lès-Quercy (24550)' => '24075',
- 'Campagne (24260)' => '24076',
- 'Campagne (40090)' => '40061',
- 'Campet-et-Lamolère (40090)' => '40062',
- 'Camps-Saint-Mathurin-Léobazel (19430)' => '19034',
- 'Camps-sur-l\'Isle (33660)' => '33088',
- 'Campsegret (24140)' => '24077',
- 'Campugnan (33390)' => '33089',
- 'Cancon (47290)' => '47048',
- 'Candresse (40180)' => '40063',
- 'Canéjan (33610)' => '33090',
- 'Canenx-et-Réaut (40090)' => '40064',
- 'Cantenac (33460)' => '33091',
- 'Cantillac (24530)' => '24079',
- 'Cantois (33760)' => '33092',
- 'Capbreton (40130)' => '40065',
- 'Capdrot (24540)' => '24080',
- 'Capian (33550)' => '33093',
- 'Caplong (33220)' => '33094',
- 'Captieux (33840)' => '33095',
- 'Carbon-Blanc (33560)' => '33096',
- 'Carcans (33121)' => '33097',
- 'Carcarès-Sainte-Croix (40400)' => '40066',
- 'Carcen-Ponson (40400)' => '40067',
- 'Cardan (33410)' => '33098',
- 'Cardesse (64360)' => '64165',
- 'Carignan-de-Bordeaux (33360)' => '33099',
- 'Carlux (24370)' => '24081',
- 'Caro (64220)' => '64166',
- 'Carrère (64160)' => '64167',
- 'Carresse-Cassaber (64270)' => '64168',
- 'Cars (33390)' => '33100',
- 'Carsac-Aillac (24200)' => '24082',
- 'Carsac-de-Gurson (24610)' => '24083',
- 'Cartelègue (33390)' => '33101',
- 'Carves (24170)' => '24084',
- 'Cassen (40380)' => '40068',
- 'Casseneuil (47440)' => '47049',
- 'Casseuil (33190)' => '33102',
- 'Cassignas (47340)' => '47050',
- 'Castagnède (64270)' => '64170',
- 'Castaignos-Souslens (40700)' => '40069',
- 'Castandet (40270)' => '40070',
- 'Casteide-Cami (64170)' => '64171',
- 'Casteide-Candau (64370)' => '64172',
- 'Casteide-Doat (64460)' => '64173',
- 'Castel-Sarrazin (40330)' => '40074',
- 'Castelculier (47240)' => '47051',
- 'Casteljaloux (47700)' => '47052',
- 'Castella (47340)' => '47053',
- 'Castelmoron-d\'Albret (33540)' => '33103',
- 'Castelmoron-sur-Lot (47260)' => '47054',
- 'Castelnau-Chalosse (40360)' => '40071',
- 'Castelnau-de-Médoc (33480)' => '33104',
- 'Castelnau-sur-Gupie (47180)' => '47056',
- 'Castelnau-Tursan (40320)' => '40072',
- 'Castelnaud-de-Gratecambe (47290)' => '47055',
- 'Castelnaud-la-Chapelle (24250)' => '24086',
- 'Castelner (40700)' => '40073',
- 'Castels (24220)' => '24087',
- 'Castelviel (33540)' => '33105',
- 'Castéra-Loubix (64460)' => '64174',
- 'Castet (64260)' => '64175',
- 'Castetbon (64190)' => '64176',
- 'Castétis (64300)' => '64177',
- 'Castetnau-Camblong (64190)' => '64178',
- 'Castetner (64300)' => '64179',
- 'Castetpugon (64330)' => '64180',
- 'Castets (40260)' => '40075',
- 'Castets-en-Dorthe (33210)' => '33106',
- 'Castillon (Canton d\'Arthez-de-Béarn) (64370)' => '64181',
- 'Castillon (Canton de Lembeye) (64350)' => '64182',
- 'Castillon-de-Castets (33210)' => '33107',
- 'Castillon-la-Bataille (33350)' => '33108',
- 'Castillonnès (47330)' => '47057',
- 'Castres-Gironde (33640)' => '33109',
- 'Caubeyres (47160)' => '47058',
- 'Caubios-Loos (64230)' => '64183',
- 'Caubon-Saint-Sauveur (47120)' => '47059',
- 'Caudecoste (47220)' => '47060',
- 'Caudrot (33490)' => '33111',
- 'Caumont (33540)' => '33112',
- 'Caumont-sur-Garonne (47430)' => '47061',
- 'Cauna (40500)' => '40076',
- 'Caunay (79190)' => '79060',
- 'Cauneille (40300)' => '40077',
- 'Caupenne (40250)' => '40078',
- 'Cause-de-Clérans (24150)' => '24088',
- 'Cauvignac (33690)' => '33113',
- 'Cauzac (47470)' => '47062',
- 'Cavarc (47330)' => '47063',
- 'Cavignac (33620)' => '33114',
- 'Cazalis (33113)' => '33115',
- 'Cazalis (40700)' => '40079',
- 'Cazats (33430)' => '33116',
- 'Cazaugitat (33790)' => '33117',
- 'Cazères-sur-l\'Adour (40270)' => '40080',
- 'Cazideroque (47370)' => '47064',
- 'Cazoulès (24370)' => '24089',
- 'Ceaux-en-Couhé (86700)' => '86043',
- 'Ceaux-en-Loudun (86200)' => '86044',
- 'Celle-Lévescault (86600)' => '86045',
- 'Cellefrouin (16260)' => '16068',
- 'Celles (17520)' => '17076',
- 'Celles (24600)' => '24090',
- 'Celles-sur-Belle (79370)' => '79061',
- 'Cellettes (16230)' => '16069',
- 'Cénac (33360)' => '33118',
- 'Cénac-et-Saint-Julien (24250)' => '24091',
- 'Cendrieux (24380)' => '24092',
- 'Cenon (33150)' => '33119',
- 'Cenon-sur-Vienne (86530)' => '86046',
- 'Cercles (24320)' => '24093',
- 'Cercoux (17270)' => '17077',
- 'Cère (40090)' => '40081',
- 'Cerizay (79140)' => '79062',
- 'Cernay (86140)' => '86047',
- 'Cérons (33720)' => '33120',
- 'Cersay (79290)' => '79063',
- 'Cescau (64170)' => '64184',
- 'Cessac (33760)' => '33121',
- 'Cestas (33610)' => '33122',
- 'Cette-Eygun (64490)' => '64185',
- 'Ceyroux (23210)' => '23042',
- 'Cézac (33620)' => '33123',
- 'Chabanais (16150)' => '16070',
- 'Chabournay (86380)' => '86048',
- 'Chabrac (16150)' => '16071',
- 'Chabrignac (19350)' => '19035',
- 'Chadenac (17800)' => '17078',
- 'Chadurie (16250)' => '16072',
- 'Chail (79500)' => '79064',
- 'Chaillac-sur-Vienne (87200)' => '87030',
- 'Chaillevette (17890)' => '17079',
- 'Chalagnac (24380)' => '24094',
- 'Chalais (16210)' => '16073',
- 'Chalais (24800)' => '24095',
- 'Chalais (86200)' => '86049',
- 'Chalandray (86190)' => '86050',
- 'Challignac (16300)' => '16074',
- 'Châlus (87230)' => '87032',
- 'Chamadelle (33230)' => '33124',
- 'Chamberaud (23480)' => '23043',
- 'Chamberet (19370)' => '19036',
- 'Chambon (17290)' => '17080',
- 'Chambon-Sainte-Croix (23220)' => '23044',
- 'Chambon-sur-Voueize (23170)' => '23045',
- 'Chambonchard (23110)' => '23046',
- 'Chamborand (23240)' => '23047',
- 'Chamboret (87140)' => '87033',
- 'Chamboulive (19450)' => '19037',
- 'Chameyrat (19330)' => '19038',
- 'Chamouillac (17130)' => '17081',
- 'Champagnac (17500)' => '17082',
- 'Champagnac-de-Belair (24530)' => '24096',
- 'Champagnac-la-Noaille (19320)' => '19039',
- 'Champagnac-la-Prune (19320)' => '19040',
- 'Champagnac-la-Rivière (87150)' => '87034',
- 'Champagnat (23190)' => '23048',
- 'Champagne (17620)' => '17083',
- 'Champagne-et-Fontaine (24320)' => '24097',
- 'Champagné-le-Sec (86510)' => '86051',
- 'Champagne-Mouton (16350)' => '16076',
- 'Champagné-Saint-Hilaire (86160)' => '86052',
- 'Champagne-Vigny (16250)' => '16075',
- 'Champagnolles (17240)' => '17084',
- 'Champcevinel (24750)' => '24098',
- 'Champdeniers-Saint-Denis (79220)' => '79066',
- 'Champdolent (17430)' => '17085',
- 'Champeaux-et-la-Chapelle-Pommier (24340)' => '24099',
- 'Champigny-le-Sec (86170)' => '86053',
- 'Champmillon (16290)' => '16077',
- 'Champnétery (87400)' => '87035',
- 'Champniers (16430)' => '16078',
- 'Champniers (86400)' => '86054',
- 'Champniers-et-Reilhac (24360)' => '24100',
- 'Champs-Romain (24470)' => '24101',
- 'Champsac (87230)' => '87036',
- 'Champsanglard (23220)' => '23049',
- 'Chanac-les-Mines (19150)' => '19041',
- 'Chancelade (24650)' => '24102',
- 'Chaniers (17610)' => '17086',
- 'Chantecorps (79340)' => '79068',
- 'Chanteix (19330)' => '19042',
- 'Chanteloup (79320)' => '79069',
- 'Chantemerle-sur-la-Soie (17380)' => '17087',
- 'Chantérac (24190)' => '24104',
- 'Chantillac (16360)' => '16079',
- 'Chapdeuil (24320)' => '24105',
- 'Chapelle-Spinasse (19300)' => '19046',
- 'Chapelle-Viviers (86300)' => '86059',
- 'Chaptelat (87270)' => '87038',
- 'Chard (23700)' => '23053',
- 'Charmé (16140)' => '16083',
- 'Charrais (86170)' => '86060',
- 'Charras (16380)' => '16084',
- 'Charre (64190)' => '64186',
- 'Charritte-de-Bas (64130)' => '64187',
- 'Charron (17230)' => '17091',
- 'Charron (23700)' => '23054',
- 'Charroux (86250)' => '86061',
- 'Chartrier-Ferrière (19600)' => '19047',
- 'Chartuzac (17130)' => '17092',
- 'Chassaignes (24600)' => '24114',
- 'Chasseneuil-du-Poitou (86360)' => '86062',
- 'Chasseneuil-sur-Bonnieure (16260)' => '16085',
- 'Chassenon (16150)' => '16086',
- 'Chassiecq (16350)' => '16087',
- 'Chassors (16200)' => '16088',
- 'Chasteaux (19600)' => '19049',
- 'Chatain (86250)' => '86063',
- 'Château-Chervix (87380)' => '87039',
- 'Château-Garnier (86350)' => '86064',
- 'Château-l\'Évêque (24460)' => '24115',
- 'Château-Larcher (86370)' => '86065',
- 'Châteaubernard (16100)' => '16089',
- 'Châteauneuf-la-Forêt (87130)' => '87040',
- 'Châteauneuf-sur-Charente (16120)' => '16090',
- 'Châteauponsac (87290)' => '87041',
- 'Châtelaillon-Plage (17340)' => '17094',
- 'Châtelard (23700)' => '23055',
- 'Châtellerault (86100)' => '86066',
- 'Châtelus-le-Marcheix (23430)' => '23056',
- 'Châtelus-Malvaleix (23270)' => '23057',
- 'Chatenet (17210)' => '17095',
- 'Châtignac (16480)' => '16091',
- 'Châtillon (86700)' => '86067',
- 'Châtillon-sur-Thouet (79200)' => '79080',
- 'Châtres (24120)' => '24116',
- 'Chauffour-sur-Vell (19500)' => '19050',
- 'Chaumeil (19390)' => '19051',
- 'Chaunac (17130)' => '17096',
- 'Chaunay (86510)' => '86068',
- 'Chauray (79180)' => '79081',
- 'Chauvigny (86300)' => '86070',
- 'Chavagnac (24120)' => '24117',
- 'Chavanac (19290)' => '19052',
- 'Chavanat (23250)' => '23060',
- 'Chaveroche (19200)' => '19053',
- 'Chazelles (16380)' => '16093',
- 'Chef-Boutonne (79110)' => '79083',
- 'Cheissoux (87460)' => '87043',
- 'Chenac-Saint-Seurin-d\'Uzet (17120)' => '17098',
- 'Chenailler-Mascheix (19120)' => '19054',
- 'Chenay (79120)' => '79084',
- 'Cheneché (86380)' => '86071',
- 'Chénérailles (23130)' => '23061',
- 'Chenevelles (86450)' => '86072',
- 'Chéniers (23220)' => '23062',
- 'Chenommet (16460)' => '16094',
- 'Chenon (16460)' => '16095',
- 'Chepniers (17210)' => '17099',
- 'Chérac (17610)' => '17100',
- 'Chéraute (64130)' => '64188',
- 'Cherbonnières (17470)' => '17101',
- 'Chérigné (79170)' => '79085',
- 'Chermignac (17460)' => '17102',
- 'Chéronnac (87600)' => '87044',
- 'Cherval (24320)' => '24119',
- 'Cherveix-Cubas (24390)' => '24120',
- 'Cherves (86170)' => '86073',
- 'Cherves-Châtelars (16310)' => '16096',
- 'Cherves-Richemont (16370)' => '16097',
- 'Chervettes (17380)' => '17103',
- 'Cherveux (79410)' => '79086',
- 'Chevanceaux (17210)' => '17104',
- 'Chey (79120)' => '79087',
- 'Chiché (79350)' => '79088',
- 'Chillac (16480)' => '16099',
- 'Chirac (16150)' => '16100',
- 'Chirac-Bellevue (19160)' => '19055',
- 'Chiré-en-Montreuil (86190)' => '86074',
- 'Chives (17510)' => '17105',
- 'Chizé (79170)' => '79090',
- 'Chouppes (86110)' => '86075',
- 'Chourgnac (24640)' => '24121',
- 'Ciboure (64500)' => '64189',
- 'Cierzac (17520)' => '17106',
- 'Cieux (87520)' => '87045',
- 'Ciré-d\'Aunis (17290)' => '17107',
- 'Cirières (79140)' => '79091',
- 'Cissac-Médoc (33250)' => '33125',
- 'Cissé (86170)' => '86076',
- 'Civaux (86320)' => '86077',
- 'Civrac-de-Blaye (33920)' => '33126',
- 'Civrac-en-Médoc (33340)' => '33128',
- 'Civrac-sur-Dordogne (33350)' => '33127',
- 'Civray (86400)' => '86078',
- 'Cladech (24170)' => '24122',
- 'Clairac (47320)' => '47065',
- 'Clairavaux (23500)' => '23063',
- 'Claix (16440)' => '16101',
- 'Clam (17500)' => '17108',
- 'Claracq (64330)' => '64190',
- 'Classun (40320)' => '40082',
- 'Clavé (79420)' => '79092',
- 'Clavette (17220)' => '17109',
- 'Clèdes (40320)' => '40083',
- 'Clérac (17270)' => '17110',
- 'Clergoux (19320)' => '19056',
- 'Clermont (40180)' => '40084',
- 'Clermont-d\'Excideuil (24160)' => '24124',
- 'Clermont-de-Beauregard (24140)' => '24123',
- 'Clermont-Dessous (47130)' => '47066',
- 'Clermont-Soubiran (47270)' => '47067',
- 'Clessé (79350)' => '79094',
- 'Cleyrac (33540)' => '33129',
- 'Clion (17240)' => '17111',
- 'Cloué (86600)' => '86080',
- 'Clugnat (23270)' => '23064',
- 'Clussais-la-Pommeraie (79190)' => '79095',
- 'Coarraze (64800)' => '64191',
- 'Cocumont (47250)' => '47068',
- 'Cognac (16100)' => '16102',
- 'Cognac-la-Forêt (87310)' => '87046',
- 'Coimères (33210)' => '33130',
- 'Coirac (33540)' => '33131',
- 'Coivert (17330)' => '17114',
- 'Colayrac-Saint-Cirq (47450)' => '47069',
- 'Collonges-la-Rouge (19500)' => '19057',
- 'Colombier (24560)' => '24126',
- 'Colombiers (17460)' => '17115',
- 'Colombiers (86490)' => '86081',
- 'Colondannes (23800)' => '23065',
- 'Coly (24120)' => '24127',
- 'Comberanche-et-Épeluche (24600)' => '24128',
- 'Combiers (16320)' => '16103',
- 'Combrand (79140)' => '79096',
- 'Combressol (19250)' => '19058',
- 'Commensacq (40210)' => '40085',
- 'Compreignac (87140)' => '87047',
- 'Comps (33710)' => '33132',
- 'Concèze (19350)' => '19059',
- 'Conchez-de-Béarn (64330)' => '64192',
- 'Condac (16700)' => '16104',
- 'Condat-sur-Ganaveix (19140)' => '19060',
- 'Condat-sur-Trincou (24530)' => '24129',
- 'Condat-sur-Vézère (24570)' => '24130',
- 'Condat-sur-Vienne (87920)' => '87048',
- 'Condéon (16360)' => '16105',
- 'Condezaygues (47500)' => '47070',
- 'Confolens (16500)' => '16106',
- 'Confolent-Port-Dieu (19200)' => '19167',
- 'Conne-de-Labarde (24560)' => '24132',
- 'Connezac (24300)' => '24131',
- 'Consac (17150)' => '17116',
- 'Contré (17470)' => '17117',
- 'Corbère-Abères (64350)' => '64193',
- 'Corgnac-sur-l\'Isle (24800)' => '24134',
- 'Corignac (17130)' => '17118',
- 'Corme-Écluse (17600)' => '17119',
- 'Corme-Royal (17600)' => '17120',
- 'Cornil (19150)' => '19061',
- 'Cornille (24750)' => '24135',
- 'Corrèze (19800)' => '19062',
- 'Coslédaà-Lube-Boast (64160)' => '64194',
- 'Cosnac (19360)' => '19063',
- 'Coubeyrac (33890)' => '33133',
- 'Coubjours (24390)' => '24136',
- 'Coublucq (64410)' => '64195',
- 'Coudures (40500)' => '40086',
- 'Couffy-sur-Sarsonne (19340)' => '19064',
- 'Couhé (86700)' => '86082',
- 'Coulaures (24420)' => '24137',
- 'Coulgens (16560)' => '16107',
- 'Coulombiers (86600)' => '86083',
- 'Coulon (79510)' => '79100',
- 'Coulonges (16330)' => '16108',
- 'Coulonges (17800)' => '17122',
- 'Coulonges (86290)' => '86084',
- 'Coulonges-sur-l\'Autize (79160)' => '79101',
- 'Coulonges-Thouarsais (79330)' => '79102',
- 'Coulounieix-Chamiers (24660)' => '24138',
- 'Coulx (47260)' => '47071',
- 'Couquèques (33340)' => '33134',
- 'Courant (17330)' => '17124',
- 'Courbiac (47370)' => '47072',
- 'Courbillac (16200)' => '16109',
- 'Courcelles (17400)' => '17125',
- 'Courcerac (17160)' => '17126',
- 'Courcôme (16240)' => '16110',
- 'Courçon (17170)' => '17127',
- 'Courcoury (17100)' => '17128',
- 'Courgeac (16190)' => '16111',
- 'Courlac (16210)' => '16112',
- 'Courlay (79440)' => '79103',
- 'Courpiac (33760)' => '33135',
- 'Courpignac (17130)' => '17129',
- 'Cours (47360)' => '47073',
- 'Cours (79220)' => '79104',
- 'Cours-de-Monségur (33580)' => '33136',
- 'Cours-de-Pile (24520)' => '24140',
- 'Cours-les-Bains (33690)' => '33137',
- 'Coursac (24430)' => '24139',
- 'Courteix (19340)' => '19065',
- 'Coussac-Bonneval (87500)' => '87049',
- 'Coussay (86110)' => '86085',
- 'Coussay-les-Bois (86270)' => '86086',
- 'Couthures-sur-Garonne (47180)' => '47074',
- 'Coutières (79340)' => '79105',
- 'Coutras (33230)' => '33138',
- 'Couture (16460)' => '16114',
- 'Couture-d\'Argenson (79110)' => '79106',
- 'Coutures (24320)' => '24141',
- 'Coutures (33580)' => '33139',
- 'Coux (17130)' => '17130',
- 'Coux et Bigaroque-Mouzens (24220)' => '24142',
- 'Couze-et-Saint-Front (24150)' => '24143',
- 'Couzeix (87270)' => '87050',
- 'Cozes (17120)' => '17131',
- 'Cramchaban (17170)' => '17132',
- 'Craon (86110)' => '86087',
- 'Cravans (17260)' => '17133',
- 'Crazannes (17350)' => '17134',
- 'Créon (33670)' => '33140',
- 'Créon-d\'Armagnac (40240)' => '40087',
- 'Cressac-Saint-Genis (16250)' => '16115',
- 'Cressat (23140)' => '23068',
- 'Cressé (17160)' => '17135',
- 'Creyssac (24350)' => '24144',
- 'Creysse (24100)' => '24145',
- 'Creyssensac-et-Pissot (24380)' => '24146',
- 'Crézières (79110)' => '79107',
- 'Criteuil-la-Magdeleine (16300)' => '16116',
- 'Crocq (23260)' => '23069',
- 'Croignon (33750)' => '33141',
- 'Croix-Chapeau (17220)' => '17136',
- 'Cromac (87160)' => '87053',
- 'Crouseilles (64350)' => '64196',
- 'Croutelle (86240)' => '86088',
- 'Crozant (23160)' => '23070',
- 'Croze (23500)' => '23071',
- 'Cubjac (24640)' => '24147',
- 'Cublac (19520)' => '19066',
- 'Cubnezais (33620)' => '33142',
- 'Cubzac-les-Ponts (33240)' => '33143',
- 'Cudos (33430)' => '33144',
- 'Cuhon (86110)' => '86089',
- 'Cunèges (24240)' => '24148',
- 'Cuq (47220)' => '47076',
- 'Cuqueron (64360)' => '64197',
- 'Curac (16210)' => '16117',
- 'Curçay-sur-Dive (86120)' => '86090',
- 'Curemonte (19500)' => '19067',
- 'Cursan (33670)' => '33145',
- 'Curzay-sur-Vonne (86600)' => '86091',
- 'Cussac (87150)' => '87054',
- 'Cussac-Fort-Médoc (33460)' => '33146',
- 'Cuzorn (47500)' => '47077',
- 'Daglan (24250)' => '24150',
- 'Daignac (33420)' => '33147',
- 'Damazan (47160)' => '47078',
- 'Dampierre-sur-Boutonne (17470)' => '17138',
- 'Dampniat (19360)' => '19068',
- 'Dangé-Saint-Romain (86220)' => '86092',
- 'Darazac (19220)' => '19069',
- 'Dardenac (33420)' => '33148',
- 'Darnac (87320)' => '87055',
- 'Darnets (19300)' => '19070',
- 'Daubèze (33540)' => '33149',
- 'Dausse (47140)' => '47079',
- 'Davignac (19250)' => '19071',
- 'Dax (40100)' => '40088',
- 'Denguin (64230)' => '64198',
- 'Dercé (86420)' => '86093',
- 'Deviat (16190)' => '16118',
- 'Dévillac (47210)' => '47080',
- 'Dienné (86410)' => '86094',
- 'Dieulivol (33580)' => '33150',
- 'Dignac (16410)' => '16119',
- 'Dinsac (87210)' => '87056',
- 'Dirac (16410)' => '16120',
- 'Dissay (86130)' => '86095',
- 'Diusse (64330)' => '64199',
- 'Doazit (40700)' => '40089',
- 'Doazon (64370)' => '64200',
- 'Doeuil-sur-le-Mignon (17330)' => '17139',
- 'Dognen (64190)' => '64201',
- 'Doissat (24170)' => '24151',
- 'Dolmayrac (47110)' => '47081',
- 'Dolus-d\'Oléron (17550)' => '17140',
- 'Domeyrot (23140)' => '23072',
- 'Domezain-Berraute (64120)' => '64202',
- 'Domme (24250)' => '24152',
- 'Dompierre-les-Églises (87190)' => '87057',
- 'Dompierre-sur-Charente (17610)' => '17141',
- 'Dompierre-sur-Mer (17139)' => '17142',
- 'Domps (87120)' => '87058',
- 'Dondas (47470)' => '47082',
- 'Donnezac (33860)' => '33151',
- 'Dontreix (23700)' => '23073',
- 'Donzac (33410)' => '33152',
- 'Donzacq (40360)' => '40090',
- 'Donzenac (19270)' => '19072',
- 'Douchapt (24350)' => '24154',
- 'Doudrac (47210)' => '47083',
- 'Doulezon (33350)' => '33153',
- 'Doumy (64450)' => '64203',
- 'Dournazac (87230)' => '87060',
- 'Doussay (86140)' => '86096',
- 'Douville (24140)' => '24155',
- 'Doux (79390)' => '79108',
- 'Douzains (47330)' => '47084',
- 'Douzat (16290)' => '16121',
- 'Douzillac (24190)' => '24157',
- 'Droux (87190)' => '87061',
- 'Duhort-Bachen (40800)' => '40091',
- 'Dumes (40500)' => '40092',
- 'Dun-le-Palestel (23800)' => '23075',
- 'Durance (47420)' => '47085',
- 'Duras (47120)' => '47086',
- 'Dussac (24270)' => '24158',
- 'Eaux-Bonnes (64440)' => '64204',
- 'Ébréon (16140)' => '16122',
- 'Échallat (16170)' => '16123',
- 'Échebrune (17800)' => '17145',
- 'Échillais (17620)' => '17146',
- 'Échiré (79410)' => '79109',
- 'Échourgnac (24410)' => '24159',
- 'Écoyeux (17770)' => '17147',
- 'Écuras (16220)' => '16124',
- 'Écurat (17810)' => '17148',
- 'Édon (16320)' => '16125',
- 'Égletons (19300)' => '19073',
- 'Église-Neuve-d\'Issac (24400)' => '24161',
- 'Église-Neuve-de-Vergt (24380)' => '24160',
- 'Empuré (16240)' => '16127',
- 'Engayrac (47470)' => '47087',
- 'Ensigné (79170)' => '79111',
- 'Épannes (79270)' => '79112',
- 'Épargnes (17120)' => '17152',
- 'Épenède (16490)' => '16128',
- 'Éraville (16120)' => '16129',
- 'Escalans (40310)' => '40093',
- 'Escassefort (47350)' => '47088',
- 'Escaudes (33840)' => '33155',
- 'Escaunets (65500)' => '65160',
- 'Esclottes (47120)' => '47089',
- 'Escoire (24420)' => '24162',
- 'Escos (64270)' => '64205',
- 'Escot (64490)' => '64206',
- 'Escou (64870)' => '64207',
- 'Escoubès (64160)' => '64208',
- 'Escource (40210)' => '40094',
- 'Escoussans (33760)' => '33156',
- 'Escout (64870)' => '64209',
- 'Escurès (64350)' => '64210',
- 'Eslourenties-Daban (64420)' => '64211',
- 'Esnandes (17137)' => '17153',
- 'Espagnac (19150)' => '19075',
- 'Espartignac (19140)' => '19076',
- 'Espéchède (64160)' => '64212',
- 'Espelette (64250)' => '64213',
- 'Espès-Undurein (64130)' => '64214',
- 'Espiens (47600)' => '47090',
- 'Espiet (33420)' => '33157',
- 'Espiute (64390)' => '64215',
- 'Espoey (64420)' => '64216',
- 'Esquiule (64400)' => '64217',
- 'Esse (16500)' => '16131',
- 'Essouvert (17400)' => '17277',
- 'Estérençuby (64220)' => '64218',
- 'Estialescq (64290)' => '64219',
- 'Estibeaux (40290)' => '40095',
- 'Estigarde (40240)' => '40096',
- 'Estillac (47310)' => '47091',
- 'Estivals (19600)' => '19077',
- 'Estivaux (19410)' => '19078',
- 'Estos (64400)' => '64220',
- 'Étagnac (16150)' => '16132',
- 'Étaules (17750)' => '17155',
- 'Étauliers (33820)' => '33159',
- 'Etcharry (64120)' => '64221',
- 'Etchebar (64470)' => '64222',
- 'Étouars (24360)' => '24163',
- 'Étriac (16250)' => '16133',
- 'Etsaut (64490)' => '64223',
- 'Eugénie-les-Bains (40320)' => '40097',
- 'Évaux-les-Bains (23110)' => '23076',
- 'Excideuil (24160)' => '24164',
- 'Exideuil (16150)' => '16134',
- 'Exireuil (79400)' => '79114',
- 'Exoudun (79800)' => '79115',
- 'Expiremont (17130)' => '17156',
- 'Eybouleuf (87400)' => '87062',
- 'Eyburie (19140)' => '19079',
- 'Eygurande (19340)' => '19080',
- 'Eygurande-et-Gardedeuil (24700)' => '24165',
- 'Eyjeaux (87220)' => '87063',
- 'Eyliac (24330)' => '24166',
- 'Eymet (24500)' => '24167',
- 'Eymouthiers (16220)' => '16135',
- 'Eymoutiers (87120)' => '87064',
- 'Eynesse (33220)' => '33160',
- 'Eyrans (33390)' => '33161',
- 'Eyrein (19800)' => '19081',
- 'Eyres-Moncube (40500)' => '40098',
- 'Eysines (33320)' => '33162',
- 'Eysus (64400)' => '64224',
- 'Eyvirat (24460)' => '24170',
- 'Eyzerac (24800)' => '24171',
- 'Faleyras (33760)' => '33163',
- 'Fals (47220)' => '47092',
- 'Fanlac (24290)' => '24174',
- 'Fargues (33210)' => '33164',
- 'Fargues (40500)' => '40099',
- 'Fargues-Saint-Hilaire (33370)' => '33165',
- 'Fargues-sur-Ourbise (47700)' => '47093',
- 'Fauguerolles (47400)' => '47094',
- 'Fauillet (47400)' => '47095',
- 'Faurilles (24560)' => '24176',
- 'Faux (24560)' => '24177',
- 'Faux-la-Montagne (23340)' => '23077',
- 'Faux-Mazuras (23400)' => '23078',
- 'Favars (19330)' => '19082',
- 'Faye-l\'Abbesse (79350)' => '79116',
- 'Faye-sur-Ardin (79160)' => '79117',
- 'Féas (64570)' => '64225',
- 'Felletin (23500)' => '23079',
- 'Fénery (79450)' => '79118',
- 'Féniers (23100)' => '23080',
- 'Fenioux (17350)' => '17157',
- 'Fenioux (79160)' => '79119',
- 'Ferrensac (47330)' => '47096',
- 'Ferrières (17170)' => '17158',
- 'Festalemps (24410)' => '24178',
- 'Feugarolles (47230)' => '47097',
- 'Feuillade (16380)' => '16137',
- 'Feyt (19340)' => '19083',
- 'Feytiat (87220)' => '87065',
- 'Fichous-Riumayou (64410)' => '64226',
- 'Fieux (47600)' => '47098',
- 'Firbeix (24450)' => '24180',
- 'Flaugeac (24240)' => '24181',
- 'Flaujagues (33350)' => '33168',
- 'Flavignac (87230)' => '87066',
- 'Flayat (23260)' => '23081',
- 'Fléac (16730)' => '16138',
- 'Fléac-sur-Seugne (17800)' => '17159',
- 'Fleix (86300)' => '86098',
- 'Fleurac (16200)' => '16139',
- 'Fleurac (24580)' => '24183',
- 'Fleurat (23320)' => '23082',
- 'Fleuré (86340)' => '86099',
- 'Floirac (17120)' => '17160',
- 'Floirac (33270)' => '33167',
- 'Florimont-Gaumier (24250)' => '24184',
- 'Floudès (33190)' => '33169',
- 'Folles (87250)' => '87067',
- 'Fomperron (79340)' => '79121',
- 'Fongrave (47260)' => '47099',
- 'Fonroque (24500)' => '24186',
- 'Fontaine-Chalendray (17510)' => '17162',
- 'Fontaine-le-Comte (86240)' => '86100',
- 'Fontaines-d\'Ozillac (17500)' => '17163',
- 'Fontanières (23110)' => '23083',
- 'Fontclaireau (16230)' => '16140',
- 'Fontcouverte (17100)' => '17164',
- 'Fontenet (17400)' => '17165',
- 'Fontenille (16230)' => '16141',
- 'Fontenille-Saint-Martin-d\'Entraigues (79110)' => '79122',
- 'Fontet (33190)' => '33170',
- 'Forges (17290)' => '17166',
- 'Forgès (19380)' => '19084',
- 'Fors (79230)' => '79125',
- 'Fossemagne (24210)' => '24188',
- 'Fossès-et-Baleyssac (33190)' => '33171',
- 'Fougueyrolles (33220)' => '24189',
- 'Foulayronnes (47510)' => '47100',
- 'Fouleix (24380)' => '24190',
- 'Fouquebrune (16410)' => '16143',
- 'Fouqueure (16140)' => '16144',
- 'Fouras (17450)' => '17168',
- 'Fourques-sur-Garonne (47200)' => '47101',
- 'Fours (33390)' => '33172',
- 'Foussignac (16200)' => '16145',
- 'Fraisse (24130)' => '24191',
- 'Francescas (47600)' => '47102',
- 'François (79260)' => '79128',
- 'Francs (33570)' => '33173',
- 'Fransèches (23480)' => '23086',
- 'Fréchou (47600)' => '47103',
- 'Frégimont (47360)' => '47104',
- 'Frespech (47140)' => '47105',
- 'Fresselines (23450)' => '23087',
- 'Fressines (79370)' => '79129',
- 'Fromental (87250)' => '87068',
- 'Fronsac (33126)' => '33174',
- 'Frontenac (33760)' => '33175',
- 'Frontenay-Rohan-Rohan (79270)' => '79130',
- 'Frozes (86190)' => '86102',
- 'Fumel (47500)' => '47106',
- 'Gaas (40350)' => '40101',
- 'Gabarnac (33410)' => '33176',
- 'Gabarret (40310)' => '40102',
- 'Gabaston (64160)' => '64227',
- 'Gabat (64120)' => '64228',
- 'Gabillou (24210)' => '24192',
- 'Gageac-et-Rouillac (24240)' => '24193',
- 'Gaillan-en-Médoc (33340)' => '33177',
- 'Gaillères (40090)' => '40103',
- 'Gajac (33430)' => '33178',
- 'Gajoubert (87330)' => '87069',
- 'Galapian (47190)' => '47107',
- 'Galgon (33133)' => '33179',
- 'Gamarde-les-Bains (40380)' => '40104',
- 'Gamarthe (64220)' => '64229',
- 'Gan (64290)' => '64230',
- 'Gans (33430)' => '33180',
- 'Garat (16410)' => '16146',
- 'Gardegan-et-Tourtirac (33350)' => '33181',
- 'Gardères (65320)' => '65185',
- 'Gardes-le-Pontaroux (16320)' => '16147',
- 'Gardonne (24680)' => '24194',
- 'Garein (40420)' => '40105',
- 'Garindein (64130)' => '64231',
- 'Garlède-Mondebat (64450)' => '64232',
- 'Garlin (64330)' => '64233',
- 'Garos (64410)' => '64234',
- 'Garrey (40180)' => '40106',
- 'Garris (64120)' => '64235',
- 'Garrosse (40110)' => '40107',
- 'Gartempe (23320)' => '23088',
- 'Gastes (40160)' => '40108',
- 'Gaugeac (24540)' => '24195',
- 'Gaujac (47200)' => '47108',
- 'Gaujacq (40330)' => '40109',
- 'Gauriac (33710)' => '33182',
- 'Gauriaguet (33240)' => '33183',
- 'Gavaudun (47150)' => '47109',
- 'Gayon (64350)' => '64236',
- 'Geaune (40320)' => '40110',
- 'Geay (17250)' => '17171',
- 'Geay (79330)' => '79131',
- 'Gelos (64110)' => '64237',
- 'Geloux (40090)' => '40111',
- 'Gémozac (17260)' => '17172',
- 'Genac-Bignac (16170)' => '16148',
- 'Gençay (86160)' => '86103',
- 'Générac (33920)' => '33184',
- 'Génis (24160)' => '24196',
- 'Génissac (33420)' => '33185',
- 'Genneton (79150)' => '79132',
- 'Genouillac (16270)' => '16149',
- 'Genouillac (23350)' => '23089',
- 'Genouillé (17430)' => '17174',
- 'Genouillé (86250)' => '86104',
- 'Gensac (33890)' => '33186',
- 'Gensac-la-Pallue (16130)' => '16150',
- 'Genté (16130)' => '16151',
- 'Gentioux-Pigerolles (23340)' => '23090',
- 'Ger (64530)' => '64238',
- 'Gerderest (64160)' => '64239',
- 'Gère-Bélesten (64260)' => '64240',
- 'Germignac (17520)' => '17175',
- 'Germond-Rouvre (79220)' => '79133',
- 'Géronce (64400)' => '64241',
- 'Gestas (64190)' => '64242',
- 'Géus-d\'Arzacq (64370)' => '64243',
- 'Geüs-d\'Oloron (64400)' => '64244',
- 'Gibourne (17160)' => '17176',
- 'Gibret (40380)' => '40112',
- 'Gimel-les-Cascades (19800)' => '19085',
- 'Gimeux (16130)' => '16152',
- 'Ginestet (24130)' => '24197',
- 'Gioux (23500)' => '23091',
- 'Gironde-sur-Dropt (33190)' => '33187',
- 'Giscos (33840)' => '33188',
- 'Givrezac (17260)' => '17178',
- 'Gizay (86340)' => '86105',
- 'Glandon (87500)' => '87071',
- 'Glanges (87380)' => '87072',
- 'Glénay (79330)' => '79134',
- 'Glénic (23380)' => '23092',
- 'Glénouze (86200)' => '86106',
- 'Goès (64400)' => '64245',
- 'Gomer (64420)' => '64246',
- 'Gond-Pontouvre (16160)' => '16154',
- 'Gondeville (16200)' => '16153',
- 'Gontaud-de-Nogaret (47400)' => '47110',
- 'Goos (40180)' => '40113',
- 'Gornac (33540)' => '33189',
- 'Gorre (87310)' => '87073',
- 'Gotein-Libarrenx (64130)' => '64247',
- 'Goualade (33840)' => '33190',
- 'Gouex (86320)' => '86107',
- 'Goulles (19430)' => '19086',
- 'Gourbera (40990)' => '40114',
- 'Gourdon-Murat (19170)' => '19087',
- 'Gourgé (79200)' => '79135',
- 'Gournay-Loizé (79110)' => '79136',
- 'Gours (33660)' => '33191',
- 'Gourville (16170)' => '16156',
- 'Gourvillette (17490)' => '17180',
- 'Gousse (40465)' => '40115',
- 'Gout-Rossignol (24320)' => '24199',
- 'Gouts (40400)' => '40116',
- 'Gouzon (23230)' => '23093',
- 'Gradignan (33170)' => '33192',
- 'Grand-Brassac (24350)' => '24200',
- 'Grandjean (17350)' => '17181',
- 'Grandsaigne (19300)' => '19088',
- 'Granges-d\'Ans (24390)' => '24202',
- 'Granges-sur-Lot (47260)' => '47111',
- 'Granzay-Gript (79360)' => '79137',
- 'Grassac (16380)' => '16158',
- 'Grateloup-Saint-Gayrand (47400)' => '47112',
- 'Graves-Saint-Amant (16120)' => '16297',
- 'Grayan-et-l\'Hôpital (33590)' => '33193',
- 'Grayssas (47270)' => '47113',
- 'Grenade-sur-l\'Adour (40270)' => '40117',
- 'Grézac (17120)' => '17183',
- 'Grèzes (24120)' => '24204',
- 'Grézet-Cavagnan (47250)' => '47114',
- 'Grézillac (33420)' => '33194',
- 'Grignols (24110)' => '24205',
- 'Grignols (33690)' => '33195',
- 'Grives (24170)' => '24206',
- 'Groléjac (24250)' => '24207',
- 'Gros-Chastang (19320)' => '19089',
- 'Grun-Bordas (24380)' => '24208',
- 'Guéret (23000)' => '23096',
- 'Guérin (47250)' => '47115',
- 'Guesnes (86420)' => '86109',
- 'Guéthary (64210)' => '64249',
- 'Guiche (64520)' => '64250',
- 'Guillac (33420)' => '33196',
- 'Guillos (33720)' => '33197',
- 'Guimps (16300)' => '16160',
- 'Guinarthe-Parenties (64390)' => '64251',
- 'Guitinières (17500)' => '17187',
- 'Guîtres (33230)' => '33198',
- 'Guizengeard (16480)' => '16161',
- 'Gujan-Mestras (33470)' => '33199',
- 'Gumond (19320)' => '19090',
- 'Gurat (16320)' => '16162',
- 'Gurmençon (64400)' => '64252',
- 'Gurs (64190)' => '64253',
- 'Habas (40290)' => '40118',
- 'Hagetaubin (64370)' => '64254',
- 'Hagetmau (40700)' => '40119',
- 'Haimps (17160)' => '17188',
- 'Haims (86310)' => '86110',
- 'Halsou (64480)' => '64255',
- 'Hanc (79110)' => '79140',
- 'Hasparren (64240)' => '64256',
- 'Hastingues (40300)' => '40120',
- 'Hauriet (40250)' => '40121',
- 'Haut-de-Bosdarros (64800)' => '64257',
- 'Haut-Mauco (40280)' => '40122',
- 'Hautefage (19400)' => '19091',
- 'Hautefage-la-Tour (47340)' => '47117',
- 'Hautefaye (24300)' => '24209',
- 'Hautefort (24390)' => '24210',
- 'Hautesvignes (47400)' => '47118',
- 'Haux (33550)' => '33201',
- 'Haux (64470)' => '64258',
- 'Hélette (64640)' => '64259',
- 'Hendaye (64700)' => '64260',
- 'Herm (40990)' => '40123',
- 'Herré (40310)' => '40124',
- 'Herrère (64680)' => '64261',
- 'Heugas (40180)' => '40125',
- 'Hiers-Brouage (17320)' => '17189',
- 'Hiersac (16290)' => '16163',
- 'Hiesse (16490)' => '16164',
- 'Higuères-Souye (64160)' => '64262',
- 'Hinx (40180)' => '40126',
- 'Hontanx (40190)' => '40127',
- 'Horsarrieu (40700)' => '40128',
- 'Hosta (64120)' => '64265',
- 'Hostens (33125)' => '33202',
- 'Houeillès (47420)' => '47119',
- 'Houlette (16200)' => '16165',
- 'Hours (64420)' => '64266',
- 'Hourtin (33990)' => '33203',
- 'Hure (33190)' => '33204',
- 'Ibarrolle (64120)' => '64267',
- 'Idaux-Mendy (64130)' => '64268',
- 'Idron (64320)' => '64269',
- 'Igon (64800)' => '64270',
- 'Iholdy (64640)' => '64271',
- 'Île-d\'Aix (17123)' => '17004',
- 'Ilharre (64120)' => '64272',
- 'Illats (33720)' => '33205',
- 'Ingrandes (86220)' => '86111',
- 'Irais (79600)' => '79141',
- 'Irissarry (64780)' => '64273',
- 'Irouléguy (64220)' => '64274',
- 'Isle (87170)' => '87075',
- 'Isle-Saint-Georges (33640)' => '33206',
- 'Ispoure (64220)' => '64275',
- 'Issac (24400)' => '24211',
- 'Issigeac (24560)' => '24212',
- 'Issor (64570)' => '64276',
- 'Issoudun-Létrieix (23130)' => '23097',
- 'Isturits (64240)' => '64277',
- 'Iteuil (86240)' => '86113',
- 'Itxassou (64250)' => '64279',
- 'Izeste (64260)' => '64280',
- 'Izon (33450)' => '33207',
- 'Jabreilles-les-Bordes (87370)' => '87076',
- 'Jalesches (23270)' => '23098',
- 'Janailhac (87800)' => '87077',
- 'Janaillat (23250)' => '23099',
- 'Jardres (86800)' => '86114',
- 'Jarnac (16200)' => '16167',
- 'Jarnac-Champagne (17520)' => '17192',
- 'Jarnages (23140)' => '23100',
- 'Jasses (64190)' => '64281',
- 'Jatxou (64480)' => '64282',
- 'Jau-Dignac-et-Loirac (33590)' => '33208',
- 'Jauldes (16560)' => '16168',
- 'Jaunay-Clan (86130)' => '86115',
- 'Jaure (24140)' => '24213',
- 'Javerdat (87520)' => '87078',
- 'Javerlhac-et-la-Chapelle-Saint-Robert (24300)' => '24214',
- 'Javrezac (16100)' => '16169',
- 'Jaxu (64220)' => '64283',
- 'Jayac (24590)' => '24215',
- 'Jazeneuil (86600)' => '86116',
- 'Jazennes (17260)' => '17196',
- 'Jonzac (17500)' => '17197',
- 'Josse (40230)' => '40129',
- 'Jouac (87890)' => '87080',
- 'Jouhet (86500)' => '86117',
- 'Jouillat (23220)' => '23101',
- 'Jourgnac (87800)' => '87081',
- 'Journet (86290)' => '86118',
- 'Journiac (24260)' => '24217',
- 'Joussé (86350)' => '86119',
- 'Jugazan (33420)' => '33209',
- 'Jugeals-Nazareth (19500)' => '19093',
- 'Juicq (17770)' => '17198',
- 'Juignac (16190)' => '16170',
- 'Juillac (19350)' => '19094',
- 'Juillac (33890)' => '33210',
- 'Juillac-le-Coq (16130)' => '16171',
- 'Juillé (16230)' => '16173',
- 'Juillé (79170)' => '79142',
- 'Julienne (16200)' => '16174',
- 'Jumilhac-le-Grand (24630)' => '24218',
- 'Jurançon (64110)' => '64284',
- 'Juscorps (79230)' => '79144',
- 'Jusix (47180)' => '47120',
- 'Jussas (17130)' => '17199',
- 'Juxue (64120)' => '64285',
- 'L\'Absie (79240)' => '79001',
- 'L\'Église-aux-Bois (19170)' => '19074',
- 'L\'Éguille (17600)' => '17151',
- 'L\'Hôpital-d\'Orion (64270)' => '64263',
- 'L\'Hôpital-Saint-Blaise (64130)' => '64264',
- 'L\'Houmeau (17137)' => '17190',
- 'L\'Isle-d\'Espagnac (16340)' => '16166',
- 'L\'Isle-Jourdain (86150)' => '86112',
- 'La Bachellerie (24210)' => '24020',
- 'La Barde (17360)' => '17033',
- 'La Bastide-Clairence (64240)' => '64289',
- 'La Bataille (79110)' => '79027',
- 'La Bazeuge (87210)' => '87008',
- 'La Boissière-d\'Ans (24640)' => '24047',
- 'La Boissière-en-Gâtine (79310)' => '79040',
- 'La Brède (33650)' => '33213',
- 'La Brée-les-Bains (17840)' => '17486',
- 'La Brionne (23000)' => '23033',
- 'La Brousse (17160)' => '17071',
- 'La Bussière (86310)' => '86040',
- 'La Cassagne (24120)' => '24085',
- 'La Celle-Dunoise (23800)' => '23039',
- 'La Celle-sous-Gouzon (23230)' => '23040',
- 'La Cellette (23350)' => '23041',
- 'La Chapelle (16140)' => '16081',
- 'La Chapelle-Aubareil (24290)' => '24106',
- 'La Chapelle-aux-Brocs (19360)' => '19043',
- 'La Chapelle-aux-Saints (19120)' => '19044',
- 'La Chapelle-Baloue (23160)' => '23050',
- 'La Chapelle-Bâton (79220)' => '79070',
- 'La Chapelle-Bâton (86250)' => '86055',
- 'La Chapelle-Bertrand (79200)' => '79071',
- 'La Chapelle-des-Pots (17100)' => '17089',
- 'La Chapelle-Faucher (24530)' => '24107',
- 'La Chapelle-Gonaguet (24350)' => '24108',
- 'La Chapelle-Grésignac (24320)' => '24109',
- 'La Chapelle-Montabourlet (24320)' => '24110',
- 'La Chapelle-Montbrandeix (87440)' => '87037',
- 'La Chapelle-Montmoreau (24300)' => '24111',
- 'La Chapelle-Montreuil (86470)' => '86056',
- 'La Chapelle-Moulière (86210)' => '86058',
- 'La Chapelle-Pouilloux (79190)' => '79074',
- 'La Chapelle-Saint-Étienne (79240)' => '79075',
- 'La Chapelle-Saint-Géraud (19430)' => '19045',
- 'La Chapelle-Saint-Jean (24390)' => '24113',
- 'La Chapelle-Saint-Laurent (79430)' => '79076',
- 'La Chapelle-Saint-Martial (23250)' => '23051',
- 'La Chapelle-Taillefert (23000)' => '23052',
- 'La Chapelle-Thireuil (79160)' => '79077',
- 'La Chaussade (23200)' => '23059',
- 'La Chaussée (86330)' => '86069',
- 'La Chèvrerie (16240)' => '16098',
- 'La Clisse (17600)' => '17112',
- 'La Clotte (17360)' => '17113',
- 'La Coquille (24450)' => '24133',
- 'La Couarde (79800)' => '79098',
- 'La Couarde-sur-Mer (17670)' => '17121',
- 'La Couronne (16400)' => '16113',
- 'La Courtine (23100)' => '23067',
- 'La Crèche (79260)' => '79048',
- 'La Croisille-sur-Briance (87130)' => '87051',
- 'La Croix-Blanche (47340)' => '47075',
- 'La Croix-Comtesse (17330)' => '17137',
- 'La Croix-sur-Gartempe (87210)' => '87052',
- 'La Dornac (24120)' => '24153',
- 'La Douze (24330)' => '24156',
- 'La Faye (16700)' => '16136',
- 'La Ferrière-Airoux (86160)' => '86097',
- 'La Ferrière-en-Parthenay (79390)' => '79120',
- 'La Feuillade (24120)' => '24179',
- 'La Flotte (17630)' => '17161',
- 'La Force (24130)' => '24222',
- 'La Forêt-de-Tessé (16240)' => '16142',
- 'La Forêt-du-Temple (23360)' => '23084',
- 'La Forêt-sur-Sèvre (79380)' => '79123',
- 'La Foye-Monjault (79360)' => '79127',
- 'La Frédière (17770)' => '17169',
- 'La Genétouze (17360)' => '17173',
- 'La Geneytouse (87400)' => '87070',
- 'La Gonterie-Boulouneix (24310)' => '24198',
- 'La Grève-sur-Mignon (17170)' => '17182',
- 'La Grimaudière (86330)' => '86108',
- 'La Gripperie-Saint-Symphorien (17620)' => '17184',
- 'La Jard (17460)' => '17191',
- 'La Jarne (17220)' => '17193',
- 'La Jarrie (17220)' => '17194',
- 'La Jarrie-Audouin (17330)' => '17195',
- 'La Jemaye (24410)' => '24216',
- 'La Jonchère-Saint-Maurice (87340)' => '87079',
- 'La Laigne (17170)' => '17201',
- 'La Lande-de-Fronsac (33240)' => '33219',
- 'La Magdeleine (16240)' => '16197',
- 'La Mazière-aux-Bons-Hommes (23260)' => '23129',
- 'La Meyze (87800)' => '87096',
- 'La Mothe-Saint-Héray (79800)' => '79184',
- 'La Nouaille (23500)' => '23144',
- 'La Péruse (16270)' => '16259',
- 'La Petite-Boissière (79700)' => '79207',
- 'La Peyratte (79200)' => '79208',
- 'La Porcherie (87380)' => '87120',
- 'La Pouge (23250)' => '23157',
- 'La Puye (86260)' => '86202',
- 'La Réole (33190)' => '33352',
- 'La Réunion (47700)' => '47222',
- 'La Rivière (33126)' => '33356',
- 'La Roche-Canillac (19320)' => '19174',
- 'La Roche-Chalais (24490)' => '24354',
- 'La Roche-l\'Abeille (87800)' => '87127',
- 'La Roche-Posay (86270)' => '86207',
- 'La Roche-Rigault (86200)' => '86079',
- 'La Rochebeaucourt-et-Argentine (24340)' => '24353',
- 'La Rochefoucauld (16110)' => '16281',
- 'La Rochelle (17000)' => '17300',
- 'La Rochénard (79270)' => '79229',
- 'La Rochette (16110)' => '16282',
- 'La Ronde (17170)' => '17303',
- 'La Roque-Gageac (24250)' => '24355',
- 'La Roquille (33220)' => '33360',
- 'La Saunière (23000)' => '23169',
- 'La Sauve (33670)' => '33505',
- 'La Sauvetat-de-Savères (47270)' => '47289',
- 'La Sauvetat-du-Dropt (47800)' => '47290',
- 'La Sauvetat-sur-Lède (47150)' => '47291',
- 'La Serre-Bussière-Vieille (23190)' => '23172',
- 'La Souterraine (23300)' => '23176',
- 'La Tâche (16260)' => '16377',
- 'La Teste-de-Buch (33260)' => '33529',
- 'La Tour-Blanche (24320)' => '24554',
- 'La Tremblade (17390)' => '17452',
- 'La Trimouille (86290)' => '86273',
- 'La Vallée (17250)' => '17455',
- 'La Vergne (17400)' => '17465',
- 'La Villedieu (17470)' => '17471',
- 'La Villedieu (23340)' => '23264',
- 'La Villedieu-du-Clain (86340)' => '86290',
- 'La Villeneuve (23260)' => '23265',
- 'La Villetelle (23260)' => '23266',
- 'Laà-Mondrans (64300)' => '64286',
- 'Laàs (64390)' => '64287',
- 'Labarde (33460)' => '33211',
- 'Labastide-Castel-Amouroux (47250)' => '47121',
- 'Labastide-Cézéracq (64170)' => '64288',
- 'Labastide-Chalosse (40700)' => '40130',
- 'Labastide-d\'Armagnac (40240)' => '40131',
- 'Labastide-Monréjeau (64170)' => '64290',
- 'Labastide-Villefranche (64270)' => '64291',
- 'Labatmale (64530)' => '64292',
- 'Labatut (40300)' => '40132',
- 'Labatut (64460)' => '64293',
- 'Labenne (40530)' => '40133',
- 'Labescau (33690)' => '33212',
- 'Labets-Biscay (64120)' => '64294',
- 'Labeyrie (64300)' => '64295',
- 'Labouheyre (40210)' => '40134',
- 'Labretonie (47350)' => '47122',
- 'Labrit (40420)' => '40135',
- 'Lacadée (64300)' => '64296',
- 'Lacajunte (40320)' => '40136',
- 'Lacanau (33680)' => '33214',
- 'Lacapelle-Biron (47150)' => '47123',
- 'Lacarre (64220)' => '64297',
- 'Lacarry-Arhan-Charritte-de-Haut (64470)' => '64298',
- 'Lacaussade (47150)' => '47124',
- 'Lacelle (19170)' => '19095',
- 'Lacépède (47360)' => '47125',
- 'Lachaise (16300)' => '16176',
- 'Lachapelle (47350)' => '47126',
- 'Lacommande (64360)' => '64299',
- 'Lacq (64170)' => '64300',
- 'Lacquy (40120)' => '40137',
- 'Lacrabe (40700)' => '40138',
- 'Lacropte (24380)' => '24220',
- 'Ladapeyre (23270)' => '23102',
- 'Ladaux (33760)' => '33215',
- 'Ladignac-le-Long (87500)' => '87082',
- 'Ladignac-sur-Rondelles (19150)' => '19096',
- 'Ladiville (16120)' => '16177',
- 'Lados (33124)' => '33216',
- 'Lafage-sur-Sombre (19320)' => '19097',
- 'Lafat (23800)' => '23103',
- 'Lafitte-sur-Lot (47320)' => '47127',
- 'Lafox (47240)' => '47128',
- 'Lagarde-Enval (19150)' => '19098',
- 'Lagarde-sur-le-Né (16300)' => '16178',
- 'Lagarrigue (47190)' => '47129',
- 'Lageon (79200)' => '79145',
- 'Lagleygeolle (19500)' => '19099',
- 'Laglorieuse (40090)' => '40139',
- 'Lagor (64150)' => '64301',
- 'Lagorce (33230)' => '33218',
- 'Lagord (17140)' => '17200',
- 'Lagos (64800)' => '64302',
- 'Lagrange (40240)' => '40140',
- 'Lagraulière (19700)' => '19100',
- 'Lagruère (47400)' => '47130',
- 'Laguenne (19150)' => '19101',
- 'Laguinge-Restoue (64470)' => '64303',
- 'Lagupie (47180)' => '47131',
- 'Lahonce (64990)' => '64304',
- 'Lahontan (64270)' => '64305',
- 'Lahosse (40250)' => '40141',
- 'Lahourcade (64150)' => '64306',
- 'Lalande-de-Pomerol (33500)' => '33222',
- 'Lalandusse (47330)' => '47132',
- 'Lalinde (24150)' => '24223',
- 'Lalongue (64350)' => '64307',
- 'Lalonquette (64450)' => '64308',
- 'Laluque (40465)' => '40142',
- 'Lamarque (33460)' => '33220',
- 'Lamayou (64460)' => '64309',
- 'Lamazière-Basse (19160)' => '19102',
- 'Lamazière-Haute (19340)' => '19103',
- 'Lamongerie (19510)' => '19104',
- 'Lamontjoie (47310)' => '47133',
- 'Lamonzie-Montastruc (24520)' => '24224',
- 'Lamonzie-Saint-Martin (24680)' => '24225',
- 'Lamothe (40250)' => '40143',
- 'Lamothe-Landerron (33190)' => '33221',
- 'Lamothe-Montravel (24230)' => '24226',
- 'Landerrouat (33790)' => '33223',
- 'Landerrouet-sur-Ségur (33540)' => '33224',
- 'Landes (17380)' => '17202',
- 'Landiras (33720)' => '33225',
- 'Landrais (17290)' => '17203',
- 'Langoiran (33550)' => '33226',
- 'Langon (33210)' => '33227',
- 'Lanne-en-Barétous (64570)' => '64310',
- 'Lannecaube (64350)' => '64311',
- 'Lanneplaà (64300)' => '64312',
- 'Lannes (47170)' => '47134',
- 'Lanouaille (24270)' => '24227',
- 'Lanquais (24150)' => '24228',
- 'Lansac (33710)' => '33228',
- 'Lantabat (64640)' => '64313',
- 'Lanteuil (19190)' => '19105',
- 'Lanton (33138)' => '33229',
- 'Laparade (47260)' => '47135',
- 'Laperche (47800)' => '47136',
- 'Lapleau (19550)' => '19106',
- 'Laplume (47310)' => '47137',
- 'Lapouyade (33620)' => '33230',
- 'Laprade (16390)' => '16180',
- 'Larbey (40250)' => '40144',
- 'Larceveau-Arros-Cibits (64120)' => '64314',
- 'Larche (19600)' => '19107',
- 'Largeasse (79240)' => '79147',
- 'Laroche-près-Feyt (19340)' => '19108',
- 'Laroin (64110)' => '64315',
- 'Laroque (33410)' => '33231',
- 'Laroque-Timbaut (47340)' => '47138',
- 'Larrau (64560)' => '64316',
- 'Larressore (64480)' => '64317',
- 'Larreule (64410)' => '64318',
- 'Larribar-Sorhapuru (64120)' => '64319',
- 'Larrivière-Saint-Savin (40270)' => '40145',
- 'Lartigue (33840)' => '33232',
- 'Laruns (64440)' => '64320',
- 'Laruscade (33620)' => '33233',
- 'Larzac (24170)' => '24230',
- 'Lascaux (19130)' => '19109',
- 'Lasclaveries (64450)' => '64321',
- 'Lasse (64220)' => '64322',
- 'Lasserre (47600)' => '47139',
- 'Lasserre (64350)' => '64323',
- 'Lasseube (64290)' => '64324',
- 'Lasseubetat (64290)' => '64325',
- 'Lathus-Saint-Rémy (86390)' => '86120',
- 'Latillé (86190)' => '86121',
- 'Latresne (33360)' => '33234',
- 'Latrille (40800)' => '40146',
- 'Latronche (19160)' => '19110',
- 'Laugnac (47360)' => '47140',
- 'Laurède (40250)' => '40147',
- 'Lauret (40320)' => '40148',
- 'Laurière (87370)' => '87083',
- 'Laussou (47150)' => '47141',
- 'Lauthiers (86300)' => '86122',
- 'Lauzun (47410)' => '47142',
- 'Laval-sur-Luzège (19550)' => '19111',
- 'Lavalade (24540)' => '24231',
- 'Lavardac (47230)' => '47143',
- 'Lavaufranche (23600)' => '23104',
- 'Lavaur (24550)' => '24232',
- 'Lavausseau (86470)' => '86123',
- 'Lavaveix-les-Mines (23150)' => '23105',
- 'Lavazan (33690)' => '33235',
- 'Lavergne (47800)' => '47144',
- 'Laveyssière (24130)' => '24233',
- 'Lavignac (87230)' => '87084',
- 'Lavoux (86800)' => '86124',
- 'Lay-Lamidou (64190)' => '64326',
- 'Layrac (47390)' => '47145',
- 'Le Barp (33114)' => '33029',
- 'Le Beugnon (79130)' => '79035',
- 'Le Bois-Plage-en-Ré (17580)' => '17051',
- 'Le Bouchage (16350)' => '16054',
- 'Le Bourdeix (24300)' => '24056',
- 'Le Bourdet (79210)' => '79046',
- 'Le Bourg-d\'Hem (23220)' => '23029',
- 'Le Bouscat (33110)' => '33069',
- 'Le Breuil-Bernard (79320)' => '79051',
- 'Le Bugue (24260)' => '24067',
- 'Le Buis (87140)' => '87023',
- 'Le Buisson-de-Cadouin (24480)' => '24068',
- 'Le Busseau (79240)' => '79059',
- 'Le Chalard (87500)' => '87031',
- 'Le Change (24640)' => '24103',
- 'Le Chastang (19190)' => '19048',
- 'Le Château-d\'Oléron (17480)' => '17093',
- 'Le Châtenet-en-Dognon (87400)' => '87042',
- 'Le Chauchet (23130)' => '23058',
- 'Le Chay (17600)' => '17097',
- 'Le Chillou (79600)' => '79089',
- 'Le Compas (23700)' => '23066',
- 'Le Donzeil (23480)' => '23074',
- 'Le Dorat (87210)' => '87059',
- 'Le Douhet (17100)' => '17143',
- 'Le Fieu (33230)' => '33166',
- 'Le Fleix (24130)' => '24182',
- 'Le Fouilloux (17270)' => '17167',
- 'Le Frêche (40190)' => '40100',
- 'Le Gicq (17160)' => '17177',
- 'Le Grand-Bourg (23240)' => '23095',
- 'Le Grand-Madieu (16450)' => '16157',
- 'Le Grand-Village-Plage (17370)' => '17485',
- 'Le Gua (17600)' => '17185',
- 'Le Gué-d\'Alleré (17540)' => '17186',
- 'Le Haillan (33185)' => '33200',
- 'Le Jardin (19300)' => '19092',
- 'Le Lardin-Saint-Lazare (24570)' => '24229',
- 'Le Leuy (40250)' => '40153',
- 'Le Lindois (16310)' => '16188',
- 'Le Lonzac (19470)' => '19118',
- 'Le Mas-d\'Agenais (47430)' => '47159',
- 'Le Mas-d\'Artige (23100)' => '23125',
- 'Le Monteil-au-Vicomte (23460)' => '23134',
- 'Le Mung (17350)' => '17252',
- 'Le Nizan (33430)' => '33305',
- 'Le Palais-sur-Vienne (87410)' => '87113',
- 'Le Passage (47520)' => '47201',
- 'Le Pescher (19190)' => '19163',
- 'Le Pian-Médoc (33290)' => '33322',
- 'Le Pian-sur-Garonne (33490)' => '33323',
- 'Le Pin (17210)' => '17276',
- 'Le Pin (79140)' => '79210',
- 'Le Pizou (24700)' => '24329',
- 'Le Porge (33680)' => '33333',
- 'Le Pout (33670)' => '33335',
- 'Le Puy (33580)' => '33345',
- 'Le Retail (79130)' => '79226',
- 'Le Rochereau (86170)' => '86208',
- 'Le Sen (40420)' => '40297',
- 'Le Seure (17770)' => '17426',
- 'Le Taillan-Médoc (33320)' => '33519',
- 'Le Tallud (79200)' => '79322',
- 'Le Tâtre (16360)' => '16380',
- 'Le Teich (33470)' => '33527',
- 'Le Temple (33680)' => '33528',
- 'Le Temple-sur-Lot (47110)' => '47306',
- 'Le Thou (17290)' => '17447',
- 'Le Tourne (33550)' => '33534',
- 'Le Tuzan (33125)' => '33536',
- 'Le Vanneau-Irleau (79270)' => '79337',
- 'Le Verdon-sur-Mer (33123)' => '33544',
- 'Le Vert (79170)' => '79346',
- 'Le Vieux-Cérier (16350)' => '16403',
- 'Le Vigeant (86150)' => '86289',
- 'Le Vigen (87110)' => '87205',
- 'Le Vignau (40270)' => '40329',
- 'Lecumberry (64220)' => '64327',
- 'Lédat (47300)' => '47146',
- 'Ledeuix (64400)' => '64328',
- 'Lée (64320)' => '64329',
- 'Lées-Athas (64490)' => '64330',
- 'Lège-Cap-Ferret (33950)' => '33236',
- 'Léguillac-de-Cercles (24340)' => '24235',
- 'Léguillac-de-l\'Auche (24110)' => '24236',
- 'Leigné-les-Bois (86450)' => '86125',
- 'Leigné-sur-Usseau (86230)' => '86127',
- 'Leignes-sur-Fontaine (86300)' => '86126',
- 'Lembeye (64350)' => '64331',
- 'Lembras (24100)' => '24237',
- 'Lème (64450)' => '64332',
- 'Lempzours (24800)' => '24238',
- 'Lencloître (86140)' => '86128',
- 'Lencouacq (40120)' => '40149',
- 'Léogeats (33210)' => '33237',
- 'Léognan (33850)' => '33238',
- 'Léon (40550)' => '40150',
- 'Léoville (17500)' => '17204',
- 'Lépaud (23170)' => '23106',
- 'Lépinas (23150)' => '23107',
- 'Léren (64270)' => '64334',
- 'Lerm-et-Musset (33840)' => '33239',
- 'Les Adjots (16700)' => '16002',
- 'Les Alleuds (79190)' => '79006',
- 'Les Angles-sur-Corrèze (19000)' => '19009',
- 'Les Artigues-de-Lussac (33570)' => '33014',
- 'Les Billanges (87340)' => '87016',
- 'Les Billaux (33500)' => '33052',
- 'Les Cars (87230)' => '87029',
- 'Les Éduts (17510)' => '17149',
- 'Les Églises-d\'Argenteuil (17400)' => '17150',
- 'Les Églisottes-et-Chalaures (33230)' => '33154',
- 'Les Essards (16210)' => '16130',
- 'Les Essards (17250)' => '17154',
- 'Les Esseintes (33190)' => '33158',
- 'Les Eyzies-de-Tayac-Sireuil (24620)' => '24172',
- 'Les Farges (24290)' => '24175',
- 'Les Forges (79340)' => '79124',
- 'Les Fosses (79360)' => '79126',
- 'Les Gonds (17100)' => '17179',
- 'Les Gours (16140)' => '16155',
- 'Les Grands-Chézeaux (87160)' => '87074',
- 'Les Graulges (24340)' => '24203',
- 'Les Groseillers (79220)' => '79139',
- 'Les Lèches (24400)' => '24234',
- 'Les Lèves-et-Thoumeyragues (33220)' => '33242',
- 'Les Mars (23700)' => '23123',
- 'Les Mathes (17570)' => '17225',
- 'Les Métairies (16200)' => '16220',
- 'Les Nouillers (17380)' => '17266',
- 'Les Ormes (86220)' => '86183',
- 'Les Peintures (33230)' => '33315',
- 'Les Pins (16260)' => '16261',
- 'Les Portes-en-Ré (17880)' => '17286',
- 'Les Salles-de-Castillon (33350)' => '33499',
- 'Les Salles-Lavauguyon (87440)' => '87189',
- 'Les Touches-de-Périgny (17160)' => '17451',
- 'Les Trois-Moutiers (86120)' => '86274',
- 'Lescar (64230)' => '64335',
- 'Lescun (64490)' => '64336',
- 'Lesgor (40400)' => '40151',
- 'Lésignac-Durand (16310)' => '16183',
- 'Lésigny (86270)' => '86129',
- 'Lesparre-Médoc (33340)' => '33240',
- 'Lesperon (40260)' => '40152',
- 'Lespielle (64350)' => '64337',
- 'Lespourcy (64160)' => '64338',
- 'Lessac (16500)' => '16181',
- 'Lestards (19170)' => '19112',
- 'Lestelle-Bétharram (64800)' => '64339',
- 'Lesterps (16420)' => '16182',
- 'Lestiac-sur-Garonne (33550)' => '33241',
- 'Leugny (86220)' => '86130',
- 'Lévignac-de-Guyenne (47120)' => '47147',
- 'Lévignacq (40170)' => '40154',
- 'Leyrat (23600)' => '23108',
- 'Leyritz-Moncassin (47700)' => '47148',
- 'Lezay (79120)' => '79148',
- 'Lhommaizé (86410)' => '86131',
- 'Lhoumois (79390)' => '79149',
- 'Libourne (33500)' => '33243',
- 'Lichans-Sunhar (64470)' => '64340',
- 'Lichères (16460)' => '16184',
- 'Lichos (64130)' => '64341',
- 'Licq-Athérey (64560)' => '64342',
- 'Liginiac (19160)' => '19113',
- 'Liglet (86290)' => '86132',
- 'Lignan-de-Bazas (33430)' => '33244',
- 'Lignan-de-Bordeaux (33360)' => '33245',
- 'Lignareix (19200)' => '19114',
- 'Ligné (16140)' => '16185',
- 'Ligneyrac (19500)' => '19115',
- 'Lignières-Sonneville (16130)' => '16186',
- 'Ligueux (33220)' => '33246',
- 'Ligugé (86240)' => '86133',
- 'Limalonges (79190)' => '79150',
- 'Limendous (64420)' => '64343',
- 'Limeuil (24510)' => '24240',
- 'Limeyrat (24210)' => '24241',
- 'Limoges (87000)' => '87085',
- 'Linard (23220)' => '23109',
- 'Linards (87130)' => '87086',
- 'Linars (16730)' => '16187',
- 'Linazay (86400)' => '86134',
- 'Liniers (86800)' => '86135',
- 'Linxe (40260)' => '40155',
- 'Liorac-sur-Louyre (24520)' => '24242',
- 'Liourdres (19120)' => '19116',
- 'Lioux-les-Monges (23700)' => '23110',
- 'Liposthey (40410)' => '40156',
- 'Lisle (24350)' => '24243',
- 'Lissac-sur-Couze (19600)' => '19117',
- 'Listrac-de-Durèze (33790)' => '33247',
- 'Listrac-Médoc (33480)' => '33248',
- 'Lit-et-Mixe (40170)' => '40157',
- 'Livron (64530)' => '64344',
- 'Lizant (86400)' => '86136',
- 'Lizières (23240)' => '23111',
- 'Lohitzun-Oyhercq (64120)' => '64345',
- 'Loire-les-Marais (17870)' => '17205',
- 'Loiré-sur-Nie (17470)' => '17206',
- 'Loix (17111)' => '17207',
- 'Lolme (24540)' => '24244',
- 'Lombia (64160)' => '64346',
- 'Lonçon (64410)' => '64347',
- 'Londigny (16700)' => '16189',
- 'Longèves (17230)' => '17208',
- 'Longré (16240)' => '16190',
- 'Longueville (47200)' => '47150',
- 'Lonnes (16230)' => '16191',
- 'Lons (64140)' => '64348',
- 'Lonzac (17520)' => '17209',
- 'Lorignac (17240)' => '17210',
- 'Lorigné (79190)' => '79152',
- 'Lormont (33310)' => '33249',
- 'Losse (40240)' => '40158',
- 'Lostanges (19500)' => '19119',
- 'Loubejac (24550)' => '24245',
- 'Loubens (33190)' => '33250',
- 'Loubès-Bernac (47120)' => '47151',
- 'Loubieng (64300)' => '64349',
- 'Loubigné (79110)' => '79153',
- 'Loubillé (79110)' => '79154',
- 'Louchats (33125)' => '33251',
- 'Loudun (86200)' => '86137',
- 'Louer (40380)' => '40159',
- 'Lougratte (47290)' => '47152',
- 'Louhossoa (64250)' => '64350',
- 'Louignac (19310)' => '19120',
- 'Louin (79600)' => '79156',
- 'Loulay (17330)' => '17211',
- 'Loupes (33370)' => '33252',
- 'Loupiac (33410)' => '33253',
- 'Loupiac-de-la-Réole (33190)' => '33254',
- 'Lourdios-Ichère (64570)' => '64351',
- 'Lourdoueix-Saint-Pierre (23360)' => '23112',
- 'Lourenties (64420)' => '64352',
- 'Lourquen (40250)' => '40160',
- 'Louvie-Juzon (64260)' => '64353',
- 'Louvie-Soubiron (64440)' => '64354',
- 'Louvigny (64410)' => '64355',
- 'Louzac-Saint-André (16100)' => '16193',
- 'Louzignac (17160)' => '17212',
- 'Louzy (79100)' => '79157',
- 'Lozay (17330)' => '17213',
- 'Lubbon (40240)' => '40161',
- 'Lubersac (19210)' => '19121',
- 'Luc-Armau (64350)' => '64356',
- 'Lucarré (64350)' => '64357',
- 'Lucbardez-et-Bargues (40090)' => '40162',
- 'Lucgarier (64420)' => '64358',
- 'Luchapt (86430)' => '86138',
- 'Luchat (17600)' => '17214',
- 'Luché-sur-Brioux (79170)' => '79158',
- 'Luché-Thouarsais (79330)' => '79159',
- 'Lucmau (33840)' => '33255',
- 'Lucq-de-Béarn (64360)' => '64359',
- 'Ludon-Médoc (33290)' => '33256',
- 'Lüe (40210)' => '40163',
- 'Lugaignac (33420)' => '33257',
- 'Lugasson (33760)' => '33258',
- 'Luglon (40630)' => '40165',
- 'Lugon-et-l\'Île-du-Carnay (33240)' => '33259',
- 'Lugos (33830)' => '33260',
- 'Lunas (24130)' => '24246',
- 'Lupersat (23190)' => '23113',
- 'Lupsault (16140)' => '16194',
- 'Luquet (65320)' => '65292',
- 'Lurbe-Saint-Christau (64660)' => '64360',
- 'Lusignac (24320)' => '24247',
- 'Lusignan (86600)' => '86139',
- 'Lusignan-Petit (47360)' => '47154',
- 'Lussac (16450)' => '16195',
- 'Lussac (17500)' => '17215',
- 'Lussac (33570)' => '33261',
- 'Lussac-les-Châteaux (86320)' => '86140',
- 'Lussac-les-Églises (87360)' => '87087',
- 'Lussagnet (40270)' => '40166',
- 'Lussagnet-Lusson (64160)' => '64361',
- 'Lussant (17430)' => '17216',
- 'Lussas-et-Nontronneau (24300)' => '24248',
- 'Lussat (23170)' => '23114',
- 'Lusseray (79170)' => '79160',
- 'Luxé (16230)' => '16196',
- 'Luxe-Sumberraute (64120)' => '64362',
- 'Luxey (40430)' => '40167',
- 'Luzay (79100)' => '79161',
- 'Lys (64260)' => '64363',
- 'Macau (33460)' => '33262',
- 'Macaye (64240)' => '64364',
- 'Macqueville (17490)' => '17217',
- 'Madaillan (47360)' => '47155',
- 'Madirac (33670)' => '33263',
- 'Madranges (19470)' => '19122',
- 'Magescq (40140)' => '40168',
- 'Magnac-Bourg (87380)' => '87088',
- 'Magnac-Laval (87190)' => '87089',
- 'Magnac-Lavalette-Villars (16320)' => '16198',
- 'Magnac-sur-Touvre (16600)' => '16199',
- 'Magnat-l\'Étrange (23260)' => '23115',
- 'Magné (79460)' => '79162',
- 'Magné (86160)' => '86141',
- 'Mailhac-sur-Benaize (87160)' => '87090',
- 'Maillas (40120)' => '40169',
- 'Maillé (86190)' => '86142',
- 'Maillères (40120)' => '40170',
- 'Maine-de-Boixe (16230)' => '16200',
- 'Mainsat (23700)' => '23116',
- 'Mainxe (16200)' => '16202',
- 'Mainzac (16380)' => '16203',
- 'Mairé (86270)' => '86143',
- 'Mairé-Levescault (79190)' => '79163',
- 'Maison-Feyne (23800)' => '23117',
- 'Maisonnais-sur-Tardoire (87440)' => '87091',
- 'Maisonnay (79500)' => '79164',
- 'Maisonneuve (86170)' => '86144',
- 'Maisonnisses (23150)' => '23118',
- 'Maisontiers (79600)' => '79165',
- 'Malaussanne (64410)' => '64365',
- 'Malaville (16120)' => '16204',
- 'Malemort (19360)' => '19123',
- 'Malleret (23260)' => '23119',
- 'Malleret-Boussac (23600)' => '23120',
- 'Malval (23220)' => '23121',
- 'Manaurie (24620)' => '24249',
- 'Mano (40410)' => '40171',
- 'Manot (16500)' => '16205',
- 'Mansac (19520)' => '19124',
- 'Mansat-la-Courrière (23400)' => '23122',
- 'Mansle (16230)' => '16206',
- 'Mant (40700)' => '40172',
- 'Manzac-sur-Vern (24110)' => '24251',
- 'Marans (17230)' => '17218',
- 'Maransin (33230)' => '33264',
- 'Marc-la-Tour (19150)' => '19127',
- 'Marçay (86370)' => '86145',
- 'Marcellus (47200)' => '47156',
- 'Marcenais (33620)' => '33266',
- 'Marcheprime (33380)' => '33555',
- 'Marcillac (33860)' => '33267',
- 'Marcillac-la-Croisille (19320)' => '19125',
- 'Marcillac-la-Croze (19500)' => '19126',
- 'Marcillac-Lanville (16140)' => '16207',
- 'Marcillac-Saint-Quentin (24200)' => '24252',
- 'Marennes (17320)' => '17219',
- 'Mareuil (16170)' => '16208',
- 'Mareuil (24340)' => '24253',
- 'Margaux (33460)' => '33268',
- 'Margerides (19200)' => '19128',
- 'Margueron (33220)' => '33269',
- 'Marignac (17800)' => '17220',
- 'Marigny (79360)' => '79166',
- 'Marigny-Brizay (86380)' => '86146',
- 'Marigny-Chemereau (86370)' => '86147',
- 'Marillac-le-Franc (16110)' => '16209',
- 'Marimbault (33430)' => '33270',
- 'Marions (33690)' => '33271',
- 'Marmande (47200)' => '47157',
- 'Marmont-Pachas (47220)' => '47158',
- 'Marnac (24220)' => '24254',
- 'Marnay (86160)' => '86148',
- 'Marnes (79600)' => '79167',
- 'Marpaps (40330)' => '40173',
- 'Marquay (24620)' => '24255',
- 'Marsac (16570)' => '16210',
- 'Marsac (23210)' => '23124',
- 'Marsac-sur-l\'Isle (24430)' => '24256',
- 'Marsais (17700)' => '17221',
- 'Marsalès (24540)' => '24257',
- 'Marsaneix (24750)' => '24258',
- 'Marsas (33620)' => '33272',
- 'Marsilly (17137)' => '17222',
- 'Martaizé (86330)' => '86149',
- 'Marthon (16380)' => '16211',
- 'Martignas-sur-Jalle (33127)' => '33273',
- 'Martillac (33650)' => '33274',
- 'Martres (33760)' => '33275',
- 'Marval (87440)' => '87092',
- 'Masbaraud-Mérignat (23400)' => '23126',
- 'Mascaraàs-Haron (64330)' => '64366',
- 'Maslacq (64300)' => '64367',
- 'Masléon (87130)' => '87093',
- 'Masparraute (64120)' => '64368',
- 'Maspie-Lalonquère-Juillacq (64350)' => '64369',
- 'Masquières (47370)' => '47160',
- 'Massac (17490)' => '17223',
- 'Massais (79150)' => '79168',
- 'Masseilles (33690)' => '33276',
- 'Massels (47140)' => '47161',
- 'Masseret (19510)' => '19129',
- 'Massignac (16310)' => '16212',
- 'Massognes (86170)' => '86150',
- 'Massoulès (47140)' => '47162',
- 'Massugas (33790)' => '33277',
- 'Matha (17160)' => '17224',
- 'Maucor (64160)' => '64370',
- 'Maulay (86200)' => '86151',
- 'Mauléon (79700)' => '79079',
- 'Mauléon-Licharre (64130)' => '64371',
- 'Mauprévoir (86460)' => '86152',
- 'Maure (64460)' => '64372',
- 'Maurens (24140)' => '24259',
- 'Mauriac (33540)' => '33278',
- 'Mauries (40320)' => '40174',
- 'Maurrin (40270)' => '40175',
- 'Maussac (19250)' => '19130',
- 'Mautes (23190)' => '23127',
- 'Mauvezin-d\'Armagnac (40240)' => '40176',
- 'Mauvezin-sur-Gupie (47200)' => '47163',
- 'Mauzac-et-Grand-Castang (24150)' => '24260',
- 'Mauzé-sur-le-Mignon (79210)' => '79170',
- 'Mauzé-Thouarsais (79100)' => '79171',
- 'Mauzens-et-Miremont (24260)' => '24261',
- 'Mayac (24420)' => '24262',
- 'Maylis (40250)' => '40177',
- 'Mazeirat (23150)' => '23128',
- 'Mazeray (17400)' => '17226',
- 'Mazères (33210)' => '33279',
- 'Mazères-Lezons (64110)' => '64373',
- 'Mazerolles (16310)' => '16213',
- 'Mazerolles (17800)' => '17227',
- 'Mazerolles (40090)' => '40178',
- 'Mazerolles (64230)' => '64374',
- 'Mazerolles (86320)' => '86153',
- 'Mazeuil (86110)' => '86154',
- 'Mazeyrolles (24550)' => '24263',
- 'Mazières (16270)' => '16214',
- 'Mazières-en-Gâtine (79310)' => '79172',
- 'Mazières-Naresse (47210)' => '47164',
- 'Mazières-sur-Béronne (79500)' => '79173',
- 'Mazion (33390)' => '33280',
- 'Méasnes (23360)' => '23130',
- 'Médillac (16210)' => '16215',
- 'Médis (17600)' => '17228',
- 'Mées (40990)' => '40179',
- 'Méharin (64120)' => '64375',
- 'Meilhac (87800)' => '87094',
- 'Meilhan (40400)' => '40180',
- 'Meilhan-sur-Garonne (47180)' => '47165',
- 'Meilhards (19510)' => '19131',
- 'Meillon (64510)' => '64376',
- 'Melle (79500)' => '79174',
- 'Melleran (79190)' => '79175',
- 'Mendionde (64240)' => '64377',
- 'Menditte (64130)' => '64378',
- 'Mendive (64220)' => '64379',
- 'Ménesplet (24700)' => '24264',
- 'Ménigoute (79340)' => '79176',
- 'Ménoire (19190)' => '19132',
- 'Mensignac (24350)' => '24266',
- 'Méracq (64410)' => '64380',
- 'Mercoeur (19430)' => '19133',
- 'Mérignac (16200)' => '16216',
- 'Mérignac (17210)' => '17229',
- 'Mérignac (33700)' => '33281',
- 'Mérignas (33350)' => '33282',
- 'Mérinchal (23420)' => '23131',
- 'Méritein (64190)' => '64381',
- 'Merlines (19340)' => '19134',
- 'Merpins (16100)' => '16217',
- 'Meschers-sur-Gironde (17132)' => '17230',
- 'Mescoules (24240)' => '24267',
- 'Mesnac (16370)' => '16218',
- 'Mesplède (64370)' => '64382',
- 'Messac (17130)' => '17231',
- 'Messanges (40660)' => '40181',
- 'Messé (79120)' => '79177',
- 'Messemé (86200)' => '86156',
- 'Mesterrieux (33540)' => '33283',
- 'Mestes (19200)' => '19135',
- 'Meursac (17120)' => '17232',
- 'Meux (17500)' => '17233',
- 'Meuzac (87380)' => '87095',
- 'Meymac (19250)' => '19136',
- 'Meyrals (24220)' => '24268',
- 'Meyrignac-l\'Église (19800)' => '19137',
- 'Meyssac (19500)' => '19138',
- 'Mézin (47170)' => '47167',
- 'Mézos (40170)' => '40182',
- 'Mialet (24450)' => '24269',
- 'Mialos (64410)' => '64383',
- 'Mignaloux-Beauvoir (86550)' => '86157',
- 'Migné-Auxances (86440)' => '86158',
- 'Migré (17330)' => '17234',
- 'Migron (17770)' => '17235',
- 'Milhac-d\'Auberoche (24330)' => '24270',
- 'Milhac-de-Nontron (24470)' => '24271',
- 'Millac (86150)' => '86159',
- 'Millevaches (19290)' => '19139',
- 'Mimbaste (40350)' => '40183',
- 'Mimizan (40200)' => '40184',
- 'Minzac (24610)' => '24272',
- 'Mios (33380)' => '33284',
- 'Miossens-Lanusse (64450)' => '64385',
- 'Mirambeau (17150)' => '17236',
- 'Miramont-de-Guyenne (47800)' => '47168',
- 'Miramont-Sensacq (40320)' => '40185',
- 'Mirebeau (86110)' => '86160',
- 'Mirepeix (64800)' => '64386',
- 'Missé (79100)' => '79178',
- 'Misson (40290)' => '40186',
- 'Moëze (17780)' => '17237',
- 'Moirax (47310)' => '47169',
- 'Moissannes (87400)' => '87099',
- 'Molières (24480)' => '24273',
- 'Moliets-et-Maa (40660)' => '40187',
- 'Momas (64230)' => '64387',
- 'Mombrier (33710)' => '33285',
- 'Momuy (40700)' => '40188',
- 'Momy (64350)' => '64388',
- 'Monassut-Audiracq (64160)' => '64389',
- 'Monbahus (47290)' => '47170',
- 'Monbalen (47340)' => '47171',
- 'Monbazillac (24240)' => '24274',
- 'Moncaup (64350)' => '64390',
- 'Moncaut (47310)' => '47172',
- 'Moncayolle-Larrory-Mendibieu (64130)' => '64391',
- 'Monceaux-sur-Dordogne (19400)' => '19140',
- 'Moncla (64330)' => '64392',
- 'Monclar (47380)' => '47173',
- 'Moncontour (86330)' => '86161',
- 'Moncoutant (79320)' => '79179',
- 'Moncrabeau (47600)' => '47174',
- 'Mondion (86230)' => '86162',
- 'Monein (64360)' => '64393',
- 'Monestier (24240)' => '24276',
- 'Monestier-Merlines (19340)' => '19141',
- 'Monestier-Port-Dieu (19110)' => '19142',
- 'Monfaucon (24130)' => '24277',
- 'Monflanquin (47150)' => '47175',
- 'Mongaillard (47230)' => '47176',
- 'Mongauzy (33190)' => '33287',
- 'Monget (40700)' => '40189',
- 'Monheurt (47160)' => '47177',
- 'Monmadalès (24560)' => '24278',
- 'Monmarvès (24560)' => '24279',
- 'Monpazier (24540)' => '24280',
- 'Monpezat (64350)' => '64394',
- 'Monplaisant (24170)' => '24293',
- 'Monprimblanc (33410)' => '33288',
- 'Mons (16140)' => '16221',
- 'Mons (17160)' => '17239',
- 'Monsac (24440)' => '24281',
- 'Monsaguel (24560)' => '24282',
- 'Monsec (24340)' => '24283',
- 'Monségur (33580)' => '33289',
- 'Monségur (40700)' => '40190',
- 'Monségur (47150)' => '47178',
- 'Monségur (64460)' => '64395',
- 'Monsempron-Libos (47500)' => '47179',
- 'Mont (64300)' => '64396',
- 'Mont-de-Marsan (40000)' => '40192',
- 'Mont-Disse (64330)' => '64401',
- 'Montagnac-d\'Auberoche (24210)' => '24284',
- 'Montagnac-la-Crempse (24140)' => '24285',
- 'Montagnac-sur-Auvignon (47600)' => '47180',
- 'Montagnac-sur-Lède (47150)' => '47181',
- 'Montagne (33570)' => '33290',
- 'Montagoudin (33190)' => '33291',
- 'Montagrier (24350)' => '24286',
- 'Montagut (64410)' => '64397',
- 'Montaignac-Saint-Hippolyte (19300)' => '19143',
- 'Montaigut-le-Blanc (23320)' => '23132',
- 'Montalembert (79190)' => '79180',
- 'Montamisé (86360)' => '86163',
- 'Montaner (64460)' => '64398',
- 'Montardon (64121)' => '64399',
- 'Montastruc (47380)' => '47182',
- 'Montauriol (47330)' => '47183',
- 'Montaut (24560)' => '24287',
- 'Montaut (40500)' => '40191',
- 'Montaut (47210)' => '47184',
- 'Montaut (64800)' => '64400',
- 'Montayral (47500)' => '47185',
- 'Montazeau (24230)' => '24288',
- 'Montboucher (23400)' => '23133',
- 'Montboyer (16620)' => '16222',
- 'Montbron (16220)' => '16223',
- 'Montcaret (24230)' => '24289',
- 'Montégut (40190)' => '40193',
- 'Montemboeuf (16310)' => '16225',
- 'Montendre (17130)' => '17240',
- 'Montesquieu (47130)' => '47186',
- 'Monteton (47120)' => '47187',
- 'Montferrand-du-Périgord (24440)' => '24290',
- 'Montfort (64190)' => '64403',
- 'Montfort-en-Chalosse (40380)' => '40194',
- 'Montgaillard (40500)' => '40195',
- 'Montgibaud (19210)' => '19144',
- 'Montguyon (17270)' => '17241',
- 'Monthoiron (86210)' => '86164',
- 'Montignac (24290)' => '24291',
- 'Montignac (33760)' => '33292',
- 'Montignac-Charente (16330)' => '16226',
- 'Montignac-de-Lauzun (47800)' => '47188',
- 'Montignac-le-Coq (16390)' => '16227',
- 'Montignac-Toupinerie (47350)' => '47189',
- 'Montigné (16170)' => '16228',
- 'Montils (17800)' => '17242',
- 'Montjean (16240)' => '16229',
- 'Montlieu-la-Garde (17210)' => '17243',
- 'Montmérac (16300)' => '16224',
- 'Montmoreau-Saint-Cybard (16190)' => '16230',
- 'Montmorillon (86500)' => '86165',
- 'Montory (64470)' => '64404',
- 'Montpellier-de-Médillan (17260)' => '17244',
- 'Montpeyroux (24610)' => '24292',
- 'Montpezat (47360)' => '47190',
- 'Montpon-Ménestérol (24700)' => '24294',
- 'Montpouillan (47200)' => '47191',
- 'Montravers (79140)' => '79183',
- 'Montrem (24110)' => '24295',
- 'Montreuil-Bonnin (86470)' => '86166',
- 'Montrol-Sénard (87330)' => '87100',
- 'Montrollet (16420)' => '16231',
- 'Montroy (17220)' => '17245',
- 'Monts-sur-Guesnes (86420)' => '86167',
- 'Montsoué (40500)' => '40196',
- 'Montussan (33450)' => '33293',
- 'Monviel (47290)' => '47192',
- 'Moragne (17430)' => '17246',
- 'Morcenx (40110)' => '40197',
- 'Morganx (40700)' => '40198',
- 'Morizès (33190)' => '33294',
- 'Morlaàs (64160)' => '64405',
- 'Morlanne (64370)' => '64406',
- 'Mornac (16600)' => '16232',
- 'Mornac-sur-Seudre (17113)' => '17247',
- 'Mortagne-sur-Gironde (17120)' => '17248',
- 'Mortemart (87330)' => '87101',
- 'Mortiers (17500)' => '17249',
- 'Morton (86120)' => '86169',
- 'Mortroux (23220)' => '23136',
- 'Mosnac (16120)' => '16233',
- 'Mosnac (17240)' => '17250',
- 'Mougon (79370)' => '79185',
- 'Mouguerre (64990)' => '64407',
- 'Mouhous (64330)' => '64408',
- 'Mouillac (33240)' => '33295',
- 'Mouleydier (24520)' => '24296',
- 'Moulidars (16290)' => '16234',
- 'Mouliets-et-Villemartin (33350)' => '33296',
- 'Moulin-Neuf (24700)' => '24297',
- 'Moulinet (47290)' => '47193',
- 'Moulis-en-Médoc (33480)' => '33297',
- 'Moulismes (86500)' => '86170',
- 'Moulon (33420)' => '33298',
- 'Moumour (64400)' => '64409',
- 'Mourens (33410)' => '33299',
- 'Mourenx (64150)' => '64410',
- 'Mourioux-Vieilleville (23210)' => '23137',
- 'Mouscardès (40290)' => '40199',
- 'Moussac (86150)' => '86171',
- 'Moustey (40410)' => '40200',
- 'Moustier (47800)' => '47194',
- 'Moustier-Ventadour (19300)' => '19145',
- 'Mouterre-Silly (86200)' => '86173',
- 'Mouterre-sur-Blourde (86430)' => '86172',
- 'Mouthiers-sur-Boëme (16440)' => '16236',
- 'Moutier-d\'Ahun (23150)' => '23138',
- 'Moutier-Malcard (23220)' => '23139',
- 'Moutier-Rozeille (23200)' => '23140',
- 'Moutiers-sous-Chantemerle (79320)' => '79188',
- 'Mouton (16460)' => '16237',
- 'Moutonneau (16460)' => '16238',
- 'Mouzon (16310)' => '16239',
- 'Mugron (40250)' => '40201',
- 'Muron (17430)' => '17253',
- 'Musculdy (64130)' => '64411',
- 'Mussidan (24400)' => '24299',
- 'Nabas (64190)' => '64412',
- 'Nabinaud (16390)' => '16240',
- 'Nabirat (24250)' => '24300',
- 'Nachamps (17380)' => '17254',
- 'Nadaillac (24590)' => '24301',
- 'Nailhac (24390)' => '24302',
- 'Naillat (23800)' => '23141',
- 'Naintré (86530)' => '86174',
- 'Nalliers (86310)' => '86175',
- 'Nanclars (16230)' => '16241',
- 'Nancras (17600)' => '17255',
- 'Nanteuil (79400)' => '79189',
- 'Nanteuil-Auriac-de-Bourzac (24320)' => '24303',
- 'Nanteuil-en-Vallée (16700)' => '16242',
- 'Nantheuil (24800)' => '24304',
- 'Nanthiat (24800)' => '24305',
- 'Nantiat (87140)' => '87103',
- 'Nantillé (17770)' => '17256',
- 'Narcastet (64510)' => '64413',
- 'Narp (64190)' => '64414',
- 'Narrosse (40180)' => '40202',
- 'Nassiet (40330)' => '40203',
- 'Nastringues (24230)' => '24306',
- 'Naujac-sur-Mer (33990)' => '33300',
- 'Naujan-et-Postiac (33420)' => '33301',
- 'Naussannes (24440)' => '24307',
- 'Navailles-Angos (64450)' => '64415',
- 'Navarrenx (64190)' => '64416',
- 'Naves (19460)' => '19146',
- 'Nay (64800)' => '64417',
- 'Néac (33500)' => '33302',
- 'Nedde (87120)' => '87104',
- 'Négrondes (24460)' => '24308',
- 'Néoux (23200)' => '23142',
- 'Nérac (47600)' => '47195',
- 'Nerbis (40250)' => '40204',
- 'Nercillac (16200)' => '16243',
- 'Néré (17510)' => '17257',
- 'Nérigean (33750)' => '33303',
- 'Nérignac (86150)' => '86176',
- 'Nersac (16440)' => '16244',
- 'Nespouls (19600)' => '19147',
- 'Neuffons (33580)' => '33304',
- 'Neuillac (17520)' => '17258',
- 'Neulles (17500)' => '17259',
- 'Neuvic (19160)' => '19148',
- 'Neuvic (24190)' => '24309',
- 'Neuvic-Entier (87130)' => '87105',
- 'Neuvicq (17270)' => '17260',
- 'Neuvicq-le-Château (17490)' => '17261',
- 'Neuville (19380)' => '19149',
- 'Neuville-de-Poitou (86170)' => '86177',
- 'Neuvy-Bouin (79130)' => '79190',
- 'Nexon (87800)' => '87106',
- 'Nicole (47190)' => '47196',
- 'Nieuil (16270)' => '16245',
- 'Nieuil-l\'Espoir (86340)' => '86178',
- 'Nieul (87510)' => '87107',
- 'Nieul-le-Virouil (17150)' => '17263',
- 'Nieul-lès-Saintes (17810)' => '17262',
- 'Nieul-sur-Mer (17137)' => '17264',
- 'Nieulle-sur-Seudre (17600)' => '17265',
- 'Niort (79000)' => '79191',
- 'Noailhac (19500)' => '19150',
- 'Noaillac (33190)' => '33306',
- 'Noaillan (33730)' => '33307',
- 'Noailles (19600)' => '19151',
- 'Noguères (64150)' => '64418',
- 'Nomdieu (47600)' => '47197',
- 'Nonac (16190)' => '16246',
- 'Nonards (19120)' => '19152',
- 'Nonaville (16120)' => '16247',
- 'Nontron (24300)' => '24311',
- 'Noth (23300)' => '23143',
- 'Notre-Dame-de-Sanilhac (24660)' => '24312',
- 'Nouaillé-Maupertuis (86340)' => '86180',
- 'Nouhant (23170)' => '23145',
- 'Nouic (87330)' => '87108',
- 'Nousse (40380)' => '40205',
- 'Nousty (64420)' => '64419',
- 'Nouzerines (23600)' => '23146',
- 'Nouzerolles (23360)' => '23147',
- 'Nouziers (23350)' => '23148',
- 'Nuaillé-d\'Aunis (17540)' => '17267',
- 'Nuaillé-sur-Boutonne (17470)' => '17268',
- 'Nueil-les-Aubiers (79250)' => '79195',
- 'Nueil-sous-Faye (86200)' => '86181',
- 'Objat (19130)' => '19153',
- 'Oeyregave (40300)' => '40206',
- 'Oeyreluy (40180)' => '40207',
- 'Ogenne-Camptort (64190)' => '64420',
- 'Ogeu-les-Bains (64680)' => '64421',
- 'Oiron (79100)' => '79196',
- 'Oloron-Sainte-Marie (64400)' => '64422',
- 'Omet (33410)' => '33308',
- 'Onard (40380)' => '40208',
- 'Ondres (40440)' => '40209',
- 'Onesse-Laharie (40110)' => '40210',
- 'Oraàs (64390)' => '64423',
- 'Oradour (16140)' => '16248',
- 'Oradour-Fanais (16500)' => '16249',
- 'Oradour-Saint-Genest (87210)' => '87109',
- 'Oradour-sur-Glane (87520)' => '87110',
- 'Oradour-sur-Vayres (87150)' => '87111',
- 'Orches (86230)' => '86182',
- 'Ordiarp (64130)' => '64424',
- 'Ordonnac (33340)' => '33309',
- 'Orègue (64120)' => '64425',
- 'Orgedeuil (16220)' => '16250',
- 'Orgnac-sur-Vézère (19410)' => '19154',
- 'Origne (33113)' => '33310',
- 'Orignolles (17210)' => '17269',
- 'Orin (64400)' => '64426',
- 'Oriolles (16480)' => '16251',
- 'Orion (64390)' => '64427',
- 'Orist (40300)' => '40211',
- 'Orival (16210)' => '16252',
- 'Orliac (24170)' => '24313',
- 'Orliac-de-Bar (19390)' => '19155',
- 'Orliaguet (24370)' => '24314',
- 'Oroux (79390)' => '79197',
- 'Orriule (64390)' => '64428',
- 'Orsanco (64120)' => '64429',
- 'Orthevielle (40300)' => '40212',
- 'Orthez (64300)' => '64430',
- 'Orx (40230)' => '40213',
- 'Os-Marsillon (64150)' => '64431',
- 'Ossages (40290)' => '40214',
- 'Ossas-Suhare (64470)' => '64432',
- 'Osse-en-Aspe (64490)' => '64433',
- 'Ossenx (64190)' => '64434',
- 'Osserain-Rivareyte (64390)' => '64435',
- 'Ossès (64780)' => '64436',
- 'Ostabat-Asme (64120)' => '64437',
- 'Ouillon (64160)' => '64438',
- 'Ousse (64320)' => '64439',
- 'Ousse-Suzan (40110)' => '40215',
- 'Ouzilly (86380)' => '86184',
- 'Oyré (86220)' => '86186',
- 'Ozenx-Montestrucq (64300)' => '64440',
- 'Ozillac (17500)' => '17270',
- 'Ozourt (40380)' => '40216',
- 'Pageas (87230)' => '87112',
- 'Pagolle (64120)' => '64441',
- 'Paillé (17470)' => '17271',
- 'Paillet (33550)' => '33311',
- 'Pailloles (47440)' => '47198',
- 'Paizay-le-Chapt (79170)' => '79198',
- 'Paizay-le-Sec (86300)' => '86187',
- 'Paizay-le-Tort (79500)' => '79199',
- 'Paizay-Naudouin-Embourie (16240)' => '16253',
- 'Palazinges (19190)' => '19156',
- 'Palisse (19160)' => '19157',
- 'Palluaud (16390)' => '16254',
- 'Pamplie (79220)' => '79200',
- 'Pamproux (79800)' => '79201',
- 'Panazol (87350)' => '87114',
- 'Pandrignes (19150)' => '19158',
- 'Parbayse (64360)' => '64442',
- 'Parcoul-Chenaud (24410)' => '24316',
- 'Pardaillan (47120)' => '47199',
- 'Pardies (64150)' => '64443',
- 'Pardies-Piétat (64800)' => '64444',
- 'Parempuyre (33290)' => '33312',
- 'Parentis-en-Born (40160)' => '40217',
- 'Parleboscq (40310)' => '40218',
- 'Parranquet (47210)' => '47200',
- 'Parsac-Rimondeix (23140)' => '23149',
- 'Parthenay (79200)' => '79202',
- 'Parzac (16450)' => '16255',
- 'Pas-de-Jeu (79100)' => '79203',
- 'Passirac (16480)' => '16256',
- 'Pau (64000)' => '64445',
- 'Pauillac (33250)' => '33314',
- 'Paulhiac (47150)' => '47202',
- 'Paulin (24590)' => '24317',
- 'Paunat (24510)' => '24318',
- 'Paussac-et-Saint-Vivien (24310)' => '24319',
- 'Payré (86700)' => '86188',
- 'Payros-Cazautets (40320)' => '40219',
- 'Payroux (86350)' => '86189',
- 'Pays de Belvès (24170)' => '24035',
- 'Payzac (24270)' => '24320',
- 'Pazayac (24120)' => '24321',
- 'Pécorade (40320)' => '40220',
- 'Pellegrue (33790)' => '33316',
- 'Penne-d\'Agenais (47140)' => '47203',
- 'Pensol (87440)' => '87115',
- 'Péré (17700)' => '17272',
- 'Péret-Bel-Air (19300)' => '19159',
- 'Pérignac (16250)' => '16258',
- 'Pérignac (17800)' => '17273',
- 'Périgné (79170)' => '79204',
- 'Périgny (17180)' => '17274',
- 'Périgueux (24000)' => '24322',
- 'Périssac (33240)' => '33317',
- 'Pérols-sur-Vézère (19170)' => '19160',
- 'Perpezac-le-Blanc (19310)' => '19161',
- 'Perpezac-le-Noir (19410)' => '19162',
- 'Perquie (40190)' => '40221',
- 'Pers (79190)' => '79205',
- 'Persac (86320)' => '86190',
- 'Pessac (33600)' => '33318',
- 'Pessac-sur-Dordogne (33890)' => '33319',
- 'Pessines (17810)' => '17275',
- 'Petit-Bersac (24600)' => '24323',
- 'Petit-Palais-et-Cornemps (33570)' => '33320',
- 'Peujard (33240)' => '33321',
- 'Pey (40300)' => '40222',
- 'Peyrabout (23000)' => '23150',
- 'Peyrat-de-Bellac (87300)' => '87116',
- 'Peyrat-la-Nonière (23130)' => '23151',
- 'Peyrat-le-Château (87470)' => '87117',
- 'Peyre (40700)' => '40223',
- 'Peyrehorade (40300)' => '40224',
- 'Peyrelevade (19290)' => '19164',
- 'Peyrelongue-Abos (64350)' => '64446',
- 'Peyrière (47350)' => '47204',
- 'Peyrignac (24210)' => '24324',
- 'Peyrilhac (87510)' => '87118',
- 'Peyrillac-et-Millac (24370)' => '24325',
- 'Peyrissac (19260)' => '19165',
- 'Peyzac-le-Moustier (24620)' => '24326',
- 'Pezuls (24510)' => '24327',
- 'Philondenx (40320)' => '40225',
- 'Piégut-Pluviers (24360)' => '24328',
- 'Pierre-Buffière (87260)' => '87119',
- 'Pierrefitte (19450)' => '19166',
- 'Pierrefitte (23130)' => '23152',
- 'Pierrefitte (79330)' => '79209',
- 'Piets-Plasence-Moustrou (64410)' => '64447',
- 'Pillac (16390)' => '16260',
- 'Pimbo (40320)' => '40226',
- 'Pindères (47700)' => '47205',
- 'Pindray (86500)' => '86191',
- 'Pinel-Hauterive (47380)' => '47206',
- 'Pineuilh (33220)' => '33324',
- 'Pionnat (23140)' => '23154',
- 'Pioussay (79110)' => '79211',
- 'Pisany (17600)' => '17278',
- 'Pissos (40410)' => '40227',
- 'Plaisance (24560)' => '24168',
- 'Plaisance (86500)' => '86192',
- 'Plassac (17240)' => '17279',
- 'Plassac (33390)' => '33325',
- 'Plassac-Rouffiac (16250)' => '16263',
- 'Plassay (17250)' => '17280',
- 'Plazac (24580)' => '24330',
- 'Pleine-Selve (33820)' => '33326',
- 'Pleumartin (86450)' => '86193',
- 'Pleuville (16490)' => '16264',
- 'Pliboux (79190)' => '79212',
- 'Podensac (33720)' => '33327',
- 'Poey-d\'Oloron (64400)' => '64449',
- 'Poey-de-Lescar (64230)' => '64448',
- 'Poitiers (86000)' => '86194',
- 'Polignac (17210)' => '17281',
- 'Pomarez (40360)' => '40228',
- 'Pomerol (33500)' => '33328',
- 'Pommiers-Moulons (17130)' => '17282',
- 'Pompaire (79200)' => '79213',
- 'Pompéjac (33730)' => '33329',
- 'Pompiey (47230)' => '47207',
- 'Pompignac (33370)' => '33330',
- 'Pompogne (47420)' => '47208',
- 'Pomport (24240)' => '24331',
- 'Pomps (64370)' => '64450',
- 'Pondaurat (33190)' => '33331',
- 'Pons (17800)' => '17283',
- 'Ponson-Debat-Pouts (64460)' => '64451',
- 'Ponson-Dessus (64460)' => '64452',
- 'Pont-du-Casse (47480)' => '47209',
- 'Pont-l\'Abbé-d\'Arnoult (17250)' => '17284',
- 'Pontacq (64530)' => '64453',
- 'Pontarion (23250)' => '23155',
- 'Pontcharraud (23260)' => '23156',
- 'Pontenx-les-Forges (40200)' => '40229',
- 'Ponteyraud (24410)' => '24333',
- 'Pontiacq-Viellepinte (64460)' => '64454',
- 'Pontonx-sur-l\'Adour (40465)' => '40230',
- 'Pontours (24150)' => '24334',
- 'Porchères (33660)' => '33332',
- 'Port-d\'Envaux (17350)' => '17285',
- 'Port-de-Lanne (40300)' => '40231',
- 'Port-de-Piles (86220)' => '86195',
- 'Port-des-Barques (17730)' => '17484',
- 'Port-Sainte-Foy-et-Ponchapt (33220)' => '24335',
- 'Port-Sainte-Marie (47130)' => '47210',
- 'Portet (64330)' => '64455',
- 'Portets (33640)' => '33334',
- 'Pouançay (86120)' => '86196',
- 'Pouant (86200)' => '86197',
- 'Poudenas (47170)' => '47211',
- 'Poudenx (40700)' => '40232',
- 'Pouffonds (79500)' => '79214',
- 'Pougne-Hérisson (79130)' => '79215',
- 'Pouillac (17210)' => '17287',
- 'Pouillé (86800)' => '86198',
- 'Pouillon (40350)' => '40233',
- 'Pouliacq (64410)' => '64456',
- 'Poullignac (16190)' => '16267',
- 'Poursac (16700)' => '16268',
- 'Poursay-Garnaud (17400)' => '17288',
- 'Poursiugues-Boucoue (64410)' => '64457',
- 'Poussanges (23500)' => '23158',
- 'Poussignac (47700)' => '47212',
- 'Pouydesseaux (40120)' => '40234',
- 'Poyanne (40380)' => '40235',
- 'Poyartin (40380)' => '40236',
- 'Pradines (19170)' => '19168',
- 'Prahecq (79230)' => '79216',
- 'Prailles (79370)' => '79217',
- 'Pranzac (16110)' => '16269',
- 'Prats-de-Carlux (24370)' => '24336',
- 'Prats-du-Périgord (24550)' => '24337',
- 'Prayssas (47360)' => '47213',
- 'Préchac (33730)' => '33336',
- 'Préchacq-Josbaig (64190)' => '64458',
- 'Préchacq-les-Bains (40465)' => '40237',
- 'Préchacq-Navarrenx (64190)' => '64459',
- 'Précilhon (64400)' => '64460',
- 'Préguillac (17460)' => '17289',
- 'Preignac (33210)' => '33337',
- 'Pressac (86460)' => '86200',
- 'Pressignac (16150)' => '16270',
- 'Pressignac-Vicq (24150)' => '24338',
- 'Pressigny (79390)' => '79218',
- 'Preyssac-d\'Excideuil (24160)' => '24339',
- 'Priaires (79210)' => '79219',
- 'Prignac (17160)' => '17290',
- 'Prignac-en-Médoc (33340)' => '33338',
- 'Prignac-et-Marcamps (33710)' => '33339',
- 'Prigonrieux (24130)' => '24340',
- 'Prin-Deyrançon (79210)' => '79220',
- 'Prinçay (86420)' => '86201',
- 'Prissé-la-Charrière (79360)' => '79078',
- 'Proissans (24200)' => '24341',
- 'Puch-d\'Agenais (47160)' => '47214',
- 'Pugnac (33710)' => '33341',
- 'Pugny (79320)' => '79222',
- 'Puihardy (79160)' => '79223',
- 'Puilboreau (17138)' => '17291',
- 'Puisseguin (33570)' => '33342',
- 'Pujo-le-Plan (40190)' => '40238',
- 'Pujols (33350)' => '33344',
- 'Pujols (47300)' => '47215',
- 'Pujols-sur-Ciron (33210)' => '33343',
- 'Puy-d\'Arnac (19120)' => '19169',
- 'Puy-du-Lac (17380)' => '17292',
- 'Puy-Malsignat (23130)' => '23159',
- 'Puybarban (33190)' => '33346',
- 'Puymiclan (47350)' => '47216',
- 'Puymirol (47270)' => '47217',
- 'Puymoyen (16400)' => '16271',
- 'Puynormand (33660)' => '33347',
- 'Puyol-Cazalet (40320)' => '40239',
- 'Puyoô (64270)' => '64461',
- 'Puyravault (17700)' => '17293',
- 'Puyréaux (16230)' => '16272',
- 'Puyrenier (24340)' => '24344',
- 'Puyrolland (17380)' => '17294',
- 'Puysserampion (47800)' => '47218',
- 'Queaux (86150)' => '86203',
- 'Queyrac (33340)' => '33348',
- 'Queyssac (24140)' => '24345',
- 'Queyssac-les-Vignes (19120)' => '19170',
- 'Quinçay (86190)' => '86204',
- 'Quinsac (24530)' => '24346',
- 'Quinsac (33360)' => '33349',
- 'Raix (16240)' => '16273',
- 'Ramous (64270)' => '64462',
- 'Rampieux (24440)' => '24347',
- 'Rancogne (16110)' => '16274',
- 'Rancon (87290)' => '87121',
- 'Ranton (86200)' => '86205',
- 'Ranville-Breuillaud (16140)' => '16275',
- 'Raslay (86120)' => '86206',
- 'Rauzan (33420)' => '33350',
- 'Rayet (47210)' => '47219',
- 'Razac-d\'Eymet (24500)' => '24348',
- 'Razac-de-Saussignac (24240)' => '24349',
- 'Razac-sur-l\'Isle (24430)' => '24350',
- 'Razès (87640)' => '87122',
- 'Razimet (47160)' => '47220',
- 'Réaup-Lisse (47170)' => '47221',
- 'Réaux sur Trèfle (17500)' => '17295',
- 'Rébénacq (64260)' => '64463',
- 'Reffannes (79420)' => '79225',
- 'Reignac (16360)' => '16276',
- 'Reignac (33860)' => '33351',
- 'Rempnat (87120)' => '87123',
- 'Renung (40270)' => '40240',
- 'Réparsac (16200)' => '16277',
- 'Rétaud (17460)' => '17296',
- 'Reterre (23110)' => '23160',
- 'Retjons (40120)' => '40164',
- 'Reygade (19430)' => '19171',
- 'Ribagnac (24240)' => '24351',
- 'Ribarrouy (64330)' => '64464',
- 'Ribérac (24600)' => '24352',
- 'Rilhac-Lastours (87800)' => '87124',
- 'Rilhac-Rancon (87570)' => '87125',
- 'Rilhac-Treignac (19260)' => '19172',
- 'Rilhac-Xaintrie (19220)' => '19173',
- 'Rimbez-et-Baudiets (40310)' => '40242',
- 'Rimons (33580)' => '33353',
- 'Riocaud (33220)' => '33354',
- 'Rion-des-Landes (40370)' => '40243',
- 'Rions (33410)' => '33355',
- 'Rioux (17460)' => '17298',
- 'Rioux-Martin (16210)' => '16279',
- 'Riupeyrous (64160)' => '64465',
- 'Rivedoux-Plage (17940)' => '17297',
- 'Rivehaute (64190)' => '64466',
- 'Rives (47210)' => '47223',
- 'Rivière-Saas-et-Gourby (40180)' => '40244',
- 'Rivières (16110)' => '16280',
- 'Roaillan (33210)' => '33357',
- 'Roche-le-Peyroux (19160)' => '19175',
- 'Rochechouart (87600)' => '87126',
- 'Rochefort (17300)' => '17299',
- 'Roches (23270)' => '23162',
- 'Roches-Prémarie-Andillé (86340)' => '86209',
- 'Roiffé (86120)' => '86210',
- 'Rom (79120)' => '79230',
- 'Romagne (33760)' => '33358',
- 'Romagne (86700)' => '86211',
- 'Romans (79260)' => '79231',
- 'Romazières (17510)' => '17301',
- 'Romegoux (17250)' => '17302',
- 'Romestaing (47250)' => '47224',
- 'Ronsenac (16320)' => '16283',
- 'Rontignon (64110)' => '64467',
- 'Roquebrune (33580)' => '33359',
- 'Roquefort (40120)' => '40245',
- 'Roquefort (47310)' => '47225',
- 'Roquiague (64130)' => '64468',
- 'Rosiers-d\'Égletons (19300)' => '19176',
- 'Rosiers-de-Juillac (19350)' => '19177',
- 'Rouffiac (16210)' => '16284',
- 'Rouffiac (17800)' => '17304',
- 'Rouffignac (17130)' => '17305',
- 'Rouffignac-de-Sigoulès (24240)' => '24357',
- 'Rouffignac-Saint-Cernin-de-Reilhac (24580)' => '24356',
- 'Rougnac (16320)' => '16285',
- 'Rougnat (23700)' => '23164',
- 'Rouillac (16170)' => '16286',
- 'Rouillé (86480)' => '86213',
- 'Roullet-Saint-Estèphe (16440)' => '16287',
- 'Roumagne (47800)' => '47226',
- 'Roumazières-Loubert (16270)' => '16192',
- 'Roussac (87140)' => '87128',
- 'Roussines (16310)' => '16289',
- 'Rouzède (16220)' => '16290',
- 'Royan (17200)' => '17306',
- 'Royère-de-Vassivière (23460)' => '23165',
- 'Royères (87400)' => '87129',
- 'Roziers-Saint-Georges (87130)' => '87130',
- 'Ruch (33350)' => '33361',
- 'Rudeau-Ladosse (24340)' => '24221',
- 'Ruelle-sur-Touvre (16600)' => '16291',
- 'Ruffec (16700)' => '16292',
- 'Ruffiac (47700)' => '47227',
- 'Sablonceaux (17600)' => '17307',
- 'Sablons (33910)' => '33362',
- 'Sabres (40630)' => '40246',
- 'Sadillac (24500)' => '24359',
- 'Sadirac (33670)' => '33363',
- 'Sadroc (19270)' => '19178',
- 'Sagelat (24170)' => '24360',
- 'Sagnat (23800)' => '23166',
- 'Saillac (19500)' => '19179',
- 'Saillans (33141)' => '33364',
- 'Saillat-sur-Vienne (87720)' => '87131',
- 'Saint Aulaye-Puymangou (24410)' => '24376',
- 'Saint Maurice Étusson (79150)' => '79280',
- 'Saint-Abit (64800)' => '64469',
- 'Saint-Adjutory (16310)' => '16293',
- 'Saint-Agnant (17620)' => '17308',
- 'Saint-Agnant-de-Versillat (23300)' => '23177',
- 'Saint-Agnant-près-Crocq (23260)' => '23178',
- 'Saint-Agne (24520)' => '24361',
- 'Saint-Agnet (40800)' => '40247',
- 'Saint-Aignan (33126)' => '33365',
- 'Saint-Aigulin (17360)' => '17309',
- 'Saint-Alpinien (23200)' => '23179',
- 'Saint-Amand (23200)' => '23180',
- 'Saint-Amand-de-Coly (24290)' => '24364',
- 'Saint-Amand-de-Vergt (24380)' => '24365',
- 'Saint-Amand-Jartoudeix (23400)' => '23181',
- 'Saint-Amand-le-Petit (87120)' => '87132',
- 'Saint-Amand-Magnazeix (87290)' => '87133',
- 'Saint-Amand-sur-Sèvre (79700)' => '79235',
- 'Saint-Amant-de-Boixe (16330)' => '16295',
- 'Saint-Amant-de-Bonnieure (16230)' => '16296',
- 'Saint-Amant-de-Montmoreau (16190)' => '16294',
- 'Saint-Amant-de-Nouère (16170)' => '16298',
- 'Saint-André-d\'Allas (24200)' => '24366',
- 'Saint-André-de-Cubzac (33240)' => '33366',
- 'Saint-André-de-Double (24190)' => '24367',
- 'Saint-André-de-Lidon (17260)' => '17310',
- 'Saint-André-de-Seignanx (40390)' => '40248',
- 'Saint-André-du-Bois (33490)' => '33367',
- 'Saint-André-et-Appelles (33220)' => '33369',
- 'Saint-André-sur-Sèvre (79380)' => '79236',
- 'Saint-Androny (33390)' => '33370',
- 'Saint-Angeau (16230)' => '16300',
- 'Saint-Angel (19200)' => '19180',
- 'Saint-Antoine-Cumond (24410)' => '24368',
- 'Saint-Antoine-d\'Auberoche (24330)' => '24369',
- 'Saint-Antoine-de-Breuilh (24230)' => '24370',
- 'Saint-Antoine-de-Ficalba (47340)' => '47228',
- 'Saint-Antoine-du-Queyret (33790)' => '33372',
- 'Saint-Antoine-sur-l\'Isle (33660)' => '33373',
- 'Saint-Aquilin (24110)' => '24371',
- 'Saint-Armou (64160)' => '64470',
- 'Saint-Astier (24110)' => '24372',
- 'Saint-Astier (47120)' => '47229',
- 'Saint-Aubin (40250)' => '40249',
- 'Saint-Aubin (47150)' => '47230',
- 'Saint-Aubin-de-Blaye (33820)' => '33374',
- 'Saint-Aubin-de-Branne (33420)' => '33375',
- 'Saint-Aubin-de-Cadelech (24500)' => '24373',
- 'Saint-Aubin-de-Lanquais (24560)' => '24374',
- 'Saint-Aubin-de-Médoc (33160)' => '33376',
- 'Saint-Aubin-de-Nabirat (24250)' => '24375',
- 'Saint-Aubin-du-Plain (79300)' => '79238',
- 'Saint-Aubin-le-Cloud (79450)' => '79239',
- 'Saint-Augustin (17570)' => '17311',
- 'Saint-Augustin (19390)' => '19181',
- 'Saint-Aulaire (19130)' => '19182',
- 'Saint-Aulais-la-Chapelle (16300)' => '16301',
- 'Saint-Auvent (87310)' => '87135',
- 'Saint-Avit (16210)' => '16302',
- 'Saint-Avit (40090)' => '40250',
- 'Saint-Avit (47350)' => '47231',
- 'Saint-Avit-de-Soulège (33220)' => '33377',
- 'Saint-Avit-de-Tardes (23200)' => '23182',
- 'Saint-Avit-de-Vialard (24260)' => '24377',
- 'Saint-Avit-le-Pauvre (23480)' => '23183',
- 'Saint-Avit-Rivière (24540)' => '24378',
- 'Saint-Avit-Saint-Nazaire (33220)' => '33378',
- 'Saint-Avit-Sénieur (24440)' => '24379',
- 'Saint-Barbant (87330)' => '87136',
- 'Saint-Bard (23260)' => '23184',
- 'Saint-Barthélemy (40390)' => '40251',
- 'Saint-Barthélemy-d\'Agenais (47350)' => '47232',
- 'Saint-Barthélemy-de-Bellegarde (24700)' => '24380',
- 'Saint-Barthélemy-de-Bussière (24360)' => '24381',
- 'Saint-Bazile (87150)' => '87137',
- 'Saint-Bazile-de-la-Roche (19320)' => '19183',
- 'Saint-Bazile-de-Meyssac (19500)' => '19184',
- 'Saint-Benoît (86280)' => '86214',
- 'Saint-Boès (64300)' => '64471',
- 'Saint-Bonnet (16300)' => '16303',
- 'Saint-Bonnet-Avalouze (19150)' => '19185',
- 'Saint-Bonnet-Briance (87260)' => '87138',
- 'Saint-Bonnet-de-Bellac (87300)' => '87139',
- 'Saint-Bonnet-Elvert (19380)' => '19186',
- 'Saint-Bonnet-l\'Enfantier (19410)' => '19188',
- 'Saint-Bonnet-la-Rivière (19130)' => '19187',
- 'Saint-Bonnet-les-Tours-de-Merle (19430)' => '19189',
- 'Saint-Bonnet-près-Bort (19200)' => '19190',
- 'Saint-Bonnet-sur-Gironde (17150)' => '17312',
- 'Saint-Brice (16100)' => '16304',
- 'Saint-Brice (33540)' => '33379',
- 'Saint-Brice-sur-Vienne (87200)' => '87140',
- 'Saint-Bris-des-Bois (17770)' => '17313',
- 'Saint-Caprais-de-Blaye (33820)' => '33380',
- 'Saint-Caprais-de-Bordeaux (33880)' => '33381',
- 'Saint-Caprais-de-Lerm (47270)' => '47234',
- 'Saint-Capraise-d\'Eymet (24500)' => '24383',
- 'Saint-Capraise-de-Lalinde (24150)' => '24382',
- 'Saint-Cassien (24540)' => '24384',
- 'Saint-Castin (64160)' => '64472',
- 'Saint-Cernin-de-l\'Herm (24550)' => '24386',
- 'Saint-Cernin-de-Labarde (24560)' => '24385',
- 'Saint-Cernin-de-Larche (19600)' => '19191',
- 'Saint-Césaire (17770)' => '17314',
- 'Saint-Chabrais (23130)' => '23185',
- 'Saint-Chamant (19380)' => '19192',
- 'Saint-Chamassy (24260)' => '24388',
- 'Saint-Christoly-de-Blaye (33920)' => '33382',
- 'Saint-Christoly-Médoc (33340)' => '33383',
- 'Saint-Christophe (16420)' => '16306',
- 'Saint-Christophe (17220)' => '17315',
- 'Saint-Christophe (23000)' => '23186',
- 'Saint-Christophe (86230)' => '86217',
- 'Saint-Christophe-de-Double (33230)' => '33385',
- 'Saint-Christophe-des-Bardes (33330)' => '33384',
- 'Saint-Christophe-sur-Roc (79220)' => '79241',
- 'Saint-Cibard (33570)' => '33386',
- 'Saint-Ciers-Champagne (17520)' => '17316',
- 'Saint-Ciers-d\'Abzac (33910)' => '33387',
- 'Saint-Ciers-de-Canesse (33710)' => '33388',
- 'Saint-Ciers-du-Taillon (17240)' => '17317',
- 'Saint-Ciers-sur-Bonnieure (16230)' => '16307',
- 'Saint-Ciers-sur-Gironde (33820)' => '33389',
- 'Saint-Cirgues-la-Loutre (19220)' => '19193',
- 'Saint-Cirq (24260)' => '24389',
- 'Saint-Clair (86330)' => '86218',
- 'Saint-Claud (16450)' => '16308',
- 'Saint-Clément (19700)' => '19194',
- 'Saint-Clément-des-Baleines (17590)' => '17318',
- 'Saint-Colomb-de-Lauzun (47410)' => '47235',
- 'Saint-Côme (33430)' => '33391',
- 'Saint-Coutant (16350)' => '16310',
- 'Saint-Coutant (79120)' => '79243',
- 'Saint-Coutant-le-Grand (17430)' => '17320',
- 'Saint-Crépin (17380)' => '17321',
- 'Saint-Crépin-d\'Auberoche (24330)' => '24390',
- 'Saint-Crépin-de-Richemont (24310)' => '24391',
- 'Saint-Crépin-et-Carlucet (24590)' => '24392',
- 'Saint-Cricq-Chalosse (40700)' => '40253',
- 'Saint-Cricq-du-Gave (40300)' => '40254',
- 'Saint-Cricq-Villeneuve (40190)' => '40255',
- 'Saint-Cybardeaux (16170)' => '16312',
- 'Saint-Cybranet (24250)' => '24395',
- 'Saint-Cyprien (19130)' => '19195',
- 'Saint-Cyprien (24220)' => '24396',
- 'Saint-Cyr (86130)' => '86219',
- 'Saint-Cyr (87310)' => '87141',
- 'Saint-Cyr-du-Doret (17170)' => '17322',
- 'Saint-Cyr-la-Lande (79100)' => '79244',
- 'Saint-Cyr-la-Roche (19130)' => '19196',
- 'Saint-Cyr-les-Champagnes (24270)' => '24397',
- 'Saint-Denis-d\'Oléron (17650)' => '17323',
- 'Saint-Denis-de-Pile (33910)' => '33393',
- 'Saint-Denis-des-Murs (87400)' => '87142',
- 'Saint-Dizant-du-Bois (17150)' => '17324',
- 'Saint-Dizant-du-Gua (17240)' => '17325',
- 'Saint-Dizier-la-Tour (23130)' => '23187',
- 'Saint-Dizier-les-Domaines (23270)' => '23188',
- 'Saint-Dizier-Leyrenne (23400)' => '23189',
- 'Saint-Domet (23190)' => '23190',
- 'Saint-Dos (64270)' => '64474',
- 'Saint-Éloi (23000)' => '23191',
- 'Saint-Éloy-les-Tuileries (19210)' => '19198',
- 'Saint-Émilion (33330)' => '33394',
- 'Saint-Esteben (64640)' => '64476',
- 'Saint-Estèphe (24360)' => '24398',
- 'Saint-Estèphe (33180)' => '33395',
- 'Saint-Étienne-aux-Clos (19200)' => '19199',
- 'Saint-Étienne-d\'Orthe (40300)' => '40256',
- 'Saint-Étienne-de-Baïgorry (64430)' => '64477',
- 'Saint-Étienne-de-Fougères (47380)' => '47239',
- 'Saint-Étienne-de-Fursac (23290)' => '23192',
- 'Saint-Étienne-de-Lisse (33330)' => '33396',
- 'Saint-Étienne-de-Puycorbier (24400)' => '24399',
- 'Saint-Étienne-de-Villeréal (47210)' => '47240',
- 'Saint-Étienne-la-Cigogne (79360)' => '79247',
- 'Saint-Étienne-la-Geneste (19160)' => '19200',
- 'Saint-Eugène (17520)' => '17326',
- 'Saint-Eutrope (16190)' => '16314',
- 'Saint-Eutrope-de-Born (47210)' => '47241',
- 'Saint-Exupéry (33190)' => '33398',
- 'Saint-Exupéry-les-Roches (19200)' => '19201',
- 'Saint-Faust (64110)' => '64478',
- 'Saint-Félix (16480)' => '16315',
- 'Saint-Félix (17330)' => '17327',
- 'Saint-Félix-de-Bourdeilles (24340)' => '24403',
- 'Saint-Félix-de-Foncaude (33540)' => '33399',
- 'Saint-Félix-de-Reillac-et-Mortemart (24260)' => '24404',
- 'Saint-Félix-de-Villadeix (24510)' => '24405',
- 'Saint-Ferme (33580)' => '33400',
- 'Saint-Fiel (23000)' => '23195',
- 'Saint-Fort-sur-Gironde (17240)' => '17328',
- 'Saint-Fort-sur-le-Né (16130)' => '16316',
- 'Saint-Fraigne (16140)' => '16317',
- 'Saint-Fréjoux (19200)' => '19204',
- 'Saint-Frion (23500)' => '23196',
- 'Saint-Front (16460)' => '16318',
- 'Saint-Front-d\'Alemps (24460)' => '24408',
- 'Saint-Front-de-Pradoux (24400)' => '24409',
- 'Saint-Front-la-Rivière (24300)' => '24410',
- 'Saint-Front-sur-Lémance (47500)' => '47242',
- 'Saint-Front-sur-Nizonne (24300)' => '24411',
- 'Saint-Froult (17780)' => '17329',
- 'Saint-Gaudent (86400)' => '86220',
- 'Saint-Gein (40190)' => '40259',
- 'Saint-Gelais (79410)' => '79249',
- 'Saint-Génard (79500)' => '79251',
- 'Saint-Gence (87510)' => '87143',
- 'Saint-Généroux (79600)' => '79252',
- 'Saint-Genès-de-Blaye (33390)' => '33405',
- 'Saint-Genès-de-Castillon (33350)' => '33406',
- 'Saint-Genès-de-Fronsac (33240)' => '33407',
- 'Saint-Genès-de-Lombaud (33670)' => '33408',
- 'Saint-Genest-d\'Ambière (86140)' => '86221',
- 'Saint-Genest-sur-Roselle (87260)' => '87144',
- 'Saint-Geniès (24590)' => '24412',
- 'Saint-Geniez-ô-Merle (19220)' => '19205',
- 'Saint-Genis-d\'Hiersac (16570)' => '16320',
- 'Saint-Genis-de-Saintonge (17240)' => '17331',
- 'Saint-Genis-du-Bois (33760)' => '33409',
- 'Saint-Georges (16700)' => '16321',
- 'Saint-Georges (47370)' => '47328',
- 'Saint-Georges-Antignac (17240)' => '17332',
- 'Saint-Georges-Blancaneix (24130)' => '24413',
- 'Saint-Georges-d\'Oléron (17190)' => '17337',
- 'Saint-Georges-de-Didonne (17110)' => '17333',
- 'Saint-Georges-de-Longuepierre (17470)' => '17334',
- 'Saint-Georges-de-Montclard (24140)' => '24414',
- 'Saint-Georges-de-Noisné (79400)' => '79253',
- 'Saint-Georges-de-Rex (79210)' => '79254',
- 'Saint-Georges-des-Agoûts (17150)' => '17335',
- 'Saint-Georges-des-Coteaux (17810)' => '17336',
- 'Saint-Georges-du-Bois (17700)' => '17338',
- 'Saint-Georges-la-Pouge (23250)' => '23197',
- 'Saint-Georges-lès-Baillargeaux (86130)' => '86222',
- 'Saint-Georges-les-Landes (87160)' => '87145',
- 'Saint-Georges-Nigremont (23500)' => '23198',
- 'Saint-Geours-d\'Auribat (40380)' => '40260',
- 'Saint-Geours-de-Maremne (40230)' => '40261',
- 'Saint-Géraud (47120)' => '47245',
- 'Saint-Géraud-de-Corps (24700)' => '24415',
- 'Saint-Germain (86310)' => '86223',
- 'Saint-Germain-Beaupré (23160)' => '23199',
- 'Saint-Germain-d\'Esteuil (33340)' => '33412',
- 'Saint-Germain-de-Belvès (24170)' => '24416',
- 'Saint-Germain-de-Grave (33490)' => '33411',
- 'Saint-Germain-de-la-Rivière (33240)' => '33414',
- 'Saint-Germain-de-Longue-Chaume (79200)' => '79255',
- 'Saint-Germain-de-Lusignan (17500)' => '17339',
- 'Saint-Germain-de-Marencennes (17700)' => '17340',
- 'Saint-Germain-de-Montbron (16380)' => '16323',
- 'Saint-Germain-de-Vibrac (17500)' => '17341',
- 'Saint-Germain-des-Prés (24160)' => '24417',
- 'Saint-Germain-du-Puch (33750)' => '33413',
- 'Saint-Germain-du-Salembre (24190)' => '24418',
- 'Saint-Germain-du-Seudre (17240)' => '17342',
- 'Saint-Germain-et-Mons (24520)' => '24419',
- 'Saint-Germain-Lavolps (19290)' => '19206',
- 'Saint-Germain-les-Belles (87380)' => '87146',
- 'Saint-Germain-les-Vergnes (19330)' => '19207',
- 'Saint-Germier (79340)' => '79256',
- 'Saint-Gervais (33240)' => '33415',
- 'Saint-Gervais-les-Trois-Clochers (86230)' => '86224',
- 'Saint-Géry (24400)' => '24420',
- 'Saint-Geyrac (24330)' => '24421',
- 'Saint-Gilles-les-Forêts (87130)' => '87147',
- 'Saint-Girons-d\'Aiguevives (33920)' => '33416',
- 'Saint-Girons-en-Béarn (64300)' => '64479',
- 'Saint-Gladie-Arrive-Munein (64390)' => '64480',
- 'Saint-Goin (64400)' => '64481',
- 'Saint-Gor (40120)' => '40262',
- 'Saint-Gourson (16700)' => '16325',
- 'Saint-Goussaud (23430)' => '23200',
- 'Saint-Grégoire-d\'Ardennes (17240)' => '17343',
- 'Saint-Groux (16230)' => '16326',
- 'Saint-Hilaire-Bonneval (87260)' => '87148',
- 'Saint-Hilaire-d\'Estissac (24140)' => '24422',
- 'Saint-Hilaire-de-la-Noaille (33190)' => '33418',
- 'Saint-Hilaire-de-Lusignan (47450)' => '47246',
- 'Saint-Hilaire-de-Villefranche (17770)' => '17344',
- 'Saint-Hilaire-du-Bois (17500)' => '17345',
- 'Saint-Hilaire-du-Bois (33540)' => '33419',
- 'Saint-Hilaire-Foissac (19550)' => '19208',
- 'Saint-Hilaire-la-Palud (79210)' => '79257',
- 'Saint-Hilaire-la-Plaine (23150)' => '23201',
- 'Saint-Hilaire-la-Treille (87190)' => '87149',
- 'Saint-Hilaire-le-Château (23250)' => '23202',
- 'Saint-Hilaire-les-Courbes (19170)' => '19209',
- 'Saint-Hilaire-les-Places (87800)' => '87150',
- 'Saint-Hilaire-Luc (19160)' => '19210',
- 'Saint-Hilaire-Peyroux (19560)' => '19211',
- 'Saint-Hilaire-Taurieux (19400)' => '19212',
- 'Saint-Hippolyte (17430)' => '17346',
- 'Saint-Hippolyte (33330)' => '33420',
- 'Saint-Jacques-de-Thouars (79100)' => '79258',
- 'Saint-Jal (19700)' => '19213',
- 'Saint-Jammes (64160)' => '64482',
- 'Saint-Jean-d\'Angély (17400)' => '17347',
- 'Saint-Jean-d\'Angle (17620)' => '17348',
- 'Saint-Jean-d\'Ataux (24190)' => '24424',
- 'Saint-Jean-d\'Estissac (24140)' => '24426',
- 'Saint-Jean-d\'Eyraud (24140)' => '24427',
- 'Saint-Jean-d\'Illac (33127)' => '33422',
- 'Saint-Jean-de-Blaignac (33420)' => '33421',
- 'Saint-Jean-de-Côle (24800)' => '24425',
- 'Saint-Jean-de-Duras (47120)' => '47247',
- 'Saint-Jean-de-Lier (40380)' => '40263',
- 'Saint-Jean-de-Liversay (17170)' => '17349',
- 'Saint-Jean-de-Luz (64500)' => '64483',
- 'Saint-Jean-de-Marsacq (40230)' => '40264',
- 'Saint-Jean-de-Sauves (86330)' => '86225',
- 'Saint-Jean-de-Thouars (79100)' => '79259',
- 'Saint-Jean-de-Thurac (47270)' => '47248',
- 'Saint-Jean-le-Vieux (64220)' => '64484',
- 'Saint-Jean-Ligoure (87260)' => '87151',
- 'Saint-Jean-Pied-de-Port (64220)' => '64485',
- 'Saint-Jean-Poudge (64330)' => '64486',
- 'Saint-Jory-de-Chalais (24800)' => '24428',
- 'Saint-Jory-las-Bloux (24160)' => '24429',
- 'Saint-Jouin-de-Marnes (79600)' => '79260',
- 'Saint-Jouin-de-Milly (79380)' => '79261',
- 'Saint-Jouvent (87510)' => '87152',
- 'Saint-Julien-aux-Bois (19220)' => '19214',
- 'Saint-Julien-Beychevelle (33250)' => '33423',
- 'Saint-Julien-d\'Armagnac (40240)' => '40265',
- 'Saint-Julien-d\'Eymet (24500)' => '24433',
- 'Saint-Julien-de-Crempse (24140)' => '24431',
- 'Saint-Julien-de-l\'Escap (17400)' => '17350',
- 'Saint-Julien-de-Lampon (24370)' => '24432',
- 'Saint-Julien-en-Born (40170)' => '40266',
- 'Saint-Julien-l\'Ars (86800)' => '86226',
- 'Saint-Julien-la-Genête (23110)' => '23203',
- 'Saint-Julien-le-Châtel (23130)' => '23204',
- 'Saint-Julien-le-Pèlerin (19430)' => '19215',
- 'Saint-Julien-le-Petit (87460)' => '87153',
- 'Saint-Julien-le-Vendômois (19210)' => '19216',
- 'Saint-Julien-Maumont (19500)' => '19217',
- 'Saint-Julien-près-Bort (19110)' => '19218',
- 'Saint-Junien (87200)' => '87154',
- 'Saint-Junien-la-Bregère (23400)' => '23205',
- 'Saint-Junien-les-Combes (87300)' => '87155',
- 'Saint-Just (24320)' => '24434',
- 'Saint-Just-Ibarre (64120)' => '64487',
- 'Saint-Just-le-Martel (87590)' => '87156',
- 'Saint-Just-Luzac (17320)' => '17351',
- 'Saint-Justin (40240)' => '40267',
- 'Saint-Laon (86200)' => '86227',
- 'Saint-Laurent (23000)' => '23206',
- 'Saint-Laurent (47130)' => '47249',
- 'Saint-Laurent-Bretagne (64160)' => '64488',
- 'Saint-Laurent-d\'Arce (33240)' => '33425',
- 'Saint-Laurent-de-Belzagot (16190)' => '16328',
- 'Saint-Laurent-de-Céris (16450)' => '16329',
- 'Saint-Laurent-de-Cognac (16100)' => '16330',
- 'Saint-Laurent-de-Gosse (40390)' => '40268',
- 'Saint-Laurent-de-Jourdes (86410)' => '86228',
- 'Saint-Laurent-de-la-Barrière (17380)' => '17352',
- 'Saint-Laurent-de-la-Prée (17450)' => '17353',
- 'Saint-Laurent-des-Combes (16480)' => '16331',
- 'Saint-Laurent-des-Combes (33330)' => '33426',
- 'Saint-Laurent-des-Hommes (24400)' => '24436',
- 'Saint-Laurent-des-Vignes (24100)' => '24437',
- 'Saint-Laurent-du-Bois (33540)' => '33427',
- 'Saint-Laurent-du-Plan (33190)' => '33428',
- 'Saint-Laurent-la-Vallée (24170)' => '24438',
- 'Saint-Laurent-les-Églises (87240)' => '87157',
- 'Saint-Laurent-Médoc (33112)' => '33424',
- 'Saint-Laurent-sur-Gorre (87310)' => '87158',
- 'Saint-Laurs (79160)' => '79263',
- 'Saint-Léger (16250)' => '16332',
- 'Saint-Léger (17800)' => '17354',
- 'Saint-Léger (47160)' => '47250',
- 'Saint-Léger-Bridereix (23300)' => '23207',
- 'Saint-Léger-de-Balson (33113)' => '33429',
- 'Saint-Léger-de-la-Martinière (79500)' => '79264',
- 'Saint-Léger-de-Montbrillais (86120)' => '86229',
- 'Saint-Léger-de-Montbrun (79100)' => '79265',
- 'Saint-Léger-la-Montagne (87340)' => '87159',
- 'Saint-Léger-le-Guérétois (23000)' => '23208',
- 'Saint-Léger-Magnazeix (87190)' => '87160',
- 'Saint-Léomer (86290)' => '86230',
- 'Saint-Léon (33670)' => '33431',
- 'Saint-Léon (47160)' => '47251',
- 'Saint-Léon-d\'Issigeac (24560)' => '24441',
- 'Saint-Léon-sur-l\'Isle (24110)' => '24442',
- 'Saint-Léon-sur-Vézère (24290)' => '24443',
- 'Saint-Léonard-de-Noblat (87400)' => '87161',
- 'Saint-Lin (79420)' => '79267',
- 'Saint-Lon-les-Mines (40300)' => '40269',
- 'Saint-Loubert (33210)' => '33432',
- 'Saint-Loubès (33450)' => '33433',
- 'Saint-Loubouer (40320)' => '40270',
- 'Saint-Louis-de-Montferrand (33440)' => '33434',
- 'Saint-Louis-en-l\'Isle (24400)' => '24444',
- 'Saint-Loup (17380)' => '17356',
- 'Saint-Loup (23130)' => '23209',
- 'Saint-Loup-Lamairé (79600)' => '79268',
- 'Saint-Macaire (33490)' => '33435',
- 'Saint-Macoux (86400)' => '86231',
- 'Saint-Magne (33125)' => '33436',
- 'Saint-Magne-de-Castillon (33350)' => '33437',
- 'Saint-Maigrin (17520)' => '17357',
- 'Saint-Maime-de-Péreyrol (24380)' => '24459',
- 'Saint-Maixant (23200)' => '23210',
- 'Saint-Maixant (33490)' => '33438',
- 'Saint-Maixent-de-Beugné (79160)' => '79269',
- 'Saint-Maixent-l\'École (79400)' => '79270',
- 'Saint-Mandé-sur-Brédoire (17470)' => '17358',
- 'Saint-Marc-à-Frongier (23200)' => '23211',
- 'Saint-Marc-à-Loubaud (23460)' => '23212',
- 'Saint-Marc-la-Lande (79310)' => '79271',
- 'Saint-Marcel-du-Périgord (24510)' => '24445',
- 'Saint-Marcory (24540)' => '24446',
- 'Saint-Mard (17700)' => '17359',
- 'Saint-Marien (23600)' => '23213',
- 'Saint-Mariens (33620)' => '33439',
- 'Saint-Martial (16190)' => '16334',
- 'Saint-Martial (17330)' => '17361',
- 'Saint-Martial (33490)' => '33440',
- 'Saint-Martial-d\'Albarède (24160)' => '24448',
- 'Saint-Martial-d\'Artenset (24700)' => '24449',
- 'Saint-Martial-de-Gimel (19150)' => '19220',
- 'Saint-Martial-de-Mirambeau (17150)' => '17362',
- 'Saint-Martial-de-Nabirat (24250)' => '24450',
- 'Saint-Martial-de-Valette (24300)' => '24451',
- 'Saint-Martial-de-Vitaterne (17500)' => '17363',
- 'Saint-Martial-Entraygues (19400)' => '19221',
- 'Saint-Martial-le-Mont (23150)' => '23214',
- 'Saint-Martial-le-Vieux (23100)' => '23215',
- 'Saint-Martial-sur-Isop (87330)' => '87163',
- 'Saint-Martial-sur-Né (17520)' => '17364',
- 'Saint-Martial-Viveyrol (24320)' => '24452',
- 'Saint-Martin-Château (23460)' => '23216',
- 'Saint-Martin-Curton (47700)' => '47254',
- 'Saint-Martin-d\'Arberoue (64640)' => '64489',
- 'Saint-Martin-d\'Arrossa (64780)' => '64490',
- 'Saint-Martin-d\'Ary (17270)' => '17365',
- 'Saint-Martin-d\'Oney (40090)' => '40274',
- 'Saint-Martin-de-Beauville (47270)' => '47255',
- 'Saint-Martin-de-Bernegoue (79230)' => '79273',
- 'Saint-Martin-de-Coux (17360)' => '17366',
- 'Saint-Martin-de-Fressengeas (24800)' => '24453',
- 'Saint-Martin-de-Gurson (24610)' => '24454',
- 'Saint-Martin-de-Hinx (40390)' => '40272',
- 'Saint-Martin-de-Juillers (17400)' => '17367',
- 'Saint-Martin-de-Jussac (87200)' => '87164',
- 'Saint-Martin-de-Laye (33910)' => '33442',
- 'Saint-Martin-de-Lerm (33540)' => '33443',
- 'Saint-Martin-de-Mâcon (79100)' => '79274',
- 'Saint-Martin-de-Ré (17410)' => '17369',
- 'Saint-Martin-de-Ribérac (24600)' => '24455',
- 'Saint-Martin-de-Saint-Maixent (79400)' => '79276',
- 'Saint-Martin-de-Sanzay (79290)' => '79277',
- 'Saint-Martin-de-Seignanx (40390)' => '40273',
- 'Saint-Martin-de-Sescas (33490)' => '33444',
- 'Saint-Martin-de-Villeréal (47210)' => '47256',
- 'Saint-Martin-des-Combes (24140)' => '24456',
- 'Saint-Martin-du-Bois (33910)' => '33445',
- 'Saint-Martin-du-Clocher (16700)' => '16335',
- 'Saint-Martin-du-Fouilloux (79420)' => '79278',
- 'Saint-Martin-du-Puy (33540)' => '33446',
- 'Saint-Martin-l\'Ars (86350)' => '86234',
- 'Saint-Martin-l\'Astier (24400)' => '24457',
- 'Saint-Martin-la-Méanne (19320)' => '19222',
- 'Saint-Martin-Lacaussade (33390)' => '33441',
- 'Saint-Martin-le-Mault (87360)' => '87165',
- 'Saint-Martin-le-Pin (24300)' => '24458',
- 'Saint-Martin-le-Vieux (87700)' => '87166',
- 'Saint-Martin-lès-Melle (79500)' => '79279',
- 'Saint-Martin-Petit (47180)' => '47257',
- 'Saint-Martin-Sainte-Catherine (23430)' => '23217',
- 'Saint-Martin-Sepert (19210)' => '19223',
- 'Saint-Martin-Terressus (87400)' => '87167',
- 'Saint-Mary (16260)' => '16336',
- 'Saint-Mathieu (87440)' => '87168',
- 'Saint-Maurice-de-Lestapel (47290)' => '47259',
- 'Saint-Maurice-des-Lions (16500)' => '16337',
- 'Saint-Maurice-la-Clouère (86160)' => '86235',
- 'Saint-Maurice-la-Souterraine (23300)' => '23219',
- 'Saint-Maurice-les-Brousses (87800)' => '87169',
- 'Saint-Maurice-près-Crocq (23260)' => '23218',
- 'Saint-Maurice-sur-Adour (40270)' => '40275',
- 'Saint-Maurin (47270)' => '47260',
- 'Saint-Maxire (79410)' => '79281',
- 'Saint-Méard (87130)' => '87170',
- 'Saint-Méard-de-Drône (24600)' => '24460',
- 'Saint-Méard-de-Gurçon (24610)' => '24461',
- 'Saint-Médard (16300)' => '16338',
- 'Saint-Médard (17500)' => '17372',
- 'Saint-Médard (64370)' => '64491',
- 'Saint-Médard (79370)' => '79282',
- 'Saint-Médard-d\'Aunis (17220)' => '17373',
- 'Saint-Médard-d\'Excideuil (24160)' => '24463',
- 'Saint-Médard-d\'Eyrans (33650)' => '33448',
- 'Saint-Médard-de-Guizières (33230)' => '33447',
- 'Saint-Médard-de-Mussidan (24400)' => '24462',
- 'Saint-Médard-en-Jalles (33160)' => '33449',
- 'Saint-Médard-la-Rochette (23200)' => '23220',
- 'Saint-Même-les-Carrières (16720)' => '16340',
- 'Saint-Merd-de-Lapleau (19320)' => '19225',
- 'Saint-Merd-la-Breuille (23100)' => '23221',
- 'Saint-Merd-les-Oussines (19170)' => '19226',
- 'Saint-Mesmin (24270)' => '24464',
- 'Saint-Mexant (19330)' => '19227',
- 'Saint-Michel (16470)' => '16341',
- 'Saint-Michel (64220)' => '64492',
- 'Saint-Michel-de-Castelnau (33840)' => '33450',
- 'Saint-Michel-de-Double (24400)' => '24465',
- 'Saint-Michel-de-Fronsac (33126)' => '33451',
- 'Saint-Michel-de-Lapujade (33190)' => '33453',
- 'Saint-Michel-de-Montaigne (24230)' => '24466',
- 'Saint-Michel-de-Rieufret (33720)' => '33452',
- 'Saint-Michel-de-Veisse (23480)' => '23222',
- 'Saint-Michel-de-Villadeix (24380)' => '24468',
- 'Saint-Michel-Escalus (40550)' => '40276',
- 'Saint-Moreil (23400)' => '23223',
- 'Saint-Morillon (33650)' => '33454',
- 'Saint-Nazaire-sur-Charente (17780)' => '17375',
- 'Saint-Nexans (24520)' => '24472',
- 'Saint-Nicolas-de-la-Balerme (47220)' => '47262',
- 'Saint-Oradoux-de-Chirouze (23100)' => '23224',
- 'Saint-Oradoux-près-Crocq (23260)' => '23225',
- 'Saint-Ouen-d\'Aunis (17230)' => '17376',
- 'Saint-Ouen-la-Thène (17490)' => '17377',
- 'Saint-Ouen-sur-Gartempe (87300)' => '87172',
- 'Saint-Palais (33820)' => '33456',
- 'Saint-Palais (64120)' => '64493',
- 'Saint-Palais-de-Négrignac (17210)' => '17378',
- 'Saint-Palais-de-Phiolin (17800)' => '17379',
- 'Saint-Palais-du-Né (16300)' => '16342',
- 'Saint-Palais-sur-Mer (17420)' => '17380',
- 'Saint-Pancrace (24530)' => '24474',
- 'Saint-Pandelon (40180)' => '40277',
- 'Saint-Pantaléon-de-Lapleau (19160)' => '19228',
- 'Saint-Pantaléon-de-Larche (19600)' => '19229',
- 'Saint-Pantaly-d\'Ans (24640)' => '24475',
- 'Saint-Pantaly-d\'Excideuil (24160)' => '24476',
- 'Saint-Pardon-de-Conques (33210)' => '33457',
- 'Saint-Pardoult (17400)' => '17381',
- 'Saint-Pardoux (79310)' => '79285',
- 'Saint-Pardoux (87250)' => '87173',
- 'Saint-Pardoux-Corbier (19210)' => '19230',
- 'Saint-Pardoux-d\'Arnet (23260)' => '23226',
- 'Saint-Pardoux-de-Drône (24600)' => '24477',
- 'Saint-Pardoux-du-Breuil (47200)' => '47263',
- 'Saint-Pardoux-et-Vielvic (24170)' => '24478',
- 'Saint-Pardoux-Isaac (47800)' => '47264',
- 'Saint-Pardoux-l\'Ortigier (19270)' => '19234',
- 'Saint-Pardoux-la-Croisille (19320)' => '19231',
- 'Saint-Pardoux-la-Rivière (24470)' => '24479',
- 'Saint-Pardoux-le-Neuf (19200)' => '19232',
- 'Saint-Pardoux-le-Neuf (23200)' => '23228',
- 'Saint-Pardoux-le-Vieux (19200)' => '19233',
- 'Saint-Pardoux-les-Cards (23150)' => '23229',
- 'Saint-Pardoux-Morterolles (23400)' => '23227',
- 'Saint-Pastour (47290)' => '47265',
- 'Saint-Paul (19150)' => '19235',
- 'Saint-Paul (33390)' => '33458',
- 'Saint-Paul (87260)' => '87174',
- 'Saint-Paul-de-Serre (24380)' => '24480',
- 'Saint-Paul-en-Born (40200)' => '40278',
- 'Saint-Paul-en-Gâtine (79240)' => '79286',
- 'Saint-Paul-la-Roche (24800)' => '24481',
- 'Saint-Paul-lès-Dax (40990)' => '40279',
- 'Saint-Paul-Lizonne (24320)' => '24482',
- 'Saint-Pé-de-Léren (64270)' => '64494',
- 'Saint-Pé-Saint-Simon (47170)' => '47266',
- 'Saint-Pée-sur-Nivelle (64310)' => '64495',
- 'Saint-Perdon (40090)' => '40280',
- 'Saint-Perdoux (24560)' => '24483',
- 'Saint-Pey-d\'Armens (33330)' => '33459',
- 'Saint-Pey-de-Castets (33350)' => '33460',
- 'Saint-Philippe-d\'Aiguille (33350)' => '33461',
- 'Saint-Philippe-du-Seignal (33220)' => '33462',
- 'Saint-Pierre-Bellevue (23460)' => '23232',
- 'Saint-Pierre-Chérignat (23430)' => '23230',
- 'Saint-Pierre-d\'Amilly (17700)' => '17382',
- 'Saint-Pierre-d\'Aurillac (33490)' => '33463',
- 'Saint-Pierre-d\'Exideuil (86400)' => '86237',
- 'Saint-Pierre-d\'Eyraud (24130)' => '24487',
- 'Saint-Pierre-d\'Irube (64990)' => '64496',
- 'Saint-Pierre-d\'Oléron (17310)' => '17385',
- 'Saint-Pierre-de-Bat (33760)' => '33464',
- 'Saint-Pierre-de-Buzet (47160)' => '47267',
- 'Saint-Pierre-de-Chignac (24330)' => '24484',
- 'Saint-Pierre-de-Clairac (47270)' => '47269',
- 'Saint-Pierre-de-Côle (24800)' => '24485',
- 'Saint-Pierre-de-Frugie (24450)' => '24486',
- 'Saint-Pierre-de-Fursac (23290)' => '23231',
- 'Saint-Pierre-de-Juillers (17400)' => '17383',
- 'Saint-Pierre-de-l\'Isle (17330)' => '17384',
- 'Saint-Pierre-de-Maillé (86260)' => '86236',
- 'Saint-Pierre-de-Mons (33210)' => '33465',
- 'Saint-Pierre-des-Échaubrognes (79700)' => '79289',
- 'Saint-Pierre-du-Mont (40280)' => '40281',
- 'Saint-Pierre-du-Palais (17270)' => '17386',
- 'Saint-Pierre-le-Bost (23600)' => '23233',
- 'Saint-Pierre-sur-Dropt (47120)' => '47271',
- 'Saint-Pompain (79160)' => '79290',
- 'Saint-Pompont (24170)' => '24488',
- 'Saint-Porchaire (17250)' => '17387',
- 'Saint-Preuil (16130)' => '16343',
- 'Saint-Priest (23110)' => '23234',
- 'Saint-Priest-de-Gimel (19800)' => '19236',
- 'Saint-Priest-la-Feuille (23300)' => '23235',
- 'Saint-Priest-la-Plaine (23240)' => '23236',
- 'Saint-Priest-les-Fougères (24450)' => '24489',
- 'Saint-Priest-Ligoure (87800)' => '87176',
- 'Saint-Priest-Palus (23400)' => '23237',
- 'Saint-Priest-sous-Aixe (87700)' => '87177',
- 'Saint-Priest-Taurion (87480)' => '87178',
- 'Saint-Privat (19220)' => '19237',
- 'Saint-Privat-des-Prés (24410)' => '24490',
- 'Saint-Projet-Saint-Constant (16110)' => '16344',
- 'Saint-Quantin-de-Rançanne (17800)' => '17388',
- 'Saint-Quentin-de-Baron (33750)' => '33466',
- 'Saint-Quentin-de-Caplong (33220)' => '33467',
- 'Saint-Quentin-de-Chalais (16210)' => '16346',
- 'Saint-Quentin-du-Dropt (47330)' => '47272',
- 'Saint-Quentin-la-Chabanne (23500)' => '23238',
- 'Saint-Quentin-sur-Charente (16150)' => '16345',
- 'Saint-Rabier (24210)' => '24491',
- 'Saint-Raphaël (24160)' => '24493',
- 'Saint-Rémy (19290)' => '19238',
- 'Saint-Rémy (24700)' => '24494',
- 'Saint-Rémy (79410)' => '79293',
- 'Saint-Rémy-sur-Creuse (86220)' => '86241',
- 'Saint-Robert (19310)' => '19239',
- 'Saint-Robert (47340)' => '47273',
- 'Saint-Rogatien (17220)' => '17391',
- 'Saint-Romain (16210)' => '16347',
- 'Saint-Romain (86250)' => '86242',
- 'Saint-Romain-de-Benet (17600)' => '17393',
- 'Saint-Romain-de-Monpazier (24540)' => '24495',
- 'Saint-Romain-et-Saint-Clément (24800)' => '24496',
- 'Saint-Romain-la-Virvée (33240)' => '33470',
- 'Saint-Romain-le-Noble (47270)' => '47274',
- 'Saint-Romain-sur-Gironde (17240)' => '17392',
- 'Saint-Romans-des-Champs (79230)' => '79294',
- 'Saint-Romans-lès-Melle (79500)' => '79295',
- 'Saint-Salvadour (19700)' => '19240',
- 'Saint-Salvy (47360)' => '47275',
- 'Saint-Sardos (47360)' => '47276',
- 'Saint-Saturnin (16290)' => '16348',
- 'Saint-Saturnin-du-Bois (17700)' => '17394',
- 'Saint-Saud-Lacoussière (24470)' => '24498',
- 'Saint-Sauvant (17610)' => '17395',
- 'Saint-Sauvant (86600)' => '86244',
- 'Saint-Sauveur (24520)' => '24499',
- 'Saint-Sauveur (33250)' => '33471',
- 'Saint-Sauveur-d\'Aunis (17540)' => '17396',
- 'Saint-Sauveur-de-Meilhan (47180)' => '47277',
- 'Saint-Sauveur-de-Puynormand (33660)' => '33472',
- 'Saint-Sauveur-Lalande (24700)' => '24500',
- 'Saint-Savin (33920)' => '33473',
- 'Saint-Savin (86310)' => '86246',
- 'Saint-Savinien (17350)' => '17397',
- 'Saint-Saviol (86400)' => '86247',
- 'Saint-Sébastien (23160)' => '23239',
- 'Saint-Secondin (86350)' => '86248',
- 'Saint-Selve (33650)' => '33474',
- 'Saint-Sernin (47120)' => '47278',
- 'Saint-Setiers (19290)' => '19241',
- 'Saint-Seurin-de-Bourg (33710)' => '33475',
- 'Saint-Seurin-de-Cadourne (33180)' => '33476',
- 'Saint-Seurin-de-Cursac (33390)' => '33477',
- 'Saint-Seurin-de-Palenne (17800)' => '17398',
- 'Saint-Seurin-de-Prats (24230)' => '24501',
- 'Saint-Seurin-sur-l\'Isle (33660)' => '33478',
- 'Saint-Sève (33190)' => '33479',
- 'Saint-Sever (40500)' => '40282',
- 'Saint-Sever-de-Saintonge (17800)' => '17400',
- 'Saint-Séverin (16390)' => '16350',
- 'Saint-Séverin-d\'Estissac (24190)' => '24502',
- 'Saint-Séverin-sur-Boutonne (17330)' => '17401',
- 'Saint-Sigismond-de-Clermont (17240)' => '17402',
- 'Saint-Silvain-Bas-le-Roc (23600)' => '23240',
- 'Saint-Silvain-Bellegarde (23190)' => '23241',
- 'Saint-Silvain-Montaigut (23320)' => '23242',
- 'Saint-Silvain-sous-Toulx (23140)' => '23243',
- 'Saint-Simeux (16120)' => '16351',
- 'Saint-Simon (16120)' => '16352',
- 'Saint-Simon-de-Bordes (17500)' => '17403',
- 'Saint-Simon-de-Pellouaille (17260)' => '17404',
- 'Saint-Sixte (47220)' => '47279',
- 'Saint-Solve (19130)' => '19242',
- 'Saint-Sorlin-de-Conac (17150)' => '17405',
- 'Saint-Sornin (16220)' => '16353',
- 'Saint-Sornin (17600)' => '17406',
- 'Saint-Sornin-la-Marche (87210)' => '87179',
- 'Saint-Sornin-Lavolps (19230)' => '19243',
- 'Saint-Sornin-Leulac (87290)' => '87180',
- 'Saint-Sulpice-d\'Arnoult (17250)' => '17408',
- 'Saint-Sulpice-d\'Excideuil (24800)' => '24505',
- 'Saint-Sulpice-de-Cognac (16370)' => '16355',
- 'Saint-Sulpice-de-Faleyrens (33330)' => '33480',
- 'Saint-Sulpice-de-Guilleragues (33580)' => '33481',
- 'Saint-Sulpice-de-Mareuil (24340)' => '24503',
- 'Saint-Sulpice-de-Pommiers (33540)' => '33482',
- 'Saint-Sulpice-de-Roumagnac (24600)' => '24504',
- 'Saint-Sulpice-de-Royan (17200)' => '17409',
- 'Saint-Sulpice-de-Ruffec (16460)' => '16356',
- 'Saint-Sulpice-et-Cameyrac (33450)' => '33483',
- 'Saint-Sulpice-Laurière (87370)' => '87181',
- 'Saint-Sulpice-le-Dunois (23800)' => '23244',
- 'Saint-Sulpice-le-Guérétois (23000)' => '23245',
- 'Saint-Sulpice-les-Bois (19250)' => '19244',
- 'Saint-Sulpice-les-Champs (23480)' => '23246',
- 'Saint-Sulpice-les-Feuilles (87160)' => '87182',
- 'Saint-Sylvain (19380)' => '19245',
- 'Saint-Sylvestre (87240)' => '87183',
- 'Saint-Sylvestre-sur-Lot (47140)' => '47280',
- 'Saint-Symphorien (33113)' => '33484',
- 'Saint-Symphorien (79270)' => '79298',
- 'Saint-Symphorien-sur-Couze (87140)' => '87184',
- 'Saint-Thomas-de-Conac (17150)' => '17410',
- 'Saint-Trojan (33710)' => '33486',
- 'Saint-Trojan-les-Bains (17370)' => '17411',
- 'Saint-Urcisse (47270)' => '47281',
- 'Saint-Vaize (17100)' => '17412',
- 'Saint-Vallier (16480)' => '16357',
- 'Saint-Varent (79330)' => '79299',
- 'Saint-Vaury (23320)' => '23247',
- 'Saint-Viance (19240)' => '19246',
- 'Saint-Victor (24350)' => '24508',
- 'Saint-Victor-en-Marche (23000)' => '23248',
- 'Saint-Victour (19200)' => '19247',
- 'Saint-Victurnien (87420)' => '87185',
- 'Saint-Vincent (64800)' => '64498',
- 'Saint-Vincent-de-Connezac (24190)' => '24509',
- 'Saint-Vincent-de-Cosse (24220)' => '24510',
- 'Saint-Vincent-de-Lamontjoie (47310)' => '47282',
- 'Saint-Vincent-de-Paul (33440)' => '33487',
- 'Saint-Vincent-de-Paul (40990)' => '40283',
- 'Saint-Vincent-de-Pertignas (33420)' => '33488',
- 'Saint-Vincent-de-Tyrosse (40230)' => '40284',
- 'Saint-Vincent-Jalmoutiers (24410)' => '24511',
- 'Saint-Vincent-la-Châtre (79500)' => '79301',
- 'Saint-Vincent-le-Paluel (24200)' => '24512',
- 'Saint-Vincent-sur-l\'Isle (24420)' => '24513',
- 'Saint-Vite (47500)' => '47283',
- 'Saint-Vitte-sur-Briance (87380)' => '87186',
- 'Saint-Vivien (17220)' => '17413',
- 'Saint-Vivien (24230)' => '24514',
- 'Saint-Vivien-de-Blaye (33920)' => '33489',
- 'Saint-Vivien-de-Médoc (33590)' => '33490',
- 'Saint-Vivien-de-Monségur (33580)' => '33491',
- 'Saint-Xandre (17138)' => '17414',
- 'Saint-Yaguen (40400)' => '40285',
- 'Saint-Ybard (19140)' => '19248',
- 'Saint-Yrieix-la-Montagne (23460)' => '23249',
- 'Saint-Yrieix-la-Perche (87500)' => '87187',
- 'Saint-Yrieix-le-Déjalat (19300)' => '19249',
- 'Saint-Yrieix-les-Bois (23150)' => '23250',
- 'Saint-Yrieix-sous-Aixe (87700)' => '87188',
- 'Saint-Yrieix-sur-Charente (16710)' => '16358',
- 'Saint-Yzan-de-Soudiac (33920)' => '33492',
- 'Saint-Yzans-de-Médoc (33340)' => '33493',
- 'Sainte-Alvère-Saint-Laurent Les Bâtons (24510)' => '24362',
- 'Sainte-Anne-Saint-Priest (87120)' => '87134',
- 'Sainte-Bazeille (47180)' => '47233',
- 'Sainte-Blandine (79370)' => '79240',
- 'Sainte-Colombe (16230)' => '16309',
- 'Sainte-Colombe (17210)' => '17319',
- 'Sainte-Colombe (33350)' => '33390',
- 'Sainte-Colombe (40700)' => '40252',
- 'Sainte-Colombe-de-Duras (47120)' => '47236',
- 'Sainte-Colombe-de-Villeneuve (47300)' => '47237',
- 'Sainte-Colombe-en-Bruilhois (47310)' => '47238',
- 'Sainte-Colome (64260)' => '64473',
- 'Sainte-Croix (24440)' => '24393',
- 'Sainte-Croix-de-Mareuil (24340)' => '24394',
- 'Sainte-Croix-du-Mont (33410)' => '33392',
- 'Sainte-Eanne (79800)' => '79246',
- 'Sainte-Engrâce (64560)' => '64475',
- 'Sainte-Eulalie (33560)' => '33397',
- 'Sainte-Eulalie-d\'Ans (24640)' => '24401',
- 'Sainte-Eulalie-d\'Eymet (24500)' => '24402',
- 'Sainte-Eulalie-en-Born (40200)' => '40257',
- 'Sainte-Féréole (19270)' => '19202',
- 'Sainte-Feyre (23000)' => '23193',
- 'Sainte-Feyre-la-Montagne (23500)' => '23194',
- 'Sainte-Florence (33350)' => '33401',
- 'Sainte-Fortunade (19490)' => '19203',
- 'Sainte-Foy (40190)' => '40258',
- 'Sainte-Foy-de-Belvès (24170)' => '24406',
- 'Sainte-Foy-de-Longas (24510)' => '24407',
- 'Sainte-Foy-la-Grande (33220)' => '33402',
- 'Sainte-Foy-la-Longue (33490)' => '33403',
- 'Sainte-Gemme (17250)' => '17330',
- 'Sainte-Gemme (33580)' => '33404',
- 'Sainte-Gemme (79330)' => '79250',
- 'Sainte-Gemme-Martaillac (47250)' => '47244',
- 'Sainte-Hélène (33480)' => '33417',
- 'Sainte-Innocence (24500)' => '24423',
- 'Sainte-Lheurine (17520)' => '17355',
- 'Sainte-Livrade-sur-Lot (47110)' => '47252',
- 'Sainte-Marie-de-Chignac (24330)' => '24447',
- 'Sainte-Marie-de-Gosse (40390)' => '40271',
- 'Sainte-Marie-de-Ré (17740)' => '17360',
- 'Sainte-Marie-de-Vaux (87420)' => '87162',
- 'Sainte-Marie-Lapanouze (19160)' => '19219',
- 'Sainte-Marthe (47430)' => '47253',
- 'Sainte-Maure-de-Peyriac (47170)' => '47258',
- 'Sainte-Même (17770)' => '17374',
- 'Sainte-Mondane (24370)' => '24470',
- 'Sainte-Nathalène (24200)' => '24471',
- 'Sainte-Néomaye (79260)' => '79283',
- 'Sainte-Orse (24210)' => '24473',
- 'Sainte-Ouenne (79220)' => '79284',
- 'Sainte-Radegonde (17250)' => '17389',
- 'Sainte-Radegonde (24560)' => '24492',
- 'Sainte-Radegonde (33350)' => '33468',
- 'Sainte-Radegonde (79100)' => '79292',
- 'Sainte-Radégonde (86300)' => '86239',
- 'Sainte-Ramée (17240)' => '17390',
- 'Sainte-Sévère (16200)' => '16349',
- 'Sainte-Soline (79120)' => '79297',
- 'Sainte-Souline (16480)' => '16354',
- 'Sainte-Soulle (17220)' => '17407',
- 'Sainte-Terre (33350)' => '33485',
- 'Sainte-Trie (24160)' => '24507',
- 'Sainte-Verge (79100)' => '79300',
- 'Saintes (17100)' => '17415',
- 'Saires (86420)' => '86249',
- 'Saivres (79400)' => '79302',
- 'Saix (86120)' => '86250',
- 'Salagnac (24160)' => '24515',
- 'Salaunes (33160)' => '33494',
- 'Saleignes (17510)' => '17416',
- 'Salies-de-Béarn (64270)' => '64499',
- 'Salignac-de-Mirambeau (17130)' => '17417',
- 'Salignac-Eyvigues (24590)' => '24516',
- 'Salignac-sur-Charente (17800)' => '17418',
- 'Salleboeuf (33370)' => '33496',
- 'Salles (33770)' => '33498',
- 'Salles (47150)' => '47284',
- 'Salles (79800)' => '79303',
- 'Salles-d\'Angles (16130)' => '16359',
- 'Salles-de-Barbezieux (16300)' => '16360',
- 'Salles-de-Belvès (24170)' => '24517',
- 'Salles-de-Villefagnan (16700)' => '16361',
- 'Salles-Lavalette (16190)' => '16362',
- 'Salles-Mongiscard (64300)' => '64500',
- 'Salles-sur-Mer (17220)' => '17420',
- 'Sallespisse (64300)' => '64501',
- 'Salon (24380)' => '24518',
- 'Salon-la-Tour (19510)' => '19250',
- 'Samadet (40320)' => '40286',
- 'Samazan (47250)' => '47285',
- 'Sames (64520)' => '64502',
- 'Sammarçolles (86200)' => '86252',
- 'Samonac (33710)' => '33500',
- 'Samsons-Lion (64350)' => '64503',
- 'Sanguinet (40460)' => '40287',
- 'Sannat (23110)' => '23167',
- 'Sansais (79270)' => '79304',
- 'Sanxay (86600)' => '86253',
- 'Sarbazan (40120)' => '40288',
- 'Sardent (23250)' => '23168',
- 'Sare (64310)' => '64504',
- 'Sarlande (24270)' => '24519',
- 'Sarlat-la-Canéda (24200)' => '24520',
- 'Sarliac-sur-l\'Isle (24420)' => '24521',
- 'Sarpourenx (64300)' => '64505',
- 'Sarran (19800)' => '19251',
- 'Sarrance (64490)' => '64506',
- 'Sarrazac (24800)' => '24522',
- 'Sarraziet (40500)' => '40289',
- 'Sarron (40800)' => '40290',
- 'Sarroux (19110)' => '19252',
- 'Saubion (40230)' => '40291',
- 'Saubole (64420)' => '64507',
- 'Saubrigues (40230)' => '40292',
- 'Saubusse (40180)' => '40293',
- 'Saucats (33650)' => '33501',
- 'Saucède (64400)' => '64508',
- 'Saugnac-et-Cambran (40180)' => '40294',
- 'Saugnacq-et-Muret (40410)' => '40295',
- 'Saugon (33920)' => '33502',
- 'Sauguis-Saint-Étienne (64470)' => '64509',
- 'Saujon (17600)' => '17421',
- 'Saulgé (86500)' => '86254',
- 'Saulgond (16420)' => '16363',
- 'Sault-de-Navailles (64300)' => '64510',
- 'Sauméjan (47420)' => '47286',
- 'Saumont (47600)' => '47287',
- 'Saumos (33680)' => '33503',
- 'Saurais (79200)' => '79306',
- 'Saussignac (24240)' => '24523',
- 'Sauternes (33210)' => '33504',
- 'Sauvagnac (16310)' => '16364',
- 'Sauvagnas (47340)' => '47288',
- 'Sauvagnon (64230)' => '64511',
- 'Sauvelade (64150)' => '64512',
- 'Sauveterre-de-Béarn (64390)' => '64513',
- 'Sauveterre-de-Guyenne (33540)' => '33506',
- 'Sauveterre-la-Lémance (47500)' => '47292',
- 'Sauveterre-Saint-Denis (47220)' => '47293',
- 'Sauviac (33430)' => '33507',
- 'Sauviat-sur-Vige (87400)' => '87190',
- 'Sauvignac (16480)' => '16365',
- 'Sauzé-Vaussais (79190)' => '79307',
- 'Savennes (23000)' => '23170',
- 'Savignac (33124)' => '33508',
- 'Savignac-de-Duras (47120)' => '47294',
- 'Savignac-de-l\'Isle (33910)' => '33509',
- 'Savignac-de-Miremont (24260)' => '24524',
- 'Savignac-de-Nontron (24300)' => '24525',
- 'Savignac-Lédrier (24270)' => '24526',
- 'Savignac-les-Églises (24420)' => '24527',
- 'Savignac-sur-Leyze (47150)' => '47295',
- 'Savigné (86400)' => '86255',
- 'Savigny-Lévescault (86800)' => '86256',
- 'Savigny-sous-Faye (86140)' => '86257',
- 'Sceau-Saint-Angel (24300)' => '24528',
- 'Sciecq (79000)' => '79308',
- 'Scillé (79240)' => '79309',
- 'Scorbé-Clairvaux (86140)' => '86258',
- 'Séby (64410)' => '64514',
- 'Secondigné-sur-Belle (79170)' => '79310',
- 'Secondigny (79130)' => '79311',
- 'Sedze-Maubecq (64160)' => '64515',
- 'Sedzère (64160)' => '64516',
- 'Ségalas (47410)' => '47296',
- 'Segonzac (16130)' => '16366',
- 'Segonzac (19310)' => '19253',
- 'Segonzac (24600)' => '24529',
- 'Ségur-le-Château (19230)' => '19254',
- 'Seigné (17510)' => '17422',
- 'Seignosse (40510)' => '40296',
- 'Seilhac (19700)' => '19255',
- 'Séligné (79170)' => '79312',
- 'Sembas (47360)' => '47297',
- 'Séméacq-Blachon (64350)' => '64517',
- 'Semens (33490)' => '33510',
- 'Semillac (17150)' => '17423',
- 'Semoussac (17150)' => '17424',
- 'Semussac (17120)' => '17425',
- 'Sencenac-Puy-de-Fourches (24310)' => '24530',
- 'Sendets (33690)' => '33511',
- 'Sendets (64320)' => '64518',
- 'Sénestis (47430)' => '47298',
- 'Senillé-Saint-Sauveur (86100)' => '86245',
- 'Sepvret (79120)' => '79313',
- 'Sérandon (19160)' => '19256',
- 'Séreilhac (87620)' => '87191',
- 'Sergeac (24290)' => '24531',
- 'Sérignac-Péboudou (47410)' => '47299',
- 'Sérignac-sur-Garonne (47310)' => '47300',
- 'Sérigny (86230)' => '86260',
- 'Sérilhac (19190)' => '19257',
- 'Sermur (23700)' => '23171',
- 'Séron (65320)' => '65422',
- 'Serres-Castet (64121)' => '64519',
- 'Serres-et-Montguyard (24500)' => '24532',
- 'Serres-Gaston (40700)' => '40298',
- 'Serres-Morlaàs (64160)' => '64520',
- 'Serres-Sainte-Marie (64170)' => '64521',
- 'Serreslous-et-Arribans (40700)' => '40299',
- 'Sers (16410)' => '16368',
- 'Servanches (24410)' => '24533',
- 'Servières-le-Château (19220)' => '19258',
- 'Sévignacq (64160)' => '64523',
- 'Sévignacq-Meyracq (64260)' => '64522',
- 'Sèvres-Anxaumont (86800)' => '86261',
- 'Sexcles (19430)' => '19259',
- 'Seyches (47350)' => '47301',
- 'Seyresse (40180)' => '40300',
- 'Siecq (17490)' => '17427',
- 'Siest (40180)' => '40301',
- 'Sigalens (33690)' => '33512',
- 'Sigogne (16200)' => '16369',
- 'Sigoulès (24240)' => '24534',
- 'Sillars (86320)' => '86262',
- 'Sillas (33690)' => '33513',
- 'Simacourbe (64350)' => '64524',
- 'Simeyrols (24370)' => '24535',
- 'Sindères (40110)' => '40302',
- 'Singleyrac (24500)' => '24536',
- 'Sioniac (19120)' => '19260',
- 'Siorac-de-Ribérac (24600)' => '24537',
- 'Siorac-en-Périgord (24170)' => '24538',
- 'Sireuil (16440)' => '16370',
- 'Siros (64230)' => '64525',
- 'Smarves (86240)' => '86263',
- 'Solférino (40210)' => '40303',
- 'Solignac (87110)' => '87192',
- 'Sommières-du-Clain (86160)' => '86264',
- 'Sompt (79110)' => '79314',
- 'Sonnac (17160)' => '17428',
- 'Soorts-Hossegor (40150)' => '40304',
- 'Sorbets (40320)' => '40305',
- 'Sorde-l\'Abbaye (40300)' => '40306',
- 'Sore (40430)' => '40307',
- 'Sorges et Ligueux en Périgord (24420)' => '24540',
- 'Sornac (19290)' => '19261',
- 'Sort-en-Chalosse (40180)' => '40308',
- 'Sos (47170)' => '47302',
- 'Sossais (86230)' => '86265',
- 'Soubise (17780)' => '17429',
- 'Soubran (17150)' => '17430',
- 'Soubrebost (23250)' => '23173',
- 'Soudaine-Lavinadière (19370)' => '19262',
- 'Soudan (79800)' => '79316',
- 'Soudat (24360)' => '24541',
- 'Soudeilles (19300)' => '19263',
- 'Souffrignac (16380)' => '16372',
- 'Soulac-sur-Mer (33780)' => '33514',
- 'Soulaures (24540)' => '24542',
- 'Soulignac (33760)' => '33515',
- 'Soulignonne (17250)' => '17431',
- 'Soumans (23600)' => '23174',
- 'Soumensac (47120)' => '47303',
- 'Souméras (17130)' => '17432',
- 'Soumoulou (64420)' => '64526',
- 'Souprosse (40250)' => '40309',
- 'Souraïde (64250)' => '64527',
- 'Soursac (19550)' => '19264',
- 'Sourzac (24400)' => '24543',
- 'Sous-Parsat (23150)' => '23175',
- 'Sousmoulins (17130)' => '17433',
- 'Soussac (33790)' => '33516',
- 'Soussans (33460)' => '33517',
- 'Soustons (40140)' => '40310',
- 'Soutiers (79310)' => '79318',
- 'Souvigné (16240)' => '16373',
- 'Souvigné (79800)' => '79319',
- 'Soyaux (16800)' => '16374',
- 'Suaux (16260)' => '16375',
- 'Suhescun (64780)' => '64528',
- 'Surdoux (87130)' => '87193',
- 'Surgères (17700)' => '17434',
- 'Surin (79220)' => '79320',
- 'Surin (86250)' => '86266',
- 'Suris (16270)' => '16376',
- 'Sus (64190)' => '64529',
- 'Susmiou (64190)' => '64530',
- 'Sussac (87130)' => '87194',
- 'Tabaille-Usquain (64190)' => '64531',
- 'Tabanac (33550)' => '33518',
- 'Tadousse-Ussau (64330)' => '64532',
- 'Taillant (17350)' => '17435',
- 'Taillebourg (17350)' => '17436',
- 'Taillebourg (47200)' => '47304',
- 'Taillecavat (33580)' => '33520',
- 'Taizé (79100)' => '79321',
- 'Taizé-Aizie (16700)' => '16378',
- 'Talais (33590)' => '33521',
- 'Talence (33400)' => '33522',
- 'Taller (40260)' => '40311',
- 'Talmont-sur-Gironde (17120)' => '17437',
- 'Tamniès (24620)' => '24544',
- 'Tanzac (17260)' => '17438',
- 'Taponnat-Fleurignac (16110)' => '16379',
- 'Tardes (23170)' => '23251',
- 'Tardets-Sorholus (64470)' => '64533',
- 'Targon (33760)' => '33523',
- 'Tarnac (19170)' => '19265',
- 'Tarnès (33240)' => '33524',
- 'Tarnos (40220)' => '40312',
- 'Taron-Sadirac-Viellenave (64330)' => '64534',
- 'Tarsacq (64360)' => '64535',
- 'Tartas (40400)' => '40313',
- 'Taugon (17170)' => '17439',
- 'Tauriac (33710)' => '33525',
- 'Tayac (33570)' => '33526',
- 'Tayrac (47270)' => '47305',
- 'Teillots (24390)' => '24545',
- 'Temple-Laguyon (24390)' => '24546',
- 'Tercé (86800)' => '86268',
- 'Tercillat (23350)' => '23252',
- 'Tercis-les-Bains (40180)' => '40314',
- 'Ternant (17400)' => '17440',
- 'Ternay (86120)' => '86269',
- 'Terrasson-Lavilledieu (24120)' => '24547',
- 'Tersannes (87360)' => '87195',
- 'Tesson (17460)' => '17441',
- 'Tessonnière (79600)' => '79325',
- 'Téthieu (40990)' => '40315',
- 'Teuillac (33710)' => '33530',
- 'Teyjat (24300)' => '24548',
- 'Thaims (17120)' => '17442',
- 'Thairé (17290)' => '17443',
- 'Thalamy (19200)' => '19266',
- 'Thauron (23250)' => '23253',
- 'Theil-Rabier (16240)' => '16381',
- 'Thénac (17460)' => '17444',
- 'Thénac (24240)' => '24549',
- 'Thénezay (79390)' => '79326',
- 'Thenon (24210)' => '24550',
- 'Thézac (17600)' => '17445',
- 'Thézac (47370)' => '47307',
- 'Thèze (64450)' => '64536',
- 'Thiat (87320)' => '87196',
- 'Thiviers (24800)' => '24551',
- 'Thollet (86290)' => '86270',
- 'Thonac (24290)' => '24552',
- 'Thorigné (79370)' => '79327',
- 'Thorigny-sur-le-Mignon (79360)' => '79328',
- 'Thors (17160)' => '17446',
- 'Thouars (79100)' => '79329',
- 'Thouars-sur-Garonne (47230)' => '47308',
- 'Thouron (87140)' => '87197',
- 'Thurageau (86110)' => '86271',
- 'Thuré (86540)' => '86272',
- 'Tilh (40360)' => '40316',
- 'Tillou (79110)' => '79330',
- 'Tizac-de-Curton (33420)' => '33531',
- 'Tizac-de-Lapouyade (33620)' => '33532',
- 'Tocane-Saint-Apre (24350)' => '24553',
- 'Tombeboeuf (47380)' => '47309',
- 'Tonnay-Boutonne (17380)' => '17448',
- 'Tonnay-Charente (17430)' => '17449',
- 'Tonneins (47400)' => '47310',
- 'Torsac (16410)' => '16382',
- 'Torxé (17380)' => '17450',
- 'Tosse (40230)' => '40317',
- 'Toulenne (33210)' => '33533',
- 'Toulouzette (40250)' => '40318',
- 'Toulx-Sainte-Croix (23600)' => '23254',
- 'Tourliac (47210)' => '47311',
- 'Tournon-d\'Agenais (47370)' => '47312',
- 'Tourriers (16560)' => '16383',
- 'Tourtenay (79100)' => '79331',
- 'Tourtoirac (24390)' => '24555',
- 'Tourtrès (47380)' => '47313',
- 'Touvérac (16360)' => '16384',
- 'Touvre (16600)' => '16385',
- 'Touzac (16120)' => '16386',
- 'Toy-Viam (19170)' => '19268',
- 'Trayes (79240)' => '79332',
- 'Treignac (19260)' => '19269',
- 'Trélissac (24750)' => '24557',
- 'Trémolat (24510)' => '24558',
- 'Trémons (47140)' => '47314',
- 'Trensacq (40630)' => '40319',
- 'Trentels (47140)' => '47315',
- 'Tresses (33370)' => '33535',
- 'Triac-Lautrait (16200)' => '16387',
- 'Trizay (17250)' => '17453',
- 'Troche (19230)' => '19270',
- 'Trois-Fonds (23230)' => '23255',
- 'Trois-Palis (16730)' => '16388',
- 'Trois-Villes (64470)' => '64537',
- 'Tudeils (19120)' => '19271',
- 'Tugéras-Saint-Maurice (17130)' => '17454',
- 'Tulle (19000)' => '19272',
- 'Turenne (19500)' => '19273',
- 'Turgon (16350)' => '16389',
- 'Tursac (24620)' => '24559',
- 'Tusson (16140)' => '16390',
- 'Tuzie (16700)' => '16391',
- 'Uchacq-et-Parentis (40090)' => '40320',
- 'Uhart-Cize (64220)' => '64538',
- 'Uhart-Mixe (64120)' => '64539',
- 'Urcuit (64990)' => '64540',
- 'Urdès (64370)' => '64541',
- 'Urdos (64490)' => '64542',
- 'Urepel (64430)' => '64543',
- 'Urgons (40320)' => '40321',
- 'Urost (64160)' => '64544',
- 'Urrugne (64122)' => '64545',
- 'Urt (64240)' => '64546',
- 'Urval (24480)' => '24560',
- 'Ussac (19270)' => '19274',
- 'Usseau (79210)' => '79334',
- 'Usseau (86230)' => '86275',
- 'Ussel (19200)' => '19275',
- 'Usson-du-Poitou (86350)' => '86276',
- 'Ustaritz (64480)' => '64547',
- 'Uza (40170)' => '40322',
- 'Uzan (64370)' => '64548',
- 'Uzein (64230)' => '64549',
- 'Uzerche (19140)' => '19276',
- 'Uzeste (33730)' => '33537',
- 'Uzos (64110)' => '64550',
- 'Val d\'Issoire (87330)' => '87097',
- 'Val de Virvée (33240)' => '33018',
- 'Val des Vignes (16250)' => '16175',
- 'Valdivienne (86300)' => '86233',
- 'Valence (16460)' => '16392',
- 'Valeuil (24310)' => '24561',
- 'Valeyrac (33340)' => '33538',
- 'Valiergues (19200)' => '19277',
- 'Vallans (79270)' => '79335',
- 'Vallereuil (24190)' => '24562',
- 'Vallière (23120)' => '23257',
- 'Valojoulx (24290)' => '24563',
- 'Vançais (79120)' => '79336',
- 'Vandré (17700)' => '17457',
- 'Vanxains (24600)' => '24564',
- 'Vanzac (17500)' => '17458',
- 'Vanzay (79120)' => '79338',
- 'Varaignes (24360)' => '24565',
- 'Varaize (17400)' => '17459',
- 'Vareilles (23300)' => '23258',
- 'Varennes (24150)' => '24566',
- 'Varennes (86110)' => '86277',
- 'Varès (47400)' => '47316',
- 'Varetz (19240)' => '19278',
- 'Vars (16330)' => '16393',
- 'Vars-sur-Roseix (19130)' => '19279',
- 'Varzay (17460)' => '17460',
- 'Vasles (79340)' => '79339',
- 'Vaulry (87140)' => '87198',
- 'Vaunac (24800)' => '24567',
- 'Vausseroux (79420)' => '79340',
- 'Vautebis (79420)' => '79341',
- 'Vaux (86700)' => '86278',
- 'Vaux-Lavalette (16320)' => '16394',
- 'Vaux-Rouillac (16170)' => '16395',
- 'Vaux-sur-Mer (17640)' => '17461',
- 'Vaux-sur-Vienne (86220)' => '86279',
- 'Vayres (33870)' => '33539',
- 'Vayres (87600)' => '87199',
- 'Végennes (19120)' => '19280',
- 'Veix (19260)' => '19281',
- 'Vélines (24230)' => '24568',
- 'Vellèches (86230)' => '86280',
- 'Vendays-Montalivet (33930)' => '33540',
- 'Vendeuvre-du-Poitou (86380)' => '86281',
- 'Vendoire (24320)' => '24569',
- 'Vénérand (17100)' => '17462',
- 'Vensac (33590)' => '33541',
- 'Ventouse (16460)' => '16396',
- 'Vérac (33240)' => '33542',
- 'Verdelais (33490)' => '33543',
- 'Verdets (64400)' => '64551',
- 'Verdille (16140)' => '16397',
- 'Verdon (24520)' => '24570',
- 'Vergeroux (17300)' => '17463',
- 'Vergné (17330)' => '17464',
- 'Vergt (24380)' => '24571',
- 'Vergt-de-Biron (24540)' => '24572',
- 'Vérines (17540)' => '17466',
- 'Verneiges (23170)' => '23259',
- 'Verneuil (16310)' => '16398',
- 'Verneuil-Moustiers (87360)' => '87200',
- 'Verneuil-sur-Vienne (87430)' => '87201',
- 'Vernon (86340)' => '86284',
- 'Vernoux-en-Gâtine (79240)' => '79342',
- 'Vernoux-sur-Boutonne (79170)' => '79343',
- 'Verrières (16130)' => '16399',
- 'Verrières (86410)' => '86285',
- 'Verrue (86420)' => '86286',
- 'Verruyes (79310)' => '79345',
- 'Vert (40420)' => '40323',
- 'Verteillac (24320)' => '24573',
- 'Verteuil-d\'Agenais (47260)' => '47317',
- 'Verteuil-sur-Charente (16510)' => '16400',
- 'Vertheuil (33180)' => '33545',
- 'Vervant (16330)' => '16401',
- 'Vervant (17400)' => '17467',
- 'Veyrac (87520)' => '87202',
- 'Veyrières (19200)' => '19283',
- 'Veyrignac (24370)' => '24574',
- 'Veyrines-de-Domme (24250)' => '24575',
- 'Veyrines-de-Vergt (24380)' => '24576',
- 'Vézac (24220)' => '24577',
- 'Vézières (86120)' => '86287',
- 'Vialer (64330)' => '64552',
- 'Viam (19170)' => '19284',
- 'Vianne (47230)' => '47318',
- 'Vibrac (16120)' => '16402',
- 'Vibrac (17130)' => '17468',
- 'Vicq-d\'Auribat (40380)' => '40324',
- 'Vicq-sur-Breuilh (87260)' => '87203',
- 'Vicq-sur-Gartempe (86260)' => '86288',
- 'Vidaillat (23250)' => '23260',
- 'Videix (87600)' => '87204',
- 'Vielle-Saint-Girons (40560)' => '40326',
- 'Vielle-Soubiran (40240)' => '40327',
- 'Vielle-Tursan (40320)' => '40325',
- 'Viellenave-d\'Arthez (64170)' => '64554',
- 'Viellenave-de-Navarrenx (64190)' => '64555',
- 'Vielleségure (64150)' => '64556',
- 'Viennay (79200)' => '79347',
- 'Viersat (23170)' => '23261',
- 'Vieux-Boucau-les-Bains (40480)' => '40328',
- 'Vieux-Mareuil (24340)' => '24579',
- 'Vieux-Ruffec (16350)' => '16404',
- 'Vigeois (19410)' => '19285',
- 'Vigeville (23140)' => '23262',
- 'Vignes (64410)' => '64557',
- 'Vignolles (16300)' => '16405',
- 'Vignols (19130)' => '19286',
- 'Vignonet (33330)' => '33546',
- 'Vilhonneur (16220)' => '16406',
- 'Villac (24120)' => '24580',
- 'Villamblard (24140)' => '24581',
- 'Villandraut (33730)' => '33547',
- 'Villard (23800)' => '23263',
- 'Villars (24530)' => '24582',
- 'Villars-en-Pons (17260)' => '17469',
- 'Villars-les-Bois (17770)' => '17470',
- 'Villebois-Lavalette (16320)' => '16408',
- 'Villebramar (47380)' => '47319',
- 'Villedoux (17230)' => '17472',
- 'Villefagnan (16240)' => '16409',
- 'Villefavard (87190)' => '87206',
- 'Villefollet (79170)' => '79348',
- 'Villefranche-de-Lonchat (24610)' => '24584',
- 'Villefranche-du-Périgord (24550)' => '24585',
- 'Villefranche-du-Queyran (47160)' => '47320',
- 'Villefranque (64990)' => '64558',
- 'Villegats (16700)' => '16410',
- 'Villegouge (33141)' => '33548',
- 'Villejésus (16140)' => '16411',
- 'Villejoubert (16560)' => '16412',
- 'Villemain (79110)' => '79349',
- 'Villemorin (17470)' => '17473',
- 'Villemort (86310)' => '86291',
- 'Villenave (40110)' => '40330',
- 'Villenave-d\'Ornon (33140)' => '33550',
- 'Villenave-de-Rions (33550)' => '33549',
- 'Villenave-près-Béarn (65500)' => '65476',
- 'Villeneuve (33710)' => '33551',
- 'Villeneuve-de-Duras (47120)' => '47321',
- 'Villeneuve-de-Marsan (40190)' => '40331',
- 'Villeneuve-la-Comtesse (17330)' => '17474',
- 'Villeneuve-sur-Lot (47300)' => '47323',
- 'Villeréal (47210)' => '47324',
- 'Villeton (47400)' => '47325',
- 'Villetoureix (24600)' => '24586',
- 'Villexavier (17500)' => '17476',
- 'Villiers (86190)' => '86292',
- 'Villiers-Couture (17510)' => '17477',
- 'Villiers-en-Bois (79360)' => '79350',
- 'Villiers-en-Plaine (79160)' => '79351',
- 'Villiers-le-Roux (16240)' => '16413',
- 'Villiers-sur-Chizé (79170)' => '79352',
- 'Villognon (16230)' => '16414',
- 'Vinax (17510)' => '17478',
- 'Vindelle (16430)' => '16415',
- 'Viodos-Abense-de-Bas (64130)' => '64559',
- 'Virazeil (47200)' => '47326',
- 'Virelade (33720)' => '33552',
- 'Virollet (17260)' => '17479',
- 'Virsac (33240)' => '33553',
- 'Virson (17290)' => '17480',
- 'Vitrac (24200)' => '24587',
- 'Vitrac-Saint-Vincent (16310)' => '16416',
- 'Vitrac-sur-Montane (19800)' => '19287',
- 'Viven (64450)' => '64560',
- 'Viville (16120)' => '16417',
- 'Vivonne (86370)' => '86293',
- 'Voeuil-et-Giget (16400)' => '16418',
- 'Voissay (17400)' => '17481',
- 'Vouharte (16330)' => '16419',
- 'Vouhé (17700)' => '17482',
- 'Vouhé (79310)' => '79354',
- 'Vouillé (79230)' => '79355',
- 'Vouillé (86190)' => '86294',
- 'Voulême (86400)' => '86295',
- 'Voulgézac (16250)' => '16420',
- 'Voulmentin (79150)' => '79242',
- 'Voulon (86700)' => '86296',
- 'Vouneuil-sous-Biard (86580)' => '86297',
- 'Vouneuil-sur-Vienne (86210)' => '86298',
- 'Voutezac (19130)' => '19288',
- 'Vouthon (16220)' => '16421',
- 'Vouzailles (86170)' => '86299',
- 'Vouzan (16410)' => '16422',
- 'Xaintrailles (47230)' => '47327',
- 'Xaintray (79220)' => '79357',
- 'Xambes (16330)' => '16423',
- 'Ychoux (40160)' => '40332',
- 'Ygos-Saint-Saturnin (40110)' => '40333',
- 'Yssandon (19310)' => '19289',
- 'Yversay (86170)' => '86300',
- 'Yves (17340)' => '17483',
- 'Yviers (16210)' => '16424',
- 'Yvrac (33370)' => '33554',
- 'Yvrac-et-Malleyrand (16110)' => '16425',
- 'Yzosse (40180)' => '40334'
- );
+ const CITIES = [
+ 'Aast (64460)' => '64001',
+ 'Abère (64160)' => '64002',
+ 'Abidos (64150)' => '64003',
+ 'Abitain (64390)' => '64004',
+ 'Abjat-sur-Bandiat (24300)' => '24001',
+ 'Abos (64360)' => '64005',
+ 'Abzac (16500)' => '16001',
+ 'Abzac (33230)' => '33001',
+ 'Accous (64490)' => '64006',
+ 'Adilly (79200)' => '79002',
+ 'Adriers (86430)' => '86001',
+ 'Affieux (19260)' => '19001',
+ 'Agen (47000)' => '47001',
+ 'Agmé (47350)' => '47002',
+ 'Agnac (47800)' => '47003',
+ 'Agnos (64400)' => '64007',
+ 'Agonac (24460)' => '24002',
+ 'Agris (16110)' => '16003',
+ 'Agudelle (17500)' => '17002',
+ 'Ahaxe-Alciette-Bascassan (64220)' => '64008',
+ 'Ahetze (64210)' => '64009',
+ 'Ahun (23150)' => '23001',
+ 'Aïcirits-Camou-Suhast (64120)' => '64010',
+ 'Aiffres (79230)' => '79003',
+ 'Aignes-et-Puypéroux (16190)' => '16004',
+ 'Aigonnay (79370)' => '79004',
+ 'Aigre (16140)' => '16005',
+ 'Aigrefeuille-d\'Aunis (17290)' => '17003',
+ 'Aiguillon (47190)' => '47004',
+ 'Aillas (33124)' => '33002',
+ 'Aincille (64220)' => '64011',
+ 'Ainharp (64130)' => '64012',
+ 'Ainhice-Mongelos (64220)' => '64013',
+ 'Ainhoa (64250)' => '64014',
+ 'Aire-sur-l\'Adour (40800)' => '40001',
+ 'Airvault (79600)' => '79005',
+ 'Aix (19200)' => '19002',
+ 'Aixe-sur-Vienne (87700)' => '87001',
+ 'Ajain (23380)' => '23002',
+ 'Ajat (24210)' => '24004',
+ 'Albignac (19190)' => '19003',
+ 'Albussac (19380)' => '19004',
+ 'Alçay-Alçabéhéty-Sunharette (64470)' => '64015',
+ 'Aldudes (64430)' => '64016',
+ 'Allas-Bocage (17150)' => '17005',
+ 'Allas-Champagne (17500)' => '17006',
+ 'Allas-les-Mines (24220)' => '24006',
+ 'Allassac (19240)' => '19005',
+ 'Allemans (24600)' => '24007',
+ 'Allemans-du-Dropt (47800)' => '47005',
+ 'Alles-sur-Dordogne (24480)' => '24005',
+ 'Alleyrat (19200)' => '19006',
+ 'Alleyrat (23200)' => '23003',
+ 'Allez-et-Cazeneuve (47110)' => '47006',
+ 'Allonne (79130)' => '79007',
+ 'Allons (47420)' => '47007',
+ 'Alloue (16490)' => '16007',
+ 'Alos-Sibas-Abense (64470)' => '64017',
+ 'Altillac (19120)' => '19007',
+ 'Amailloux (79350)' => '79008',
+ 'Ambarès-et-Lagrave (33440)' => '33003',
+ 'Ambazac (87240)' => '87002',
+ 'Ambérac (16140)' => '16008',
+ 'Ambernac (16490)' => '16009',
+ 'Amberre (86110)' => '86002',
+ 'Ambès (33810)' => '33004',
+ 'Ambleville (16300)' => '16010',
+ 'Ambrugeat (19250)' => '19008',
+ 'Ambrus (47160)' => '47008',
+ 'Amendeuix-Oneix (64120)' => '64018',
+ 'Amorots-Succos (64120)' => '64019',
+ 'Amou (40330)' => '40002',
+ 'Amuré (79210)' => '79009',
+ 'Anais (16560)' => '16011',
+ 'Anais (17540)' => '17007',
+ 'Ance (64570)' => '64020',
+ 'Anché (86700)' => '86003',
+ 'Andernos-les-Bains (33510)' => '33005',
+ 'Andilly (17230)' => '17008',
+ 'Andiran (47170)' => '47009',
+ 'Andoins (64420)' => '64021',
+ 'Andrein (64390)' => '64022',
+ 'Angaïs (64510)' => '64023',
+ 'Angeac-Champagne (16130)' => '16012',
+ 'Angeac-Charente (16120)' => '16013',
+ 'Angeduc (16300)' => '16014',
+ 'Anglade (33390)' => '33006',
+ 'Angles-sur-l\'Anglin (86260)' => '86004',
+ 'Anglet (64600)' => '64024',
+ 'Angliers (17540)' => '17009',
+ 'Angliers (86330)' => '86005',
+ 'Angoisse (24270)' => '24008',
+ 'Angoulême (16000)' => '16015',
+ 'Angoulins (17690)' => '17010',
+ 'Angoumé (40990)' => '40003',
+ 'Angous (64190)' => '64025',
+ 'Angresse (40150)' => '40004',
+ 'Anhaux (64220)' => '64026',
+ 'Anlhiac (24160)' => '24009',
+ 'Annepont (17350)' => '17011',
+ 'Annesse-et-Beaulieu (24430)' => '24010',
+ 'Annezay (17380)' => '17012',
+ 'Anos (64160)' => '64027',
+ 'Anoye (64350)' => '64028',
+ 'Ansac-sur-Vienne (16500)' => '16016',
+ 'Antagnac (47700)' => '47010',
+ 'Antezant-la-Chapelle (17400)' => '17013',
+ 'Anthé (47370)' => '47011',
+ 'Antigny (86310)' => '86006',
+ 'Antonne-et-Trigonant (24420)' => '24011',
+ 'Antran (86100)' => '86007',
+ 'Anville (16170)' => '16017',
+ 'Anzême (23000)' => '23004',
+ 'Anzex (47700)' => '47012',
+ 'Aramits (64570)' => '64029',
+ 'Arancou (64270)' => '64031',
+ 'Araujuzon (64190)' => '64032',
+ 'Araux (64190)' => '64033',
+ 'Arbanats (33640)' => '33007',
+ 'Arbérats-Sillègue (64120)' => '64034',
+ 'Arbis (33760)' => '33008',
+ 'Arbonne (64210)' => '64035',
+ 'Arboucave (40320)' => '40005',
+ 'Arbouet-Sussaute (64120)' => '64036',
+ 'Arbus (64230)' => '64037',
+ 'Arcachon (33120)' => '33009',
+ 'Arçais (79210)' => '79010',
+ 'Arcangues (64200)' => '64038',
+ 'Arçay (86200)' => '86008',
+ 'Arces (17120)' => '17015',
+ 'Archiac (17520)' => '17016',
+ 'Archignac (24590)' => '24012',
+ 'Archigny (86210)' => '86009',
+ 'Archingeay (17380)' => '17017',
+ 'Arcins (33460)' => '33010',
+ 'Ardilleux (79110)' => '79011',
+ 'Ardillières (17290)' => '17018',
+ 'Ardin (79160)' => '79012',
+ 'Aren (64400)' => '64039',
+ 'Arengosse (40110)' => '40006',
+ 'Arès (33740)' => '33011',
+ 'Aressy (64320)' => '64041',
+ 'Arette (64570)' => '64040',
+ 'Arfeuille-Châtain (23700)' => '23005',
+ 'Argagnon (64300)' => '64042',
+ 'Argelos (40700)' => '40007',
+ 'Argelos (64450)' => '64043',
+ 'Argelouse (40430)' => '40008',
+ 'Argentat (19400)' => '19010',
+ 'Argenton (47250)' => '47013',
+ 'Argenton-l\'Église (79290)' => '79014',
+ 'Argentonnay (79150)' => '79013',
+ 'Arget (64410)' => '64044',
+ 'Arhansus (64120)' => '64045',
+ 'Arjuzanx (40110)' => '40009',
+ 'Armendarits (64640)' => '64046',
+ 'Armillac (47800)' => '47014',
+ 'Arnac-la-Poste (87160)' => '87003',
+ 'Arnac-Pompadour (19230)' => '19011',
+ 'Arnéguy (64220)' => '64047',
+ 'Arnos (64370)' => '64048',
+ 'Aroue-Ithorots-Olhaïby (64120)' => '64049',
+ 'Arrast-Larrebieu (64130)' => '64050',
+ 'Arraute-Charritte (64120)' => '64051',
+ 'Arrènes (23210)' => '23006',
+ 'Arricau-Bordes (64350)' => '64052',
+ 'Arrien (64420)' => '64053',
+ 'Arros-de-Nay (64800)' => '64054',
+ 'Arrosès (64350)' => '64056',
+ 'Ars (16130)' => '16018',
+ 'Ars (23480)' => '23007',
+ 'Ars-en-Ré (17590)' => '17019',
+ 'Arsac (33460)' => '33012',
+ 'Arsague (40330)' => '40011',
+ 'Artassenx (40090)' => '40012',
+ 'Arthenac (17520)' => '17020',
+ 'Arthez-d\'Armagnac (40190)' => '40013',
+ 'Arthez-d\'Asson (64800)' => '64058',
+ 'Arthez-de-Béarn (64370)' => '64057',
+ 'Artigueloutan (64420)' => '64059',
+ 'Artiguelouve (64230)' => '64060',
+ 'Artigues-près-Bordeaux (33370)' => '33013',
+ 'Artix (64170)' => '64061',
+ 'Arudy (64260)' => '64062',
+ 'Arue (40120)' => '40014',
+ 'Arvert (17530)' => '17021',
+ 'Arveyres (33500)' => '33015',
+ 'Arx (40310)' => '40015',
+ 'Arzacq-Arraziguet (64410)' => '64063',
+ 'Asasp-Arros (64660)' => '64064',
+ 'Ascain (64310)' => '64065',
+ 'Ascarat (64220)' => '64066',
+ 'Aslonnes (86340)' => '86010',
+ 'Asnières-en-Poitou (79170)' => '79015',
+ 'Asnières-la-Giraud (17400)' => '17022',
+ 'Asnières-sur-Blour (86430)' => '86011',
+ 'Asnières-sur-Nouère (16290)' => '16019',
+ 'Asnois (86250)' => '86012',
+ 'Asques (33240)' => '33016',
+ 'Assais-les-Jumeaux (79600)' => '79016',
+ 'Assat (64510)' => '64067',
+ 'Asson (64800)' => '64068',
+ 'Astaffort (47220)' => '47015',
+ 'Astaillac (19120)' => '19012',
+ 'Aste-Béon (64260)' => '64069',
+ 'Astis (64450)' => '64070',
+ 'Athos-Aspis (64390)' => '64071',
+ 'Aubagnan (40700)' => '40016',
+ 'Aubas (24290)' => '24014',
+ 'Aubazines (19190)' => '19013',
+ 'Aubertin (64290)' => '64072',
+ 'Aubeterre-sur-Dronne (16390)' => '16020',
+ 'Aubiac (33430)' => '33017',
+ 'Aubiac (47310)' => '47016',
+ 'Aubigné (79110)' => '79018',
+ 'Aubigny (79390)' => '79019',
+ 'Aubin (64230)' => '64073',
+ 'Aubous (64330)' => '64074',
+ 'Aubusson (23200)' => '23008',
+ 'Audaux (64190)' => '64075',
+ 'Audenge (33980)' => '33019',
+ 'Audignon (40500)' => '40017',
+ 'Audon (40400)' => '40018',
+ 'Audrix (24260)' => '24015',
+ 'Auga (64450)' => '64077',
+ 'Auge (23170)' => '23009',
+ 'Augé (79400)' => '79020',
+ 'Auge-Saint-Médard (16170)' => '16339',
+ 'Augères (23210)' => '23010',
+ 'Augignac (24300)' => '24016',
+ 'Augne (87120)' => '87004',
+ 'Aujac (17770)' => '17023',
+ 'Aulnay (17470)' => '17024',
+ 'Aulnay (86330)' => '86013',
+ 'Aulon (23210)' => '23011',
+ 'Aumagne (17770)' => '17025',
+ 'Aunac (16460)' => '16023',
+ 'Auradou (47140)' => '47017',
+ 'Aureil (87220)' => '87005',
+ 'Aureilhan (40200)' => '40019',
+ 'Auriac (19220)' => '19014',
+ 'Auriac (64450)' => '64078',
+ 'Auriac-du-Périgord (24290)' => '24018',
+ 'Auriac-sur-Dropt (47120)' => '47018',
+ 'Auriat (23400)' => '23012',
+ 'Aurice (40500)' => '40020',
+ 'Auriolles (33790)' => '33020',
+ 'Aurions-Idernes (64350)' => '64079',
+ 'Auros (33124)' => '33021',
+ 'Aussac-Vadalle (16560)' => '16024',
+ 'Aussevielle (64230)' => '64080',
+ 'Aussurucq (64130)' => '64081',
+ 'Auterrive (64270)' => '64082',
+ 'Autevielle-Saint-Martin-Bideren (64390)' => '64083',
+ 'Authon-Ébéon (17770)' => '17026',
+ 'Auzances (23700)' => '23013',
+ 'Availles-en-Châtellerault (86530)' => '86014',
+ 'Availles-Limouzine (86460)' => '86015',
+ 'Availles-Thouarsais (79600)' => '79022',
+ 'Avanton (86170)' => '86016',
+ 'Avensan (33480)' => '33022',
+ 'Avon (79800)' => '79023',
+ 'Avy (17800)' => '17027',
+ 'Aydie (64330)' => '64084',
+ 'Aydius (64490)' => '64085',
+ 'Ayen (19310)' => '19015',
+ 'Ayguemorte-les-Graves (33640)' => '33023',
+ 'Ayherre (64240)' => '64086',
+ 'Ayron (86190)' => '86017',
+ 'Aytré (17440)' => '17028',
+ 'Azat-Châtenet (23210)' => '23014',
+ 'Azat-le-Ris (87360)' => '87006',
+ 'Azay-le-Brûlé (79400)' => '79024',
+ 'Azay-sur-Thouet (79130)' => '79025',
+ 'Azerables (23160)' => '23015',
+ 'Azerat (24210)' => '24019',
+ 'Azur (40140)' => '40021',
+ 'Badefols-d\'Ans (24390)' => '24021',
+ 'Badefols-sur-Dordogne (24150)' => '24022',
+ 'Bagas (33190)' => '33024',
+ 'Bagnizeau (17160)' => '17029',
+ 'Bahus-Soubiran (40320)' => '40022',
+ 'Baigneaux (33760)' => '33025',
+ 'Baignes-Sainte-Radegonde (16360)' => '16025',
+ 'Baigts (40380)' => '40023',
+ 'Baigts-de-Béarn (64300)' => '64087',
+ 'Bajamont (47480)' => '47019',
+ 'Balansun (64300)' => '64088',
+ 'Balanzac (17600)' => '17030',
+ 'Baleix (64460)' => '64089',
+ 'Baleyssagues (47120)' => '47020',
+ 'Baliracq-Maumusson (64330)' => '64090',
+ 'Baliros (64510)' => '64091',
+ 'Balizac (33730)' => '33026',
+ 'Ballans (17160)' => '17031',
+ 'Balledent (87290)' => '87007',
+ 'Ballon (17290)' => '17032',
+ 'Balzac (16430)' => '16026',
+ 'Banca (64430)' => '64092',
+ 'Baneuil (24150)' => '24023',
+ 'Banize (23120)' => '23016',
+ 'Banos (40500)' => '40024',
+ 'Bar (19800)' => '19016',
+ 'Barbaste (47230)' => '47021',
+ 'Barbezières (16140)' => '16027',
+ 'Barbezieux-Saint-Hilaire (16300)' => '16028',
+ 'Barcus (64130)' => '64093',
+ 'Bardenac (16210)' => '16029',
+ 'Bardos (64520)' => '64094',
+ 'Bardou (24560)' => '24024',
+ 'Barie (33190)' => '33027',
+ 'Barinque (64160)' => '64095',
+ 'Baron (33750)' => '33028',
+ 'Barraute-Camu (64390)' => '64096',
+ 'Barret (16300)' => '16030',
+ 'Barro (16700)' => '16031',
+ 'Bars (24210)' => '24025',
+ 'Barsac (33720)' => '33030',
+ 'Barzan (17120)' => '17034',
+ 'Barzun (64530)' => '64097',
+ 'Bas-Mauco (40500)' => '40026',
+ 'Bascons (40090)' => '40025',
+ 'Bassac (16120)' => '16032',
+ 'Bassanne (33190)' => '33031',
+ 'Bassens (33530)' => '33032',
+ 'Bassercles (40700)' => '40027',
+ 'Basses (86200)' => '86018',
+ 'Bassignac-le-Bas (19430)' => '19017',
+ 'Bassignac-le-Haut (19220)' => '19018',
+ 'Bassillac (24330)' => '24026',
+ 'Bassillon-Vauzé (64350)' => '64098',
+ 'Bassussarry (64200)' => '64100',
+ 'Bastanès (64190)' => '64099',
+ 'Bastennes (40360)' => '40028',
+ 'Basville (23260)' => '23017',
+ 'Bats (40320)' => '40029',
+ 'Baudignan (40310)' => '40030',
+ 'Baudreix (64800)' => '64101',
+ 'Baurech (33880)' => '33033',
+ 'Bayac (24150)' => '24027',
+ 'Bayas (33230)' => '33034',
+ 'Bayers (16460)' => '16033',
+ 'Bayon-sur-Gironde (33710)' => '33035',
+ 'Bayonne (64100)' => '64102',
+ 'Bazac (16210)' => '16034',
+ 'Bazas (33430)' => '33036',
+ 'Bazauges (17490)' => '17035',
+ 'Bazelat (23160)' => '23018',
+ 'Bazens (47130)' => '47022',
+ 'Beaugas (47290)' => '47023',
+ 'Beaugeay (17620)' => '17036',
+ 'Beaulieu-sous-Parthenay (79420)' => '79029',
+ 'Beaulieu-sur-Dordogne (19120)' => '19019',
+ 'Beaulieu-sur-Sonnette (16450)' => '16035',
+ 'Beaumont (19390)' => '19020',
+ 'Beaumont (86490)' => '86019',
+ 'Beaumont-du-Lac (87120)' => '87009',
+ 'Beaumontois en Périgord (24440)' => '24028',
+ 'Beaupouyet (24400)' => '24029',
+ 'Beaupuy (47200)' => '47024',
+ 'Beauregard-de-Terrasson (24120)' => '24030',
+ 'Beauregard-et-Bassac (24140)' => '24031',
+ 'Beauronne (24400)' => '24032',
+ 'Beaussac (24340)' => '24033',
+ 'Beaussais-Vitré (79370)' => '79030',
+ 'Beautiran (33640)' => '33037',
+ 'Beauvais-sur-Matha (17490)' => '17037',
+ 'Beauville (47470)' => '47025',
+ 'Beauvoir-sur-Niort (79360)' => '79031',
+ 'Beauziac (47700)' => '47026',
+ 'Béceleuf (79160)' => '79032',
+ 'Bécheresse (16250)' => '16036',
+ 'Bédeille (64460)' => '64103',
+ 'Bedenac (17210)' => '17038',
+ 'Bedous (64490)' => '64104',
+ 'Bégaar (40400)' => '40031',
+ 'Bégadan (33340)' => '33038',
+ 'Bègles (33130)' => '33039',
+ 'Béguey (33410)' => '33040',
+ 'Béguios (64120)' => '64105',
+ 'Béhasque-Lapiste (64120)' => '64106',
+ 'Béhorléguy (64220)' => '64107',
+ 'Beissat (23260)' => '23019',
+ 'Beleymas (24140)' => '24034',
+ 'Belhade (40410)' => '40032',
+ 'Belin-Béliet (33830)' => '33042',
+ 'Bélis (40120)' => '40033',
+ 'Bellac (87300)' => '87011',
+ 'Bellebat (33760)' => '33043',
+ 'Bellechassagne (19290)' => '19021',
+ 'Bellefond (33760)' => '33044',
+ 'Bellefonds (86210)' => '86020',
+ 'Bellegarde-en-Marche (23190)' => '23020',
+ 'Belleville (79360)' => '79033',
+ 'Bellocq (64270)' => '64108',
+ 'Bellon (16210)' => '16037',
+ 'Belluire (17800)' => '17039',
+ 'Bélus (40300)' => '40034',
+ 'Belvès-de-Castillon (33350)' => '33045',
+ 'Benassay (86470)' => '86021',
+ 'Benayes (19510)' => '19022',
+ 'Bénéjacq (64800)' => '64109',
+ 'Bénesse-lès-Dax (40180)' => '40035',
+ 'Bénesse-Maremne (40230)' => '40036',
+ 'Benest (16350)' => '16038',
+ 'Bénévent-l\'Abbaye (23210)' => '23021',
+ 'Benon (17170)' => '17041',
+ 'Benquet (40280)' => '40037',
+ 'Bentayou-Sérée (64460)' => '64111',
+ 'Béost (64440)' => '64110',
+ 'Berbiguières (24220)' => '24036',
+ 'Bercloux (17770)' => '17042',
+ 'Bérenx (64300)' => '64112',
+ 'Bergerac (24100)' => '24037',
+ 'Bergouey (40250)' => '40038',
+ 'Bergouey-Viellenave (64270)' => '64113',
+ 'Bernac (16700)' => '16039',
+ 'Bernadets (64160)' => '64114',
+ 'Bernay-Saint-Martin (17330)' => '17043',
+ 'Berneuil (16480)' => '16040',
+ 'Berneuil (17460)' => '17044',
+ 'Berneuil (87300)' => '87012',
+ 'Bernos-Beaulac (33430)' => '33046',
+ 'Berrie (86120)' => '86022',
+ 'Berrogain-Laruns (64130)' => '64115',
+ 'Bersac-sur-Rivalier (87370)' => '87013',
+ 'Berson (33390)' => '33047',
+ 'Berthegon (86420)' => '86023',
+ 'Berthez (33124)' => '33048',
+ 'Bertric-Burée (24320)' => '24038',
+ 'Béruges (86190)' => '86024',
+ 'Bescat (64260)' => '64116',
+ 'Bésingrand (64150)' => '64117',
+ 'Bessac (16250)' => '16041',
+ 'Bessé (16140)' => '16042',
+ 'Besse (24550)' => '24039',
+ 'Bessines (79000)' => '79034',
+ 'Bessines-sur-Gartempe (87250)' => '87014',
+ 'Betbezer-d\'Armagnac (40240)' => '40039',
+ 'Bétête (23270)' => '23022',
+ 'Béthines (86310)' => '86025',
+ 'Bétracq (64350)' => '64118',
+ 'Beurlay (17250)' => '17045',
+ 'Beuste (64800)' => '64119',
+ 'Beuxes (86120)' => '86026',
+ 'Beychac-et-Caillau (33750)' => '33049',
+ 'Beylongue (40370)' => '40040',
+ 'Beynac (87700)' => '87015',
+ 'Beynac-et-Cazenac (24220)' => '24040',
+ 'Beynat (19190)' => '19023',
+ 'Beyrie-en-Béarn (64230)' => '64121',
+ 'Beyrie-sur-Joyeuse (64120)' => '64120',
+ 'Beyries (40700)' => '40041',
+ 'Beyssac (19230)' => '19024',
+ 'Beyssenac (19230)' => '19025',
+ 'Bézenac (24220)' => '24041',
+ 'Biard (86580)' => '86027',
+ 'Biarritz (64200)' => '64122',
+ 'Biarrotte (40390)' => '40042',
+ 'Bias (40170)' => '40043',
+ 'Bias (47300)' => '47027',
+ 'Biaudos (40390)' => '40044',
+ 'Bidache (64520)' => '64123',
+ 'Bidarray (64780)' => '64124',
+ 'Bidart (64210)' => '64125',
+ 'Bidos (64400)' => '64126',
+ 'Bielle (64260)' => '64127',
+ 'Bieujac (33210)' => '33050',
+ 'Biganos (33380)' => '33051',
+ 'Bignay (17400)' => '17046',
+ 'Bignoux (86800)' => '86028',
+ 'Bilhac (19120)' => '19026',
+ 'Bilhères (64260)' => '64128',
+ 'Billère (64140)' => '64129',
+ 'Bioussac (16700)' => '16044',
+ 'Birac (16120)' => '16045',
+ 'Birac (33430)' => '33053',
+ 'Birac-sur-Trec (47200)' => '47028',
+ 'Biras (24310)' => '24042',
+ 'Biriatou (64700)' => '64130',
+ 'Biron (17800)' => '17047',
+ 'Biron (24540)' => '24043',
+ 'Biron (64300)' => '64131',
+ 'Biscarrosse (40600)' => '40046',
+ 'Bizanos (64320)' => '64132',
+ 'Blaignac (33190)' => '33054',
+ 'Blaignan (33340)' => '33055',
+ 'Blanquefort (33290)' => '33056',
+ 'Blanquefort-sur-Briolance (47500)' => '47029',
+ 'Blanzac (87300)' => '87017',
+ 'Blanzac-lès-Matha (17160)' => '17048',
+ 'Blanzac-Porcheresse (16250)' => '16046',
+ 'Blanzaguet-Saint-Cybard (16320)' => '16047',
+ 'Blanzay (86400)' => '86029',
+ 'Blanzay-sur-Boutonne (17470)' => '17049',
+ 'Blasimon (33540)' => '33057',
+ 'Blaslay (86170)' => '86030',
+ 'Blaudeix (23140)' => '23023',
+ 'Blaye (33390)' => '33058',
+ 'Blaymont (47470)' => '47030',
+ 'Blésignac (33670)' => '33059',
+ 'Blessac (23200)' => '23024',
+ 'Blis-et-Born (24330)' => '24044',
+ 'Blond (87300)' => '87018',
+ 'Boé (47550)' => '47031',
+ 'Boeil-Bezing (64510)' => '64133',
+ 'Bois (17240)' => '17050',
+ 'Boisbreteau (16480)' => '16048',
+ 'Boismé (79300)' => '79038',
+ 'Boisné-La Tude (16320)' => '16082',
+ 'Boisredon (17150)' => '17052',
+ 'Boisse (24560)' => '24045',
+ 'Boisserolles (79360)' => '79039',
+ 'Boisseuil (87220)' => '87019',
+ 'Boisseuilh (24390)' => '24046',
+ 'Bommes (33210)' => '33060',
+ 'Bon-Encontre (47240)' => '47032',
+ 'Bonloc (64240)' => '64134',
+ 'Bonnac-la-Côte (87270)' => '87020',
+ 'Bonnat (23220)' => '23025',
+ 'Bonnefond (19170)' => '19027',
+ 'Bonnegarde (40330)' => '40047',
+ 'Bonnes (16390)' => '16049',
+ 'Bonnes (86300)' => '86031',
+ 'Bonnetan (33370)' => '33061',
+ 'Bonneuil (16120)' => '16050',
+ 'Bonneuil-Matours (86210)' => '86032',
+ 'Bonneville (16170)' => '16051',
+ 'Bonneville-et-Saint-Avit-de-Fumadières (24230)' => '24048',
+ 'Bonnut (64300)' => '64135',
+ 'Bonzac (33910)' => '33062',
+ 'Boos (40370)' => '40048',
+ 'Borce (64490)' => '64136',
+ 'Bord-Saint-Georges (23230)' => '23026',
+ 'Bordeaux (33000)' => '33063',
+ 'Bordères (64800)' => '64137',
+ 'Bordères-et-Lamensans (40270)' => '40049',
+ 'Bordes (64510)' => '64138',
+ 'Bords (17430)' => '17053',
+ 'Boresse-et-Martron (17270)' => '17054',
+ 'Borrèze (24590)' => '24050',
+ 'Bors (Canton de Baignes-Sainte-Radegonde) (16360)' => '16053',
+ 'Bors (Canton de Montmoreau-Saint-Cybard) (16190)' => '16052',
+ 'Bort-les-Orgues (19110)' => '19028',
+ 'Boscamnant (17360)' => '17055',
+ 'Bosdarros (64290)' => '64139',
+ 'Bosmie-l\'Aiguille (87110)' => '87021',
+ 'Bosmoreau-les-Mines (23400)' => '23027',
+ 'Bosroger (23200)' => '23028',
+ 'Bosset (24130)' => '24051',
+ 'Bossugan (33350)' => '33064',
+ 'Bostens (40090)' => '40050',
+ 'Boucau (64340)' => '64140',
+ 'Boudy-de-Beauregard (47290)' => '47033',
+ 'Boueilh-Boueilho-Lasque (64330)' => '64141',
+ 'Bouëx (16410)' => '16055',
+ 'Bougarber (64230)' => '64142',
+ 'Bouglon (47250)' => '47034',
+ 'Bougneau (17800)' => '17056',
+ 'Bougon (79800)' => '79042',
+ 'Bougue (40090)' => '40051',
+ 'Bouhet (17540)' => '17057',
+ 'Bouillac (24480)' => '24052',
+ 'Bouillé-Loretz (79290)' => '79043',
+ 'Bouillé-Saint-Paul (79290)' => '79044',
+ 'Bouillon (64410)' => '64143',
+ 'Bouin (79110)' => '79045',
+ 'Boulazac Isle Manoire (24750)' => '24053',
+ 'Bouliac (33270)' => '33065',
+ 'Boumourt (64370)' => '64144',
+ 'Bouniagues (24560)' => '24054',
+ 'Bourcefranc-le-Chapus (17560)' => '17058',
+ 'Bourdalat (40190)' => '40052',
+ 'Bourdeilles (24310)' => '24055',
+ 'Bourdelles (33190)' => '33066',
+ 'Bourdettes (64800)' => '64145',
+ 'Bouresse (86410)' => '86034',
+ 'Bourg (33710)' => '33067',
+ 'Bourg-Archambault (86390)' => '86035',
+ 'Bourg-Charente (16200)' => '16056',
+ 'Bourg-des-Maisons (24320)' => '24057',
+ 'Bourg-du-Bost (24600)' => '24058',
+ 'Bourganeuf (23400)' => '23030',
+ 'Bourgnac (24400)' => '24059',
+ 'Bourgneuf (17220)' => '17059',
+ 'Bourgougnague (47410)' => '47035',
+ 'Bourideys (33113)' => '33068',
+ 'Bourlens (47370)' => '47036',
+ 'Bournand (86120)' => '86036',
+ 'Bournel (47210)' => '47037',
+ 'Bourniquel (24150)' => '24060',
+ 'Bournos (64450)' => '64146',
+ 'Bourran (47320)' => '47038',
+ 'Bourriot-Bergonce (40120)' => '40053',
+ 'Bourrou (24110)' => '24061',
+ 'Boussac (23600)' => '23031',
+ 'Boussac-Bourg (23600)' => '23032',
+ 'Boussais (79600)' => '79047',
+ 'Boussès (47420)' => '47039',
+ 'Bouteilles-Saint-Sébastien (24320)' => '24062',
+ 'Boutenac-Touvent (17120)' => '17060',
+ 'Bouteville (16120)' => '16057',
+ 'Boutiers-Saint-Trojan (16100)' => '16058',
+ 'Bouzic (24250)' => '24063',
+ 'Brach (33480)' => '33070',
+ 'Bran (17210)' => '17061',
+ 'Branceilles (19500)' => '19029',
+ 'Branne (33420)' => '33071',
+ 'Brannens (33124)' => '33072',
+ 'Brantôme en Périgord (24310)' => '24064',
+ 'Brassempouy (40330)' => '40054',
+ 'Braud-et-Saint-Louis (33820)' => '33073',
+ 'Brax (47310)' => '47040',
+ 'Bresdon (17490)' => '17062',
+ 'Bressuire (79300)' => '79049',
+ 'Bretagne-de-Marsan (40280)' => '40055',
+ 'Bretignolles (79140)' => '79050',
+ 'Brettes (16240)' => '16059',
+ 'Breuil-la-Réorte (17700)' => '17063',
+ 'Breuil-Magné (17870)' => '17065',
+ 'Breuilaufa (87300)' => '87022',
+ 'Breuilh (24380)' => '24065',
+ 'Breuillet (17920)' => '17064',
+ 'Bréville (16370)' => '16060',
+ 'Brie (16590)' => '16061',
+ 'Brie (79100)' => '79054',
+ 'Brie-sous-Archiac (17520)' => '17066',
+ 'Brie-sous-Barbezieux (16300)' => '16062',
+ 'Brie-sous-Chalais (16210)' => '16063',
+ 'Brie-sous-Matha (17160)' => '17067',
+ 'Brie-sous-Mortagne (17120)' => '17068',
+ 'Brieuil-sur-Chizé (79170)' => '79055',
+ 'Brignac-la-Plaine (19310)' => '19030',
+ 'Brigueil-le-Chantre (86290)' => '86037',
+ 'Brigueuil (16420)' => '16064',
+ 'Brillac (16500)' => '16065',
+ 'Brion (86160)' => '86038',
+ 'Brion-près-Thouet (79290)' => '79056',
+ 'Brioux-sur-Boutonne (79170)' => '79057',
+ 'Briscous (64240)' => '64147',
+ 'Brive-la-Gaillarde (19100)' => '19031',
+ 'Brives-sur-Charente (17800)' => '17069',
+ 'Brivezac (19120)' => '19032',
+ 'Brizambourg (17770)' => '17070',
+ 'Brocas (40420)' => '40056',
+ 'Brossac (16480)' => '16066',
+ 'Brouchaud (24210)' => '24066',
+ 'Brouqueyran (33124)' => '33074',
+ 'Brousse (23700)' => '23034',
+ 'Bruch (47130)' => '47041',
+ 'Bruges (33520)' => '33075',
+ 'Bruges-Capbis-Mifaget (64800)' => '64148',
+ 'Brugnac (47260)' => '47042',
+ 'Brûlain (79230)' => '79058',
+ 'Brux (86510)' => '86039',
+ 'Buanes (40320)' => '40057',
+ 'Budelière (23170)' => '23035',
+ 'Budos (33720)' => '33076',
+ 'Bugeat (19170)' => '19033',
+ 'Bugnein (64190)' => '64149',
+ 'Bujaleuf (87460)' => '87024',
+ 'Bunus (64120)' => '64150',
+ 'Bunzac (16110)' => '16067',
+ 'Burgaronne (64390)' => '64151',
+ 'Burgnac (87800)' => '87025',
+ 'Burie (17770)' => '17072',
+ 'Buros (64160)' => '64152',
+ 'Burosse-Mendousse (64330)' => '64153',
+ 'Bussac (24350)' => '24069',
+ 'Bussac-Forêt (17210)' => '17074',
+ 'Bussac-sur-Charente (17100)' => '17073',
+ 'Busserolles (24360)' => '24070',
+ 'Bussière-Badil (24360)' => '24071',
+ 'Bussière-Dunoise (23320)' => '23036',
+ 'Bussière-Galant (87230)' => '87027',
+ 'Bussière-Nouvelle (23700)' => '23037',
+ 'Bussière-Poitevine (87320)' => '87028',
+ 'Bussière-Saint-Georges (23600)' => '23038',
+ 'Bussunarits-Sarrasquette (64220)' => '64154',
+ 'Bustince-Iriberry (64220)' => '64155',
+ 'Buxerolles (86180)' => '86041',
+ 'Buxeuil (37160)' => '86042',
+ 'Buzet-sur-Baïse (47160)' => '47043',
+ 'Buziet (64680)' => '64156',
+ 'Buzy (64260)' => '64157',
+ 'Cabanac-et-Villagrains (33650)' => '33077',
+ 'Cabara (33420)' => '33078',
+ 'Cabariot (17430)' => '17075',
+ 'Cabidos (64410)' => '64158',
+ 'Cachen (40120)' => '40058',
+ 'Cadarsac (33750)' => '33079',
+ 'Cadaujac (33140)' => '33080',
+ 'Cadillac (33410)' => '33081',
+ 'Cadillac-en-Fronsadais (33240)' => '33082',
+ 'Cadillon (64330)' => '64159',
+ 'Cagnotte (40300)' => '40059',
+ 'Cahuzac (47330)' => '47044',
+ 'Calès (24150)' => '24073',
+ 'Calignac (47600)' => '47045',
+ 'Callen (40430)' => '40060',
+ 'Calonges (47430)' => '47046',
+ 'Calviac-en-Périgord (24370)' => '24074',
+ 'Camarsac (33750)' => '33083',
+ 'Cambes (33880)' => '33084',
+ 'Cambes (47350)' => '47047',
+ 'Camblanes-et-Meynac (33360)' => '33085',
+ 'Cambo-les-Bains (64250)' => '64160',
+ 'Came (64520)' => '64161',
+ 'Camiac-et-Saint-Denis (33420)' => '33086',
+ 'Camiran (33190)' => '33087',
+ 'Camou-Cihigue (64470)' => '64162',
+ 'Campagnac-lès-Quercy (24550)' => '24075',
+ 'Campagne (24260)' => '24076',
+ 'Campagne (40090)' => '40061',
+ 'Campet-et-Lamolère (40090)' => '40062',
+ 'Camps-Saint-Mathurin-Léobazel (19430)' => '19034',
+ 'Camps-sur-l\'Isle (33660)' => '33088',
+ 'Campsegret (24140)' => '24077',
+ 'Campugnan (33390)' => '33089',
+ 'Cancon (47290)' => '47048',
+ 'Candresse (40180)' => '40063',
+ 'Canéjan (33610)' => '33090',
+ 'Canenx-et-Réaut (40090)' => '40064',
+ 'Cantenac (33460)' => '33091',
+ 'Cantillac (24530)' => '24079',
+ 'Cantois (33760)' => '33092',
+ 'Capbreton (40130)' => '40065',
+ 'Capdrot (24540)' => '24080',
+ 'Capian (33550)' => '33093',
+ 'Caplong (33220)' => '33094',
+ 'Captieux (33840)' => '33095',
+ 'Carbon-Blanc (33560)' => '33096',
+ 'Carcans (33121)' => '33097',
+ 'Carcarès-Sainte-Croix (40400)' => '40066',
+ 'Carcen-Ponson (40400)' => '40067',
+ 'Cardan (33410)' => '33098',
+ 'Cardesse (64360)' => '64165',
+ 'Carignan-de-Bordeaux (33360)' => '33099',
+ 'Carlux (24370)' => '24081',
+ 'Caro (64220)' => '64166',
+ 'Carrère (64160)' => '64167',
+ 'Carresse-Cassaber (64270)' => '64168',
+ 'Cars (33390)' => '33100',
+ 'Carsac-Aillac (24200)' => '24082',
+ 'Carsac-de-Gurson (24610)' => '24083',
+ 'Cartelègue (33390)' => '33101',
+ 'Carves (24170)' => '24084',
+ 'Cassen (40380)' => '40068',
+ 'Casseneuil (47440)' => '47049',
+ 'Casseuil (33190)' => '33102',
+ 'Cassignas (47340)' => '47050',
+ 'Castagnède (64270)' => '64170',
+ 'Castaignos-Souslens (40700)' => '40069',
+ 'Castandet (40270)' => '40070',
+ 'Casteide-Cami (64170)' => '64171',
+ 'Casteide-Candau (64370)' => '64172',
+ 'Casteide-Doat (64460)' => '64173',
+ 'Castel-Sarrazin (40330)' => '40074',
+ 'Castelculier (47240)' => '47051',
+ 'Casteljaloux (47700)' => '47052',
+ 'Castella (47340)' => '47053',
+ 'Castelmoron-d\'Albret (33540)' => '33103',
+ 'Castelmoron-sur-Lot (47260)' => '47054',
+ 'Castelnau-Chalosse (40360)' => '40071',
+ 'Castelnau-de-Médoc (33480)' => '33104',
+ 'Castelnau-sur-Gupie (47180)' => '47056',
+ 'Castelnau-Tursan (40320)' => '40072',
+ 'Castelnaud-de-Gratecambe (47290)' => '47055',
+ 'Castelnaud-la-Chapelle (24250)' => '24086',
+ 'Castelner (40700)' => '40073',
+ 'Castels (24220)' => '24087',
+ 'Castelviel (33540)' => '33105',
+ 'Castéra-Loubix (64460)' => '64174',
+ 'Castet (64260)' => '64175',
+ 'Castetbon (64190)' => '64176',
+ 'Castétis (64300)' => '64177',
+ 'Castetnau-Camblong (64190)' => '64178',
+ 'Castetner (64300)' => '64179',
+ 'Castetpugon (64330)' => '64180',
+ 'Castets (40260)' => '40075',
+ 'Castets-en-Dorthe (33210)' => '33106',
+ 'Castillon (Canton d\'Arthez-de-Béarn) (64370)' => '64181',
+ 'Castillon (Canton de Lembeye) (64350)' => '64182',
+ 'Castillon-de-Castets (33210)' => '33107',
+ 'Castillon-la-Bataille (33350)' => '33108',
+ 'Castillonnès (47330)' => '47057',
+ 'Castres-Gironde (33640)' => '33109',
+ 'Caubeyres (47160)' => '47058',
+ 'Caubios-Loos (64230)' => '64183',
+ 'Caubon-Saint-Sauveur (47120)' => '47059',
+ 'Caudecoste (47220)' => '47060',
+ 'Caudrot (33490)' => '33111',
+ 'Caumont (33540)' => '33112',
+ 'Caumont-sur-Garonne (47430)' => '47061',
+ 'Cauna (40500)' => '40076',
+ 'Caunay (79190)' => '79060',
+ 'Cauneille (40300)' => '40077',
+ 'Caupenne (40250)' => '40078',
+ 'Cause-de-Clérans (24150)' => '24088',
+ 'Cauvignac (33690)' => '33113',
+ 'Cauzac (47470)' => '47062',
+ 'Cavarc (47330)' => '47063',
+ 'Cavignac (33620)' => '33114',
+ 'Cazalis (33113)' => '33115',
+ 'Cazalis (40700)' => '40079',
+ 'Cazats (33430)' => '33116',
+ 'Cazaugitat (33790)' => '33117',
+ 'Cazères-sur-l\'Adour (40270)' => '40080',
+ 'Cazideroque (47370)' => '47064',
+ 'Cazoulès (24370)' => '24089',
+ 'Ceaux-en-Couhé (86700)' => '86043',
+ 'Ceaux-en-Loudun (86200)' => '86044',
+ 'Celle-Lévescault (86600)' => '86045',
+ 'Cellefrouin (16260)' => '16068',
+ 'Celles (17520)' => '17076',
+ 'Celles (24600)' => '24090',
+ 'Celles-sur-Belle (79370)' => '79061',
+ 'Cellettes (16230)' => '16069',
+ 'Cénac (33360)' => '33118',
+ 'Cénac-et-Saint-Julien (24250)' => '24091',
+ 'Cendrieux (24380)' => '24092',
+ 'Cenon (33150)' => '33119',
+ 'Cenon-sur-Vienne (86530)' => '86046',
+ 'Cercles (24320)' => '24093',
+ 'Cercoux (17270)' => '17077',
+ 'Cère (40090)' => '40081',
+ 'Cerizay (79140)' => '79062',
+ 'Cernay (86140)' => '86047',
+ 'Cérons (33720)' => '33120',
+ 'Cersay (79290)' => '79063',
+ 'Cescau (64170)' => '64184',
+ 'Cessac (33760)' => '33121',
+ 'Cestas (33610)' => '33122',
+ 'Cette-Eygun (64490)' => '64185',
+ 'Ceyroux (23210)' => '23042',
+ 'Cézac (33620)' => '33123',
+ 'Chabanais (16150)' => '16070',
+ 'Chabournay (86380)' => '86048',
+ 'Chabrac (16150)' => '16071',
+ 'Chabrignac (19350)' => '19035',
+ 'Chadenac (17800)' => '17078',
+ 'Chadurie (16250)' => '16072',
+ 'Chail (79500)' => '79064',
+ 'Chaillac-sur-Vienne (87200)' => '87030',
+ 'Chaillevette (17890)' => '17079',
+ 'Chalagnac (24380)' => '24094',
+ 'Chalais (16210)' => '16073',
+ 'Chalais (24800)' => '24095',
+ 'Chalais (86200)' => '86049',
+ 'Chalandray (86190)' => '86050',
+ 'Challignac (16300)' => '16074',
+ 'Châlus (87230)' => '87032',
+ 'Chamadelle (33230)' => '33124',
+ 'Chamberaud (23480)' => '23043',
+ 'Chamberet (19370)' => '19036',
+ 'Chambon (17290)' => '17080',
+ 'Chambon-Sainte-Croix (23220)' => '23044',
+ 'Chambon-sur-Voueize (23170)' => '23045',
+ 'Chambonchard (23110)' => '23046',
+ 'Chamborand (23240)' => '23047',
+ 'Chamboret (87140)' => '87033',
+ 'Chamboulive (19450)' => '19037',
+ 'Chameyrat (19330)' => '19038',
+ 'Chamouillac (17130)' => '17081',
+ 'Champagnac (17500)' => '17082',
+ 'Champagnac-de-Belair (24530)' => '24096',
+ 'Champagnac-la-Noaille (19320)' => '19039',
+ 'Champagnac-la-Prune (19320)' => '19040',
+ 'Champagnac-la-Rivière (87150)' => '87034',
+ 'Champagnat (23190)' => '23048',
+ 'Champagne (17620)' => '17083',
+ 'Champagne-et-Fontaine (24320)' => '24097',
+ 'Champagné-le-Sec (86510)' => '86051',
+ 'Champagne-Mouton (16350)' => '16076',
+ 'Champagné-Saint-Hilaire (86160)' => '86052',
+ 'Champagne-Vigny (16250)' => '16075',
+ 'Champagnolles (17240)' => '17084',
+ 'Champcevinel (24750)' => '24098',
+ 'Champdeniers-Saint-Denis (79220)' => '79066',
+ 'Champdolent (17430)' => '17085',
+ 'Champeaux-et-la-Chapelle-Pommier (24340)' => '24099',
+ 'Champigny-le-Sec (86170)' => '86053',
+ 'Champmillon (16290)' => '16077',
+ 'Champnétery (87400)' => '87035',
+ 'Champniers (16430)' => '16078',
+ 'Champniers (86400)' => '86054',
+ 'Champniers-et-Reilhac (24360)' => '24100',
+ 'Champs-Romain (24470)' => '24101',
+ 'Champsac (87230)' => '87036',
+ 'Champsanglard (23220)' => '23049',
+ 'Chanac-les-Mines (19150)' => '19041',
+ 'Chancelade (24650)' => '24102',
+ 'Chaniers (17610)' => '17086',
+ 'Chantecorps (79340)' => '79068',
+ 'Chanteix (19330)' => '19042',
+ 'Chanteloup (79320)' => '79069',
+ 'Chantemerle-sur-la-Soie (17380)' => '17087',
+ 'Chantérac (24190)' => '24104',
+ 'Chantillac (16360)' => '16079',
+ 'Chapdeuil (24320)' => '24105',
+ 'Chapelle-Spinasse (19300)' => '19046',
+ 'Chapelle-Viviers (86300)' => '86059',
+ 'Chaptelat (87270)' => '87038',
+ 'Chard (23700)' => '23053',
+ 'Charmé (16140)' => '16083',
+ 'Charrais (86170)' => '86060',
+ 'Charras (16380)' => '16084',
+ 'Charre (64190)' => '64186',
+ 'Charritte-de-Bas (64130)' => '64187',
+ 'Charron (17230)' => '17091',
+ 'Charron (23700)' => '23054',
+ 'Charroux (86250)' => '86061',
+ 'Chartrier-Ferrière (19600)' => '19047',
+ 'Chartuzac (17130)' => '17092',
+ 'Chassaignes (24600)' => '24114',
+ 'Chasseneuil-du-Poitou (86360)' => '86062',
+ 'Chasseneuil-sur-Bonnieure (16260)' => '16085',
+ 'Chassenon (16150)' => '16086',
+ 'Chassiecq (16350)' => '16087',
+ 'Chassors (16200)' => '16088',
+ 'Chasteaux (19600)' => '19049',
+ 'Chatain (86250)' => '86063',
+ 'Château-Chervix (87380)' => '87039',
+ 'Château-Garnier (86350)' => '86064',
+ 'Château-l\'Évêque (24460)' => '24115',
+ 'Château-Larcher (86370)' => '86065',
+ 'Châteaubernard (16100)' => '16089',
+ 'Châteauneuf-la-Forêt (87130)' => '87040',
+ 'Châteauneuf-sur-Charente (16120)' => '16090',
+ 'Châteauponsac (87290)' => '87041',
+ 'Châtelaillon-Plage (17340)' => '17094',
+ 'Châtelard (23700)' => '23055',
+ 'Châtellerault (86100)' => '86066',
+ 'Châtelus-le-Marcheix (23430)' => '23056',
+ 'Châtelus-Malvaleix (23270)' => '23057',
+ 'Chatenet (17210)' => '17095',
+ 'Châtignac (16480)' => '16091',
+ 'Châtillon (86700)' => '86067',
+ 'Châtillon-sur-Thouet (79200)' => '79080',
+ 'Châtres (24120)' => '24116',
+ 'Chauffour-sur-Vell (19500)' => '19050',
+ 'Chaumeil (19390)' => '19051',
+ 'Chaunac (17130)' => '17096',
+ 'Chaunay (86510)' => '86068',
+ 'Chauray (79180)' => '79081',
+ 'Chauvigny (86300)' => '86070',
+ 'Chavagnac (24120)' => '24117',
+ 'Chavanac (19290)' => '19052',
+ 'Chavanat (23250)' => '23060',
+ 'Chaveroche (19200)' => '19053',
+ 'Chazelles (16380)' => '16093',
+ 'Chef-Boutonne (79110)' => '79083',
+ 'Cheissoux (87460)' => '87043',
+ 'Chenac-Saint-Seurin-d\'Uzet (17120)' => '17098',
+ 'Chenailler-Mascheix (19120)' => '19054',
+ 'Chenay (79120)' => '79084',
+ 'Cheneché (86380)' => '86071',
+ 'Chénérailles (23130)' => '23061',
+ 'Chenevelles (86450)' => '86072',
+ 'Chéniers (23220)' => '23062',
+ 'Chenommet (16460)' => '16094',
+ 'Chenon (16460)' => '16095',
+ 'Chepniers (17210)' => '17099',
+ 'Chérac (17610)' => '17100',
+ 'Chéraute (64130)' => '64188',
+ 'Cherbonnières (17470)' => '17101',
+ 'Chérigné (79170)' => '79085',
+ 'Chermignac (17460)' => '17102',
+ 'Chéronnac (87600)' => '87044',
+ 'Cherval (24320)' => '24119',
+ 'Cherveix-Cubas (24390)' => '24120',
+ 'Cherves (86170)' => '86073',
+ 'Cherves-Châtelars (16310)' => '16096',
+ 'Cherves-Richemont (16370)' => '16097',
+ 'Chervettes (17380)' => '17103',
+ 'Cherveux (79410)' => '79086',
+ 'Chevanceaux (17210)' => '17104',
+ 'Chey (79120)' => '79087',
+ 'Chiché (79350)' => '79088',
+ 'Chillac (16480)' => '16099',
+ 'Chirac (16150)' => '16100',
+ 'Chirac-Bellevue (19160)' => '19055',
+ 'Chiré-en-Montreuil (86190)' => '86074',
+ 'Chives (17510)' => '17105',
+ 'Chizé (79170)' => '79090',
+ 'Chouppes (86110)' => '86075',
+ 'Chourgnac (24640)' => '24121',
+ 'Ciboure (64500)' => '64189',
+ 'Cierzac (17520)' => '17106',
+ 'Cieux (87520)' => '87045',
+ 'Ciré-d\'Aunis (17290)' => '17107',
+ 'Cirières (79140)' => '79091',
+ 'Cissac-Médoc (33250)' => '33125',
+ 'Cissé (86170)' => '86076',
+ 'Civaux (86320)' => '86077',
+ 'Civrac-de-Blaye (33920)' => '33126',
+ 'Civrac-en-Médoc (33340)' => '33128',
+ 'Civrac-sur-Dordogne (33350)' => '33127',
+ 'Civray (86400)' => '86078',
+ 'Cladech (24170)' => '24122',
+ 'Clairac (47320)' => '47065',
+ 'Clairavaux (23500)' => '23063',
+ 'Claix (16440)' => '16101',
+ 'Clam (17500)' => '17108',
+ 'Claracq (64330)' => '64190',
+ 'Classun (40320)' => '40082',
+ 'Clavé (79420)' => '79092',
+ 'Clavette (17220)' => '17109',
+ 'Clèdes (40320)' => '40083',
+ 'Clérac (17270)' => '17110',
+ 'Clergoux (19320)' => '19056',
+ 'Clermont (40180)' => '40084',
+ 'Clermont-d\'Excideuil (24160)' => '24124',
+ 'Clermont-de-Beauregard (24140)' => '24123',
+ 'Clermont-Dessous (47130)' => '47066',
+ 'Clermont-Soubiran (47270)' => '47067',
+ 'Clessé (79350)' => '79094',
+ 'Cleyrac (33540)' => '33129',
+ 'Clion (17240)' => '17111',
+ 'Cloué (86600)' => '86080',
+ 'Clugnat (23270)' => '23064',
+ 'Clussais-la-Pommeraie (79190)' => '79095',
+ 'Coarraze (64800)' => '64191',
+ 'Cocumont (47250)' => '47068',
+ 'Cognac (16100)' => '16102',
+ 'Cognac-la-Forêt (87310)' => '87046',
+ 'Coimères (33210)' => '33130',
+ 'Coirac (33540)' => '33131',
+ 'Coivert (17330)' => '17114',
+ 'Colayrac-Saint-Cirq (47450)' => '47069',
+ 'Collonges-la-Rouge (19500)' => '19057',
+ 'Colombier (24560)' => '24126',
+ 'Colombiers (17460)' => '17115',
+ 'Colombiers (86490)' => '86081',
+ 'Colondannes (23800)' => '23065',
+ 'Coly (24120)' => '24127',
+ 'Comberanche-et-Épeluche (24600)' => '24128',
+ 'Combiers (16320)' => '16103',
+ 'Combrand (79140)' => '79096',
+ 'Combressol (19250)' => '19058',
+ 'Commensacq (40210)' => '40085',
+ 'Compreignac (87140)' => '87047',
+ 'Comps (33710)' => '33132',
+ 'Concèze (19350)' => '19059',
+ 'Conchez-de-Béarn (64330)' => '64192',
+ 'Condac (16700)' => '16104',
+ 'Condat-sur-Ganaveix (19140)' => '19060',
+ 'Condat-sur-Trincou (24530)' => '24129',
+ 'Condat-sur-Vézère (24570)' => '24130',
+ 'Condat-sur-Vienne (87920)' => '87048',
+ 'Condéon (16360)' => '16105',
+ 'Condezaygues (47500)' => '47070',
+ 'Confolens (16500)' => '16106',
+ 'Confolent-Port-Dieu (19200)' => '19167',
+ 'Conne-de-Labarde (24560)' => '24132',
+ 'Connezac (24300)' => '24131',
+ 'Consac (17150)' => '17116',
+ 'Contré (17470)' => '17117',
+ 'Corbère-Abères (64350)' => '64193',
+ 'Corgnac-sur-l\'Isle (24800)' => '24134',
+ 'Corignac (17130)' => '17118',
+ 'Corme-Écluse (17600)' => '17119',
+ 'Corme-Royal (17600)' => '17120',
+ 'Cornil (19150)' => '19061',
+ 'Cornille (24750)' => '24135',
+ 'Corrèze (19800)' => '19062',
+ 'Coslédaà-Lube-Boast (64160)' => '64194',
+ 'Cosnac (19360)' => '19063',
+ 'Coubeyrac (33890)' => '33133',
+ 'Coubjours (24390)' => '24136',
+ 'Coublucq (64410)' => '64195',
+ 'Coudures (40500)' => '40086',
+ 'Couffy-sur-Sarsonne (19340)' => '19064',
+ 'Couhé (86700)' => '86082',
+ 'Coulaures (24420)' => '24137',
+ 'Coulgens (16560)' => '16107',
+ 'Coulombiers (86600)' => '86083',
+ 'Coulon (79510)' => '79100',
+ 'Coulonges (16330)' => '16108',
+ 'Coulonges (17800)' => '17122',
+ 'Coulonges (86290)' => '86084',
+ 'Coulonges-sur-l\'Autize (79160)' => '79101',
+ 'Coulonges-Thouarsais (79330)' => '79102',
+ 'Coulounieix-Chamiers (24660)' => '24138',
+ 'Coulx (47260)' => '47071',
+ 'Couquèques (33340)' => '33134',
+ 'Courant (17330)' => '17124',
+ 'Courbiac (47370)' => '47072',
+ 'Courbillac (16200)' => '16109',
+ 'Courcelles (17400)' => '17125',
+ 'Courcerac (17160)' => '17126',
+ 'Courcôme (16240)' => '16110',
+ 'Courçon (17170)' => '17127',
+ 'Courcoury (17100)' => '17128',
+ 'Courgeac (16190)' => '16111',
+ 'Courlac (16210)' => '16112',
+ 'Courlay (79440)' => '79103',
+ 'Courpiac (33760)' => '33135',
+ 'Courpignac (17130)' => '17129',
+ 'Cours (47360)' => '47073',
+ 'Cours (79220)' => '79104',
+ 'Cours-de-Monségur (33580)' => '33136',
+ 'Cours-de-Pile (24520)' => '24140',
+ 'Cours-les-Bains (33690)' => '33137',
+ 'Coursac (24430)' => '24139',
+ 'Courteix (19340)' => '19065',
+ 'Coussac-Bonneval (87500)' => '87049',
+ 'Coussay (86110)' => '86085',
+ 'Coussay-les-Bois (86270)' => '86086',
+ 'Couthures-sur-Garonne (47180)' => '47074',
+ 'Coutières (79340)' => '79105',
+ 'Coutras (33230)' => '33138',
+ 'Couture (16460)' => '16114',
+ 'Couture-d\'Argenson (79110)' => '79106',
+ 'Coutures (24320)' => '24141',
+ 'Coutures (33580)' => '33139',
+ 'Coux (17130)' => '17130',
+ 'Coux et Bigaroque-Mouzens (24220)' => '24142',
+ 'Couze-et-Saint-Front (24150)' => '24143',
+ 'Couzeix (87270)' => '87050',
+ 'Cozes (17120)' => '17131',
+ 'Cramchaban (17170)' => '17132',
+ 'Craon (86110)' => '86087',
+ 'Cravans (17260)' => '17133',
+ 'Crazannes (17350)' => '17134',
+ 'Créon (33670)' => '33140',
+ 'Créon-d\'Armagnac (40240)' => '40087',
+ 'Cressac-Saint-Genis (16250)' => '16115',
+ 'Cressat (23140)' => '23068',
+ 'Cressé (17160)' => '17135',
+ 'Creyssac (24350)' => '24144',
+ 'Creysse (24100)' => '24145',
+ 'Creyssensac-et-Pissot (24380)' => '24146',
+ 'Crézières (79110)' => '79107',
+ 'Criteuil-la-Magdeleine (16300)' => '16116',
+ 'Crocq (23260)' => '23069',
+ 'Croignon (33750)' => '33141',
+ 'Croix-Chapeau (17220)' => '17136',
+ 'Cromac (87160)' => '87053',
+ 'Crouseilles (64350)' => '64196',
+ 'Croutelle (86240)' => '86088',
+ 'Crozant (23160)' => '23070',
+ 'Croze (23500)' => '23071',
+ 'Cubjac (24640)' => '24147',
+ 'Cublac (19520)' => '19066',
+ 'Cubnezais (33620)' => '33142',
+ 'Cubzac-les-Ponts (33240)' => '33143',
+ 'Cudos (33430)' => '33144',
+ 'Cuhon (86110)' => '86089',
+ 'Cunèges (24240)' => '24148',
+ 'Cuq (47220)' => '47076',
+ 'Cuqueron (64360)' => '64197',
+ 'Curac (16210)' => '16117',
+ 'Curçay-sur-Dive (86120)' => '86090',
+ 'Curemonte (19500)' => '19067',
+ 'Cursan (33670)' => '33145',
+ 'Curzay-sur-Vonne (86600)' => '86091',
+ 'Cussac (87150)' => '87054',
+ 'Cussac-Fort-Médoc (33460)' => '33146',
+ 'Cuzorn (47500)' => '47077',
+ 'Daglan (24250)' => '24150',
+ 'Daignac (33420)' => '33147',
+ 'Damazan (47160)' => '47078',
+ 'Dampierre-sur-Boutonne (17470)' => '17138',
+ 'Dampniat (19360)' => '19068',
+ 'Dangé-Saint-Romain (86220)' => '86092',
+ 'Darazac (19220)' => '19069',
+ 'Dardenac (33420)' => '33148',
+ 'Darnac (87320)' => '87055',
+ 'Darnets (19300)' => '19070',
+ 'Daubèze (33540)' => '33149',
+ 'Dausse (47140)' => '47079',
+ 'Davignac (19250)' => '19071',
+ 'Dax (40100)' => '40088',
+ 'Denguin (64230)' => '64198',
+ 'Dercé (86420)' => '86093',
+ 'Deviat (16190)' => '16118',
+ 'Dévillac (47210)' => '47080',
+ 'Dienné (86410)' => '86094',
+ 'Dieulivol (33580)' => '33150',
+ 'Dignac (16410)' => '16119',
+ 'Dinsac (87210)' => '87056',
+ 'Dirac (16410)' => '16120',
+ 'Dissay (86130)' => '86095',
+ 'Diusse (64330)' => '64199',
+ 'Doazit (40700)' => '40089',
+ 'Doazon (64370)' => '64200',
+ 'Doeuil-sur-le-Mignon (17330)' => '17139',
+ 'Dognen (64190)' => '64201',
+ 'Doissat (24170)' => '24151',
+ 'Dolmayrac (47110)' => '47081',
+ 'Dolus-d\'Oléron (17550)' => '17140',
+ 'Domeyrot (23140)' => '23072',
+ 'Domezain-Berraute (64120)' => '64202',
+ 'Domme (24250)' => '24152',
+ 'Dompierre-les-Églises (87190)' => '87057',
+ 'Dompierre-sur-Charente (17610)' => '17141',
+ 'Dompierre-sur-Mer (17139)' => '17142',
+ 'Domps (87120)' => '87058',
+ 'Dondas (47470)' => '47082',
+ 'Donnezac (33860)' => '33151',
+ 'Dontreix (23700)' => '23073',
+ 'Donzac (33410)' => '33152',
+ 'Donzacq (40360)' => '40090',
+ 'Donzenac (19270)' => '19072',
+ 'Douchapt (24350)' => '24154',
+ 'Doudrac (47210)' => '47083',
+ 'Doulezon (33350)' => '33153',
+ 'Doumy (64450)' => '64203',
+ 'Dournazac (87230)' => '87060',
+ 'Doussay (86140)' => '86096',
+ 'Douville (24140)' => '24155',
+ 'Doux (79390)' => '79108',
+ 'Douzains (47330)' => '47084',
+ 'Douzat (16290)' => '16121',
+ 'Douzillac (24190)' => '24157',
+ 'Droux (87190)' => '87061',
+ 'Duhort-Bachen (40800)' => '40091',
+ 'Dumes (40500)' => '40092',
+ 'Dun-le-Palestel (23800)' => '23075',
+ 'Durance (47420)' => '47085',
+ 'Duras (47120)' => '47086',
+ 'Dussac (24270)' => '24158',
+ 'Eaux-Bonnes (64440)' => '64204',
+ 'Ébréon (16140)' => '16122',
+ 'Échallat (16170)' => '16123',
+ 'Échebrune (17800)' => '17145',
+ 'Échillais (17620)' => '17146',
+ 'Échiré (79410)' => '79109',
+ 'Échourgnac (24410)' => '24159',
+ 'Écoyeux (17770)' => '17147',
+ 'Écuras (16220)' => '16124',
+ 'Écurat (17810)' => '17148',
+ 'Édon (16320)' => '16125',
+ 'Égletons (19300)' => '19073',
+ 'Église-Neuve-d\'Issac (24400)' => '24161',
+ 'Église-Neuve-de-Vergt (24380)' => '24160',
+ 'Empuré (16240)' => '16127',
+ 'Engayrac (47470)' => '47087',
+ 'Ensigné (79170)' => '79111',
+ 'Épannes (79270)' => '79112',
+ 'Épargnes (17120)' => '17152',
+ 'Épenède (16490)' => '16128',
+ 'Éraville (16120)' => '16129',
+ 'Escalans (40310)' => '40093',
+ 'Escassefort (47350)' => '47088',
+ 'Escaudes (33840)' => '33155',
+ 'Escaunets (65500)' => '65160',
+ 'Esclottes (47120)' => '47089',
+ 'Escoire (24420)' => '24162',
+ 'Escos (64270)' => '64205',
+ 'Escot (64490)' => '64206',
+ 'Escou (64870)' => '64207',
+ 'Escoubès (64160)' => '64208',
+ 'Escource (40210)' => '40094',
+ 'Escoussans (33760)' => '33156',
+ 'Escout (64870)' => '64209',
+ 'Escurès (64350)' => '64210',
+ 'Eslourenties-Daban (64420)' => '64211',
+ 'Esnandes (17137)' => '17153',
+ 'Espagnac (19150)' => '19075',
+ 'Espartignac (19140)' => '19076',
+ 'Espéchède (64160)' => '64212',
+ 'Espelette (64250)' => '64213',
+ 'Espès-Undurein (64130)' => '64214',
+ 'Espiens (47600)' => '47090',
+ 'Espiet (33420)' => '33157',
+ 'Espiute (64390)' => '64215',
+ 'Espoey (64420)' => '64216',
+ 'Esquiule (64400)' => '64217',
+ 'Esse (16500)' => '16131',
+ 'Essouvert (17400)' => '17277',
+ 'Estérençuby (64220)' => '64218',
+ 'Estialescq (64290)' => '64219',
+ 'Estibeaux (40290)' => '40095',
+ 'Estigarde (40240)' => '40096',
+ 'Estillac (47310)' => '47091',
+ 'Estivals (19600)' => '19077',
+ 'Estivaux (19410)' => '19078',
+ 'Estos (64400)' => '64220',
+ 'Étagnac (16150)' => '16132',
+ 'Étaules (17750)' => '17155',
+ 'Étauliers (33820)' => '33159',
+ 'Etcharry (64120)' => '64221',
+ 'Etchebar (64470)' => '64222',
+ 'Étouars (24360)' => '24163',
+ 'Étriac (16250)' => '16133',
+ 'Etsaut (64490)' => '64223',
+ 'Eugénie-les-Bains (40320)' => '40097',
+ 'Évaux-les-Bains (23110)' => '23076',
+ 'Excideuil (24160)' => '24164',
+ 'Exideuil (16150)' => '16134',
+ 'Exireuil (79400)' => '79114',
+ 'Exoudun (79800)' => '79115',
+ 'Expiremont (17130)' => '17156',
+ 'Eybouleuf (87400)' => '87062',
+ 'Eyburie (19140)' => '19079',
+ 'Eygurande (19340)' => '19080',
+ 'Eygurande-et-Gardedeuil (24700)' => '24165',
+ 'Eyjeaux (87220)' => '87063',
+ 'Eyliac (24330)' => '24166',
+ 'Eymet (24500)' => '24167',
+ 'Eymouthiers (16220)' => '16135',
+ 'Eymoutiers (87120)' => '87064',
+ 'Eynesse (33220)' => '33160',
+ 'Eyrans (33390)' => '33161',
+ 'Eyrein (19800)' => '19081',
+ 'Eyres-Moncube (40500)' => '40098',
+ 'Eysines (33320)' => '33162',
+ 'Eysus (64400)' => '64224',
+ 'Eyvirat (24460)' => '24170',
+ 'Eyzerac (24800)' => '24171',
+ 'Faleyras (33760)' => '33163',
+ 'Fals (47220)' => '47092',
+ 'Fanlac (24290)' => '24174',
+ 'Fargues (33210)' => '33164',
+ 'Fargues (40500)' => '40099',
+ 'Fargues-Saint-Hilaire (33370)' => '33165',
+ 'Fargues-sur-Ourbise (47700)' => '47093',
+ 'Fauguerolles (47400)' => '47094',
+ 'Fauillet (47400)' => '47095',
+ 'Faurilles (24560)' => '24176',
+ 'Faux (24560)' => '24177',
+ 'Faux-la-Montagne (23340)' => '23077',
+ 'Faux-Mazuras (23400)' => '23078',
+ 'Favars (19330)' => '19082',
+ 'Faye-l\'Abbesse (79350)' => '79116',
+ 'Faye-sur-Ardin (79160)' => '79117',
+ 'Féas (64570)' => '64225',
+ 'Felletin (23500)' => '23079',
+ 'Fénery (79450)' => '79118',
+ 'Féniers (23100)' => '23080',
+ 'Fenioux (17350)' => '17157',
+ 'Fenioux (79160)' => '79119',
+ 'Ferrensac (47330)' => '47096',
+ 'Ferrières (17170)' => '17158',
+ 'Festalemps (24410)' => '24178',
+ 'Feugarolles (47230)' => '47097',
+ 'Feuillade (16380)' => '16137',
+ 'Feyt (19340)' => '19083',
+ 'Feytiat (87220)' => '87065',
+ 'Fichous-Riumayou (64410)' => '64226',
+ 'Fieux (47600)' => '47098',
+ 'Firbeix (24450)' => '24180',
+ 'Flaugeac (24240)' => '24181',
+ 'Flaujagues (33350)' => '33168',
+ 'Flavignac (87230)' => '87066',
+ 'Flayat (23260)' => '23081',
+ 'Fléac (16730)' => '16138',
+ 'Fléac-sur-Seugne (17800)' => '17159',
+ 'Fleix (86300)' => '86098',
+ 'Fleurac (16200)' => '16139',
+ 'Fleurac (24580)' => '24183',
+ 'Fleurat (23320)' => '23082',
+ 'Fleuré (86340)' => '86099',
+ 'Floirac (17120)' => '17160',
+ 'Floirac (33270)' => '33167',
+ 'Florimont-Gaumier (24250)' => '24184',
+ 'Floudès (33190)' => '33169',
+ 'Folles (87250)' => '87067',
+ 'Fomperron (79340)' => '79121',
+ 'Fongrave (47260)' => '47099',
+ 'Fonroque (24500)' => '24186',
+ 'Fontaine-Chalendray (17510)' => '17162',
+ 'Fontaine-le-Comte (86240)' => '86100',
+ 'Fontaines-d\'Ozillac (17500)' => '17163',
+ 'Fontanières (23110)' => '23083',
+ 'Fontclaireau (16230)' => '16140',
+ 'Fontcouverte (17100)' => '17164',
+ 'Fontenet (17400)' => '17165',
+ 'Fontenille (16230)' => '16141',
+ 'Fontenille-Saint-Martin-d\'Entraigues (79110)' => '79122',
+ 'Fontet (33190)' => '33170',
+ 'Forges (17290)' => '17166',
+ 'Forgès (19380)' => '19084',
+ 'Fors (79230)' => '79125',
+ 'Fossemagne (24210)' => '24188',
+ 'Fossès-et-Baleyssac (33190)' => '33171',
+ 'Fougueyrolles (33220)' => '24189',
+ 'Foulayronnes (47510)' => '47100',
+ 'Fouleix (24380)' => '24190',
+ 'Fouquebrune (16410)' => '16143',
+ 'Fouqueure (16140)' => '16144',
+ 'Fouras (17450)' => '17168',
+ 'Fourques-sur-Garonne (47200)' => '47101',
+ 'Fours (33390)' => '33172',
+ 'Foussignac (16200)' => '16145',
+ 'Fraisse (24130)' => '24191',
+ 'Francescas (47600)' => '47102',
+ 'François (79260)' => '79128',
+ 'Francs (33570)' => '33173',
+ 'Fransèches (23480)' => '23086',
+ 'Fréchou (47600)' => '47103',
+ 'Frégimont (47360)' => '47104',
+ 'Frespech (47140)' => '47105',
+ 'Fresselines (23450)' => '23087',
+ 'Fressines (79370)' => '79129',
+ 'Fromental (87250)' => '87068',
+ 'Fronsac (33126)' => '33174',
+ 'Frontenac (33760)' => '33175',
+ 'Frontenay-Rohan-Rohan (79270)' => '79130',
+ 'Frozes (86190)' => '86102',
+ 'Fumel (47500)' => '47106',
+ 'Gaas (40350)' => '40101',
+ 'Gabarnac (33410)' => '33176',
+ 'Gabarret (40310)' => '40102',
+ 'Gabaston (64160)' => '64227',
+ 'Gabat (64120)' => '64228',
+ 'Gabillou (24210)' => '24192',
+ 'Gageac-et-Rouillac (24240)' => '24193',
+ 'Gaillan-en-Médoc (33340)' => '33177',
+ 'Gaillères (40090)' => '40103',
+ 'Gajac (33430)' => '33178',
+ 'Gajoubert (87330)' => '87069',
+ 'Galapian (47190)' => '47107',
+ 'Galgon (33133)' => '33179',
+ 'Gamarde-les-Bains (40380)' => '40104',
+ 'Gamarthe (64220)' => '64229',
+ 'Gan (64290)' => '64230',
+ 'Gans (33430)' => '33180',
+ 'Garat (16410)' => '16146',
+ 'Gardegan-et-Tourtirac (33350)' => '33181',
+ 'Gardères (65320)' => '65185',
+ 'Gardes-le-Pontaroux (16320)' => '16147',
+ 'Gardonne (24680)' => '24194',
+ 'Garein (40420)' => '40105',
+ 'Garindein (64130)' => '64231',
+ 'Garlède-Mondebat (64450)' => '64232',
+ 'Garlin (64330)' => '64233',
+ 'Garos (64410)' => '64234',
+ 'Garrey (40180)' => '40106',
+ 'Garris (64120)' => '64235',
+ 'Garrosse (40110)' => '40107',
+ 'Gartempe (23320)' => '23088',
+ 'Gastes (40160)' => '40108',
+ 'Gaugeac (24540)' => '24195',
+ 'Gaujac (47200)' => '47108',
+ 'Gaujacq (40330)' => '40109',
+ 'Gauriac (33710)' => '33182',
+ 'Gauriaguet (33240)' => '33183',
+ 'Gavaudun (47150)' => '47109',
+ 'Gayon (64350)' => '64236',
+ 'Geaune (40320)' => '40110',
+ 'Geay (17250)' => '17171',
+ 'Geay (79330)' => '79131',
+ 'Gelos (64110)' => '64237',
+ 'Geloux (40090)' => '40111',
+ 'Gémozac (17260)' => '17172',
+ 'Genac-Bignac (16170)' => '16148',
+ 'Gençay (86160)' => '86103',
+ 'Générac (33920)' => '33184',
+ 'Génis (24160)' => '24196',
+ 'Génissac (33420)' => '33185',
+ 'Genneton (79150)' => '79132',
+ 'Genouillac (16270)' => '16149',
+ 'Genouillac (23350)' => '23089',
+ 'Genouillé (17430)' => '17174',
+ 'Genouillé (86250)' => '86104',
+ 'Gensac (33890)' => '33186',
+ 'Gensac-la-Pallue (16130)' => '16150',
+ 'Genté (16130)' => '16151',
+ 'Gentioux-Pigerolles (23340)' => '23090',
+ 'Ger (64530)' => '64238',
+ 'Gerderest (64160)' => '64239',
+ 'Gère-Bélesten (64260)' => '64240',
+ 'Germignac (17520)' => '17175',
+ 'Germond-Rouvre (79220)' => '79133',
+ 'Géronce (64400)' => '64241',
+ 'Gestas (64190)' => '64242',
+ 'Géus-d\'Arzacq (64370)' => '64243',
+ 'Geüs-d\'Oloron (64400)' => '64244',
+ 'Gibourne (17160)' => '17176',
+ 'Gibret (40380)' => '40112',
+ 'Gimel-les-Cascades (19800)' => '19085',
+ 'Gimeux (16130)' => '16152',
+ 'Ginestet (24130)' => '24197',
+ 'Gioux (23500)' => '23091',
+ 'Gironde-sur-Dropt (33190)' => '33187',
+ 'Giscos (33840)' => '33188',
+ 'Givrezac (17260)' => '17178',
+ 'Gizay (86340)' => '86105',
+ 'Glandon (87500)' => '87071',
+ 'Glanges (87380)' => '87072',
+ 'Glénay (79330)' => '79134',
+ 'Glénic (23380)' => '23092',
+ 'Glénouze (86200)' => '86106',
+ 'Goès (64400)' => '64245',
+ 'Gomer (64420)' => '64246',
+ 'Gond-Pontouvre (16160)' => '16154',
+ 'Gondeville (16200)' => '16153',
+ 'Gontaud-de-Nogaret (47400)' => '47110',
+ 'Goos (40180)' => '40113',
+ 'Gornac (33540)' => '33189',
+ 'Gorre (87310)' => '87073',
+ 'Gotein-Libarrenx (64130)' => '64247',
+ 'Goualade (33840)' => '33190',
+ 'Gouex (86320)' => '86107',
+ 'Goulles (19430)' => '19086',
+ 'Gourbera (40990)' => '40114',
+ 'Gourdon-Murat (19170)' => '19087',
+ 'Gourgé (79200)' => '79135',
+ 'Gournay-Loizé (79110)' => '79136',
+ 'Gours (33660)' => '33191',
+ 'Gourville (16170)' => '16156',
+ 'Gourvillette (17490)' => '17180',
+ 'Gousse (40465)' => '40115',
+ 'Gout-Rossignol (24320)' => '24199',
+ 'Gouts (40400)' => '40116',
+ 'Gouzon (23230)' => '23093',
+ 'Gradignan (33170)' => '33192',
+ 'Grand-Brassac (24350)' => '24200',
+ 'Grandjean (17350)' => '17181',
+ 'Grandsaigne (19300)' => '19088',
+ 'Granges-d\'Ans (24390)' => '24202',
+ 'Granges-sur-Lot (47260)' => '47111',
+ 'Granzay-Gript (79360)' => '79137',
+ 'Grassac (16380)' => '16158',
+ 'Grateloup-Saint-Gayrand (47400)' => '47112',
+ 'Graves-Saint-Amant (16120)' => '16297',
+ 'Grayan-et-l\'Hôpital (33590)' => '33193',
+ 'Grayssas (47270)' => '47113',
+ 'Grenade-sur-l\'Adour (40270)' => '40117',
+ 'Grézac (17120)' => '17183',
+ 'Grèzes (24120)' => '24204',
+ 'Grézet-Cavagnan (47250)' => '47114',
+ 'Grézillac (33420)' => '33194',
+ 'Grignols (24110)' => '24205',
+ 'Grignols (33690)' => '33195',
+ 'Grives (24170)' => '24206',
+ 'Groléjac (24250)' => '24207',
+ 'Gros-Chastang (19320)' => '19089',
+ 'Grun-Bordas (24380)' => '24208',
+ 'Guéret (23000)' => '23096',
+ 'Guérin (47250)' => '47115',
+ 'Guesnes (86420)' => '86109',
+ 'Guéthary (64210)' => '64249',
+ 'Guiche (64520)' => '64250',
+ 'Guillac (33420)' => '33196',
+ 'Guillos (33720)' => '33197',
+ 'Guimps (16300)' => '16160',
+ 'Guinarthe-Parenties (64390)' => '64251',
+ 'Guitinières (17500)' => '17187',
+ 'Guîtres (33230)' => '33198',
+ 'Guizengeard (16480)' => '16161',
+ 'Gujan-Mestras (33470)' => '33199',
+ 'Gumond (19320)' => '19090',
+ 'Gurat (16320)' => '16162',
+ 'Gurmençon (64400)' => '64252',
+ 'Gurs (64190)' => '64253',
+ 'Habas (40290)' => '40118',
+ 'Hagetaubin (64370)' => '64254',
+ 'Hagetmau (40700)' => '40119',
+ 'Haimps (17160)' => '17188',
+ 'Haims (86310)' => '86110',
+ 'Halsou (64480)' => '64255',
+ 'Hanc (79110)' => '79140',
+ 'Hasparren (64240)' => '64256',
+ 'Hastingues (40300)' => '40120',
+ 'Hauriet (40250)' => '40121',
+ 'Haut-de-Bosdarros (64800)' => '64257',
+ 'Haut-Mauco (40280)' => '40122',
+ 'Hautefage (19400)' => '19091',
+ 'Hautefage-la-Tour (47340)' => '47117',
+ 'Hautefaye (24300)' => '24209',
+ 'Hautefort (24390)' => '24210',
+ 'Hautesvignes (47400)' => '47118',
+ 'Haux (33550)' => '33201',
+ 'Haux (64470)' => '64258',
+ 'Hélette (64640)' => '64259',
+ 'Hendaye (64700)' => '64260',
+ 'Herm (40990)' => '40123',
+ 'Herré (40310)' => '40124',
+ 'Herrère (64680)' => '64261',
+ 'Heugas (40180)' => '40125',
+ 'Hiers-Brouage (17320)' => '17189',
+ 'Hiersac (16290)' => '16163',
+ 'Hiesse (16490)' => '16164',
+ 'Higuères-Souye (64160)' => '64262',
+ 'Hinx (40180)' => '40126',
+ 'Hontanx (40190)' => '40127',
+ 'Horsarrieu (40700)' => '40128',
+ 'Hosta (64120)' => '64265',
+ 'Hostens (33125)' => '33202',
+ 'Houeillès (47420)' => '47119',
+ 'Houlette (16200)' => '16165',
+ 'Hours (64420)' => '64266',
+ 'Hourtin (33990)' => '33203',
+ 'Hure (33190)' => '33204',
+ 'Ibarrolle (64120)' => '64267',
+ 'Idaux-Mendy (64130)' => '64268',
+ 'Idron (64320)' => '64269',
+ 'Igon (64800)' => '64270',
+ 'Iholdy (64640)' => '64271',
+ 'Île-d\'Aix (17123)' => '17004',
+ 'Ilharre (64120)' => '64272',
+ 'Illats (33720)' => '33205',
+ 'Ingrandes (86220)' => '86111',
+ 'Irais (79600)' => '79141',
+ 'Irissarry (64780)' => '64273',
+ 'Irouléguy (64220)' => '64274',
+ 'Isle (87170)' => '87075',
+ 'Isle-Saint-Georges (33640)' => '33206',
+ 'Ispoure (64220)' => '64275',
+ 'Issac (24400)' => '24211',
+ 'Issigeac (24560)' => '24212',
+ 'Issor (64570)' => '64276',
+ 'Issoudun-Létrieix (23130)' => '23097',
+ 'Isturits (64240)' => '64277',
+ 'Iteuil (86240)' => '86113',
+ 'Itxassou (64250)' => '64279',
+ 'Izeste (64260)' => '64280',
+ 'Izon (33450)' => '33207',
+ 'Jabreilles-les-Bordes (87370)' => '87076',
+ 'Jalesches (23270)' => '23098',
+ 'Janailhac (87800)' => '87077',
+ 'Janaillat (23250)' => '23099',
+ 'Jardres (86800)' => '86114',
+ 'Jarnac (16200)' => '16167',
+ 'Jarnac-Champagne (17520)' => '17192',
+ 'Jarnages (23140)' => '23100',
+ 'Jasses (64190)' => '64281',
+ 'Jatxou (64480)' => '64282',
+ 'Jau-Dignac-et-Loirac (33590)' => '33208',
+ 'Jauldes (16560)' => '16168',
+ 'Jaunay-Clan (86130)' => '86115',
+ 'Jaure (24140)' => '24213',
+ 'Javerdat (87520)' => '87078',
+ 'Javerlhac-et-la-Chapelle-Saint-Robert (24300)' => '24214',
+ 'Javrezac (16100)' => '16169',
+ 'Jaxu (64220)' => '64283',
+ 'Jayac (24590)' => '24215',
+ 'Jazeneuil (86600)' => '86116',
+ 'Jazennes (17260)' => '17196',
+ 'Jonzac (17500)' => '17197',
+ 'Josse (40230)' => '40129',
+ 'Jouac (87890)' => '87080',
+ 'Jouhet (86500)' => '86117',
+ 'Jouillat (23220)' => '23101',
+ 'Jourgnac (87800)' => '87081',
+ 'Journet (86290)' => '86118',
+ 'Journiac (24260)' => '24217',
+ 'Joussé (86350)' => '86119',
+ 'Jugazan (33420)' => '33209',
+ 'Jugeals-Nazareth (19500)' => '19093',
+ 'Juicq (17770)' => '17198',
+ 'Juignac (16190)' => '16170',
+ 'Juillac (19350)' => '19094',
+ 'Juillac (33890)' => '33210',
+ 'Juillac-le-Coq (16130)' => '16171',
+ 'Juillé (16230)' => '16173',
+ 'Juillé (79170)' => '79142',
+ 'Julienne (16200)' => '16174',
+ 'Jumilhac-le-Grand (24630)' => '24218',
+ 'Jurançon (64110)' => '64284',
+ 'Juscorps (79230)' => '79144',
+ 'Jusix (47180)' => '47120',
+ 'Jussas (17130)' => '17199',
+ 'Juxue (64120)' => '64285',
+ 'L\'Absie (79240)' => '79001',
+ 'L\'Église-aux-Bois (19170)' => '19074',
+ 'L\'Éguille (17600)' => '17151',
+ 'L\'Hôpital-d\'Orion (64270)' => '64263',
+ 'L\'Hôpital-Saint-Blaise (64130)' => '64264',
+ 'L\'Houmeau (17137)' => '17190',
+ 'L\'Isle-d\'Espagnac (16340)' => '16166',
+ 'L\'Isle-Jourdain (86150)' => '86112',
+ 'La Bachellerie (24210)' => '24020',
+ 'La Barde (17360)' => '17033',
+ 'La Bastide-Clairence (64240)' => '64289',
+ 'La Bataille (79110)' => '79027',
+ 'La Bazeuge (87210)' => '87008',
+ 'La Boissière-d\'Ans (24640)' => '24047',
+ 'La Boissière-en-Gâtine (79310)' => '79040',
+ 'La Brède (33650)' => '33213',
+ 'La Brée-les-Bains (17840)' => '17486',
+ 'La Brionne (23000)' => '23033',
+ 'La Brousse (17160)' => '17071',
+ 'La Bussière (86310)' => '86040',
+ 'La Cassagne (24120)' => '24085',
+ 'La Celle-Dunoise (23800)' => '23039',
+ 'La Celle-sous-Gouzon (23230)' => '23040',
+ 'La Cellette (23350)' => '23041',
+ 'La Chapelle (16140)' => '16081',
+ 'La Chapelle-Aubareil (24290)' => '24106',
+ 'La Chapelle-aux-Brocs (19360)' => '19043',
+ 'La Chapelle-aux-Saints (19120)' => '19044',
+ 'La Chapelle-Baloue (23160)' => '23050',
+ 'La Chapelle-Bâton (79220)' => '79070',
+ 'La Chapelle-Bâton (86250)' => '86055',
+ 'La Chapelle-Bertrand (79200)' => '79071',
+ 'La Chapelle-des-Pots (17100)' => '17089',
+ 'La Chapelle-Faucher (24530)' => '24107',
+ 'La Chapelle-Gonaguet (24350)' => '24108',
+ 'La Chapelle-Grésignac (24320)' => '24109',
+ 'La Chapelle-Montabourlet (24320)' => '24110',
+ 'La Chapelle-Montbrandeix (87440)' => '87037',
+ 'La Chapelle-Montmoreau (24300)' => '24111',
+ 'La Chapelle-Montreuil (86470)' => '86056',
+ 'La Chapelle-Moulière (86210)' => '86058',
+ 'La Chapelle-Pouilloux (79190)' => '79074',
+ 'La Chapelle-Saint-Étienne (79240)' => '79075',
+ 'La Chapelle-Saint-Géraud (19430)' => '19045',
+ 'La Chapelle-Saint-Jean (24390)' => '24113',
+ 'La Chapelle-Saint-Laurent (79430)' => '79076',
+ 'La Chapelle-Saint-Martial (23250)' => '23051',
+ 'La Chapelle-Taillefert (23000)' => '23052',
+ 'La Chapelle-Thireuil (79160)' => '79077',
+ 'La Chaussade (23200)' => '23059',
+ 'La Chaussée (86330)' => '86069',
+ 'La Chèvrerie (16240)' => '16098',
+ 'La Clisse (17600)' => '17112',
+ 'La Clotte (17360)' => '17113',
+ 'La Coquille (24450)' => '24133',
+ 'La Couarde (79800)' => '79098',
+ 'La Couarde-sur-Mer (17670)' => '17121',
+ 'La Couronne (16400)' => '16113',
+ 'La Courtine (23100)' => '23067',
+ 'La Crèche (79260)' => '79048',
+ 'La Croisille-sur-Briance (87130)' => '87051',
+ 'La Croix-Blanche (47340)' => '47075',
+ 'La Croix-Comtesse (17330)' => '17137',
+ 'La Croix-sur-Gartempe (87210)' => '87052',
+ 'La Dornac (24120)' => '24153',
+ 'La Douze (24330)' => '24156',
+ 'La Faye (16700)' => '16136',
+ 'La Ferrière-Airoux (86160)' => '86097',
+ 'La Ferrière-en-Parthenay (79390)' => '79120',
+ 'La Feuillade (24120)' => '24179',
+ 'La Flotte (17630)' => '17161',
+ 'La Force (24130)' => '24222',
+ 'La Forêt-de-Tessé (16240)' => '16142',
+ 'La Forêt-du-Temple (23360)' => '23084',
+ 'La Forêt-sur-Sèvre (79380)' => '79123',
+ 'La Foye-Monjault (79360)' => '79127',
+ 'La Frédière (17770)' => '17169',
+ 'La Genétouze (17360)' => '17173',
+ 'La Geneytouse (87400)' => '87070',
+ 'La Gonterie-Boulouneix (24310)' => '24198',
+ 'La Grève-sur-Mignon (17170)' => '17182',
+ 'La Grimaudière (86330)' => '86108',
+ 'La Gripperie-Saint-Symphorien (17620)' => '17184',
+ 'La Jard (17460)' => '17191',
+ 'La Jarne (17220)' => '17193',
+ 'La Jarrie (17220)' => '17194',
+ 'La Jarrie-Audouin (17330)' => '17195',
+ 'La Jemaye (24410)' => '24216',
+ 'La Jonchère-Saint-Maurice (87340)' => '87079',
+ 'La Laigne (17170)' => '17201',
+ 'La Lande-de-Fronsac (33240)' => '33219',
+ 'La Magdeleine (16240)' => '16197',
+ 'La Mazière-aux-Bons-Hommes (23260)' => '23129',
+ 'La Meyze (87800)' => '87096',
+ 'La Mothe-Saint-Héray (79800)' => '79184',
+ 'La Nouaille (23500)' => '23144',
+ 'La Péruse (16270)' => '16259',
+ 'La Petite-Boissière (79700)' => '79207',
+ 'La Peyratte (79200)' => '79208',
+ 'La Porcherie (87380)' => '87120',
+ 'La Pouge (23250)' => '23157',
+ 'La Puye (86260)' => '86202',
+ 'La Réole (33190)' => '33352',
+ 'La Réunion (47700)' => '47222',
+ 'La Rivière (33126)' => '33356',
+ 'La Roche-Canillac (19320)' => '19174',
+ 'La Roche-Chalais (24490)' => '24354',
+ 'La Roche-l\'Abeille (87800)' => '87127',
+ 'La Roche-Posay (86270)' => '86207',
+ 'La Roche-Rigault (86200)' => '86079',
+ 'La Rochebeaucourt-et-Argentine (24340)' => '24353',
+ 'La Rochefoucauld (16110)' => '16281',
+ 'La Rochelle (17000)' => '17300',
+ 'La Rochénard (79270)' => '79229',
+ 'La Rochette (16110)' => '16282',
+ 'La Ronde (17170)' => '17303',
+ 'La Roque-Gageac (24250)' => '24355',
+ 'La Roquille (33220)' => '33360',
+ 'La Saunière (23000)' => '23169',
+ 'La Sauve (33670)' => '33505',
+ 'La Sauvetat-de-Savères (47270)' => '47289',
+ 'La Sauvetat-du-Dropt (47800)' => '47290',
+ 'La Sauvetat-sur-Lède (47150)' => '47291',
+ 'La Serre-Bussière-Vieille (23190)' => '23172',
+ 'La Souterraine (23300)' => '23176',
+ 'La Tâche (16260)' => '16377',
+ 'La Teste-de-Buch (33260)' => '33529',
+ 'La Tour-Blanche (24320)' => '24554',
+ 'La Tremblade (17390)' => '17452',
+ 'La Trimouille (86290)' => '86273',
+ 'La Vallée (17250)' => '17455',
+ 'La Vergne (17400)' => '17465',
+ 'La Villedieu (17470)' => '17471',
+ 'La Villedieu (23340)' => '23264',
+ 'La Villedieu-du-Clain (86340)' => '86290',
+ 'La Villeneuve (23260)' => '23265',
+ 'La Villetelle (23260)' => '23266',
+ 'Laà-Mondrans (64300)' => '64286',
+ 'Laàs (64390)' => '64287',
+ 'Labarde (33460)' => '33211',
+ 'Labastide-Castel-Amouroux (47250)' => '47121',
+ 'Labastide-Cézéracq (64170)' => '64288',
+ 'Labastide-Chalosse (40700)' => '40130',
+ 'Labastide-d\'Armagnac (40240)' => '40131',
+ 'Labastide-Monréjeau (64170)' => '64290',
+ 'Labastide-Villefranche (64270)' => '64291',
+ 'Labatmale (64530)' => '64292',
+ 'Labatut (40300)' => '40132',
+ 'Labatut (64460)' => '64293',
+ 'Labenne (40530)' => '40133',
+ 'Labescau (33690)' => '33212',
+ 'Labets-Biscay (64120)' => '64294',
+ 'Labeyrie (64300)' => '64295',
+ 'Labouheyre (40210)' => '40134',
+ 'Labretonie (47350)' => '47122',
+ 'Labrit (40420)' => '40135',
+ 'Lacadée (64300)' => '64296',
+ 'Lacajunte (40320)' => '40136',
+ 'Lacanau (33680)' => '33214',
+ 'Lacapelle-Biron (47150)' => '47123',
+ 'Lacarre (64220)' => '64297',
+ 'Lacarry-Arhan-Charritte-de-Haut (64470)' => '64298',
+ 'Lacaussade (47150)' => '47124',
+ 'Lacelle (19170)' => '19095',
+ 'Lacépède (47360)' => '47125',
+ 'Lachaise (16300)' => '16176',
+ 'Lachapelle (47350)' => '47126',
+ 'Lacommande (64360)' => '64299',
+ 'Lacq (64170)' => '64300',
+ 'Lacquy (40120)' => '40137',
+ 'Lacrabe (40700)' => '40138',
+ 'Lacropte (24380)' => '24220',
+ 'Ladapeyre (23270)' => '23102',
+ 'Ladaux (33760)' => '33215',
+ 'Ladignac-le-Long (87500)' => '87082',
+ 'Ladignac-sur-Rondelles (19150)' => '19096',
+ 'Ladiville (16120)' => '16177',
+ 'Lados (33124)' => '33216',
+ 'Lafage-sur-Sombre (19320)' => '19097',
+ 'Lafat (23800)' => '23103',
+ 'Lafitte-sur-Lot (47320)' => '47127',
+ 'Lafox (47240)' => '47128',
+ 'Lagarde-Enval (19150)' => '19098',
+ 'Lagarde-sur-le-Né (16300)' => '16178',
+ 'Lagarrigue (47190)' => '47129',
+ 'Lageon (79200)' => '79145',
+ 'Lagleygeolle (19500)' => '19099',
+ 'Laglorieuse (40090)' => '40139',
+ 'Lagor (64150)' => '64301',
+ 'Lagorce (33230)' => '33218',
+ 'Lagord (17140)' => '17200',
+ 'Lagos (64800)' => '64302',
+ 'Lagrange (40240)' => '40140',
+ 'Lagraulière (19700)' => '19100',
+ 'Lagruère (47400)' => '47130',
+ 'Laguenne (19150)' => '19101',
+ 'Laguinge-Restoue (64470)' => '64303',
+ 'Lagupie (47180)' => '47131',
+ 'Lahonce (64990)' => '64304',
+ 'Lahontan (64270)' => '64305',
+ 'Lahosse (40250)' => '40141',
+ 'Lahourcade (64150)' => '64306',
+ 'Lalande-de-Pomerol (33500)' => '33222',
+ 'Lalandusse (47330)' => '47132',
+ 'Lalinde (24150)' => '24223',
+ 'Lalongue (64350)' => '64307',
+ 'Lalonquette (64450)' => '64308',
+ 'Laluque (40465)' => '40142',
+ 'Lamarque (33460)' => '33220',
+ 'Lamayou (64460)' => '64309',
+ 'Lamazière-Basse (19160)' => '19102',
+ 'Lamazière-Haute (19340)' => '19103',
+ 'Lamongerie (19510)' => '19104',
+ 'Lamontjoie (47310)' => '47133',
+ 'Lamonzie-Montastruc (24520)' => '24224',
+ 'Lamonzie-Saint-Martin (24680)' => '24225',
+ 'Lamothe (40250)' => '40143',
+ 'Lamothe-Landerron (33190)' => '33221',
+ 'Lamothe-Montravel (24230)' => '24226',
+ 'Landerrouat (33790)' => '33223',
+ 'Landerrouet-sur-Ségur (33540)' => '33224',
+ 'Landes (17380)' => '17202',
+ 'Landiras (33720)' => '33225',
+ 'Landrais (17290)' => '17203',
+ 'Langoiran (33550)' => '33226',
+ 'Langon (33210)' => '33227',
+ 'Lanne-en-Barétous (64570)' => '64310',
+ 'Lannecaube (64350)' => '64311',
+ 'Lanneplaà (64300)' => '64312',
+ 'Lannes (47170)' => '47134',
+ 'Lanouaille (24270)' => '24227',
+ 'Lanquais (24150)' => '24228',
+ 'Lansac (33710)' => '33228',
+ 'Lantabat (64640)' => '64313',
+ 'Lanteuil (19190)' => '19105',
+ 'Lanton (33138)' => '33229',
+ 'Laparade (47260)' => '47135',
+ 'Laperche (47800)' => '47136',
+ 'Lapleau (19550)' => '19106',
+ 'Laplume (47310)' => '47137',
+ 'Lapouyade (33620)' => '33230',
+ 'Laprade (16390)' => '16180',
+ 'Larbey (40250)' => '40144',
+ 'Larceveau-Arros-Cibits (64120)' => '64314',
+ 'Larche (19600)' => '19107',
+ 'Largeasse (79240)' => '79147',
+ 'Laroche-près-Feyt (19340)' => '19108',
+ 'Laroin (64110)' => '64315',
+ 'Laroque (33410)' => '33231',
+ 'Laroque-Timbaut (47340)' => '47138',
+ 'Larrau (64560)' => '64316',
+ 'Larressore (64480)' => '64317',
+ 'Larreule (64410)' => '64318',
+ 'Larribar-Sorhapuru (64120)' => '64319',
+ 'Larrivière-Saint-Savin (40270)' => '40145',
+ 'Lartigue (33840)' => '33232',
+ 'Laruns (64440)' => '64320',
+ 'Laruscade (33620)' => '33233',
+ 'Larzac (24170)' => '24230',
+ 'Lascaux (19130)' => '19109',
+ 'Lasclaveries (64450)' => '64321',
+ 'Lasse (64220)' => '64322',
+ 'Lasserre (47600)' => '47139',
+ 'Lasserre (64350)' => '64323',
+ 'Lasseube (64290)' => '64324',
+ 'Lasseubetat (64290)' => '64325',
+ 'Lathus-Saint-Rémy (86390)' => '86120',
+ 'Latillé (86190)' => '86121',
+ 'Latresne (33360)' => '33234',
+ 'Latrille (40800)' => '40146',
+ 'Latronche (19160)' => '19110',
+ 'Laugnac (47360)' => '47140',
+ 'Laurède (40250)' => '40147',
+ 'Lauret (40320)' => '40148',
+ 'Laurière (87370)' => '87083',
+ 'Laussou (47150)' => '47141',
+ 'Lauthiers (86300)' => '86122',
+ 'Lauzun (47410)' => '47142',
+ 'Laval-sur-Luzège (19550)' => '19111',
+ 'Lavalade (24540)' => '24231',
+ 'Lavardac (47230)' => '47143',
+ 'Lavaufranche (23600)' => '23104',
+ 'Lavaur (24550)' => '24232',
+ 'Lavausseau (86470)' => '86123',
+ 'Lavaveix-les-Mines (23150)' => '23105',
+ 'Lavazan (33690)' => '33235',
+ 'Lavergne (47800)' => '47144',
+ 'Laveyssière (24130)' => '24233',
+ 'Lavignac (87230)' => '87084',
+ 'Lavoux (86800)' => '86124',
+ 'Lay-Lamidou (64190)' => '64326',
+ 'Layrac (47390)' => '47145',
+ 'Le Barp (33114)' => '33029',
+ 'Le Beugnon (79130)' => '79035',
+ 'Le Bois-Plage-en-Ré (17580)' => '17051',
+ 'Le Bouchage (16350)' => '16054',
+ 'Le Bourdeix (24300)' => '24056',
+ 'Le Bourdet (79210)' => '79046',
+ 'Le Bourg-d\'Hem (23220)' => '23029',
+ 'Le Bouscat (33110)' => '33069',
+ 'Le Breuil-Bernard (79320)' => '79051',
+ 'Le Bugue (24260)' => '24067',
+ 'Le Buis (87140)' => '87023',
+ 'Le Buisson-de-Cadouin (24480)' => '24068',
+ 'Le Busseau (79240)' => '79059',
+ 'Le Chalard (87500)' => '87031',
+ 'Le Change (24640)' => '24103',
+ 'Le Chastang (19190)' => '19048',
+ 'Le Château-d\'Oléron (17480)' => '17093',
+ 'Le Châtenet-en-Dognon (87400)' => '87042',
+ 'Le Chauchet (23130)' => '23058',
+ 'Le Chay (17600)' => '17097',
+ 'Le Chillou (79600)' => '79089',
+ 'Le Compas (23700)' => '23066',
+ 'Le Donzeil (23480)' => '23074',
+ 'Le Dorat (87210)' => '87059',
+ 'Le Douhet (17100)' => '17143',
+ 'Le Fieu (33230)' => '33166',
+ 'Le Fleix (24130)' => '24182',
+ 'Le Fouilloux (17270)' => '17167',
+ 'Le Frêche (40190)' => '40100',
+ 'Le Gicq (17160)' => '17177',
+ 'Le Grand-Bourg (23240)' => '23095',
+ 'Le Grand-Madieu (16450)' => '16157',
+ 'Le Grand-Village-Plage (17370)' => '17485',
+ 'Le Gua (17600)' => '17185',
+ 'Le Gué-d\'Alleré (17540)' => '17186',
+ 'Le Haillan (33185)' => '33200',
+ 'Le Jardin (19300)' => '19092',
+ 'Le Lardin-Saint-Lazare (24570)' => '24229',
+ 'Le Leuy (40250)' => '40153',
+ 'Le Lindois (16310)' => '16188',
+ 'Le Lonzac (19470)' => '19118',
+ 'Le Mas-d\'Agenais (47430)' => '47159',
+ 'Le Mas-d\'Artige (23100)' => '23125',
+ 'Le Monteil-au-Vicomte (23460)' => '23134',
+ 'Le Mung (17350)' => '17252',
+ 'Le Nizan (33430)' => '33305',
+ 'Le Palais-sur-Vienne (87410)' => '87113',
+ 'Le Passage (47520)' => '47201',
+ 'Le Pescher (19190)' => '19163',
+ 'Le Pian-Médoc (33290)' => '33322',
+ 'Le Pian-sur-Garonne (33490)' => '33323',
+ 'Le Pin (17210)' => '17276',
+ 'Le Pin (79140)' => '79210',
+ 'Le Pizou (24700)' => '24329',
+ 'Le Porge (33680)' => '33333',
+ 'Le Pout (33670)' => '33335',
+ 'Le Puy (33580)' => '33345',
+ 'Le Retail (79130)' => '79226',
+ 'Le Rochereau (86170)' => '86208',
+ 'Le Sen (40420)' => '40297',
+ 'Le Seure (17770)' => '17426',
+ 'Le Taillan-Médoc (33320)' => '33519',
+ 'Le Tallud (79200)' => '79322',
+ 'Le Tâtre (16360)' => '16380',
+ 'Le Teich (33470)' => '33527',
+ 'Le Temple (33680)' => '33528',
+ 'Le Temple-sur-Lot (47110)' => '47306',
+ 'Le Thou (17290)' => '17447',
+ 'Le Tourne (33550)' => '33534',
+ 'Le Tuzan (33125)' => '33536',
+ 'Le Vanneau-Irleau (79270)' => '79337',
+ 'Le Verdon-sur-Mer (33123)' => '33544',
+ 'Le Vert (79170)' => '79346',
+ 'Le Vieux-Cérier (16350)' => '16403',
+ 'Le Vigeant (86150)' => '86289',
+ 'Le Vigen (87110)' => '87205',
+ 'Le Vignau (40270)' => '40329',
+ 'Lecumberry (64220)' => '64327',
+ 'Lédat (47300)' => '47146',
+ 'Ledeuix (64400)' => '64328',
+ 'Lée (64320)' => '64329',
+ 'Lées-Athas (64490)' => '64330',
+ 'Lège-Cap-Ferret (33950)' => '33236',
+ 'Léguillac-de-Cercles (24340)' => '24235',
+ 'Léguillac-de-l\'Auche (24110)' => '24236',
+ 'Leigné-les-Bois (86450)' => '86125',
+ 'Leigné-sur-Usseau (86230)' => '86127',
+ 'Leignes-sur-Fontaine (86300)' => '86126',
+ 'Lembeye (64350)' => '64331',
+ 'Lembras (24100)' => '24237',
+ 'Lème (64450)' => '64332',
+ 'Lempzours (24800)' => '24238',
+ 'Lencloître (86140)' => '86128',
+ 'Lencouacq (40120)' => '40149',
+ 'Léogeats (33210)' => '33237',
+ 'Léognan (33850)' => '33238',
+ 'Léon (40550)' => '40150',
+ 'Léoville (17500)' => '17204',
+ 'Lépaud (23170)' => '23106',
+ 'Lépinas (23150)' => '23107',
+ 'Léren (64270)' => '64334',
+ 'Lerm-et-Musset (33840)' => '33239',
+ 'Les Adjots (16700)' => '16002',
+ 'Les Alleuds (79190)' => '79006',
+ 'Les Angles-sur-Corrèze (19000)' => '19009',
+ 'Les Artigues-de-Lussac (33570)' => '33014',
+ 'Les Billanges (87340)' => '87016',
+ 'Les Billaux (33500)' => '33052',
+ 'Les Cars (87230)' => '87029',
+ 'Les Éduts (17510)' => '17149',
+ 'Les Églises-d\'Argenteuil (17400)' => '17150',
+ 'Les Églisottes-et-Chalaures (33230)' => '33154',
+ 'Les Essards (16210)' => '16130',
+ 'Les Essards (17250)' => '17154',
+ 'Les Esseintes (33190)' => '33158',
+ 'Les Eyzies-de-Tayac-Sireuil (24620)' => '24172',
+ 'Les Farges (24290)' => '24175',
+ 'Les Forges (79340)' => '79124',
+ 'Les Fosses (79360)' => '79126',
+ 'Les Gonds (17100)' => '17179',
+ 'Les Gours (16140)' => '16155',
+ 'Les Grands-Chézeaux (87160)' => '87074',
+ 'Les Graulges (24340)' => '24203',
+ 'Les Groseillers (79220)' => '79139',
+ 'Les Lèches (24400)' => '24234',
+ 'Les Lèves-et-Thoumeyragues (33220)' => '33242',
+ 'Les Mars (23700)' => '23123',
+ 'Les Mathes (17570)' => '17225',
+ 'Les Métairies (16200)' => '16220',
+ 'Les Nouillers (17380)' => '17266',
+ 'Les Ormes (86220)' => '86183',
+ 'Les Peintures (33230)' => '33315',
+ 'Les Pins (16260)' => '16261',
+ 'Les Portes-en-Ré (17880)' => '17286',
+ 'Les Salles-de-Castillon (33350)' => '33499',
+ 'Les Salles-Lavauguyon (87440)' => '87189',
+ 'Les Touches-de-Périgny (17160)' => '17451',
+ 'Les Trois-Moutiers (86120)' => '86274',
+ 'Lescar (64230)' => '64335',
+ 'Lescun (64490)' => '64336',
+ 'Lesgor (40400)' => '40151',
+ 'Lésignac-Durand (16310)' => '16183',
+ 'Lésigny (86270)' => '86129',
+ 'Lesparre-Médoc (33340)' => '33240',
+ 'Lesperon (40260)' => '40152',
+ 'Lespielle (64350)' => '64337',
+ 'Lespourcy (64160)' => '64338',
+ 'Lessac (16500)' => '16181',
+ 'Lestards (19170)' => '19112',
+ 'Lestelle-Bétharram (64800)' => '64339',
+ 'Lesterps (16420)' => '16182',
+ 'Lestiac-sur-Garonne (33550)' => '33241',
+ 'Leugny (86220)' => '86130',
+ 'Lévignac-de-Guyenne (47120)' => '47147',
+ 'Lévignacq (40170)' => '40154',
+ 'Leyrat (23600)' => '23108',
+ 'Leyritz-Moncassin (47700)' => '47148',
+ 'Lezay (79120)' => '79148',
+ 'Lhommaizé (86410)' => '86131',
+ 'Lhoumois (79390)' => '79149',
+ 'Libourne (33500)' => '33243',
+ 'Lichans-Sunhar (64470)' => '64340',
+ 'Lichères (16460)' => '16184',
+ 'Lichos (64130)' => '64341',
+ 'Licq-Athérey (64560)' => '64342',
+ 'Liginiac (19160)' => '19113',
+ 'Liglet (86290)' => '86132',
+ 'Lignan-de-Bazas (33430)' => '33244',
+ 'Lignan-de-Bordeaux (33360)' => '33245',
+ 'Lignareix (19200)' => '19114',
+ 'Ligné (16140)' => '16185',
+ 'Ligneyrac (19500)' => '19115',
+ 'Lignières-Sonneville (16130)' => '16186',
+ 'Ligueux (33220)' => '33246',
+ 'Ligugé (86240)' => '86133',
+ 'Limalonges (79190)' => '79150',
+ 'Limendous (64420)' => '64343',
+ 'Limeuil (24510)' => '24240',
+ 'Limeyrat (24210)' => '24241',
+ 'Limoges (87000)' => '87085',
+ 'Linard (23220)' => '23109',
+ 'Linards (87130)' => '87086',
+ 'Linars (16730)' => '16187',
+ 'Linazay (86400)' => '86134',
+ 'Liniers (86800)' => '86135',
+ 'Linxe (40260)' => '40155',
+ 'Liorac-sur-Louyre (24520)' => '24242',
+ 'Liourdres (19120)' => '19116',
+ 'Lioux-les-Monges (23700)' => '23110',
+ 'Liposthey (40410)' => '40156',
+ 'Lisle (24350)' => '24243',
+ 'Lissac-sur-Couze (19600)' => '19117',
+ 'Listrac-de-Durèze (33790)' => '33247',
+ 'Listrac-Médoc (33480)' => '33248',
+ 'Lit-et-Mixe (40170)' => '40157',
+ 'Livron (64530)' => '64344',
+ 'Lizant (86400)' => '86136',
+ 'Lizières (23240)' => '23111',
+ 'Lohitzun-Oyhercq (64120)' => '64345',
+ 'Loire-les-Marais (17870)' => '17205',
+ 'Loiré-sur-Nie (17470)' => '17206',
+ 'Loix (17111)' => '17207',
+ 'Lolme (24540)' => '24244',
+ 'Lombia (64160)' => '64346',
+ 'Lonçon (64410)' => '64347',
+ 'Londigny (16700)' => '16189',
+ 'Longèves (17230)' => '17208',
+ 'Longré (16240)' => '16190',
+ 'Longueville (47200)' => '47150',
+ 'Lonnes (16230)' => '16191',
+ 'Lons (64140)' => '64348',
+ 'Lonzac (17520)' => '17209',
+ 'Lorignac (17240)' => '17210',
+ 'Lorigné (79190)' => '79152',
+ 'Lormont (33310)' => '33249',
+ 'Losse (40240)' => '40158',
+ 'Lostanges (19500)' => '19119',
+ 'Loubejac (24550)' => '24245',
+ 'Loubens (33190)' => '33250',
+ 'Loubès-Bernac (47120)' => '47151',
+ 'Loubieng (64300)' => '64349',
+ 'Loubigné (79110)' => '79153',
+ 'Loubillé (79110)' => '79154',
+ 'Louchats (33125)' => '33251',
+ 'Loudun (86200)' => '86137',
+ 'Louer (40380)' => '40159',
+ 'Lougratte (47290)' => '47152',
+ 'Louhossoa (64250)' => '64350',
+ 'Louignac (19310)' => '19120',
+ 'Louin (79600)' => '79156',
+ 'Loulay (17330)' => '17211',
+ 'Loupes (33370)' => '33252',
+ 'Loupiac (33410)' => '33253',
+ 'Loupiac-de-la-Réole (33190)' => '33254',
+ 'Lourdios-Ichère (64570)' => '64351',
+ 'Lourdoueix-Saint-Pierre (23360)' => '23112',
+ 'Lourenties (64420)' => '64352',
+ 'Lourquen (40250)' => '40160',
+ 'Louvie-Juzon (64260)' => '64353',
+ 'Louvie-Soubiron (64440)' => '64354',
+ 'Louvigny (64410)' => '64355',
+ 'Louzac-Saint-André (16100)' => '16193',
+ 'Louzignac (17160)' => '17212',
+ 'Louzy (79100)' => '79157',
+ 'Lozay (17330)' => '17213',
+ 'Lubbon (40240)' => '40161',
+ 'Lubersac (19210)' => '19121',
+ 'Luc-Armau (64350)' => '64356',
+ 'Lucarré (64350)' => '64357',
+ 'Lucbardez-et-Bargues (40090)' => '40162',
+ 'Lucgarier (64420)' => '64358',
+ 'Luchapt (86430)' => '86138',
+ 'Luchat (17600)' => '17214',
+ 'Luché-sur-Brioux (79170)' => '79158',
+ 'Luché-Thouarsais (79330)' => '79159',
+ 'Lucmau (33840)' => '33255',
+ 'Lucq-de-Béarn (64360)' => '64359',
+ 'Ludon-Médoc (33290)' => '33256',
+ 'Lüe (40210)' => '40163',
+ 'Lugaignac (33420)' => '33257',
+ 'Lugasson (33760)' => '33258',
+ 'Luglon (40630)' => '40165',
+ 'Lugon-et-l\'Île-du-Carnay (33240)' => '33259',
+ 'Lugos (33830)' => '33260',
+ 'Lunas (24130)' => '24246',
+ 'Lupersat (23190)' => '23113',
+ 'Lupsault (16140)' => '16194',
+ 'Luquet (65320)' => '65292',
+ 'Lurbe-Saint-Christau (64660)' => '64360',
+ 'Lusignac (24320)' => '24247',
+ 'Lusignan (86600)' => '86139',
+ 'Lusignan-Petit (47360)' => '47154',
+ 'Lussac (16450)' => '16195',
+ 'Lussac (17500)' => '17215',
+ 'Lussac (33570)' => '33261',
+ 'Lussac-les-Châteaux (86320)' => '86140',
+ 'Lussac-les-Églises (87360)' => '87087',
+ 'Lussagnet (40270)' => '40166',
+ 'Lussagnet-Lusson (64160)' => '64361',
+ 'Lussant (17430)' => '17216',
+ 'Lussas-et-Nontronneau (24300)' => '24248',
+ 'Lussat (23170)' => '23114',
+ 'Lusseray (79170)' => '79160',
+ 'Luxé (16230)' => '16196',
+ 'Luxe-Sumberraute (64120)' => '64362',
+ 'Luxey (40430)' => '40167',
+ 'Luzay (79100)' => '79161',
+ 'Lys (64260)' => '64363',
+ 'Macau (33460)' => '33262',
+ 'Macaye (64240)' => '64364',
+ 'Macqueville (17490)' => '17217',
+ 'Madaillan (47360)' => '47155',
+ 'Madirac (33670)' => '33263',
+ 'Madranges (19470)' => '19122',
+ 'Magescq (40140)' => '40168',
+ 'Magnac-Bourg (87380)' => '87088',
+ 'Magnac-Laval (87190)' => '87089',
+ 'Magnac-Lavalette-Villars (16320)' => '16198',
+ 'Magnac-sur-Touvre (16600)' => '16199',
+ 'Magnat-l\'Étrange (23260)' => '23115',
+ 'Magné (79460)' => '79162',
+ 'Magné (86160)' => '86141',
+ 'Mailhac-sur-Benaize (87160)' => '87090',
+ 'Maillas (40120)' => '40169',
+ 'Maillé (86190)' => '86142',
+ 'Maillères (40120)' => '40170',
+ 'Maine-de-Boixe (16230)' => '16200',
+ 'Mainsat (23700)' => '23116',
+ 'Mainxe (16200)' => '16202',
+ 'Mainzac (16380)' => '16203',
+ 'Mairé (86270)' => '86143',
+ 'Mairé-Levescault (79190)' => '79163',
+ 'Maison-Feyne (23800)' => '23117',
+ 'Maisonnais-sur-Tardoire (87440)' => '87091',
+ 'Maisonnay (79500)' => '79164',
+ 'Maisonneuve (86170)' => '86144',
+ 'Maisonnisses (23150)' => '23118',
+ 'Maisontiers (79600)' => '79165',
+ 'Malaussanne (64410)' => '64365',
+ 'Malaville (16120)' => '16204',
+ 'Malemort (19360)' => '19123',
+ 'Malleret (23260)' => '23119',
+ 'Malleret-Boussac (23600)' => '23120',
+ 'Malval (23220)' => '23121',
+ 'Manaurie (24620)' => '24249',
+ 'Mano (40410)' => '40171',
+ 'Manot (16500)' => '16205',
+ 'Mansac (19520)' => '19124',
+ 'Mansat-la-Courrière (23400)' => '23122',
+ 'Mansle (16230)' => '16206',
+ 'Mant (40700)' => '40172',
+ 'Manzac-sur-Vern (24110)' => '24251',
+ 'Marans (17230)' => '17218',
+ 'Maransin (33230)' => '33264',
+ 'Marc-la-Tour (19150)' => '19127',
+ 'Marçay (86370)' => '86145',
+ 'Marcellus (47200)' => '47156',
+ 'Marcenais (33620)' => '33266',
+ 'Marcheprime (33380)' => '33555',
+ 'Marcillac (33860)' => '33267',
+ 'Marcillac-la-Croisille (19320)' => '19125',
+ 'Marcillac-la-Croze (19500)' => '19126',
+ 'Marcillac-Lanville (16140)' => '16207',
+ 'Marcillac-Saint-Quentin (24200)' => '24252',
+ 'Marennes (17320)' => '17219',
+ 'Mareuil (16170)' => '16208',
+ 'Mareuil (24340)' => '24253',
+ 'Margaux (33460)' => '33268',
+ 'Margerides (19200)' => '19128',
+ 'Margueron (33220)' => '33269',
+ 'Marignac (17800)' => '17220',
+ 'Marigny (79360)' => '79166',
+ 'Marigny-Brizay (86380)' => '86146',
+ 'Marigny-Chemereau (86370)' => '86147',
+ 'Marillac-le-Franc (16110)' => '16209',
+ 'Marimbault (33430)' => '33270',
+ 'Marions (33690)' => '33271',
+ 'Marmande (47200)' => '47157',
+ 'Marmont-Pachas (47220)' => '47158',
+ 'Marnac (24220)' => '24254',
+ 'Marnay (86160)' => '86148',
+ 'Marnes (79600)' => '79167',
+ 'Marpaps (40330)' => '40173',
+ 'Marquay (24620)' => '24255',
+ 'Marsac (16570)' => '16210',
+ 'Marsac (23210)' => '23124',
+ 'Marsac-sur-l\'Isle (24430)' => '24256',
+ 'Marsais (17700)' => '17221',
+ 'Marsalès (24540)' => '24257',
+ 'Marsaneix (24750)' => '24258',
+ 'Marsas (33620)' => '33272',
+ 'Marsilly (17137)' => '17222',
+ 'Martaizé (86330)' => '86149',
+ 'Marthon (16380)' => '16211',
+ 'Martignas-sur-Jalle (33127)' => '33273',
+ 'Martillac (33650)' => '33274',
+ 'Martres (33760)' => '33275',
+ 'Marval (87440)' => '87092',
+ 'Masbaraud-Mérignat (23400)' => '23126',
+ 'Mascaraàs-Haron (64330)' => '64366',
+ 'Maslacq (64300)' => '64367',
+ 'Masléon (87130)' => '87093',
+ 'Masparraute (64120)' => '64368',
+ 'Maspie-Lalonquère-Juillacq (64350)' => '64369',
+ 'Masquières (47370)' => '47160',
+ 'Massac (17490)' => '17223',
+ 'Massais (79150)' => '79168',
+ 'Masseilles (33690)' => '33276',
+ 'Massels (47140)' => '47161',
+ 'Masseret (19510)' => '19129',
+ 'Massignac (16310)' => '16212',
+ 'Massognes (86170)' => '86150',
+ 'Massoulès (47140)' => '47162',
+ 'Massugas (33790)' => '33277',
+ 'Matha (17160)' => '17224',
+ 'Maucor (64160)' => '64370',
+ 'Maulay (86200)' => '86151',
+ 'Mauléon (79700)' => '79079',
+ 'Mauléon-Licharre (64130)' => '64371',
+ 'Mauprévoir (86460)' => '86152',
+ 'Maure (64460)' => '64372',
+ 'Maurens (24140)' => '24259',
+ 'Mauriac (33540)' => '33278',
+ 'Mauries (40320)' => '40174',
+ 'Maurrin (40270)' => '40175',
+ 'Maussac (19250)' => '19130',
+ 'Mautes (23190)' => '23127',
+ 'Mauvezin-d\'Armagnac (40240)' => '40176',
+ 'Mauvezin-sur-Gupie (47200)' => '47163',
+ 'Mauzac-et-Grand-Castang (24150)' => '24260',
+ 'Mauzé-sur-le-Mignon (79210)' => '79170',
+ 'Mauzé-Thouarsais (79100)' => '79171',
+ 'Mauzens-et-Miremont (24260)' => '24261',
+ 'Mayac (24420)' => '24262',
+ 'Maylis (40250)' => '40177',
+ 'Mazeirat (23150)' => '23128',
+ 'Mazeray (17400)' => '17226',
+ 'Mazères (33210)' => '33279',
+ 'Mazères-Lezons (64110)' => '64373',
+ 'Mazerolles (16310)' => '16213',
+ 'Mazerolles (17800)' => '17227',
+ 'Mazerolles (40090)' => '40178',
+ 'Mazerolles (64230)' => '64374',
+ 'Mazerolles (86320)' => '86153',
+ 'Mazeuil (86110)' => '86154',
+ 'Mazeyrolles (24550)' => '24263',
+ 'Mazières (16270)' => '16214',
+ 'Mazières-en-Gâtine (79310)' => '79172',
+ 'Mazières-Naresse (47210)' => '47164',
+ 'Mazières-sur-Béronne (79500)' => '79173',
+ 'Mazion (33390)' => '33280',
+ 'Méasnes (23360)' => '23130',
+ 'Médillac (16210)' => '16215',
+ 'Médis (17600)' => '17228',
+ 'Mées (40990)' => '40179',
+ 'Méharin (64120)' => '64375',
+ 'Meilhac (87800)' => '87094',
+ 'Meilhan (40400)' => '40180',
+ 'Meilhan-sur-Garonne (47180)' => '47165',
+ 'Meilhards (19510)' => '19131',
+ 'Meillon (64510)' => '64376',
+ 'Melle (79500)' => '79174',
+ 'Melleran (79190)' => '79175',
+ 'Mendionde (64240)' => '64377',
+ 'Menditte (64130)' => '64378',
+ 'Mendive (64220)' => '64379',
+ 'Ménesplet (24700)' => '24264',
+ 'Ménigoute (79340)' => '79176',
+ 'Ménoire (19190)' => '19132',
+ 'Mensignac (24350)' => '24266',
+ 'Méracq (64410)' => '64380',
+ 'Mercoeur (19430)' => '19133',
+ 'Mérignac (16200)' => '16216',
+ 'Mérignac (17210)' => '17229',
+ 'Mérignac (33700)' => '33281',
+ 'Mérignas (33350)' => '33282',
+ 'Mérinchal (23420)' => '23131',
+ 'Méritein (64190)' => '64381',
+ 'Merlines (19340)' => '19134',
+ 'Merpins (16100)' => '16217',
+ 'Meschers-sur-Gironde (17132)' => '17230',
+ 'Mescoules (24240)' => '24267',
+ 'Mesnac (16370)' => '16218',
+ 'Mesplède (64370)' => '64382',
+ 'Messac (17130)' => '17231',
+ 'Messanges (40660)' => '40181',
+ 'Messé (79120)' => '79177',
+ 'Messemé (86200)' => '86156',
+ 'Mesterrieux (33540)' => '33283',
+ 'Mestes (19200)' => '19135',
+ 'Meursac (17120)' => '17232',
+ 'Meux (17500)' => '17233',
+ 'Meuzac (87380)' => '87095',
+ 'Meymac (19250)' => '19136',
+ 'Meyrals (24220)' => '24268',
+ 'Meyrignac-l\'Église (19800)' => '19137',
+ 'Meyssac (19500)' => '19138',
+ 'Mézin (47170)' => '47167',
+ 'Mézos (40170)' => '40182',
+ 'Mialet (24450)' => '24269',
+ 'Mialos (64410)' => '64383',
+ 'Mignaloux-Beauvoir (86550)' => '86157',
+ 'Migné-Auxances (86440)' => '86158',
+ 'Migré (17330)' => '17234',
+ 'Migron (17770)' => '17235',
+ 'Milhac-d\'Auberoche (24330)' => '24270',
+ 'Milhac-de-Nontron (24470)' => '24271',
+ 'Millac (86150)' => '86159',
+ 'Millevaches (19290)' => '19139',
+ 'Mimbaste (40350)' => '40183',
+ 'Mimizan (40200)' => '40184',
+ 'Minzac (24610)' => '24272',
+ 'Mios (33380)' => '33284',
+ 'Miossens-Lanusse (64450)' => '64385',
+ 'Mirambeau (17150)' => '17236',
+ 'Miramont-de-Guyenne (47800)' => '47168',
+ 'Miramont-Sensacq (40320)' => '40185',
+ 'Mirebeau (86110)' => '86160',
+ 'Mirepeix (64800)' => '64386',
+ 'Missé (79100)' => '79178',
+ 'Misson (40290)' => '40186',
+ 'Moëze (17780)' => '17237',
+ 'Moirax (47310)' => '47169',
+ 'Moissannes (87400)' => '87099',
+ 'Molières (24480)' => '24273',
+ 'Moliets-et-Maa (40660)' => '40187',
+ 'Momas (64230)' => '64387',
+ 'Mombrier (33710)' => '33285',
+ 'Momuy (40700)' => '40188',
+ 'Momy (64350)' => '64388',
+ 'Monassut-Audiracq (64160)' => '64389',
+ 'Monbahus (47290)' => '47170',
+ 'Monbalen (47340)' => '47171',
+ 'Monbazillac (24240)' => '24274',
+ 'Moncaup (64350)' => '64390',
+ 'Moncaut (47310)' => '47172',
+ 'Moncayolle-Larrory-Mendibieu (64130)' => '64391',
+ 'Monceaux-sur-Dordogne (19400)' => '19140',
+ 'Moncla (64330)' => '64392',
+ 'Monclar (47380)' => '47173',
+ 'Moncontour (86330)' => '86161',
+ 'Moncoutant (79320)' => '79179',
+ 'Moncrabeau (47600)' => '47174',
+ 'Mondion (86230)' => '86162',
+ 'Monein (64360)' => '64393',
+ 'Monestier (24240)' => '24276',
+ 'Monestier-Merlines (19340)' => '19141',
+ 'Monestier-Port-Dieu (19110)' => '19142',
+ 'Monfaucon (24130)' => '24277',
+ 'Monflanquin (47150)' => '47175',
+ 'Mongaillard (47230)' => '47176',
+ 'Mongauzy (33190)' => '33287',
+ 'Monget (40700)' => '40189',
+ 'Monheurt (47160)' => '47177',
+ 'Monmadalès (24560)' => '24278',
+ 'Monmarvès (24560)' => '24279',
+ 'Monpazier (24540)' => '24280',
+ 'Monpezat (64350)' => '64394',
+ 'Monplaisant (24170)' => '24293',
+ 'Monprimblanc (33410)' => '33288',
+ 'Mons (16140)' => '16221',
+ 'Mons (17160)' => '17239',
+ 'Monsac (24440)' => '24281',
+ 'Monsaguel (24560)' => '24282',
+ 'Monsec (24340)' => '24283',
+ 'Monségur (33580)' => '33289',
+ 'Monségur (40700)' => '40190',
+ 'Monségur (47150)' => '47178',
+ 'Monségur (64460)' => '64395',
+ 'Monsempron-Libos (47500)' => '47179',
+ 'Mont (64300)' => '64396',
+ 'Mont-de-Marsan (40000)' => '40192',
+ 'Mont-Disse (64330)' => '64401',
+ 'Montagnac-d\'Auberoche (24210)' => '24284',
+ 'Montagnac-la-Crempse (24140)' => '24285',
+ 'Montagnac-sur-Auvignon (47600)' => '47180',
+ 'Montagnac-sur-Lède (47150)' => '47181',
+ 'Montagne (33570)' => '33290',
+ 'Montagoudin (33190)' => '33291',
+ 'Montagrier (24350)' => '24286',
+ 'Montagut (64410)' => '64397',
+ 'Montaignac-Saint-Hippolyte (19300)' => '19143',
+ 'Montaigut-le-Blanc (23320)' => '23132',
+ 'Montalembert (79190)' => '79180',
+ 'Montamisé (86360)' => '86163',
+ 'Montaner (64460)' => '64398',
+ 'Montardon (64121)' => '64399',
+ 'Montastruc (47380)' => '47182',
+ 'Montauriol (47330)' => '47183',
+ 'Montaut (24560)' => '24287',
+ 'Montaut (40500)' => '40191',
+ 'Montaut (47210)' => '47184',
+ 'Montaut (64800)' => '64400',
+ 'Montayral (47500)' => '47185',
+ 'Montazeau (24230)' => '24288',
+ 'Montboucher (23400)' => '23133',
+ 'Montboyer (16620)' => '16222',
+ 'Montbron (16220)' => '16223',
+ 'Montcaret (24230)' => '24289',
+ 'Montégut (40190)' => '40193',
+ 'Montemboeuf (16310)' => '16225',
+ 'Montendre (17130)' => '17240',
+ 'Montesquieu (47130)' => '47186',
+ 'Monteton (47120)' => '47187',
+ 'Montferrand-du-Périgord (24440)' => '24290',
+ 'Montfort (64190)' => '64403',
+ 'Montfort-en-Chalosse (40380)' => '40194',
+ 'Montgaillard (40500)' => '40195',
+ 'Montgibaud (19210)' => '19144',
+ 'Montguyon (17270)' => '17241',
+ 'Monthoiron (86210)' => '86164',
+ 'Montignac (24290)' => '24291',
+ 'Montignac (33760)' => '33292',
+ 'Montignac-Charente (16330)' => '16226',
+ 'Montignac-de-Lauzun (47800)' => '47188',
+ 'Montignac-le-Coq (16390)' => '16227',
+ 'Montignac-Toupinerie (47350)' => '47189',
+ 'Montigné (16170)' => '16228',
+ 'Montils (17800)' => '17242',
+ 'Montjean (16240)' => '16229',
+ 'Montlieu-la-Garde (17210)' => '17243',
+ 'Montmérac (16300)' => '16224',
+ 'Montmoreau-Saint-Cybard (16190)' => '16230',
+ 'Montmorillon (86500)' => '86165',
+ 'Montory (64470)' => '64404',
+ 'Montpellier-de-Médillan (17260)' => '17244',
+ 'Montpeyroux (24610)' => '24292',
+ 'Montpezat (47360)' => '47190',
+ 'Montpon-Ménestérol (24700)' => '24294',
+ 'Montpouillan (47200)' => '47191',
+ 'Montravers (79140)' => '79183',
+ 'Montrem (24110)' => '24295',
+ 'Montreuil-Bonnin (86470)' => '86166',
+ 'Montrol-Sénard (87330)' => '87100',
+ 'Montrollet (16420)' => '16231',
+ 'Montroy (17220)' => '17245',
+ 'Monts-sur-Guesnes (86420)' => '86167',
+ 'Montsoué (40500)' => '40196',
+ 'Montussan (33450)' => '33293',
+ 'Monviel (47290)' => '47192',
+ 'Moragne (17430)' => '17246',
+ 'Morcenx (40110)' => '40197',
+ 'Morganx (40700)' => '40198',
+ 'Morizès (33190)' => '33294',
+ 'Morlaàs (64160)' => '64405',
+ 'Morlanne (64370)' => '64406',
+ 'Mornac (16600)' => '16232',
+ 'Mornac-sur-Seudre (17113)' => '17247',
+ 'Mortagne-sur-Gironde (17120)' => '17248',
+ 'Mortemart (87330)' => '87101',
+ 'Mortiers (17500)' => '17249',
+ 'Morton (86120)' => '86169',
+ 'Mortroux (23220)' => '23136',
+ 'Mosnac (16120)' => '16233',
+ 'Mosnac (17240)' => '17250',
+ 'Mougon (79370)' => '79185',
+ 'Mouguerre (64990)' => '64407',
+ 'Mouhous (64330)' => '64408',
+ 'Mouillac (33240)' => '33295',
+ 'Mouleydier (24520)' => '24296',
+ 'Moulidars (16290)' => '16234',
+ 'Mouliets-et-Villemartin (33350)' => '33296',
+ 'Moulin-Neuf (24700)' => '24297',
+ 'Moulinet (47290)' => '47193',
+ 'Moulis-en-Médoc (33480)' => '33297',
+ 'Moulismes (86500)' => '86170',
+ 'Moulon (33420)' => '33298',
+ 'Moumour (64400)' => '64409',
+ 'Mourens (33410)' => '33299',
+ 'Mourenx (64150)' => '64410',
+ 'Mourioux-Vieilleville (23210)' => '23137',
+ 'Mouscardès (40290)' => '40199',
+ 'Moussac (86150)' => '86171',
+ 'Moustey (40410)' => '40200',
+ 'Moustier (47800)' => '47194',
+ 'Moustier-Ventadour (19300)' => '19145',
+ 'Mouterre-Silly (86200)' => '86173',
+ 'Mouterre-sur-Blourde (86430)' => '86172',
+ 'Mouthiers-sur-Boëme (16440)' => '16236',
+ 'Moutier-d\'Ahun (23150)' => '23138',
+ 'Moutier-Malcard (23220)' => '23139',
+ 'Moutier-Rozeille (23200)' => '23140',
+ 'Moutiers-sous-Chantemerle (79320)' => '79188',
+ 'Mouton (16460)' => '16237',
+ 'Moutonneau (16460)' => '16238',
+ 'Mouzon (16310)' => '16239',
+ 'Mugron (40250)' => '40201',
+ 'Muron (17430)' => '17253',
+ 'Musculdy (64130)' => '64411',
+ 'Mussidan (24400)' => '24299',
+ 'Nabas (64190)' => '64412',
+ 'Nabinaud (16390)' => '16240',
+ 'Nabirat (24250)' => '24300',
+ 'Nachamps (17380)' => '17254',
+ 'Nadaillac (24590)' => '24301',
+ 'Nailhac (24390)' => '24302',
+ 'Naillat (23800)' => '23141',
+ 'Naintré (86530)' => '86174',
+ 'Nalliers (86310)' => '86175',
+ 'Nanclars (16230)' => '16241',
+ 'Nancras (17600)' => '17255',
+ 'Nanteuil (79400)' => '79189',
+ 'Nanteuil-Auriac-de-Bourzac (24320)' => '24303',
+ 'Nanteuil-en-Vallée (16700)' => '16242',
+ 'Nantheuil (24800)' => '24304',
+ 'Nanthiat (24800)' => '24305',
+ 'Nantiat (87140)' => '87103',
+ 'Nantillé (17770)' => '17256',
+ 'Narcastet (64510)' => '64413',
+ 'Narp (64190)' => '64414',
+ 'Narrosse (40180)' => '40202',
+ 'Nassiet (40330)' => '40203',
+ 'Nastringues (24230)' => '24306',
+ 'Naujac-sur-Mer (33990)' => '33300',
+ 'Naujan-et-Postiac (33420)' => '33301',
+ 'Naussannes (24440)' => '24307',
+ 'Navailles-Angos (64450)' => '64415',
+ 'Navarrenx (64190)' => '64416',
+ 'Naves (19460)' => '19146',
+ 'Nay (64800)' => '64417',
+ 'Néac (33500)' => '33302',
+ 'Nedde (87120)' => '87104',
+ 'Négrondes (24460)' => '24308',
+ 'Néoux (23200)' => '23142',
+ 'Nérac (47600)' => '47195',
+ 'Nerbis (40250)' => '40204',
+ 'Nercillac (16200)' => '16243',
+ 'Néré (17510)' => '17257',
+ 'Nérigean (33750)' => '33303',
+ 'Nérignac (86150)' => '86176',
+ 'Nersac (16440)' => '16244',
+ 'Nespouls (19600)' => '19147',
+ 'Neuffons (33580)' => '33304',
+ 'Neuillac (17520)' => '17258',
+ 'Neulles (17500)' => '17259',
+ 'Neuvic (19160)' => '19148',
+ 'Neuvic (24190)' => '24309',
+ 'Neuvic-Entier (87130)' => '87105',
+ 'Neuvicq (17270)' => '17260',
+ 'Neuvicq-le-Château (17490)' => '17261',
+ 'Neuville (19380)' => '19149',
+ 'Neuville-de-Poitou (86170)' => '86177',
+ 'Neuvy-Bouin (79130)' => '79190',
+ 'Nexon (87800)' => '87106',
+ 'Nicole (47190)' => '47196',
+ 'Nieuil (16270)' => '16245',
+ 'Nieuil-l\'Espoir (86340)' => '86178',
+ 'Nieul (87510)' => '87107',
+ 'Nieul-le-Virouil (17150)' => '17263',
+ 'Nieul-lès-Saintes (17810)' => '17262',
+ 'Nieul-sur-Mer (17137)' => '17264',
+ 'Nieulle-sur-Seudre (17600)' => '17265',
+ 'Niort (79000)' => '79191',
+ 'Noailhac (19500)' => '19150',
+ 'Noaillac (33190)' => '33306',
+ 'Noaillan (33730)' => '33307',
+ 'Noailles (19600)' => '19151',
+ 'Noguères (64150)' => '64418',
+ 'Nomdieu (47600)' => '47197',
+ 'Nonac (16190)' => '16246',
+ 'Nonards (19120)' => '19152',
+ 'Nonaville (16120)' => '16247',
+ 'Nontron (24300)' => '24311',
+ 'Noth (23300)' => '23143',
+ 'Notre-Dame-de-Sanilhac (24660)' => '24312',
+ 'Nouaillé-Maupertuis (86340)' => '86180',
+ 'Nouhant (23170)' => '23145',
+ 'Nouic (87330)' => '87108',
+ 'Nousse (40380)' => '40205',
+ 'Nousty (64420)' => '64419',
+ 'Nouzerines (23600)' => '23146',
+ 'Nouzerolles (23360)' => '23147',
+ 'Nouziers (23350)' => '23148',
+ 'Nuaillé-d\'Aunis (17540)' => '17267',
+ 'Nuaillé-sur-Boutonne (17470)' => '17268',
+ 'Nueil-les-Aubiers (79250)' => '79195',
+ 'Nueil-sous-Faye (86200)' => '86181',
+ 'Objat (19130)' => '19153',
+ 'Oeyregave (40300)' => '40206',
+ 'Oeyreluy (40180)' => '40207',
+ 'Ogenne-Camptort (64190)' => '64420',
+ 'Ogeu-les-Bains (64680)' => '64421',
+ 'Oiron (79100)' => '79196',
+ 'Oloron-Sainte-Marie (64400)' => '64422',
+ 'Omet (33410)' => '33308',
+ 'Onard (40380)' => '40208',
+ 'Ondres (40440)' => '40209',
+ 'Onesse-Laharie (40110)' => '40210',
+ 'Oraàs (64390)' => '64423',
+ 'Oradour (16140)' => '16248',
+ 'Oradour-Fanais (16500)' => '16249',
+ 'Oradour-Saint-Genest (87210)' => '87109',
+ 'Oradour-sur-Glane (87520)' => '87110',
+ 'Oradour-sur-Vayres (87150)' => '87111',
+ 'Orches (86230)' => '86182',
+ 'Ordiarp (64130)' => '64424',
+ 'Ordonnac (33340)' => '33309',
+ 'Orègue (64120)' => '64425',
+ 'Orgedeuil (16220)' => '16250',
+ 'Orgnac-sur-Vézère (19410)' => '19154',
+ 'Origne (33113)' => '33310',
+ 'Orignolles (17210)' => '17269',
+ 'Orin (64400)' => '64426',
+ 'Oriolles (16480)' => '16251',
+ 'Orion (64390)' => '64427',
+ 'Orist (40300)' => '40211',
+ 'Orival (16210)' => '16252',
+ 'Orliac (24170)' => '24313',
+ 'Orliac-de-Bar (19390)' => '19155',
+ 'Orliaguet (24370)' => '24314',
+ 'Oroux (79390)' => '79197',
+ 'Orriule (64390)' => '64428',
+ 'Orsanco (64120)' => '64429',
+ 'Orthevielle (40300)' => '40212',
+ 'Orthez (64300)' => '64430',
+ 'Orx (40230)' => '40213',
+ 'Os-Marsillon (64150)' => '64431',
+ 'Ossages (40290)' => '40214',
+ 'Ossas-Suhare (64470)' => '64432',
+ 'Osse-en-Aspe (64490)' => '64433',
+ 'Ossenx (64190)' => '64434',
+ 'Osserain-Rivareyte (64390)' => '64435',
+ 'Ossès (64780)' => '64436',
+ 'Ostabat-Asme (64120)' => '64437',
+ 'Ouillon (64160)' => '64438',
+ 'Ousse (64320)' => '64439',
+ 'Ousse-Suzan (40110)' => '40215',
+ 'Ouzilly (86380)' => '86184',
+ 'Oyré (86220)' => '86186',
+ 'Ozenx-Montestrucq (64300)' => '64440',
+ 'Ozillac (17500)' => '17270',
+ 'Ozourt (40380)' => '40216',
+ 'Pageas (87230)' => '87112',
+ 'Pagolle (64120)' => '64441',
+ 'Paillé (17470)' => '17271',
+ 'Paillet (33550)' => '33311',
+ 'Pailloles (47440)' => '47198',
+ 'Paizay-le-Chapt (79170)' => '79198',
+ 'Paizay-le-Sec (86300)' => '86187',
+ 'Paizay-le-Tort (79500)' => '79199',
+ 'Paizay-Naudouin-Embourie (16240)' => '16253',
+ 'Palazinges (19190)' => '19156',
+ 'Palisse (19160)' => '19157',
+ 'Palluaud (16390)' => '16254',
+ 'Pamplie (79220)' => '79200',
+ 'Pamproux (79800)' => '79201',
+ 'Panazol (87350)' => '87114',
+ 'Pandrignes (19150)' => '19158',
+ 'Parbayse (64360)' => '64442',
+ 'Parcoul-Chenaud (24410)' => '24316',
+ 'Pardaillan (47120)' => '47199',
+ 'Pardies (64150)' => '64443',
+ 'Pardies-Piétat (64800)' => '64444',
+ 'Parempuyre (33290)' => '33312',
+ 'Parentis-en-Born (40160)' => '40217',
+ 'Parleboscq (40310)' => '40218',
+ 'Parranquet (47210)' => '47200',
+ 'Parsac-Rimondeix (23140)' => '23149',
+ 'Parthenay (79200)' => '79202',
+ 'Parzac (16450)' => '16255',
+ 'Pas-de-Jeu (79100)' => '79203',
+ 'Passirac (16480)' => '16256',
+ 'Pau (64000)' => '64445',
+ 'Pauillac (33250)' => '33314',
+ 'Paulhiac (47150)' => '47202',
+ 'Paulin (24590)' => '24317',
+ 'Paunat (24510)' => '24318',
+ 'Paussac-et-Saint-Vivien (24310)' => '24319',
+ 'Payré (86700)' => '86188',
+ 'Payros-Cazautets (40320)' => '40219',
+ 'Payroux (86350)' => '86189',
+ 'Pays de Belvès (24170)' => '24035',
+ 'Payzac (24270)' => '24320',
+ 'Pazayac (24120)' => '24321',
+ 'Pécorade (40320)' => '40220',
+ 'Pellegrue (33790)' => '33316',
+ 'Penne-d\'Agenais (47140)' => '47203',
+ 'Pensol (87440)' => '87115',
+ 'Péré (17700)' => '17272',
+ 'Péret-Bel-Air (19300)' => '19159',
+ 'Pérignac (16250)' => '16258',
+ 'Pérignac (17800)' => '17273',
+ 'Périgné (79170)' => '79204',
+ 'Périgny (17180)' => '17274',
+ 'Périgueux (24000)' => '24322',
+ 'Périssac (33240)' => '33317',
+ 'Pérols-sur-Vézère (19170)' => '19160',
+ 'Perpezac-le-Blanc (19310)' => '19161',
+ 'Perpezac-le-Noir (19410)' => '19162',
+ 'Perquie (40190)' => '40221',
+ 'Pers (79190)' => '79205',
+ 'Persac (86320)' => '86190',
+ 'Pessac (33600)' => '33318',
+ 'Pessac-sur-Dordogne (33890)' => '33319',
+ 'Pessines (17810)' => '17275',
+ 'Petit-Bersac (24600)' => '24323',
+ 'Petit-Palais-et-Cornemps (33570)' => '33320',
+ 'Peujard (33240)' => '33321',
+ 'Pey (40300)' => '40222',
+ 'Peyrabout (23000)' => '23150',
+ 'Peyrat-de-Bellac (87300)' => '87116',
+ 'Peyrat-la-Nonière (23130)' => '23151',
+ 'Peyrat-le-Château (87470)' => '87117',
+ 'Peyre (40700)' => '40223',
+ 'Peyrehorade (40300)' => '40224',
+ 'Peyrelevade (19290)' => '19164',
+ 'Peyrelongue-Abos (64350)' => '64446',
+ 'Peyrière (47350)' => '47204',
+ 'Peyrignac (24210)' => '24324',
+ 'Peyrilhac (87510)' => '87118',
+ 'Peyrillac-et-Millac (24370)' => '24325',
+ 'Peyrissac (19260)' => '19165',
+ 'Peyzac-le-Moustier (24620)' => '24326',
+ 'Pezuls (24510)' => '24327',
+ 'Philondenx (40320)' => '40225',
+ 'Piégut-Pluviers (24360)' => '24328',
+ 'Pierre-Buffière (87260)' => '87119',
+ 'Pierrefitte (19450)' => '19166',
+ 'Pierrefitte (23130)' => '23152',
+ 'Pierrefitte (79330)' => '79209',
+ 'Piets-Plasence-Moustrou (64410)' => '64447',
+ 'Pillac (16390)' => '16260',
+ 'Pimbo (40320)' => '40226',
+ 'Pindères (47700)' => '47205',
+ 'Pindray (86500)' => '86191',
+ 'Pinel-Hauterive (47380)' => '47206',
+ 'Pineuilh (33220)' => '33324',
+ 'Pionnat (23140)' => '23154',
+ 'Pioussay (79110)' => '79211',
+ 'Pisany (17600)' => '17278',
+ 'Pissos (40410)' => '40227',
+ 'Plaisance (24560)' => '24168',
+ 'Plaisance (86500)' => '86192',
+ 'Plassac (17240)' => '17279',
+ 'Plassac (33390)' => '33325',
+ 'Plassac-Rouffiac (16250)' => '16263',
+ 'Plassay (17250)' => '17280',
+ 'Plazac (24580)' => '24330',
+ 'Pleine-Selve (33820)' => '33326',
+ 'Pleumartin (86450)' => '86193',
+ 'Pleuville (16490)' => '16264',
+ 'Pliboux (79190)' => '79212',
+ 'Podensac (33720)' => '33327',
+ 'Poey-d\'Oloron (64400)' => '64449',
+ 'Poey-de-Lescar (64230)' => '64448',
+ 'Poitiers (86000)' => '86194',
+ 'Polignac (17210)' => '17281',
+ 'Pomarez (40360)' => '40228',
+ 'Pomerol (33500)' => '33328',
+ 'Pommiers-Moulons (17130)' => '17282',
+ 'Pompaire (79200)' => '79213',
+ 'Pompéjac (33730)' => '33329',
+ 'Pompiey (47230)' => '47207',
+ 'Pompignac (33370)' => '33330',
+ 'Pompogne (47420)' => '47208',
+ 'Pomport (24240)' => '24331',
+ 'Pomps (64370)' => '64450',
+ 'Pondaurat (33190)' => '33331',
+ 'Pons (17800)' => '17283',
+ 'Ponson-Debat-Pouts (64460)' => '64451',
+ 'Ponson-Dessus (64460)' => '64452',
+ 'Pont-du-Casse (47480)' => '47209',
+ 'Pont-l\'Abbé-d\'Arnoult (17250)' => '17284',
+ 'Pontacq (64530)' => '64453',
+ 'Pontarion (23250)' => '23155',
+ 'Pontcharraud (23260)' => '23156',
+ 'Pontenx-les-Forges (40200)' => '40229',
+ 'Ponteyraud (24410)' => '24333',
+ 'Pontiacq-Viellepinte (64460)' => '64454',
+ 'Pontonx-sur-l\'Adour (40465)' => '40230',
+ 'Pontours (24150)' => '24334',
+ 'Porchères (33660)' => '33332',
+ 'Port-d\'Envaux (17350)' => '17285',
+ 'Port-de-Lanne (40300)' => '40231',
+ 'Port-de-Piles (86220)' => '86195',
+ 'Port-des-Barques (17730)' => '17484',
+ 'Port-Sainte-Foy-et-Ponchapt (33220)' => '24335',
+ 'Port-Sainte-Marie (47130)' => '47210',
+ 'Portet (64330)' => '64455',
+ 'Portets (33640)' => '33334',
+ 'Pouançay (86120)' => '86196',
+ 'Pouant (86200)' => '86197',
+ 'Poudenas (47170)' => '47211',
+ 'Poudenx (40700)' => '40232',
+ 'Pouffonds (79500)' => '79214',
+ 'Pougne-Hérisson (79130)' => '79215',
+ 'Pouillac (17210)' => '17287',
+ 'Pouillé (86800)' => '86198',
+ 'Pouillon (40350)' => '40233',
+ 'Pouliacq (64410)' => '64456',
+ 'Poullignac (16190)' => '16267',
+ 'Poursac (16700)' => '16268',
+ 'Poursay-Garnaud (17400)' => '17288',
+ 'Poursiugues-Boucoue (64410)' => '64457',
+ 'Poussanges (23500)' => '23158',
+ 'Poussignac (47700)' => '47212',
+ 'Pouydesseaux (40120)' => '40234',
+ 'Poyanne (40380)' => '40235',
+ 'Poyartin (40380)' => '40236',
+ 'Pradines (19170)' => '19168',
+ 'Prahecq (79230)' => '79216',
+ 'Prailles (79370)' => '79217',
+ 'Pranzac (16110)' => '16269',
+ 'Prats-de-Carlux (24370)' => '24336',
+ 'Prats-du-Périgord (24550)' => '24337',
+ 'Prayssas (47360)' => '47213',
+ 'Préchac (33730)' => '33336',
+ 'Préchacq-Josbaig (64190)' => '64458',
+ 'Préchacq-les-Bains (40465)' => '40237',
+ 'Préchacq-Navarrenx (64190)' => '64459',
+ 'Précilhon (64400)' => '64460',
+ 'Préguillac (17460)' => '17289',
+ 'Preignac (33210)' => '33337',
+ 'Pressac (86460)' => '86200',
+ 'Pressignac (16150)' => '16270',
+ 'Pressignac-Vicq (24150)' => '24338',
+ 'Pressigny (79390)' => '79218',
+ 'Preyssac-d\'Excideuil (24160)' => '24339',
+ 'Priaires (79210)' => '79219',
+ 'Prignac (17160)' => '17290',
+ 'Prignac-en-Médoc (33340)' => '33338',
+ 'Prignac-et-Marcamps (33710)' => '33339',
+ 'Prigonrieux (24130)' => '24340',
+ 'Prin-Deyrançon (79210)' => '79220',
+ 'Prinçay (86420)' => '86201',
+ 'Prissé-la-Charrière (79360)' => '79078',
+ 'Proissans (24200)' => '24341',
+ 'Puch-d\'Agenais (47160)' => '47214',
+ 'Pugnac (33710)' => '33341',
+ 'Pugny (79320)' => '79222',
+ 'Puihardy (79160)' => '79223',
+ 'Puilboreau (17138)' => '17291',
+ 'Puisseguin (33570)' => '33342',
+ 'Pujo-le-Plan (40190)' => '40238',
+ 'Pujols (33350)' => '33344',
+ 'Pujols (47300)' => '47215',
+ 'Pujols-sur-Ciron (33210)' => '33343',
+ 'Puy-d\'Arnac (19120)' => '19169',
+ 'Puy-du-Lac (17380)' => '17292',
+ 'Puy-Malsignat (23130)' => '23159',
+ 'Puybarban (33190)' => '33346',
+ 'Puymiclan (47350)' => '47216',
+ 'Puymirol (47270)' => '47217',
+ 'Puymoyen (16400)' => '16271',
+ 'Puynormand (33660)' => '33347',
+ 'Puyol-Cazalet (40320)' => '40239',
+ 'Puyoô (64270)' => '64461',
+ 'Puyravault (17700)' => '17293',
+ 'Puyréaux (16230)' => '16272',
+ 'Puyrenier (24340)' => '24344',
+ 'Puyrolland (17380)' => '17294',
+ 'Puysserampion (47800)' => '47218',
+ 'Queaux (86150)' => '86203',
+ 'Queyrac (33340)' => '33348',
+ 'Queyssac (24140)' => '24345',
+ 'Queyssac-les-Vignes (19120)' => '19170',
+ 'Quinçay (86190)' => '86204',
+ 'Quinsac (24530)' => '24346',
+ 'Quinsac (33360)' => '33349',
+ 'Raix (16240)' => '16273',
+ 'Ramous (64270)' => '64462',
+ 'Rampieux (24440)' => '24347',
+ 'Rancogne (16110)' => '16274',
+ 'Rancon (87290)' => '87121',
+ 'Ranton (86200)' => '86205',
+ 'Ranville-Breuillaud (16140)' => '16275',
+ 'Raslay (86120)' => '86206',
+ 'Rauzan (33420)' => '33350',
+ 'Rayet (47210)' => '47219',
+ 'Razac-d\'Eymet (24500)' => '24348',
+ 'Razac-de-Saussignac (24240)' => '24349',
+ 'Razac-sur-l\'Isle (24430)' => '24350',
+ 'Razès (87640)' => '87122',
+ 'Razimet (47160)' => '47220',
+ 'Réaup-Lisse (47170)' => '47221',
+ 'Réaux sur Trèfle (17500)' => '17295',
+ 'Rébénacq (64260)' => '64463',
+ 'Reffannes (79420)' => '79225',
+ 'Reignac (16360)' => '16276',
+ 'Reignac (33860)' => '33351',
+ 'Rempnat (87120)' => '87123',
+ 'Renung (40270)' => '40240',
+ 'Réparsac (16200)' => '16277',
+ 'Rétaud (17460)' => '17296',
+ 'Reterre (23110)' => '23160',
+ 'Retjons (40120)' => '40164',
+ 'Reygade (19430)' => '19171',
+ 'Ribagnac (24240)' => '24351',
+ 'Ribarrouy (64330)' => '64464',
+ 'Ribérac (24600)' => '24352',
+ 'Rilhac-Lastours (87800)' => '87124',
+ 'Rilhac-Rancon (87570)' => '87125',
+ 'Rilhac-Treignac (19260)' => '19172',
+ 'Rilhac-Xaintrie (19220)' => '19173',
+ 'Rimbez-et-Baudiets (40310)' => '40242',
+ 'Rimons (33580)' => '33353',
+ 'Riocaud (33220)' => '33354',
+ 'Rion-des-Landes (40370)' => '40243',
+ 'Rions (33410)' => '33355',
+ 'Rioux (17460)' => '17298',
+ 'Rioux-Martin (16210)' => '16279',
+ 'Riupeyrous (64160)' => '64465',
+ 'Rivedoux-Plage (17940)' => '17297',
+ 'Rivehaute (64190)' => '64466',
+ 'Rives (47210)' => '47223',
+ 'Rivière-Saas-et-Gourby (40180)' => '40244',
+ 'Rivières (16110)' => '16280',
+ 'Roaillan (33210)' => '33357',
+ 'Roche-le-Peyroux (19160)' => '19175',
+ 'Rochechouart (87600)' => '87126',
+ 'Rochefort (17300)' => '17299',
+ 'Roches (23270)' => '23162',
+ 'Roches-Prémarie-Andillé (86340)' => '86209',
+ 'Roiffé (86120)' => '86210',
+ 'Rom (79120)' => '79230',
+ 'Romagne (33760)' => '33358',
+ 'Romagne (86700)' => '86211',
+ 'Romans (79260)' => '79231',
+ 'Romazières (17510)' => '17301',
+ 'Romegoux (17250)' => '17302',
+ 'Romestaing (47250)' => '47224',
+ 'Ronsenac (16320)' => '16283',
+ 'Rontignon (64110)' => '64467',
+ 'Roquebrune (33580)' => '33359',
+ 'Roquefort (40120)' => '40245',
+ 'Roquefort (47310)' => '47225',
+ 'Roquiague (64130)' => '64468',
+ 'Rosiers-d\'Égletons (19300)' => '19176',
+ 'Rosiers-de-Juillac (19350)' => '19177',
+ 'Rouffiac (16210)' => '16284',
+ 'Rouffiac (17800)' => '17304',
+ 'Rouffignac (17130)' => '17305',
+ 'Rouffignac-de-Sigoulès (24240)' => '24357',
+ 'Rouffignac-Saint-Cernin-de-Reilhac (24580)' => '24356',
+ 'Rougnac (16320)' => '16285',
+ 'Rougnat (23700)' => '23164',
+ 'Rouillac (16170)' => '16286',
+ 'Rouillé (86480)' => '86213',
+ 'Roullet-Saint-Estèphe (16440)' => '16287',
+ 'Roumagne (47800)' => '47226',
+ 'Roumazières-Loubert (16270)' => '16192',
+ 'Roussac (87140)' => '87128',
+ 'Roussines (16310)' => '16289',
+ 'Rouzède (16220)' => '16290',
+ 'Royan (17200)' => '17306',
+ 'Royère-de-Vassivière (23460)' => '23165',
+ 'Royères (87400)' => '87129',
+ 'Roziers-Saint-Georges (87130)' => '87130',
+ 'Ruch (33350)' => '33361',
+ 'Rudeau-Ladosse (24340)' => '24221',
+ 'Ruelle-sur-Touvre (16600)' => '16291',
+ 'Ruffec (16700)' => '16292',
+ 'Ruffiac (47700)' => '47227',
+ 'Sablonceaux (17600)' => '17307',
+ 'Sablons (33910)' => '33362',
+ 'Sabres (40630)' => '40246',
+ 'Sadillac (24500)' => '24359',
+ 'Sadirac (33670)' => '33363',
+ 'Sadroc (19270)' => '19178',
+ 'Sagelat (24170)' => '24360',
+ 'Sagnat (23800)' => '23166',
+ 'Saillac (19500)' => '19179',
+ 'Saillans (33141)' => '33364',
+ 'Saillat-sur-Vienne (87720)' => '87131',
+ 'Saint Aulaye-Puymangou (24410)' => '24376',
+ 'Saint Maurice Étusson (79150)' => '79280',
+ 'Saint-Abit (64800)' => '64469',
+ 'Saint-Adjutory (16310)' => '16293',
+ 'Saint-Agnant (17620)' => '17308',
+ 'Saint-Agnant-de-Versillat (23300)' => '23177',
+ 'Saint-Agnant-près-Crocq (23260)' => '23178',
+ 'Saint-Agne (24520)' => '24361',
+ 'Saint-Agnet (40800)' => '40247',
+ 'Saint-Aignan (33126)' => '33365',
+ 'Saint-Aigulin (17360)' => '17309',
+ 'Saint-Alpinien (23200)' => '23179',
+ 'Saint-Amand (23200)' => '23180',
+ 'Saint-Amand-de-Coly (24290)' => '24364',
+ 'Saint-Amand-de-Vergt (24380)' => '24365',
+ 'Saint-Amand-Jartoudeix (23400)' => '23181',
+ 'Saint-Amand-le-Petit (87120)' => '87132',
+ 'Saint-Amand-Magnazeix (87290)' => '87133',
+ 'Saint-Amand-sur-Sèvre (79700)' => '79235',
+ 'Saint-Amant-de-Boixe (16330)' => '16295',
+ 'Saint-Amant-de-Bonnieure (16230)' => '16296',
+ 'Saint-Amant-de-Montmoreau (16190)' => '16294',
+ 'Saint-Amant-de-Nouère (16170)' => '16298',
+ 'Saint-André-d\'Allas (24200)' => '24366',
+ 'Saint-André-de-Cubzac (33240)' => '33366',
+ 'Saint-André-de-Double (24190)' => '24367',
+ 'Saint-André-de-Lidon (17260)' => '17310',
+ 'Saint-André-de-Seignanx (40390)' => '40248',
+ 'Saint-André-du-Bois (33490)' => '33367',
+ 'Saint-André-et-Appelles (33220)' => '33369',
+ 'Saint-André-sur-Sèvre (79380)' => '79236',
+ 'Saint-Androny (33390)' => '33370',
+ 'Saint-Angeau (16230)' => '16300',
+ 'Saint-Angel (19200)' => '19180',
+ 'Saint-Antoine-Cumond (24410)' => '24368',
+ 'Saint-Antoine-d\'Auberoche (24330)' => '24369',
+ 'Saint-Antoine-de-Breuilh (24230)' => '24370',
+ 'Saint-Antoine-de-Ficalba (47340)' => '47228',
+ 'Saint-Antoine-du-Queyret (33790)' => '33372',
+ 'Saint-Antoine-sur-l\'Isle (33660)' => '33373',
+ 'Saint-Aquilin (24110)' => '24371',
+ 'Saint-Armou (64160)' => '64470',
+ 'Saint-Astier (24110)' => '24372',
+ 'Saint-Astier (47120)' => '47229',
+ 'Saint-Aubin (40250)' => '40249',
+ 'Saint-Aubin (47150)' => '47230',
+ 'Saint-Aubin-de-Blaye (33820)' => '33374',
+ 'Saint-Aubin-de-Branne (33420)' => '33375',
+ 'Saint-Aubin-de-Cadelech (24500)' => '24373',
+ 'Saint-Aubin-de-Lanquais (24560)' => '24374',
+ 'Saint-Aubin-de-Médoc (33160)' => '33376',
+ 'Saint-Aubin-de-Nabirat (24250)' => '24375',
+ 'Saint-Aubin-du-Plain (79300)' => '79238',
+ 'Saint-Aubin-le-Cloud (79450)' => '79239',
+ 'Saint-Augustin (17570)' => '17311',
+ 'Saint-Augustin (19390)' => '19181',
+ 'Saint-Aulaire (19130)' => '19182',
+ 'Saint-Aulais-la-Chapelle (16300)' => '16301',
+ 'Saint-Auvent (87310)' => '87135',
+ 'Saint-Avit (16210)' => '16302',
+ 'Saint-Avit (40090)' => '40250',
+ 'Saint-Avit (47350)' => '47231',
+ 'Saint-Avit-de-Soulège (33220)' => '33377',
+ 'Saint-Avit-de-Tardes (23200)' => '23182',
+ 'Saint-Avit-de-Vialard (24260)' => '24377',
+ 'Saint-Avit-le-Pauvre (23480)' => '23183',
+ 'Saint-Avit-Rivière (24540)' => '24378',
+ 'Saint-Avit-Saint-Nazaire (33220)' => '33378',
+ 'Saint-Avit-Sénieur (24440)' => '24379',
+ 'Saint-Barbant (87330)' => '87136',
+ 'Saint-Bard (23260)' => '23184',
+ 'Saint-Barthélemy (40390)' => '40251',
+ 'Saint-Barthélemy-d\'Agenais (47350)' => '47232',
+ 'Saint-Barthélemy-de-Bellegarde (24700)' => '24380',
+ 'Saint-Barthélemy-de-Bussière (24360)' => '24381',
+ 'Saint-Bazile (87150)' => '87137',
+ 'Saint-Bazile-de-la-Roche (19320)' => '19183',
+ 'Saint-Bazile-de-Meyssac (19500)' => '19184',
+ 'Saint-Benoît (86280)' => '86214',
+ 'Saint-Boès (64300)' => '64471',
+ 'Saint-Bonnet (16300)' => '16303',
+ 'Saint-Bonnet-Avalouze (19150)' => '19185',
+ 'Saint-Bonnet-Briance (87260)' => '87138',
+ 'Saint-Bonnet-de-Bellac (87300)' => '87139',
+ 'Saint-Bonnet-Elvert (19380)' => '19186',
+ 'Saint-Bonnet-l\'Enfantier (19410)' => '19188',
+ 'Saint-Bonnet-la-Rivière (19130)' => '19187',
+ 'Saint-Bonnet-les-Tours-de-Merle (19430)' => '19189',
+ 'Saint-Bonnet-près-Bort (19200)' => '19190',
+ 'Saint-Bonnet-sur-Gironde (17150)' => '17312',
+ 'Saint-Brice (16100)' => '16304',
+ 'Saint-Brice (33540)' => '33379',
+ 'Saint-Brice-sur-Vienne (87200)' => '87140',
+ 'Saint-Bris-des-Bois (17770)' => '17313',
+ 'Saint-Caprais-de-Blaye (33820)' => '33380',
+ 'Saint-Caprais-de-Bordeaux (33880)' => '33381',
+ 'Saint-Caprais-de-Lerm (47270)' => '47234',
+ 'Saint-Capraise-d\'Eymet (24500)' => '24383',
+ 'Saint-Capraise-de-Lalinde (24150)' => '24382',
+ 'Saint-Cassien (24540)' => '24384',
+ 'Saint-Castin (64160)' => '64472',
+ 'Saint-Cernin-de-l\'Herm (24550)' => '24386',
+ 'Saint-Cernin-de-Labarde (24560)' => '24385',
+ 'Saint-Cernin-de-Larche (19600)' => '19191',
+ 'Saint-Césaire (17770)' => '17314',
+ 'Saint-Chabrais (23130)' => '23185',
+ 'Saint-Chamant (19380)' => '19192',
+ 'Saint-Chamassy (24260)' => '24388',
+ 'Saint-Christoly-de-Blaye (33920)' => '33382',
+ 'Saint-Christoly-Médoc (33340)' => '33383',
+ 'Saint-Christophe (16420)' => '16306',
+ 'Saint-Christophe (17220)' => '17315',
+ 'Saint-Christophe (23000)' => '23186',
+ 'Saint-Christophe (86230)' => '86217',
+ 'Saint-Christophe-de-Double (33230)' => '33385',
+ 'Saint-Christophe-des-Bardes (33330)' => '33384',
+ 'Saint-Christophe-sur-Roc (79220)' => '79241',
+ 'Saint-Cibard (33570)' => '33386',
+ 'Saint-Ciers-Champagne (17520)' => '17316',
+ 'Saint-Ciers-d\'Abzac (33910)' => '33387',
+ 'Saint-Ciers-de-Canesse (33710)' => '33388',
+ 'Saint-Ciers-du-Taillon (17240)' => '17317',
+ 'Saint-Ciers-sur-Bonnieure (16230)' => '16307',
+ 'Saint-Ciers-sur-Gironde (33820)' => '33389',
+ 'Saint-Cirgues-la-Loutre (19220)' => '19193',
+ 'Saint-Cirq (24260)' => '24389',
+ 'Saint-Clair (86330)' => '86218',
+ 'Saint-Claud (16450)' => '16308',
+ 'Saint-Clément (19700)' => '19194',
+ 'Saint-Clément-des-Baleines (17590)' => '17318',
+ 'Saint-Colomb-de-Lauzun (47410)' => '47235',
+ 'Saint-Côme (33430)' => '33391',
+ 'Saint-Coutant (16350)' => '16310',
+ 'Saint-Coutant (79120)' => '79243',
+ 'Saint-Coutant-le-Grand (17430)' => '17320',
+ 'Saint-Crépin (17380)' => '17321',
+ 'Saint-Crépin-d\'Auberoche (24330)' => '24390',
+ 'Saint-Crépin-de-Richemont (24310)' => '24391',
+ 'Saint-Crépin-et-Carlucet (24590)' => '24392',
+ 'Saint-Cricq-Chalosse (40700)' => '40253',
+ 'Saint-Cricq-du-Gave (40300)' => '40254',
+ 'Saint-Cricq-Villeneuve (40190)' => '40255',
+ 'Saint-Cybardeaux (16170)' => '16312',
+ 'Saint-Cybranet (24250)' => '24395',
+ 'Saint-Cyprien (19130)' => '19195',
+ 'Saint-Cyprien (24220)' => '24396',
+ 'Saint-Cyr (86130)' => '86219',
+ 'Saint-Cyr (87310)' => '87141',
+ 'Saint-Cyr-du-Doret (17170)' => '17322',
+ 'Saint-Cyr-la-Lande (79100)' => '79244',
+ 'Saint-Cyr-la-Roche (19130)' => '19196',
+ 'Saint-Cyr-les-Champagnes (24270)' => '24397',
+ 'Saint-Denis-d\'Oléron (17650)' => '17323',
+ 'Saint-Denis-de-Pile (33910)' => '33393',
+ 'Saint-Denis-des-Murs (87400)' => '87142',
+ 'Saint-Dizant-du-Bois (17150)' => '17324',
+ 'Saint-Dizant-du-Gua (17240)' => '17325',
+ 'Saint-Dizier-la-Tour (23130)' => '23187',
+ 'Saint-Dizier-les-Domaines (23270)' => '23188',
+ 'Saint-Dizier-Leyrenne (23400)' => '23189',
+ 'Saint-Domet (23190)' => '23190',
+ 'Saint-Dos (64270)' => '64474',
+ 'Saint-Éloi (23000)' => '23191',
+ 'Saint-Éloy-les-Tuileries (19210)' => '19198',
+ 'Saint-Émilion (33330)' => '33394',
+ 'Saint-Esteben (64640)' => '64476',
+ 'Saint-Estèphe (24360)' => '24398',
+ 'Saint-Estèphe (33180)' => '33395',
+ 'Saint-Étienne-aux-Clos (19200)' => '19199',
+ 'Saint-Étienne-d\'Orthe (40300)' => '40256',
+ 'Saint-Étienne-de-Baïgorry (64430)' => '64477',
+ 'Saint-Étienne-de-Fougères (47380)' => '47239',
+ 'Saint-Étienne-de-Fursac (23290)' => '23192',
+ 'Saint-Étienne-de-Lisse (33330)' => '33396',
+ 'Saint-Étienne-de-Puycorbier (24400)' => '24399',
+ 'Saint-Étienne-de-Villeréal (47210)' => '47240',
+ 'Saint-Étienne-la-Cigogne (79360)' => '79247',
+ 'Saint-Étienne-la-Geneste (19160)' => '19200',
+ 'Saint-Eugène (17520)' => '17326',
+ 'Saint-Eutrope (16190)' => '16314',
+ 'Saint-Eutrope-de-Born (47210)' => '47241',
+ 'Saint-Exupéry (33190)' => '33398',
+ 'Saint-Exupéry-les-Roches (19200)' => '19201',
+ 'Saint-Faust (64110)' => '64478',
+ 'Saint-Félix (16480)' => '16315',
+ 'Saint-Félix (17330)' => '17327',
+ 'Saint-Félix-de-Bourdeilles (24340)' => '24403',
+ 'Saint-Félix-de-Foncaude (33540)' => '33399',
+ 'Saint-Félix-de-Reillac-et-Mortemart (24260)' => '24404',
+ 'Saint-Félix-de-Villadeix (24510)' => '24405',
+ 'Saint-Ferme (33580)' => '33400',
+ 'Saint-Fiel (23000)' => '23195',
+ 'Saint-Fort-sur-Gironde (17240)' => '17328',
+ 'Saint-Fort-sur-le-Né (16130)' => '16316',
+ 'Saint-Fraigne (16140)' => '16317',
+ 'Saint-Fréjoux (19200)' => '19204',
+ 'Saint-Frion (23500)' => '23196',
+ 'Saint-Front (16460)' => '16318',
+ 'Saint-Front-d\'Alemps (24460)' => '24408',
+ 'Saint-Front-de-Pradoux (24400)' => '24409',
+ 'Saint-Front-la-Rivière (24300)' => '24410',
+ 'Saint-Front-sur-Lémance (47500)' => '47242',
+ 'Saint-Front-sur-Nizonne (24300)' => '24411',
+ 'Saint-Froult (17780)' => '17329',
+ 'Saint-Gaudent (86400)' => '86220',
+ 'Saint-Gein (40190)' => '40259',
+ 'Saint-Gelais (79410)' => '79249',
+ 'Saint-Génard (79500)' => '79251',
+ 'Saint-Gence (87510)' => '87143',
+ 'Saint-Généroux (79600)' => '79252',
+ 'Saint-Genès-de-Blaye (33390)' => '33405',
+ 'Saint-Genès-de-Castillon (33350)' => '33406',
+ 'Saint-Genès-de-Fronsac (33240)' => '33407',
+ 'Saint-Genès-de-Lombaud (33670)' => '33408',
+ 'Saint-Genest-d\'Ambière (86140)' => '86221',
+ 'Saint-Genest-sur-Roselle (87260)' => '87144',
+ 'Saint-Geniès (24590)' => '24412',
+ 'Saint-Geniez-ô-Merle (19220)' => '19205',
+ 'Saint-Genis-d\'Hiersac (16570)' => '16320',
+ 'Saint-Genis-de-Saintonge (17240)' => '17331',
+ 'Saint-Genis-du-Bois (33760)' => '33409',
+ 'Saint-Georges (16700)' => '16321',
+ 'Saint-Georges (47370)' => '47328',
+ 'Saint-Georges-Antignac (17240)' => '17332',
+ 'Saint-Georges-Blancaneix (24130)' => '24413',
+ 'Saint-Georges-d\'Oléron (17190)' => '17337',
+ 'Saint-Georges-de-Didonne (17110)' => '17333',
+ 'Saint-Georges-de-Longuepierre (17470)' => '17334',
+ 'Saint-Georges-de-Montclard (24140)' => '24414',
+ 'Saint-Georges-de-Noisné (79400)' => '79253',
+ 'Saint-Georges-de-Rex (79210)' => '79254',
+ 'Saint-Georges-des-Agoûts (17150)' => '17335',
+ 'Saint-Georges-des-Coteaux (17810)' => '17336',
+ 'Saint-Georges-du-Bois (17700)' => '17338',
+ 'Saint-Georges-la-Pouge (23250)' => '23197',
+ 'Saint-Georges-lès-Baillargeaux (86130)' => '86222',
+ 'Saint-Georges-les-Landes (87160)' => '87145',
+ 'Saint-Georges-Nigremont (23500)' => '23198',
+ 'Saint-Geours-d\'Auribat (40380)' => '40260',
+ 'Saint-Geours-de-Maremne (40230)' => '40261',
+ 'Saint-Géraud (47120)' => '47245',
+ 'Saint-Géraud-de-Corps (24700)' => '24415',
+ 'Saint-Germain (86310)' => '86223',
+ 'Saint-Germain-Beaupré (23160)' => '23199',
+ 'Saint-Germain-d\'Esteuil (33340)' => '33412',
+ 'Saint-Germain-de-Belvès (24170)' => '24416',
+ 'Saint-Germain-de-Grave (33490)' => '33411',
+ 'Saint-Germain-de-la-Rivière (33240)' => '33414',
+ 'Saint-Germain-de-Longue-Chaume (79200)' => '79255',
+ 'Saint-Germain-de-Lusignan (17500)' => '17339',
+ 'Saint-Germain-de-Marencennes (17700)' => '17340',
+ 'Saint-Germain-de-Montbron (16380)' => '16323',
+ 'Saint-Germain-de-Vibrac (17500)' => '17341',
+ 'Saint-Germain-des-Prés (24160)' => '24417',
+ 'Saint-Germain-du-Puch (33750)' => '33413',
+ 'Saint-Germain-du-Salembre (24190)' => '24418',
+ 'Saint-Germain-du-Seudre (17240)' => '17342',
+ 'Saint-Germain-et-Mons (24520)' => '24419',
+ 'Saint-Germain-Lavolps (19290)' => '19206',
+ 'Saint-Germain-les-Belles (87380)' => '87146',
+ 'Saint-Germain-les-Vergnes (19330)' => '19207',
+ 'Saint-Germier (79340)' => '79256',
+ 'Saint-Gervais (33240)' => '33415',
+ 'Saint-Gervais-les-Trois-Clochers (86230)' => '86224',
+ 'Saint-Géry (24400)' => '24420',
+ 'Saint-Geyrac (24330)' => '24421',
+ 'Saint-Gilles-les-Forêts (87130)' => '87147',
+ 'Saint-Girons-d\'Aiguevives (33920)' => '33416',
+ 'Saint-Girons-en-Béarn (64300)' => '64479',
+ 'Saint-Gladie-Arrive-Munein (64390)' => '64480',
+ 'Saint-Goin (64400)' => '64481',
+ 'Saint-Gor (40120)' => '40262',
+ 'Saint-Gourson (16700)' => '16325',
+ 'Saint-Goussaud (23430)' => '23200',
+ 'Saint-Grégoire-d\'Ardennes (17240)' => '17343',
+ 'Saint-Groux (16230)' => '16326',
+ 'Saint-Hilaire-Bonneval (87260)' => '87148',
+ 'Saint-Hilaire-d\'Estissac (24140)' => '24422',
+ 'Saint-Hilaire-de-la-Noaille (33190)' => '33418',
+ 'Saint-Hilaire-de-Lusignan (47450)' => '47246',
+ 'Saint-Hilaire-de-Villefranche (17770)' => '17344',
+ 'Saint-Hilaire-du-Bois (17500)' => '17345',
+ 'Saint-Hilaire-du-Bois (33540)' => '33419',
+ 'Saint-Hilaire-Foissac (19550)' => '19208',
+ 'Saint-Hilaire-la-Palud (79210)' => '79257',
+ 'Saint-Hilaire-la-Plaine (23150)' => '23201',
+ 'Saint-Hilaire-la-Treille (87190)' => '87149',
+ 'Saint-Hilaire-le-Château (23250)' => '23202',
+ 'Saint-Hilaire-les-Courbes (19170)' => '19209',
+ 'Saint-Hilaire-les-Places (87800)' => '87150',
+ 'Saint-Hilaire-Luc (19160)' => '19210',
+ 'Saint-Hilaire-Peyroux (19560)' => '19211',
+ 'Saint-Hilaire-Taurieux (19400)' => '19212',
+ 'Saint-Hippolyte (17430)' => '17346',
+ 'Saint-Hippolyte (33330)' => '33420',
+ 'Saint-Jacques-de-Thouars (79100)' => '79258',
+ 'Saint-Jal (19700)' => '19213',
+ 'Saint-Jammes (64160)' => '64482',
+ 'Saint-Jean-d\'Angély (17400)' => '17347',
+ 'Saint-Jean-d\'Angle (17620)' => '17348',
+ 'Saint-Jean-d\'Ataux (24190)' => '24424',
+ 'Saint-Jean-d\'Estissac (24140)' => '24426',
+ 'Saint-Jean-d\'Eyraud (24140)' => '24427',
+ 'Saint-Jean-d\'Illac (33127)' => '33422',
+ 'Saint-Jean-de-Blaignac (33420)' => '33421',
+ 'Saint-Jean-de-Côle (24800)' => '24425',
+ 'Saint-Jean-de-Duras (47120)' => '47247',
+ 'Saint-Jean-de-Lier (40380)' => '40263',
+ 'Saint-Jean-de-Liversay (17170)' => '17349',
+ 'Saint-Jean-de-Luz (64500)' => '64483',
+ 'Saint-Jean-de-Marsacq (40230)' => '40264',
+ 'Saint-Jean-de-Sauves (86330)' => '86225',
+ 'Saint-Jean-de-Thouars (79100)' => '79259',
+ 'Saint-Jean-de-Thurac (47270)' => '47248',
+ 'Saint-Jean-le-Vieux (64220)' => '64484',
+ 'Saint-Jean-Ligoure (87260)' => '87151',
+ 'Saint-Jean-Pied-de-Port (64220)' => '64485',
+ 'Saint-Jean-Poudge (64330)' => '64486',
+ 'Saint-Jory-de-Chalais (24800)' => '24428',
+ 'Saint-Jory-las-Bloux (24160)' => '24429',
+ 'Saint-Jouin-de-Marnes (79600)' => '79260',
+ 'Saint-Jouin-de-Milly (79380)' => '79261',
+ 'Saint-Jouvent (87510)' => '87152',
+ 'Saint-Julien-aux-Bois (19220)' => '19214',
+ 'Saint-Julien-Beychevelle (33250)' => '33423',
+ 'Saint-Julien-d\'Armagnac (40240)' => '40265',
+ 'Saint-Julien-d\'Eymet (24500)' => '24433',
+ 'Saint-Julien-de-Crempse (24140)' => '24431',
+ 'Saint-Julien-de-l\'Escap (17400)' => '17350',
+ 'Saint-Julien-de-Lampon (24370)' => '24432',
+ 'Saint-Julien-en-Born (40170)' => '40266',
+ 'Saint-Julien-l\'Ars (86800)' => '86226',
+ 'Saint-Julien-la-Genête (23110)' => '23203',
+ 'Saint-Julien-le-Châtel (23130)' => '23204',
+ 'Saint-Julien-le-Pèlerin (19430)' => '19215',
+ 'Saint-Julien-le-Petit (87460)' => '87153',
+ 'Saint-Julien-le-Vendômois (19210)' => '19216',
+ 'Saint-Julien-Maumont (19500)' => '19217',
+ 'Saint-Julien-près-Bort (19110)' => '19218',
+ 'Saint-Junien (87200)' => '87154',
+ 'Saint-Junien-la-Bregère (23400)' => '23205',
+ 'Saint-Junien-les-Combes (87300)' => '87155',
+ 'Saint-Just (24320)' => '24434',
+ 'Saint-Just-Ibarre (64120)' => '64487',
+ 'Saint-Just-le-Martel (87590)' => '87156',
+ 'Saint-Just-Luzac (17320)' => '17351',
+ 'Saint-Justin (40240)' => '40267',
+ 'Saint-Laon (86200)' => '86227',
+ 'Saint-Laurent (23000)' => '23206',
+ 'Saint-Laurent (47130)' => '47249',
+ 'Saint-Laurent-Bretagne (64160)' => '64488',
+ 'Saint-Laurent-d\'Arce (33240)' => '33425',
+ 'Saint-Laurent-de-Belzagot (16190)' => '16328',
+ 'Saint-Laurent-de-Céris (16450)' => '16329',
+ 'Saint-Laurent-de-Cognac (16100)' => '16330',
+ 'Saint-Laurent-de-Gosse (40390)' => '40268',
+ 'Saint-Laurent-de-Jourdes (86410)' => '86228',
+ 'Saint-Laurent-de-la-Barrière (17380)' => '17352',
+ 'Saint-Laurent-de-la-Prée (17450)' => '17353',
+ 'Saint-Laurent-des-Combes (16480)' => '16331',
+ 'Saint-Laurent-des-Combes (33330)' => '33426',
+ 'Saint-Laurent-des-Hommes (24400)' => '24436',
+ 'Saint-Laurent-des-Vignes (24100)' => '24437',
+ 'Saint-Laurent-du-Bois (33540)' => '33427',
+ 'Saint-Laurent-du-Plan (33190)' => '33428',
+ 'Saint-Laurent-la-Vallée (24170)' => '24438',
+ 'Saint-Laurent-les-Églises (87240)' => '87157',
+ 'Saint-Laurent-Médoc (33112)' => '33424',
+ 'Saint-Laurent-sur-Gorre (87310)' => '87158',
+ 'Saint-Laurs (79160)' => '79263',
+ 'Saint-Léger (16250)' => '16332',
+ 'Saint-Léger (17800)' => '17354',
+ 'Saint-Léger (47160)' => '47250',
+ 'Saint-Léger-Bridereix (23300)' => '23207',
+ 'Saint-Léger-de-Balson (33113)' => '33429',
+ 'Saint-Léger-de-la-Martinière (79500)' => '79264',
+ 'Saint-Léger-de-Montbrillais (86120)' => '86229',
+ 'Saint-Léger-de-Montbrun (79100)' => '79265',
+ 'Saint-Léger-la-Montagne (87340)' => '87159',
+ 'Saint-Léger-le-Guérétois (23000)' => '23208',
+ 'Saint-Léger-Magnazeix (87190)' => '87160',
+ 'Saint-Léomer (86290)' => '86230',
+ 'Saint-Léon (33670)' => '33431',
+ 'Saint-Léon (47160)' => '47251',
+ 'Saint-Léon-d\'Issigeac (24560)' => '24441',
+ 'Saint-Léon-sur-l\'Isle (24110)' => '24442',
+ 'Saint-Léon-sur-Vézère (24290)' => '24443',
+ 'Saint-Léonard-de-Noblat (87400)' => '87161',
+ 'Saint-Lin (79420)' => '79267',
+ 'Saint-Lon-les-Mines (40300)' => '40269',
+ 'Saint-Loubert (33210)' => '33432',
+ 'Saint-Loubès (33450)' => '33433',
+ 'Saint-Loubouer (40320)' => '40270',
+ 'Saint-Louis-de-Montferrand (33440)' => '33434',
+ 'Saint-Louis-en-l\'Isle (24400)' => '24444',
+ 'Saint-Loup (17380)' => '17356',
+ 'Saint-Loup (23130)' => '23209',
+ 'Saint-Loup-Lamairé (79600)' => '79268',
+ 'Saint-Macaire (33490)' => '33435',
+ 'Saint-Macoux (86400)' => '86231',
+ 'Saint-Magne (33125)' => '33436',
+ 'Saint-Magne-de-Castillon (33350)' => '33437',
+ 'Saint-Maigrin (17520)' => '17357',
+ 'Saint-Maime-de-Péreyrol (24380)' => '24459',
+ 'Saint-Maixant (23200)' => '23210',
+ 'Saint-Maixant (33490)' => '33438',
+ 'Saint-Maixent-de-Beugné (79160)' => '79269',
+ 'Saint-Maixent-l\'École (79400)' => '79270',
+ 'Saint-Mandé-sur-Brédoire (17470)' => '17358',
+ 'Saint-Marc-à-Frongier (23200)' => '23211',
+ 'Saint-Marc-à-Loubaud (23460)' => '23212',
+ 'Saint-Marc-la-Lande (79310)' => '79271',
+ 'Saint-Marcel-du-Périgord (24510)' => '24445',
+ 'Saint-Marcory (24540)' => '24446',
+ 'Saint-Mard (17700)' => '17359',
+ 'Saint-Marien (23600)' => '23213',
+ 'Saint-Mariens (33620)' => '33439',
+ 'Saint-Martial (16190)' => '16334',
+ 'Saint-Martial (17330)' => '17361',
+ 'Saint-Martial (33490)' => '33440',
+ 'Saint-Martial-d\'Albarède (24160)' => '24448',
+ 'Saint-Martial-d\'Artenset (24700)' => '24449',
+ 'Saint-Martial-de-Gimel (19150)' => '19220',
+ 'Saint-Martial-de-Mirambeau (17150)' => '17362',
+ 'Saint-Martial-de-Nabirat (24250)' => '24450',
+ 'Saint-Martial-de-Valette (24300)' => '24451',
+ 'Saint-Martial-de-Vitaterne (17500)' => '17363',
+ 'Saint-Martial-Entraygues (19400)' => '19221',
+ 'Saint-Martial-le-Mont (23150)' => '23214',
+ 'Saint-Martial-le-Vieux (23100)' => '23215',
+ 'Saint-Martial-sur-Isop (87330)' => '87163',
+ 'Saint-Martial-sur-Né (17520)' => '17364',
+ 'Saint-Martial-Viveyrol (24320)' => '24452',
+ 'Saint-Martin-Château (23460)' => '23216',
+ 'Saint-Martin-Curton (47700)' => '47254',
+ 'Saint-Martin-d\'Arberoue (64640)' => '64489',
+ 'Saint-Martin-d\'Arrossa (64780)' => '64490',
+ 'Saint-Martin-d\'Ary (17270)' => '17365',
+ 'Saint-Martin-d\'Oney (40090)' => '40274',
+ 'Saint-Martin-de-Beauville (47270)' => '47255',
+ 'Saint-Martin-de-Bernegoue (79230)' => '79273',
+ 'Saint-Martin-de-Coux (17360)' => '17366',
+ 'Saint-Martin-de-Fressengeas (24800)' => '24453',
+ 'Saint-Martin-de-Gurson (24610)' => '24454',
+ 'Saint-Martin-de-Hinx (40390)' => '40272',
+ 'Saint-Martin-de-Juillers (17400)' => '17367',
+ 'Saint-Martin-de-Jussac (87200)' => '87164',
+ 'Saint-Martin-de-Laye (33910)' => '33442',
+ 'Saint-Martin-de-Lerm (33540)' => '33443',
+ 'Saint-Martin-de-Mâcon (79100)' => '79274',
+ 'Saint-Martin-de-Ré (17410)' => '17369',
+ 'Saint-Martin-de-Ribérac (24600)' => '24455',
+ 'Saint-Martin-de-Saint-Maixent (79400)' => '79276',
+ 'Saint-Martin-de-Sanzay (79290)' => '79277',
+ 'Saint-Martin-de-Seignanx (40390)' => '40273',
+ 'Saint-Martin-de-Sescas (33490)' => '33444',
+ 'Saint-Martin-de-Villeréal (47210)' => '47256',
+ 'Saint-Martin-des-Combes (24140)' => '24456',
+ 'Saint-Martin-du-Bois (33910)' => '33445',
+ 'Saint-Martin-du-Clocher (16700)' => '16335',
+ 'Saint-Martin-du-Fouilloux (79420)' => '79278',
+ 'Saint-Martin-du-Puy (33540)' => '33446',
+ 'Saint-Martin-l\'Ars (86350)' => '86234',
+ 'Saint-Martin-l\'Astier (24400)' => '24457',
+ 'Saint-Martin-la-Méanne (19320)' => '19222',
+ 'Saint-Martin-Lacaussade (33390)' => '33441',
+ 'Saint-Martin-le-Mault (87360)' => '87165',
+ 'Saint-Martin-le-Pin (24300)' => '24458',
+ 'Saint-Martin-le-Vieux (87700)' => '87166',
+ 'Saint-Martin-lès-Melle (79500)' => '79279',
+ 'Saint-Martin-Petit (47180)' => '47257',
+ 'Saint-Martin-Sainte-Catherine (23430)' => '23217',
+ 'Saint-Martin-Sepert (19210)' => '19223',
+ 'Saint-Martin-Terressus (87400)' => '87167',
+ 'Saint-Mary (16260)' => '16336',
+ 'Saint-Mathieu (87440)' => '87168',
+ 'Saint-Maurice-de-Lestapel (47290)' => '47259',
+ 'Saint-Maurice-des-Lions (16500)' => '16337',
+ 'Saint-Maurice-la-Clouère (86160)' => '86235',
+ 'Saint-Maurice-la-Souterraine (23300)' => '23219',
+ 'Saint-Maurice-les-Brousses (87800)' => '87169',
+ 'Saint-Maurice-près-Crocq (23260)' => '23218',
+ 'Saint-Maurice-sur-Adour (40270)' => '40275',
+ 'Saint-Maurin (47270)' => '47260',
+ 'Saint-Maxire (79410)' => '79281',
+ 'Saint-Méard (87130)' => '87170',
+ 'Saint-Méard-de-Drône (24600)' => '24460',
+ 'Saint-Méard-de-Gurçon (24610)' => '24461',
+ 'Saint-Médard (16300)' => '16338',
+ 'Saint-Médard (17500)' => '17372',
+ 'Saint-Médard (64370)' => '64491',
+ 'Saint-Médard (79370)' => '79282',
+ 'Saint-Médard-d\'Aunis (17220)' => '17373',
+ 'Saint-Médard-d\'Excideuil (24160)' => '24463',
+ 'Saint-Médard-d\'Eyrans (33650)' => '33448',
+ 'Saint-Médard-de-Guizières (33230)' => '33447',
+ 'Saint-Médard-de-Mussidan (24400)' => '24462',
+ 'Saint-Médard-en-Jalles (33160)' => '33449',
+ 'Saint-Médard-la-Rochette (23200)' => '23220',
+ 'Saint-Même-les-Carrières (16720)' => '16340',
+ 'Saint-Merd-de-Lapleau (19320)' => '19225',
+ 'Saint-Merd-la-Breuille (23100)' => '23221',
+ 'Saint-Merd-les-Oussines (19170)' => '19226',
+ 'Saint-Mesmin (24270)' => '24464',
+ 'Saint-Mexant (19330)' => '19227',
+ 'Saint-Michel (16470)' => '16341',
+ 'Saint-Michel (64220)' => '64492',
+ 'Saint-Michel-de-Castelnau (33840)' => '33450',
+ 'Saint-Michel-de-Double (24400)' => '24465',
+ 'Saint-Michel-de-Fronsac (33126)' => '33451',
+ 'Saint-Michel-de-Lapujade (33190)' => '33453',
+ 'Saint-Michel-de-Montaigne (24230)' => '24466',
+ 'Saint-Michel-de-Rieufret (33720)' => '33452',
+ 'Saint-Michel-de-Veisse (23480)' => '23222',
+ 'Saint-Michel-de-Villadeix (24380)' => '24468',
+ 'Saint-Michel-Escalus (40550)' => '40276',
+ 'Saint-Moreil (23400)' => '23223',
+ 'Saint-Morillon (33650)' => '33454',
+ 'Saint-Nazaire-sur-Charente (17780)' => '17375',
+ 'Saint-Nexans (24520)' => '24472',
+ 'Saint-Nicolas-de-la-Balerme (47220)' => '47262',
+ 'Saint-Oradoux-de-Chirouze (23100)' => '23224',
+ 'Saint-Oradoux-près-Crocq (23260)' => '23225',
+ 'Saint-Ouen-d\'Aunis (17230)' => '17376',
+ 'Saint-Ouen-la-Thène (17490)' => '17377',
+ 'Saint-Ouen-sur-Gartempe (87300)' => '87172',
+ 'Saint-Palais (33820)' => '33456',
+ 'Saint-Palais (64120)' => '64493',
+ 'Saint-Palais-de-Négrignac (17210)' => '17378',
+ 'Saint-Palais-de-Phiolin (17800)' => '17379',
+ 'Saint-Palais-du-Né (16300)' => '16342',
+ 'Saint-Palais-sur-Mer (17420)' => '17380',
+ 'Saint-Pancrace (24530)' => '24474',
+ 'Saint-Pandelon (40180)' => '40277',
+ 'Saint-Pantaléon-de-Lapleau (19160)' => '19228',
+ 'Saint-Pantaléon-de-Larche (19600)' => '19229',
+ 'Saint-Pantaly-d\'Ans (24640)' => '24475',
+ 'Saint-Pantaly-d\'Excideuil (24160)' => '24476',
+ 'Saint-Pardon-de-Conques (33210)' => '33457',
+ 'Saint-Pardoult (17400)' => '17381',
+ 'Saint-Pardoux (79310)' => '79285',
+ 'Saint-Pardoux (87250)' => '87173',
+ 'Saint-Pardoux-Corbier (19210)' => '19230',
+ 'Saint-Pardoux-d\'Arnet (23260)' => '23226',
+ 'Saint-Pardoux-de-Drône (24600)' => '24477',
+ 'Saint-Pardoux-du-Breuil (47200)' => '47263',
+ 'Saint-Pardoux-et-Vielvic (24170)' => '24478',
+ 'Saint-Pardoux-Isaac (47800)' => '47264',
+ 'Saint-Pardoux-l\'Ortigier (19270)' => '19234',
+ 'Saint-Pardoux-la-Croisille (19320)' => '19231',
+ 'Saint-Pardoux-la-Rivière (24470)' => '24479',
+ 'Saint-Pardoux-le-Neuf (19200)' => '19232',
+ 'Saint-Pardoux-le-Neuf (23200)' => '23228',
+ 'Saint-Pardoux-le-Vieux (19200)' => '19233',
+ 'Saint-Pardoux-les-Cards (23150)' => '23229',
+ 'Saint-Pardoux-Morterolles (23400)' => '23227',
+ 'Saint-Pastour (47290)' => '47265',
+ 'Saint-Paul (19150)' => '19235',
+ 'Saint-Paul (33390)' => '33458',
+ 'Saint-Paul (87260)' => '87174',
+ 'Saint-Paul-de-Serre (24380)' => '24480',
+ 'Saint-Paul-en-Born (40200)' => '40278',
+ 'Saint-Paul-en-Gâtine (79240)' => '79286',
+ 'Saint-Paul-la-Roche (24800)' => '24481',
+ 'Saint-Paul-lès-Dax (40990)' => '40279',
+ 'Saint-Paul-Lizonne (24320)' => '24482',
+ 'Saint-Pé-de-Léren (64270)' => '64494',
+ 'Saint-Pé-Saint-Simon (47170)' => '47266',
+ 'Saint-Pée-sur-Nivelle (64310)' => '64495',
+ 'Saint-Perdon (40090)' => '40280',
+ 'Saint-Perdoux (24560)' => '24483',
+ 'Saint-Pey-d\'Armens (33330)' => '33459',
+ 'Saint-Pey-de-Castets (33350)' => '33460',
+ 'Saint-Philippe-d\'Aiguille (33350)' => '33461',
+ 'Saint-Philippe-du-Seignal (33220)' => '33462',
+ 'Saint-Pierre-Bellevue (23460)' => '23232',
+ 'Saint-Pierre-Chérignat (23430)' => '23230',
+ 'Saint-Pierre-d\'Amilly (17700)' => '17382',
+ 'Saint-Pierre-d\'Aurillac (33490)' => '33463',
+ 'Saint-Pierre-d\'Exideuil (86400)' => '86237',
+ 'Saint-Pierre-d\'Eyraud (24130)' => '24487',
+ 'Saint-Pierre-d\'Irube (64990)' => '64496',
+ 'Saint-Pierre-d\'Oléron (17310)' => '17385',
+ 'Saint-Pierre-de-Bat (33760)' => '33464',
+ 'Saint-Pierre-de-Buzet (47160)' => '47267',
+ 'Saint-Pierre-de-Chignac (24330)' => '24484',
+ 'Saint-Pierre-de-Clairac (47270)' => '47269',
+ 'Saint-Pierre-de-Côle (24800)' => '24485',
+ 'Saint-Pierre-de-Frugie (24450)' => '24486',
+ 'Saint-Pierre-de-Fursac (23290)' => '23231',
+ 'Saint-Pierre-de-Juillers (17400)' => '17383',
+ 'Saint-Pierre-de-l\'Isle (17330)' => '17384',
+ 'Saint-Pierre-de-Maillé (86260)' => '86236',
+ 'Saint-Pierre-de-Mons (33210)' => '33465',
+ 'Saint-Pierre-des-Échaubrognes (79700)' => '79289',
+ 'Saint-Pierre-du-Mont (40280)' => '40281',
+ 'Saint-Pierre-du-Palais (17270)' => '17386',
+ 'Saint-Pierre-le-Bost (23600)' => '23233',
+ 'Saint-Pierre-sur-Dropt (47120)' => '47271',
+ 'Saint-Pompain (79160)' => '79290',
+ 'Saint-Pompont (24170)' => '24488',
+ 'Saint-Porchaire (17250)' => '17387',
+ 'Saint-Preuil (16130)' => '16343',
+ 'Saint-Priest (23110)' => '23234',
+ 'Saint-Priest-de-Gimel (19800)' => '19236',
+ 'Saint-Priest-la-Feuille (23300)' => '23235',
+ 'Saint-Priest-la-Plaine (23240)' => '23236',
+ 'Saint-Priest-les-Fougères (24450)' => '24489',
+ 'Saint-Priest-Ligoure (87800)' => '87176',
+ 'Saint-Priest-Palus (23400)' => '23237',
+ 'Saint-Priest-sous-Aixe (87700)' => '87177',
+ 'Saint-Priest-Taurion (87480)' => '87178',
+ 'Saint-Privat (19220)' => '19237',
+ 'Saint-Privat-des-Prés (24410)' => '24490',
+ 'Saint-Projet-Saint-Constant (16110)' => '16344',
+ 'Saint-Quantin-de-Rançanne (17800)' => '17388',
+ 'Saint-Quentin-de-Baron (33750)' => '33466',
+ 'Saint-Quentin-de-Caplong (33220)' => '33467',
+ 'Saint-Quentin-de-Chalais (16210)' => '16346',
+ 'Saint-Quentin-du-Dropt (47330)' => '47272',
+ 'Saint-Quentin-la-Chabanne (23500)' => '23238',
+ 'Saint-Quentin-sur-Charente (16150)' => '16345',
+ 'Saint-Rabier (24210)' => '24491',
+ 'Saint-Raphaël (24160)' => '24493',
+ 'Saint-Rémy (19290)' => '19238',
+ 'Saint-Rémy (24700)' => '24494',
+ 'Saint-Rémy (79410)' => '79293',
+ 'Saint-Rémy-sur-Creuse (86220)' => '86241',
+ 'Saint-Robert (19310)' => '19239',
+ 'Saint-Robert (47340)' => '47273',
+ 'Saint-Rogatien (17220)' => '17391',
+ 'Saint-Romain (16210)' => '16347',
+ 'Saint-Romain (86250)' => '86242',
+ 'Saint-Romain-de-Benet (17600)' => '17393',
+ 'Saint-Romain-de-Monpazier (24540)' => '24495',
+ 'Saint-Romain-et-Saint-Clément (24800)' => '24496',
+ 'Saint-Romain-la-Virvée (33240)' => '33470',
+ 'Saint-Romain-le-Noble (47270)' => '47274',
+ 'Saint-Romain-sur-Gironde (17240)' => '17392',
+ 'Saint-Romans-des-Champs (79230)' => '79294',
+ 'Saint-Romans-lès-Melle (79500)' => '79295',
+ 'Saint-Salvadour (19700)' => '19240',
+ 'Saint-Salvy (47360)' => '47275',
+ 'Saint-Sardos (47360)' => '47276',
+ 'Saint-Saturnin (16290)' => '16348',
+ 'Saint-Saturnin-du-Bois (17700)' => '17394',
+ 'Saint-Saud-Lacoussière (24470)' => '24498',
+ 'Saint-Sauvant (17610)' => '17395',
+ 'Saint-Sauvant (86600)' => '86244',
+ 'Saint-Sauveur (24520)' => '24499',
+ 'Saint-Sauveur (33250)' => '33471',
+ 'Saint-Sauveur-d\'Aunis (17540)' => '17396',
+ 'Saint-Sauveur-de-Meilhan (47180)' => '47277',
+ 'Saint-Sauveur-de-Puynormand (33660)' => '33472',
+ 'Saint-Sauveur-Lalande (24700)' => '24500',
+ 'Saint-Savin (33920)' => '33473',
+ 'Saint-Savin (86310)' => '86246',
+ 'Saint-Savinien (17350)' => '17397',
+ 'Saint-Saviol (86400)' => '86247',
+ 'Saint-Sébastien (23160)' => '23239',
+ 'Saint-Secondin (86350)' => '86248',
+ 'Saint-Selve (33650)' => '33474',
+ 'Saint-Sernin (47120)' => '47278',
+ 'Saint-Setiers (19290)' => '19241',
+ 'Saint-Seurin-de-Bourg (33710)' => '33475',
+ 'Saint-Seurin-de-Cadourne (33180)' => '33476',
+ 'Saint-Seurin-de-Cursac (33390)' => '33477',
+ 'Saint-Seurin-de-Palenne (17800)' => '17398',
+ 'Saint-Seurin-de-Prats (24230)' => '24501',
+ 'Saint-Seurin-sur-l\'Isle (33660)' => '33478',
+ 'Saint-Sève (33190)' => '33479',
+ 'Saint-Sever (40500)' => '40282',
+ 'Saint-Sever-de-Saintonge (17800)' => '17400',
+ 'Saint-Séverin (16390)' => '16350',
+ 'Saint-Séverin-d\'Estissac (24190)' => '24502',
+ 'Saint-Séverin-sur-Boutonne (17330)' => '17401',
+ 'Saint-Sigismond-de-Clermont (17240)' => '17402',
+ 'Saint-Silvain-Bas-le-Roc (23600)' => '23240',
+ 'Saint-Silvain-Bellegarde (23190)' => '23241',
+ 'Saint-Silvain-Montaigut (23320)' => '23242',
+ 'Saint-Silvain-sous-Toulx (23140)' => '23243',
+ 'Saint-Simeux (16120)' => '16351',
+ 'Saint-Simon (16120)' => '16352',
+ 'Saint-Simon-de-Bordes (17500)' => '17403',
+ 'Saint-Simon-de-Pellouaille (17260)' => '17404',
+ 'Saint-Sixte (47220)' => '47279',
+ 'Saint-Solve (19130)' => '19242',
+ 'Saint-Sorlin-de-Conac (17150)' => '17405',
+ 'Saint-Sornin (16220)' => '16353',
+ 'Saint-Sornin (17600)' => '17406',
+ 'Saint-Sornin-la-Marche (87210)' => '87179',
+ 'Saint-Sornin-Lavolps (19230)' => '19243',
+ 'Saint-Sornin-Leulac (87290)' => '87180',
+ 'Saint-Sulpice-d\'Arnoult (17250)' => '17408',
+ 'Saint-Sulpice-d\'Excideuil (24800)' => '24505',
+ 'Saint-Sulpice-de-Cognac (16370)' => '16355',
+ 'Saint-Sulpice-de-Faleyrens (33330)' => '33480',
+ 'Saint-Sulpice-de-Guilleragues (33580)' => '33481',
+ 'Saint-Sulpice-de-Mareuil (24340)' => '24503',
+ 'Saint-Sulpice-de-Pommiers (33540)' => '33482',
+ 'Saint-Sulpice-de-Roumagnac (24600)' => '24504',
+ 'Saint-Sulpice-de-Royan (17200)' => '17409',
+ 'Saint-Sulpice-de-Ruffec (16460)' => '16356',
+ 'Saint-Sulpice-et-Cameyrac (33450)' => '33483',
+ 'Saint-Sulpice-Laurière (87370)' => '87181',
+ 'Saint-Sulpice-le-Dunois (23800)' => '23244',
+ 'Saint-Sulpice-le-Guérétois (23000)' => '23245',
+ 'Saint-Sulpice-les-Bois (19250)' => '19244',
+ 'Saint-Sulpice-les-Champs (23480)' => '23246',
+ 'Saint-Sulpice-les-Feuilles (87160)' => '87182',
+ 'Saint-Sylvain (19380)' => '19245',
+ 'Saint-Sylvestre (87240)' => '87183',
+ 'Saint-Sylvestre-sur-Lot (47140)' => '47280',
+ 'Saint-Symphorien (33113)' => '33484',
+ 'Saint-Symphorien (79270)' => '79298',
+ 'Saint-Symphorien-sur-Couze (87140)' => '87184',
+ 'Saint-Thomas-de-Conac (17150)' => '17410',
+ 'Saint-Trojan (33710)' => '33486',
+ 'Saint-Trojan-les-Bains (17370)' => '17411',
+ 'Saint-Urcisse (47270)' => '47281',
+ 'Saint-Vaize (17100)' => '17412',
+ 'Saint-Vallier (16480)' => '16357',
+ 'Saint-Varent (79330)' => '79299',
+ 'Saint-Vaury (23320)' => '23247',
+ 'Saint-Viance (19240)' => '19246',
+ 'Saint-Victor (24350)' => '24508',
+ 'Saint-Victor-en-Marche (23000)' => '23248',
+ 'Saint-Victour (19200)' => '19247',
+ 'Saint-Victurnien (87420)' => '87185',
+ 'Saint-Vincent (64800)' => '64498',
+ 'Saint-Vincent-de-Connezac (24190)' => '24509',
+ 'Saint-Vincent-de-Cosse (24220)' => '24510',
+ 'Saint-Vincent-de-Lamontjoie (47310)' => '47282',
+ 'Saint-Vincent-de-Paul (33440)' => '33487',
+ 'Saint-Vincent-de-Paul (40990)' => '40283',
+ 'Saint-Vincent-de-Pertignas (33420)' => '33488',
+ 'Saint-Vincent-de-Tyrosse (40230)' => '40284',
+ 'Saint-Vincent-Jalmoutiers (24410)' => '24511',
+ 'Saint-Vincent-la-Châtre (79500)' => '79301',
+ 'Saint-Vincent-le-Paluel (24200)' => '24512',
+ 'Saint-Vincent-sur-l\'Isle (24420)' => '24513',
+ 'Saint-Vite (47500)' => '47283',
+ 'Saint-Vitte-sur-Briance (87380)' => '87186',
+ 'Saint-Vivien (17220)' => '17413',
+ 'Saint-Vivien (24230)' => '24514',
+ 'Saint-Vivien-de-Blaye (33920)' => '33489',
+ 'Saint-Vivien-de-Médoc (33590)' => '33490',
+ 'Saint-Vivien-de-Monségur (33580)' => '33491',
+ 'Saint-Xandre (17138)' => '17414',
+ 'Saint-Yaguen (40400)' => '40285',
+ 'Saint-Ybard (19140)' => '19248',
+ 'Saint-Yrieix-la-Montagne (23460)' => '23249',
+ 'Saint-Yrieix-la-Perche (87500)' => '87187',
+ 'Saint-Yrieix-le-Déjalat (19300)' => '19249',
+ 'Saint-Yrieix-les-Bois (23150)' => '23250',
+ 'Saint-Yrieix-sous-Aixe (87700)' => '87188',
+ 'Saint-Yrieix-sur-Charente (16710)' => '16358',
+ 'Saint-Yzan-de-Soudiac (33920)' => '33492',
+ 'Saint-Yzans-de-Médoc (33340)' => '33493',
+ 'Sainte-Alvère-Saint-Laurent Les Bâtons (24510)' => '24362',
+ 'Sainte-Anne-Saint-Priest (87120)' => '87134',
+ 'Sainte-Bazeille (47180)' => '47233',
+ 'Sainte-Blandine (79370)' => '79240',
+ 'Sainte-Colombe (16230)' => '16309',
+ 'Sainte-Colombe (17210)' => '17319',
+ 'Sainte-Colombe (33350)' => '33390',
+ 'Sainte-Colombe (40700)' => '40252',
+ 'Sainte-Colombe-de-Duras (47120)' => '47236',
+ 'Sainte-Colombe-de-Villeneuve (47300)' => '47237',
+ 'Sainte-Colombe-en-Bruilhois (47310)' => '47238',
+ 'Sainte-Colome (64260)' => '64473',
+ 'Sainte-Croix (24440)' => '24393',
+ 'Sainte-Croix-de-Mareuil (24340)' => '24394',
+ 'Sainte-Croix-du-Mont (33410)' => '33392',
+ 'Sainte-Eanne (79800)' => '79246',
+ 'Sainte-Engrâce (64560)' => '64475',
+ 'Sainte-Eulalie (33560)' => '33397',
+ 'Sainte-Eulalie-d\'Ans (24640)' => '24401',
+ 'Sainte-Eulalie-d\'Eymet (24500)' => '24402',
+ 'Sainte-Eulalie-en-Born (40200)' => '40257',
+ 'Sainte-Féréole (19270)' => '19202',
+ 'Sainte-Feyre (23000)' => '23193',
+ 'Sainte-Feyre-la-Montagne (23500)' => '23194',
+ 'Sainte-Florence (33350)' => '33401',
+ 'Sainte-Fortunade (19490)' => '19203',
+ 'Sainte-Foy (40190)' => '40258',
+ 'Sainte-Foy-de-Belvès (24170)' => '24406',
+ 'Sainte-Foy-de-Longas (24510)' => '24407',
+ 'Sainte-Foy-la-Grande (33220)' => '33402',
+ 'Sainte-Foy-la-Longue (33490)' => '33403',
+ 'Sainte-Gemme (17250)' => '17330',
+ 'Sainte-Gemme (33580)' => '33404',
+ 'Sainte-Gemme (79330)' => '79250',
+ 'Sainte-Gemme-Martaillac (47250)' => '47244',
+ 'Sainte-Hélène (33480)' => '33417',
+ 'Sainte-Innocence (24500)' => '24423',
+ 'Sainte-Lheurine (17520)' => '17355',
+ 'Sainte-Livrade-sur-Lot (47110)' => '47252',
+ 'Sainte-Marie-de-Chignac (24330)' => '24447',
+ 'Sainte-Marie-de-Gosse (40390)' => '40271',
+ 'Sainte-Marie-de-Ré (17740)' => '17360',
+ 'Sainte-Marie-de-Vaux (87420)' => '87162',
+ 'Sainte-Marie-Lapanouze (19160)' => '19219',
+ 'Sainte-Marthe (47430)' => '47253',
+ 'Sainte-Maure-de-Peyriac (47170)' => '47258',
+ 'Sainte-Même (17770)' => '17374',
+ 'Sainte-Mondane (24370)' => '24470',
+ 'Sainte-Nathalène (24200)' => '24471',
+ 'Sainte-Néomaye (79260)' => '79283',
+ 'Sainte-Orse (24210)' => '24473',
+ 'Sainte-Ouenne (79220)' => '79284',
+ 'Sainte-Radegonde (17250)' => '17389',
+ 'Sainte-Radegonde (24560)' => '24492',
+ 'Sainte-Radegonde (33350)' => '33468',
+ 'Sainte-Radegonde (79100)' => '79292',
+ 'Sainte-Radégonde (86300)' => '86239',
+ 'Sainte-Ramée (17240)' => '17390',
+ 'Sainte-Sévère (16200)' => '16349',
+ 'Sainte-Soline (79120)' => '79297',
+ 'Sainte-Souline (16480)' => '16354',
+ 'Sainte-Soulle (17220)' => '17407',
+ 'Sainte-Terre (33350)' => '33485',
+ 'Sainte-Trie (24160)' => '24507',
+ 'Sainte-Verge (79100)' => '79300',
+ 'Saintes (17100)' => '17415',
+ 'Saires (86420)' => '86249',
+ 'Saivres (79400)' => '79302',
+ 'Saix (86120)' => '86250',
+ 'Salagnac (24160)' => '24515',
+ 'Salaunes (33160)' => '33494',
+ 'Saleignes (17510)' => '17416',
+ 'Salies-de-Béarn (64270)' => '64499',
+ 'Salignac-de-Mirambeau (17130)' => '17417',
+ 'Salignac-Eyvigues (24590)' => '24516',
+ 'Salignac-sur-Charente (17800)' => '17418',
+ 'Salleboeuf (33370)' => '33496',
+ 'Salles (33770)' => '33498',
+ 'Salles (47150)' => '47284',
+ 'Salles (79800)' => '79303',
+ 'Salles-d\'Angles (16130)' => '16359',
+ 'Salles-de-Barbezieux (16300)' => '16360',
+ 'Salles-de-Belvès (24170)' => '24517',
+ 'Salles-de-Villefagnan (16700)' => '16361',
+ 'Salles-Lavalette (16190)' => '16362',
+ 'Salles-Mongiscard (64300)' => '64500',
+ 'Salles-sur-Mer (17220)' => '17420',
+ 'Sallespisse (64300)' => '64501',
+ 'Salon (24380)' => '24518',
+ 'Salon-la-Tour (19510)' => '19250',
+ 'Samadet (40320)' => '40286',
+ 'Samazan (47250)' => '47285',
+ 'Sames (64520)' => '64502',
+ 'Sammarçolles (86200)' => '86252',
+ 'Samonac (33710)' => '33500',
+ 'Samsons-Lion (64350)' => '64503',
+ 'Sanguinet (40460)' => '40287',
+ 'Sannat (23110)' => '23167',
+ 'Sansais (79270)' => '79304',
+ 'Sanxay (86600)' => '86253',
+ 'Sarbazan (40120)' => '40288',
+ 'Sardent (23250)' => '23168',
+ 'Sare (64310)' => '64504',
+ 'Sarlande (24270)' => '24519',
+ 'Sarlat-la-Canéda (24200)' => '24520',
+ 'Sarliac-sur-l\'Isle (24420)' => '24521',
+ 'Sarpourenx (64300)' => '64505',
+ 'Sarran (19800)' => '19251',
+ 'Sarrance (64490)' => '64506',
+ 'Sarrazac (24800)' => '24522',
+ 'Sarraziet (40500)' => '40289',
+ 'Sarron (40800)' => '40290',
+ 'Sarroux (19110)' => '19252',
+ 'Saubion (40230)' => '40291',
+ 'Saubole (64420)' => '64507',
+ 'Saubrigues (40230)' => '40292',
+ 'Saubusse (40180)' => '40293',
+ 'Saucats (33650)' => '33501',
+ 'Saucède (64400)' => '64508',
+ 'Saugnac-et-Cambran (40180)' => '40294',
+ 'Saugnacq-et-Muret (40410)' => '40295',
+ 'Saugon (33920)' => '33502',
+ 'Sauguis-Saint-Étienne (64470)' => '64509',
+ 'Saujon (17600)' => '17421',
+ 'Saulgé (86500)' => '86254',
+ 'Saulgond (16420)' => '16363',
+ 'Sault-de-Navailles (64300)' => '64510',
+ 'Sauméjan (47420)' => '47286',
+ 'Saumont (47600)' => '47287',
+ 'Saumos (33680)' => '33503',
+ 'Saurais (79200)' => '79306',
+ 'Saussignac (24240)' => '24523',
+ 'Sauternes (33210)' => '33504',
+ 'Sauvagnac (16310)' => '16364',
+ 'Sauvagnas (47340)' => '47288',
+ 'Sauvagnon (64230)' => '64511',
+ 'Sauvelade (64150)' => '64512',
+ 'Sauveterre-de-Béarn (64390)' => '64513',
+ 'Sauveterre-de-Guyenne (33540)' => '33506',
+ 'Sauveterre-la-Lémance (47500)' => '47292',
+ 'Sauveterre-Saint-Denis (47220)' => '47293',
+ 'Sauviac (33430)' => '33507',
+ 'Sauviat-sur-Vige (87400)' => '87190',
+ 'Sauvignac (16480)' => '16365',
+ 'Sauzé-Vaussais (79190)' => '79307',
+ 'Savennes (23000)' => '23170',
+ 'Savignac (33124)' => '33508',
+ 'Savignac-de-Duras (47120)' => '47294',
+ 'Savignac-de-l\'Isle (33910)' => '33509',
+ 'Savignac-de-Miremont (24260)' => '24524',
+ 'Savignac-de-Nontron (24300)' => '24525',
+ 'Savignac-Lédrier (24270)' => '24526',
+ 'Savignac-les-Églises (24420)' => '24527',
+ 'Savignac-sur-Leyze (47150)' => '47295',
+ 'Savigné (86400)' => '86255',
+ 'Savigny-Lévescault (86800)' => '86256',
+ 'Savigny-sous-Faye (86140)' => '86257',
+ 'Sceau-Saint-Angel (24300)' => '24528',
+ 'Sciecq (79000)' => '79308',
+ 'Scillé (79240)' => '79309',
+ 'Scorbé-Clairvaux (86140)' => '86258',
+ 'Séby (64410)' => '64514',
+ 'Secondigné-sur-Belle (79170)' => '79310',
+ 'Secondigny (79130)' => '79311',
+ 'Sedze-Maubecq (64160)' => '64515',
+ 'Sedzère (64160)' => '64516',
+ 'Ségalas (47410)' => '47296',
+ 'Segonzac (16130)' => '16366',
+ 'Segonzac (19310)' => '19253',
+ 'Segonzac (24600)' => '24529',
+ 'Ségur-le-Château (19230)' => '19254',
+ 'Seigné (17510)' => '17422',
+ 'Seignosse (40510)' => '40296',
+ 'Seilhac (19700)' => '19255',
+ 'Séligné (79170)' => '79312',
+ 'Sembas (47360)' => '47297',
+ 'Séméacq-Blachon (64350)' => '64517',
+ 'Semens (33490)' => '33510',
+ 'Semillac (17150)' => '17423',
+ 'Semoussac (17150)' => '17424',
+ 'Semussac (17120)' => '17425',
+ 'Sencenac-Puy-de-Fourches (24310)' => '24530',
+ 'Sendets (33690)' => '33511',
+ 'Sendets (64320)' => '64518',
+ 'Sénestis (47430)' => '47298',
+ 'Senillé-Saint-Sauveur (86100)' => '86245',
+ 'Sepvret (79120)' => '79313',
+ 'Sérandon (19160)' => '19256',
+ 'Séreilhac (87620)' => '87191',
+ 'Sergeac (24290)' => '24531',
+ 'Sérignac-Péboudou (47410)' => '47299',
+ 'Sérignac-sur-Garonne (47310)' => '47300',
+ 'Sérigny (86230)' => '86260',
+ 'Sérilhac (19190)' => '19257',
+ 'Sermur (23700)' => '23171',
+ 'Séron (65320)' => '65422',
+ 'Serres-Castet (64121)' => '64519',
+ 'Serres-et-Montguyard (24500)' => '24532',
+ 'Serres-Gaston (40700)' => '40298',
+ 'Serres-Morlaàs (64160)' => '64520',
+ 'Serres-Sainte-Marie (64170)' => '64521',
+ 'Serreslous-et-Arribans (40700)' => '40299',
+ 'Sers (16410)' => '16368',
+ 'Servanches (24410)' => '24533',
+ 'Servières-le-Château (19220)' => '19258',
+ 'Sévignacq (64160)' => '64523',
+ 'Sévignacq-Meyracq (64260)' => '64522',
+ 'Sèvres-Anxaumont (86800)' => '86261',
+ 'Sexcles (19430)' => '19259',
+ 'Seyches (47350)' => '47301',
+ 'Seyresse (40180)' => '40300',
+ 'Siecq (17490)' => '17427',
+ 'Siest (40180)' => '40301',
+ 'Sigalens (33690)' => '33512',
+ 'Sigogne (16200)' => '16369',
+ 'Sigoulès (24240)' => '24534',
+ 'Sillars (86320)' => '86262',
+ 'Sillas (33690)' => '33513',
+ 'Simacourbe (64350)' => '64524',
+ 'Simeyrols (24370)' => '24535',
+ 'Sindères (40110)' => '40302',
+ 'Singleyrac (24500)' => '24536',
+ 'Sioniac (19120)' => '19260',
+ 'Siorac-de-Ribérac (24600)' => '24537',
+ 'Siorac-en-Périgord (24170)' => '24538',
+ 'Sireuil (16440)' => '16370',
+ 'Siros (64230)' => '64525',
+ 'Smarves (86240)' => '86263',
+ 'Solférino (40210)' => '40303',
+ 'Solignac (87110)' => '87192',
+ 'Sommières-du-Clain (86160)' => '86264',
+ 'Sompt (79110)' => '79314',
+ 'Sonnac (17160)' => '17428',
+ 'Soorts-Hossegor (40150)' => '40304',
+ 'Sorbets (40320)' => '40305',
+ 'Sorde-l\'Abbaye (40300)' => '40306',
+ 'Sore (40430)' => '40307',
+ 'Sorges et Ligueux en Périgord (24420)' => '24540',
+ 'Sornac (19290)' => '19261',
+ 'Sort-en-Chalosse (40180)' => '40308',
+ 'Sos (47170)' => '47302',
+ 'Sossais (86230)' => '86265',
+ 'Soubise (17780)' => '17429',
+ 'Soubran (17150)' => '17430',
+ 'Soubrebost (23250)' => '23173',
+ 'Soudaine-Lavinadière (19370)' => '19262',
+ 'Soudan (79800)' => '79316',
+ 'Soudat (24360)' => '24541',
+ 'Soudeilles (19300)' => '19263',
+ 'Souffrignac (16380)' => '16372',
+ 'Soulac-sur-Mer (33780)' => '33514',
+ 'Soulaures (24540)' => '24542',
+ 'Soulignac (33760)' => '33515',
+ 'Soulignonne (17250)' => '17431',
+ 'Soumans (23600)' => '23174',
+ 'Soumensac (47120)' => '47303',
+ 'Souméras (17130)' => '17432',
+ 'Soumoulou (64420)' => '64526',
+ 'Souprosse (40250)' => '40309',
+ 'Souraïde (64250)' => '64527',
+ 'Soursac (19550)' => '19264',
+ 'Sourzac (24400)' => '24543',
+ 'Sous-Parsat (23150)' => '23175',
+ 'Sousmoulins (17130)' => '17433',
+ 'Soussac (33790)' => '33516',
+ 'Soussans (33460)' => '33517',
+ 'Soustons (40140)' => '40310',
+ 'Soutiers (79310)' => '79318',
+ 'Souvigné (16240)' => '16373',
+ 'Souvigné (79800)' => '79319',
+ 'Soyaux (16800)' => '16374',
+ 'Suaux (16260)' => '16375',
+ 'Suhescun (64780)' => '64528',
+ 'Surdoux (87130)' => '87193',
+ 'Surgères (17700)' => '17434',
+ 'Surin (79220)' => '79320',
+ 'Surin (86250)' => '86266',
+ 'Suris (16270)' => '16376',
+ 'Sus (64190)' => '64529',
+ 'Susmiou (64190)' => '64530',
+ 'Sussac (87130)' => '87194',
+ 'Tabaille-Usquain (64190)' => '64531',
+ 'Tabanac (33550)' => '33518',
+ 'Tadousse-Ussau (64330)' => '64532',
+ 'Taillant (17350)' => '17435',
+ 'Taillebourg (17350)' => '17436',
+ 'Taillebourg (47200)' => '47304',
+ 'Taillecavat (33580)' => '33520',
+ 'Taizé (79100)' => '79321',
+ 'Taizé-Aizie (16700)' => '16378',
+ 'Talais (33590)' => '33521',
+ 'Talence (33400)' => '33522',
+ 'Taller (40260)' => '40311',
+ 'Talmont-sur-Gironde (17120)' => '17437',
+ 'Tamniès (24620)' => '24544',
+ 'Tanzac (17260)' => '17438',
+ 'Taponnat-Fleurignac (16110)' => '16379',
+ 'Tardes (23170)' => '23251',
+ 'Tardets-Sorholus (64470)' => '64533',
+ 'Targon (33760)' => '33523',
+ 'Tarnac (19170)' => '19265',
+ 'Tarnès (33240)' => '33524',
+ 'Tarnos (40220)' => '40312',
+ 'Taron-Sadirac-Viellenave (64330)' => '64534',
+ 'Tarsacq (64360)' => '64535',
+ 'Tartas (40400)' => '40313',
+ 'Taugon (17170)' => '17439',
+ 'Tauriac (33710)' => '33525',
+ 'Tayac (33570)' => '33526',
+ 'Tayrac (47270)' => '47305',
+ 'Teillots (24390)' => '24545',
+ 'Temple-Laguyon (24390)' => '24546',
+ 'Tercé (86800)' => '86268',
+ 'Tercillat (23350)' => '23252',
+ 'Tercis-les-Bains (40180)' => '40314',
+ 'Ternant (17400)' => '17440',
+ 'Ternay (86120)' => '86269',
+ 'Terrasson-Lavilledieu (24120)' => '24547',
+ 'Tersannes (87360)' => '87195',
+ 'Tesson (17460)' => '17441',
+ 'Tessonnière (79600)' => '79325',
+ 'Téthieu (40990)' => '40315',
+ 'Teuillac (33710)' => '33530',
+ 'Teyjat (24300)' => '24548',
+ 'Thaims (17120)' => '17442',
+ 'Thairé (17290)' => '17443',
+ 'Thalamy (19200)' => '19266',
+ 'Thauron (23250)' => '23253',
+ 'Theil-Rabier (16240)' => '16381',
+ 'Thénac (17460)' => '17444',
+ 'Thénac (24240)' => '24549',
+ 'Thénezay (79390)' => '79326',
+ 'Thenon (24210)' => '24550',
+ 'Thézac (17600)' => '17445',
+ 'Thézac (47370)' => '47307',
+ 'Thèze (64450)' => '64536',
+ 'Thiat (87320)' => '87196',
+ 'Thiviers (24800)' => '24551',
+ 'Thollet (86290)' => '86270',
+ 'Thonac (24290)' => '24552',
+ 'Thorigné (79370)' => '79327',
+ 'Thorigny-sur-le-Mignon (79360)' => '79328',
+ 'Thors (17160)' => '17446',
+ 'Thouars (79100)' => '79329',
+ 'Thouars-sur-Garonne (47230)' => '47308',
+ 'Thouron (87140)' => '87197',
+ 'Thurageau (86110)' => '86271',
+ 'Thuré (86540)' => '86272',
+ 'Tilh (40360)' => '40316',
+ 'Tillou (79110)' => '79330',
+ 'Tizac-de-Curton (33420)' => '33531',
+ 'Tizac-de-Lapouyade (33620)' => '33532',
+ 'Tocane-Saint-Apre (24350)' => '24553',
+ 'Tombeboeuf (47380)' => '47309',
+ 'Tonnay-Boutonne (17380)' => '17448',
+ 'Tonnay-Charente (17430)' => '17449',
+ 'Tonneins (47400)' => '47310',
+ 'Torsac (16410)' => '16382',
+ 'Torxé (17380)' => '17450',
+ 'Tosse (40230)' => '40317',
+ 'Toulenne (33210)' => '33533',
+ 'Toulouzette (40250)' => '40318',
+ 'Toulx-Sainte-Croix (23600)' => '23254',
+ 'Tourliac (47210)' => '47311',
+ 'Tournon-d\'Agenais (47370)' => '47312',
+ 'Tourriers (16560)' => '16383',
+ 'Tourtenay (79100)' => '79331',
+ 'Tourtoirac (24390)' => '24555',
+ 'Tourtrès (47380)' => '47313',
+ 'Touvérac (16360)' => '16384',
+ 'Touvre (16600)' => '16385',
+ 'Touzac (16120)' => '16386',
+ 'Toy-Viam (19170)' => '19268',
+ 'Trayes (79240)' => '79332',
+ 'Treignac (19260)' => '19269',
+ 'Trélissac (24750)' => '24557',
+ 'Trémolat (24510)' => '24558',
+ 'Trémons (47140)' => '47314',
+ 'Trensacq (40630)' => '40319',
+ 'Trentels (47140)' => '47315',
+ 'Tresses (33370)' => '33535',
+ 'Triac-Lautrait (16200)' => '16387',
+ 'Trizay (17250)' => '17453',
+ 'Troche (19230)' => '19270',
+ 'Trois-Fonds (23230)' => '23255',
+ 'Trois-Palis (16730)' => '16388',
+ 'Trois-Villes (64470)' => '64537',
+ 'Tudeils (19120)' => '19271',
+ 'Tugéras-Saint-Maurice (17130)' => '17454',
+ 'Tulle (19000)' => '19272',
+ 'Turenne (19500)' => '19273',
+ 'Turgon (16350)' => '16389',
+ 'Tursac (24620)' => '24559',
+ 'Tusson (16140)' => '16390',
+ 'Tuzie (16700)' => '16391',
+ 'Uchacq-et-Parentis (40090)' => '40320',
+ 'Uhart-Cize (64220)' => '64538',
+ 'Uhart-Mixe (64120)' => '64539',
+ 'Urcuit (64990)' => '64540',
+ 'Urdès (64370)' => '64541',
+ 'Urdos (64490)' => '64542',
+ 'Urepel (64430)' => '64543',
+ 'Urgons (40320)' => '40321',
+ 'Urost (64160)' => '64544',
+ 'Urrugne (64122)' => '64545',
+ 'Urt (64240)' => '64546',
+ 'Urval (24480)' => '24560',
+ 'Ussac (19270)' => '19274',
+ 'Usseau (79210)' => '79334',
+ 'Usseau (86230)' => '86275',
+ 'Ussel (19200)' => '19275',
+ 'Usson-du-Poitou (86350)' => '86276',
+ 'Ustaritz (64480)' => '64547',
+ 'Uza (40170)' => '40322',
+ 'Uzan (64370)' => '64548',
+ 'Uzein (64230)' => '64549',
+ 'Uzerche (19140)' => '19276',
+ 'Uzeste (33730)' => '33537',
+ 'Uzos (64110)' => '64550',
+ 'Val d\'Issoire (87330)' => '87097',
+ 'Val de Virvée (33240)' => '33018',
+ 'Val des Vignes (16250)' => '16175',
+ 'Valdivienne (86300)' => '86233',
+ 'Valence (16460)' => '16392',
+ 'Valeuil (24310)' => '24561',
+ 'Valeyrac (33340)' => '33538',
+ 'Valiergues (19200)' => '19277',
+ 'Vallans (79270)' => '79335',
+ 'Vallereuil (24190)' => '24562',
+ 'Vallière (23120)' => '23257',
+ 'Valojoulx (24290)' => '24563',
+ 'Vançais (79120)' => '79336',
+ 'Vandré (17700)' => '17457',
+ 'Vanxains (24600)' => '24564',
+ 'Vanzac (17500)' => '17458',
+ 'Vanzay (79120)' => '79338',
+ 'Varaignes (24360)' => '24565',
+ 'Varaize (17400)' => '17459',
+ 'Vareilles (23300)' => '23258',
+ 'Varennes (24150)' => '24566',
+ 'Varennes (86110)' => '86277',
+ 'Varès (47400)' => '47316',
+ 'Varetz (19240)' => '19278',
+ 'Vars (16330)' => '16393',
+ 'Vars-sur-Roseix (19130)' => '19279',
+ 'Varzay (17460)' => '17460',
+ 'Vasles (79340)' => '79339',
+ 'Vaulry (87140)' => '87198',
+ 'Vaunac (24800)' => '24567',
+ 'Vausseroux (79420)' => '79340',
+ 'Vautebis (79420)' => '79341',
+ 'Vaux (86700)' => '86278',
+ 'Vaux-Lavalette (16320)' => '16394',
+ 'Vaux-Rouillac (16170)' => '16395',
+ 'Vaux-sur-Mer (17640)' => '17461',
+ 'Vaux-sur-Vienne (86220)' => '86279',
+ 'Vayres (33870)' => '33539',
+ 'Vayres (87600)' => '87199',
+ 'Végennes (19120)' => '19280',
+ 'Veix (19260)' => '19281',
+ 'Vélines (24230)' => '24568',
+ 'Vellèches (86230)' => '86280',
+ 'Vendays-Montalivet (33930)' => '33540',
+ 'Vendeuvre-du-Poitou (86380)' => '86281',
+ 'Vendoire (24320)' => '24569',
+ 'Vénérand (17100)' => '17462',
+ 'Vensac (33590)' => '33541',
+ 'Ventouse (16460)' => '16396',
+ 'Vérac (33240)' => '33542',
+ 'Verdelais (33490)' => '33543',
+ 'Verdets (64400)' => '64551',
+ 'Verdille (16140)' => '16397',
+ 'Verdon (24520)' => '24570',
+ 'Vergeroux (17300)' => '17463',
+ 'Vergné (17330)' => '17464',
+ 'Vergt (24380)' => '24571',
+ 'Vergt-de-Biron (24540)' => '24572',
+ 'Vérines (17540)' => '17466',
+ 'Verneiges (23170)' => '23259',
+ 'Verneuil (16310)' => '16398',
+ 'Verneuil-Moustiers (87360)' => '87200',
+ 'Verneuil-sur-Vienne (87430)' => '87201',
+ 'Vernon (86340)' => '86284',
+ 'Vernoux-en-Gâtine (79240)' => '79342',
+ 'Vernoux-sur-Boutonne (79170)' => '79343',
+ 'Verrières (16130)' => '16399',
+ 'Verrières (86410)' => '86285',
+ 'Verrue (86420)' => '86286',
+ 'Verruyes (79310)' => '79345',
+ 'Vert (40420)' => '40323',
+ 'Verteillac (24320)' => '24573',
+ 'Verteuil-d\'Agenais (47260)' => '47317',
+ 'Verteuil-sur-Charente (16510)' => '16400',
+ 'Vertheuil (33180)' => '33545',
+ 'Vervant (16330)' => '16401',
+ 'Vervant (17400)' => '17467',
+ 'Veyrac (87520)' => '87202',
+ 'Veyrières (19200)' => '19283',
+ 'Veyrignac (24370)' => '24574',
+ 'Veyrines-de-Domme (24250)' => '24575',
+ 'Veyrines-de-Vergt (24380)' => '24576',
+ 'Vézac (24220)' => '24577',
+ 'Vézières (86120)' => '86287',
+ 'Vialer (64330)' => '64552',
+ 'Viam (19170)' => '19284',
+ 'Vianne (47230)' => '47318',
+ 'Vibrac (16120)' => '16402',
+ 'Vibrac (17130)' => '17468',
+ 'Vicq-d\'Auribat (40380)' => '40324',
+ 'Vicq-sur-Breuilh (87260)' => '87203',
+ 'Vicq-sur-Gartempe (86260)' => '86288',
+ 'Vidaillat (23250)' => '23260',
+ 'Videix (87600)' => '87204',
+ 'Vielle-Saint-Girons (40560)' => '40326',
+ 'Vielle-Soubiran (40240)' => '40327',
+ 'Vielle-Tursan (40320)' => '40325',
+ 'Viellenave-d\'Arthez (64170)' => '64554',
+ 'Viellenave-de-Navarrenx (64190)' => '64555',
+ 'Vielleségure (64150)' => '64556',
+ 'Viennay (79200)' => '79347',
+ 'Viersat (23170)' => '23261',
+ 'Vieux-Boucau-les-Bains (40480)' => '40328',
+ 'Vieux-Mareuil (24340)' => '24579',
+ 'Vieux-Ruffec (16350)' => '16404',
+ 'Vigeois (19410)' => '19285',
+ 'Vigeville (23140)' => '23262',
+ 'Vignes (64410)' => '64557',
+ 'Vignolles (16300)' => '16405',
+ 'Vignols (19130)' => '19286',
+ 'Vignonet (33330)' => '33546',
+ 'Vilhonneur (16220)' => '16406',
+ 'Villac (24120)' => '24580',
+ 'Villamblard (24140)' => '24581',
+ 'Villandraut (33730)' => '33547',
+ 'Villard (23800)' => '23263',
+ 'Villars (24530)' => '24582',
+ 'Villars-en-Pons (17260)' => '17469',
+ 'Villars-les-Bois (17770)' => '17470',
+ 'Villebois-Lavalette (16320)' => '16408',
+ 'Villebramar (47380)' => '47319',
+ 'Villedoux (17230)' => '17472',
+ 'Villefagnan (16240)' => '16409',
+ 'Villefavard (87190)' => '87206',
+ 'Villefollet (79170)' => '79348',
+ 'Villefranche-de-Lonchat (24610)' => '24584',
+ 'Villefranche-du-Périgord (24550)' => '24585',
+ 'Villefranche-du-Queyran (47160)' => '47320',
+ 'Villefranque (64990)' => '64558',
+ 'Villegats (16700)' => '16410',
+ 'Villegouge (33141)' => '33548',
+ 'Villejésus (16140)' => '16411',
+ 'Villejoubert (16560)' => '16412',
+ 'Villemain (79110)' => '79349',
+ 'Villemorin (17470)' => '17473',
+ 'Villemort (86310)' => '86291',
+ 'Villenave (40110)' => '40330',
+ 'Villenave-d\'Ornon (33140)' => '33550',
+ 'Villenave-de-Rions (33550)' => '33549',
+ 'Villenave-près-Béarn (65500)' => '65476',
+ 'Villeneuve (33710)' => '33551',
+ 'Villeneuve-de-Duras (47120)' => '47321',
+ 'Villeneuve-de-Marsan (40190)' => '40331',
+ 'Villeneuve-la-Comtesse (17330)' => '17474',
+ 'Villeneuve-sur-Lot (47300)' => '47323',
+ 'Villeréal (47210)' => '47324',
+ 'Villeton (47400)' => '47325',
+ 'Villetoureix (24600)' => '24586',
+ 'Villexavier (17500)' => '17476',
+ 'Villiers (86190)' => '86292',
+ 'Villiers-Couture (17510)' => '17477',
+ 'Villiers-en-Bois (79360)' => '79350',
+ 'Villiers-en-Plaine (79160)' => '79351',
+ 'Villiers-le-Roux (16240)' => '16413',
+ 'Villiers-sur-Chizé (79170)' => '79352',
+ 'Villognon (16230)' => '16414',
+ 'Vinax (17510)' => '17478',
+ 'Vindelle (16430)' => '16415',
+ 'Viodos-Abense-de-Bas (64130)' => '64559',
+ 'Virazeil (47200)' => '47326',
+ 'Virelade (33720)' => '33552',
+ 'Virollet (17260)' => '17479',
+ 'Virsac (33240)' => '33553',
+ 'Virson (17290)' => '17480',
+ 'Vitrac (24200)' => '24587',
+ 'Vitrac-Saint-Vincent (16310)' => '16416',
+ 'Vitrac-sur-Montane (19800)' => '19287',
+ 'Viven (64450)' => '64560',
+ 'Viville (16120)' => '16417',
+ 'Vivonne (86370)' => '86293',
+ 'Voeuil-et-Giget (16400)' => '16418',
+ 'Voissay (17400)' => '17481',
+ 'Vouharte (16330)' => '16419',
+ 'Vouhé (17700)' => '17482',
+ 'Vouhé (79310)' => '79354',
+ 'Vouillé (79230)' => '79355',
+ 'Vouillé (86190)' => '86294',
+ 'Voulême (86400)' => '86295',
+ 'Voulgézac (16250)' => '16420',
+ 'Voulmentin (79150)' => '79242',
+ 'Voulon (86700)' => '86296',
+ 'Vouneuil-sous-Biard (86580)' => '86297',
+ 'Vouneuil-sur-Vienne (86210)' => '86298',
+ 'Voutezac (19130)' => '19288',
+ 'Vouthon (16220)' => '16421',
+ 'Vouzailles (86170)' => '86299',
+ 'Vouzan (16410)' => '16422',
+ 'Xaintrailles (47230)' => '47327',
+ 'Xaintray (79220)' => '79357',
+ 'Xambes (16330)' => '16423',
+ 'Ychoux (40160)' => '40332',
+ 'Ygos-Saint-Saturnin (40110)' => '40333',
+ 'Yssandon (19310)' => '19289',
+ 'Yversay (86170)' => '86300',
+ 'Yves (17340)' => '17483',
+ 'Yviers (16210)' => '16424',
+ 'Yvrac (33370)' => '33554',
+ 'Yvrac-et-Malleyrand (16110)' => '16425',
+ 'Yzosse (40180)' => '40334'
+ ];
}
diff --git a/bridges/AtmoOccitanieBridge.php b/bridges/AtmoOccitanieBridge.php
index a934bad8..2388d7e4 100644
--- a/bridges/AtmoOccitanieBridge.php
+++ b/bridges/AtmoOccitanieBridge.php
@@ -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;
-
- public function collectData() {
- $uri = self::URI . $this->getInput('city');
-
- $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');
-
- $recommendationsMessage = '';
-
- $i = 0;
- $len = count($recommendationsItemDom);
- foreach ($recommendationsItemDom as $key => $value) {
- if ($i == 0) {
- $recommendationsMessage .= trim($value->innertext) . '.';
- } else {
- $recommendationsMessage .= ' ' . trim($value->innertext) . '.';
- }
- $i++;
- }
-
- $lastRecommendationsDom = $recommendationsDom->find('.col-md-6', -1);
- $informationHeaderMessage = $lastRecommendationsDom->find('.heading', 0)->innertext;
- $indice = $lastRecommendationsDom->find('.current-indice .indice div', 0)->innertext;
- $informationDescriptionMessage = $lastRecommendationsDom->find('.current-indice .description p', 0)->innertext;
-
- $message = "$generalMessage L'indice est de $indice/10. $informationDescriptionMessage. $recommendationsMessage";
- $city = $this->getInput('city');
-
- $item['uri'] = $uri;
- $today = date('d/m/Y');
- $item['title'] = "Bulletin de l'air du $today pour la ville : $city.";
- //$item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-occitanie.org #QualiteAir. ' . $message;
- $item['title'] .= ' #QualiteAir. ' . $message;
- $item['author'] = 'floviolleau';
- $item['content'] = $message;
- $item['uid'] = hash('sha256', $item['title']);
-
- $this->items[] = $item;
- }
+
+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');
+
+ $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');
+
+ $recommendationsMessage = '';
+
+ $i = 0;
+ $len = count($recommendationsItemDom);
+ foreach ($recommendationsItemDom as $key => $value) {
+ if ($i == 0) {
+ $recommendationsMessage .= trim($value->innertext) . '.';
+ } else {
+ $recommendationsMessage .= ' ' . trim($value->innertext) . '.';
+ }
+ $i++;
+ }
+
+ $lastRecommendationsDom = $recommendationsDom->find('.col-md-6', -1);
+ $informationHeaderMessage = $lastRecommendationsDom->find('.heading', 0)->innertext;
+ $indice = $lastRecommendationsDom->find('.current-indice .indice div', 0)->innertext;
+ $informationDescriptionMessage = $lastRecommendationsDom->find('.current-indice .description p', 0)->innertext;
+
+ $message = "$generalMessage L'indice est de $indice/10. $informationDescriptionMessage. $recommendationsMessage";
+ $city = $this->getInput('city');
+
+ $item['uri'] = $uri;
+ $today = date('d/m/Y');
+ $item['title'] = "Bulletin de l'air du $today pour la ville : $city.";
+ //$item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-occitanie.org #QualiteAir. ' . $message;
+ $item['title'] .= ' #QualiteAir. ' . $message;
+ $item['author'] = 'floviolleau';
+ $item['content'] = $message;
+ $item['uid'] = hash('sha256', $item['title']);
+
+ $this->items[] = $item;
+ }
}
diff --git a/bridges/AutoJMBridge.php b/bridges/AutoJMBridge.php
index 9a68dbdd..c9aaa660 100644
--- a/bridges/AutoJMBridge.php
+++ b/bridges/AutoJMBridge.php
@@ -1,135 +1,135 @@
<?php
-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 = 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 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 = array(
- 'X-Requested-With: XMLHttpRequest'
- );
-
- // Get the JSON content of the form
- $json = getContents($search_url, $header);
-
- // Extract the HTML content from the JSON result
- $data = json_decode($json);
-
- $nb_results = $data->nbResults;
- $total_pages = ceil($nb_results / 15);
-
- // 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);
-
- // 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 = '';
- }
-
- // 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;
- }
+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;
+
+ 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 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'
+ ];
+
+ // Get the JSON content of the form
+ $json = getContents($search_url, $header);
+
+ // Extract the HTML content from the JSON result
+ $data = json_decode($json);
+
+ $nb_results = $data->nbResults;
+ $total_pages = ceil($nb_results / 15);
+
+ // 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);
+
+ // 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 = '';
+ }
+
+ // 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;
+ }
+ }
+ }
+
+ 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;
+ }
}
diff --git a/bridges/AwwwardsBridge.php b/bridges/AwwwardsBridge.php
index ad03e607..1596bec6 100644
--- a/bridges/AwwwardsBridge.php
+++ b/bridges/AwwwardsBridge.php
@@ -1,54 +1,63 @@
<?php
-class AwwwardsBridge extends BridgeAbstract {
- const NAME = 'Awwwards';
- const URI = 'https://www.awwwards.com/';
- const DESCRIPTION = 'Fetches the latest ten sites of the day from Awwwards';
- const MAINTAINER = 'Paroleen';
- const CACHE_TIMEOUT = 3600;
-
- const SITESURI = 'https://www.awwwards.com/websites/sites_of_the_day/';
- const SITEURI = 'https://www.awwwards.com/sites/';
- const ASSETSURI = 'https://assets.awwwards.com/awards/media/cache/thumb_417_299/';
-
- private $sites = array();
-
- public function getIcon() {
- return 'https://www.awwwards.com/favicon.ico';
- }
-
- private function fetchSites() {
- Debug::log('Fetching all sites');
- $sites = getSimpleHTMLDOM(self::SITESURI);
-
- Debug::log('Parsing all JSON data');
- foreach($sites->find('li[data-model]') as $site) {
- $decode = html_entity_decode($site->attr['data-model'],
- ENT_QUOTES, 'utf-8');
- $decode = json_decode($decode, true);
- $this->sites[] = $decode;
- }
- }
-
- public function collectData() {
- $this->fetchSites();
-
- Debug::log('Building RSS feed');
- foreach($this->sites as $site) {
- $item = array();
- $item['title'] = $site['title'];
- $item['timestamp'] = $site['createdAt'];
- $item['categories'] = $site['tags'];
-
- $item['content'] = '<img src="'
- . self::ASSETSURI
- . $site['images']['thumbnail']
- . '">';
- $item['uri'] = self::SITEURI . $site['slug'];
-
- $this->items[] = $item;
-
- if(count($this->items) >= 10)
- break;
- }
- }
+
+class AwwwardsBridge extends BridgeAbstract
+{
+ const NAME = 'Awwwards';
+ const URI = 'https://www.awwwards.com/';
+ const DESCRIPTION = 'Fetches the latest ten sites of the day from Awwwards';
+ const MAINTAINER = 'Paroleen';
+ const CACHE_TIMEOUT = 3600;
+
+ const SITESURI = 'https://www.awwwards.com/websites/sites_of_the_day/';
+ const SITEURI = 'https://www.awwwards.com/sites/';
+ const ASSETSURI = 'https://assets.awwwards.com/awards/media/cache/thumb_417_299/';
+
+ private $sites = [];
+
+ public function getIcon()
+ {
+ return 'https://www.awwwards.com/favicon.ico';
+ }
+
+ private function fetchSites()
+ {
+ Debug::log('Fetching all sites');
+ $sites = getSimpleHTMLDOM(self::SITESURI);
+
+ Debug::log('Parsing all JSON data');
+ foreach ($sites->find('li[data-model]') as $site) {
+ $decode = html_entity_decode(
+ $site->attr['data-model'],
+ ENT_QUOTES,
+ 'utf-8'
+ );
+ $decode = json_decode($decode, true);
+ $this->sites[] = $decode;
+ }
+ }
+
+ public function collectData()
+ {
+ $this->fetchSites();
+
+ Debug::log('Building RSS feed');
+ foreach ($this->sites as $site) {
+ $item = [];
+ $item['title'] = $site['title'];
+ $item['timestamp'] = $site['createdAt'];
+ $item['categories'] = $site['tags'];
+
+ $item['content'] = '<img src="'
+ . self::ASSETSURI
+ . $site['images']['thumbnail']
+ . '">';
+ $item['uri'] = self::SITEURI . $site['slug'];
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10) {
+ break;
+ }
+ }
+ }
}
diff --git a/bridges/BAEBridge.php b/bridges/BAEBridge.php
index 80c08362..6807d548 100644
--- a/bridges/BAEBridge.php
+++ b/bridges/BAEBridge.php
@@ -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->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;
- }
- }
- }
+ $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 removeAccents($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;
+ }
}
diff --git a/bridges/BadDragonBridge.php b/bridges/BadDragonBridge.php
index dd3de6b4..2260bbd6 100644
--- a/bridges/BadDragonBridge.php
+++ b/bridges/BadDragonBridge.php
@@ -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';
- }
- }
-
- public function detectParameters($url) {
- $params = array();
-
- // Sale
- $regex = '/^(https?:\/\/)?bad-dragon\.com\/sales/';
- if(preg_match($regex, $url, $matches) > 0) {
- return $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);
-
- $this->setParam($urlParams, $params, 'type', 'ready_made');
- $this->setParam($urlParams, $params, 'type', 'flop');
-
- 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, '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['category'])) {
- $params['category'] = strtolower($urlParams['category']);
- } else{
- $params['category'] = 'all';
- }
-
- $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['price'])) {
- isset($urlParams['price']['max'])
- && $params['maxprice'] = $urlParams['price']['max'];
- isset($urlParams['price']['min'])
- && $params['minprice'] = $urlParams['price']['min'];
- }
-
- 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 $params;
- }
-
- return null;
- }
-
- 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 getURI() {
- switch($this->queriedContext) {
- case 'Sales':
- return self::URI . 'sales';
- case 'Clearance':
- return $this->inputToURL();
- default:
- return parent::getURI();
- }
- }
-
- public function collectData() {
- switch($this->queriedContext) {
- case 'Sales':
- $sales = json_decode(getContents(self::URI . 'api/sales'));
-
- foreach($sales as $sale) {
- $item = array();
-
- $item['title'] = $sale->title;
- $item['timestamp'] = strtotime($sale->startDate);
-
- $item['uri'] = $this->getURI() . '/' . $sale->slug;
-
- $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;
-
- $this->items[] = $item;
- }
- break;
- case 'Clearance':
- $toyData = json_decode(getContents($this->inputToURL(true)));
-
- $productList = json_decode(getContents(self::URI
- . 'api/inventory-toy/product-list'));
-
- foreach($toyData->toys as $toy) {
- $item = array();
-
- $item['uri'] = $this->getURI()
- . '#'
- . $toy->id;
- $item['timestamp'] = strtotime($toy->created);
-
- foreach($productList as $product) {
- if($product->sku == $toy->sku) {
- $item['title'] = $product->name;
- break;
- }
- }
-
- // 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;
-
- $enclosures = array();
- foreach($toy->images as $image) {
- $enclosures[] = $image->fullFilename;
- }
- $item['enclosures'] = $enclosures;
-
- $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;
-
- $this->items[] = $item;
- }
- break;
- }
- }
-
- private function inputToURL($api = false) {
- $url = self::URI;
- $url .= ($api ? 'api/inventory-toys?' : 'shop/clearance?');
-
- // Default parameters
- $url .= 'limit=60';
- $url .= '&page=1';
- $url .= '&sort[field]=created';
- $url .= '&sort[direction]=desc';
-
- // Product types
- $url .= ($this->getInput('ready_made') ? '&type[]=ready_made' : '');
- $url .= ($this->getInput('flop') ? '&type[]=flop' : '');
-
- // Product names
- foreach(array_filter(explode(',', $this->getInput('skus'))) as $sku) {
- $url .= '&skus[]=' . urlencode(trim($sku));
- }
-
- // 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' : '');
-
- // Category
- $url .= ($this->getInput('category') ? '&category='
- . urlencode($this->getInput('category')) : '');
-
- // 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' : '');
- }
-
- // 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');
-
- // Features
- $url .= ($this->getInput('cumtube') ? '&cumtube=1' : '');
- $url .= ($this->getInput('suctionCup') ? '&suctionCup=1' : '');
- $url .= ($this->getInput('noAccessories') ? '&noAccessories=1' : '');
-
- return $url;
- }
+
+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'
+ ]
+ ]
+ ];
+
+ /*
+ * 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';
+ }
+ }
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ // Sale
+ $regex = '/^(https?:\/\/)?bad-dragon\.com\/sales/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ return $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);
+
+ $this->setParam($urlParams, $params, 'type', 'ready_made');
+ $this->setParam($urlParams, $params, 'type', 'flop');
+
+ 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);
+ }
+
+ $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['category'])) {
+ $params['category'] = strtolower($urlParams['category']);
+ } else {
+ $params['category'] = 'all';
+ }
+
+ $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['price'])) {
+ isset($urlParams['price']['max'])
+ && $params['maxprice'] = $urlParams['price']['max'];
+ isset($urlParams['price']['min'])
+ && $params['minprice'] = $urlParams['price']['min'];
+ }
+
+ 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 $params;
+ }
+
+ return null;
+ }
+
+ 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 getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Sales':
+ return self::URI . 'sales';
+ case 'Clearance':
+ return $this->inputToURL();
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function collectData()
+ {
+ switch ($this->queriedContext) {
+ case 'Sales':
+ $sales = json_decode(getContents(self::URI . 'api/sales'));
+
+ foreach ($sales as $sale) {
+ $item = [];
+
+ $item['title'] = $sale->title;
+ $item['timestamp'] = strtotime($sale->startDate);
+
+ $item['uri'] = $this->getURI() . '/' . $sale->slug;
+
+ $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;
+
+ $this->items[] = $item;
+ }
+ break;
+ case 'Clearance':
+ $toyData = json_decode(getContents($this->inputToURL(true)));
+
+ $productList = json_decode(getContents(self::URI
+ . 'api/inventory-toy/product-list'));
+
+ foreach ($toyData->toys as $toy) {
+ $item = [];
+
+ $item['uri'] = $this->getURI()
+ . '#'
+ . $toy->id;
+ $item['timestamp'] = strtotime($toy->created);
+
+ foreach ($productList as $product) {
+ if ($product->sku == $toy->sku) {
+ $item['title'] = $product->name;
+ break;
+ }
+ }
+
+ // 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;
+
+ $enclosures = [];
+ foreach ($toy->images as $image) {
+ $enclosures[] = $image->fullFilename;
+ }
+ $item['enclosures'] = $enclosures;
+
+ $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;
+
+ $this->items[] = $item;
+ }
+ break;
+ }
+ }
+
+ private function inputToURL($api = false)
+ {
+ $url = self::URI;
+ $url .= ($api ? 'api/inventory-toys?' : 'shop/clearance?');
+
+ // Default parameters
+ $url .= 'limit=60';
+ $url .= '&page=1';
+ $url .= '&sort[field]=created';
+ $url .= '&sort[direction]=desc';
+
+ // Product types
+ $url .= ($this->getInput('ready_made') ? '&type[]=ready_made' : '');
+ $url .= ($this->getInput('flop') ? '&type[]=flop' : '');
+
+ // Product names
+ foreach (array_filter(explode(',', $this->getInput('skus'))) as $sku) {
+ $url .= '&skus[]=' . urlencode(trim($sku));
+ }
+
+ // 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' : '');
+
+ // Category
+ $url .= ($this->getInput('category') ? '&category='
+ . urlencode($this->getInput('category')) : '');
+
+ // 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' : '');
+ }
+
+ // 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');
+
+ // Features
+ $url .= ($this->getInput('cumtube') ? '&cumtube=1' : '');
+ $url .= ($this->getInput('suctionCup') ? '&suctionCup=1' : '');
+ $url .= ($this->getInput('noAccessories') ? '&noAccessories=1' : '');
+
+ return $url;
+ }
}
diff --git a/bridges/BakaUpdatesMangaReleasesBridge.php b/bridges/BakaUpdatesMangaReleasesBridge.php
index aa4ab967..10d59c83 100644
--- a/bridges/BakaUpdatesMangaReleasesBridge.php
+++ b/bridges/BakaUpdatesMangaReleasesBridge.php
@@ -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 = '';
-
- public function collectData() {
- if($this -> queriedContext == 'By series')
- $this -> collectDataBySeries();
- else //queriedContext == 'By list'
- $this -> collectDataByList();
- }
-
- 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;
-
- return self::URI;
- }
-
- public function getName(){
- if(!empty($this->feedName)) {
- return $this->feedName . ' - ' . self::NAME;
- }
- return parent::getName();
- }
-
- private function getSanitizedHash($string) {
- return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string))));
- }
-
- private function filterText($text) {
- return rtrim($text, '* ');
- }
-
- private function filterHTML($text) {
- return $this->filterText(html_entity_decode($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 collectDataBySeries() {
- $html = getSimpleHTMLDOM($this->getURI());
-
- // 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');
-
- $rows = array_slice(
- array_chunk($cols, self::LIMIT_COLS), 0, self::LIMIT_ITEMS
- );
-
- if (isset($rows[0][1])) {
- $this->feedName = $this->filterHTML($rows[0][1]->plaintext);
- }
-
- foreach($rows as $cols) {
- if (count($cols) < self::LIMIT_COLS) continue;
-
- $item = array();
- $title = array();
-
- $item['content'] = '';
-
- $objDate = $cols[0];
- if ($objDate)
- $item['timestamp'] = strtotime($objDate->plaintext);
-
- $objTitle = $cols[1];
- if ($objTitle) {
- $title[] = $this->filterHTML($objTitle->plaintext);
- $item['content'] .= '<p>Series: ' . $this->filterText($objTitle->innertext) . '</p>';
- }
-
- $objVolume = $cols[2];
- if ($objVolume && !empty($objVolume->plaintext))
- $title[] = 'Vol.' . $objVolume->plaintext;
-
- $objChapter = $cols[3];
- if ($objChapter && !empty($objChapter->plaintext))
- $title[] = 'Chp.' . $objChapter->plaintext;
-
- $objAuthor = $cols[4];
- if ($objAuthor && !empty($objAuthor->plaintext)) {
- $item['author'] = $this->filterHTML($objAuthor->plaintext);
- $item['content'] .= '<p>Groups: ' . $this->filterText($objAuthor->innertext) . '</p>';
- }
-
- $item['title'] = implode(' ', $title);
- $item['uri'] = $this->getURI();
- $item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']);
-
- $this->items[] = $item;
- }
- }
-
- private function collectDataByList() {
- $this -> feedName = 'Releases';
- $list = array();
-
- $releasesHTML = getSimpleHTMLDOM(self::RELEASES_URL);
-
- $list_id = $this -> getInput('list_id');
- $listHTML = getSimpleHTMLDOM('https://www.mangaupdates.com/mylist.html?id=' . $list_id);
-
- //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);
- }
-
- //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);
-
- 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 = array();
- $title = array();
-
- $item['content'] = '';
-
- $objTitle = $cols[0];
- if ($objTitle) {
- $title[] = $this->filterHTML($objTitle->plaintext);
- $item['content'] .= '<p>Series: ' . $this->filterHTML($objTitle -> innertext) . '</p>';
- }
-
- $objVolChap = $cols[1];
- if ($objVolChap && !empty($objVolChap->plaintext))
- $title[] = $this -> filterHTML($objVolChap -> innertext);
-
- $objAuthor = $cols[2];
- if ($objAuthor && !empty($objAuthor->plaintext)) {
- $item['author'] = $this->filterHTML($objAuthor -> plaintext);
- $item['content'] .= '<p>Groups: ' . $this->filterHTML($objAuthor -> innertext) . '</p>';
- }
-
- $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;
- }
- }
+
+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';
+
+ private $feedName = '';
+
+ public function collectData()
+ {
+ if ($this -> queriedContext == 'By series') {
+ $this -> collectDataBySeries();
+ } else { //queriedContext == 'By list'
+ $this -> collectDataByList();
+ }
+ }
+
+ 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;
+ }
+
+ return self::URI;
+ }
+
+ public function getName()
+ {
+ if (!empty($this->feedName)) {
+ return $this->feedName . ' - ' . self::NAME;
+ }
+ return parent::getName();
+ }
+
+ private function getSanitizedHash($string)
+ {
+ return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string))));
+ }
+
+ private function filterText($text)
+ {
+ return rtrim($text, '* ');
+ }
+
+ private function filterHTML($text)
+ {
+ return $this->filterText(html_entity_decode($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 collectDataBySeries()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ // 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');
+ }
+
+ $rows = array_slice(
+ array_chunk($cols, self::LIMIT_COLS),
+ 0,
+ self::LIMIT_ITEMS
+ );
+
+ if (isset($rows[0][1])) {
+ $this->feedName = $this->filterHTML($rows[0][1]->plaintext);
+ }
+
+ foreach ($rows as $cols) {
+ if (count($cols) < self::LIMIT_COLS) {
+ continue;
+ }
+
+ $item = [];
+ $title = [];
+
+ $item['content'] = '';
+
+ $objDate = $cols[0];
+ if ($objDate) {
+ $item['timestamp'] = strtotime($objDate->plaintext);
+ }
+
+ $objTitle = $cols[1];
+ if ($objTitle) {
+ $title[] = $this->filterHTML($objTitle->plaintext);
+ $item['content'] .= '<p>Series: ' . $this->filterText($objTitle->innertext) . '</p>';
+ }
+
+ $objVolume = $cols[2];
+ if ($objVolume && !empty($objVolume->plaintext)) {
+ $title[] = 'Vol.' . $objVolume->plaintext;
+ }
+
+ $objChapter = $cols[3];
+ if ($objChapter && !empty($objChapter->plaintext)) {
+ $title[] = 'Chp.' . $objChapter->plaintext;
+ }
+
+ $objAuthor = $cols[4];
+ if ($objAuthor && !empty($objAuthor->plaintext)) {
+ $item['author'] = $this->filterHTML($objAuthor->plaintext);
+ $item['content'] .= '<p>Groups: ' . $this->filterText($objAuthor->innertext) . '</p>';
+ }
+
+ $item['title'] = implode(' ', $title);
+ $item['uri'] = $this->getURI();
+ $item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']);
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function collectDataByList()
+ {
+ $this -> feedName = 'Releases';
+ $list = [];
+
+ $releasesHTML = getSimpleHTMLDOM(self::RELEASES_URL);
+
+ $list_id = $this -> getInput('list_id');
+ $listHTML = getSimpleHTMLDOM('https://www.mangaupdates.com/mylist.html?id=' . $list_id);
+
+ //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);
+ }
+
+ //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);
+
+ 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 = [];
+ $title = [];
+
+ $item['content'] = '';
+
+ $objTitle = $cols[0];
+ if ($objTitle) {
+ $title[] = $this->filterHTML($objTitle->plaintext);
+ $item['content'] .= '<p>Series: ' . $this->filterHTML($objTitle -> innertext) . '</p>';
+ }
+
+ $objVolChap = $cols[1];
+ if ($objVolChap && !empty($objVolChap->plaintext)) {
+ $title[] = $this -> filterHTML($objVolChap -> innertext);
+ }
+
+ $objAuthor = $cols[2];
+ if ($objAuthor && !empty($objAuthor->plaintext)) {
+ $item['author'] = $this->filterHTML($objAuthor -> plaintext);
+ $item['content'] .= '<p>Groups: ' . $this->filterHTML($objAuthor -> innertext) . '</p>';
+ }
+
+ $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;
+ }
+ }
}
diff --git a/bridges/BandcampBridge.php b/bridges/BandcampBridge.php
index 181038d1..20b4ea93 100644
--- a/bridges/BandcampBridge.php
+++ b/bridges/BandcampBridge.php
@@ -1,408 +1,420 @@
<?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;
-
- private $feedName;
-
- public function getIcon() {
- return 'https://s4.bcbits.com/img/bc_favicon.ico';
- }
-
- public function collectData(){
- switch($this->queriedContext) {
- case 'By tag':
- $url = self::URI . 'api/hub/1/dig_deeper';
- $data = $this->buildRequestJson();
- $header = array(
- 'Content-Type: application/json',
- 'Content-Length: ' . strlen($data)
- );
- $opts = array(
- CURLOPT_CUSTOMREQUEST => 'POST',
- CURLOPT_POSTFIELDS => $data
- );
- $content = getContents($url, $header, $opts);
-
- $json = json_decode($content);
-
- 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;
-
- $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);
-
- 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];
-
- $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);
-
- $num_albums = min(count($band_data->discography), $this->getInput('limit'));
- for($i = 0; $i < $num_albums; $i++) {
- $album_basic_data = $band_data->discography[$i];
-
- // 'a' or 't' for albums and individual tracks respectively
- $tralbum_type = substr($album_basic_data->item_type, 0, 1);
-
- $query_data = array(
- 'band_id' => $band_id,
- 'tralbum_type' => $tralbum_type,
- 'tralbum_id' => $album_basic_data->item_id
- );
- $tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
- }
- break;
- case 'By album':
- $regex = '/album=(\d+)/';
- if(preg_match($regex, $html, $matches) == false)
- returnServerError('Unable to find album ID on: ' . $this->getURI());
- $album_id = $matches[1];
-
- $query_data = array(
- 'band_id' => $band_id,
- 'tralbum_type' => 'a',
- 'tralbum_id' => $album_id
- );
- $tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
-
- break;
- }
-
- foreach ($tralbums as $tralbum_data) {
- if ($tralbum_data->type === 'a' && $this->getInput('type') === 'tracks') {
- foreach ($tralbum_data->tracks as $track) {
- $query_data = array(
- 'band_id' => $band_id,
- 'tralbum_type' => 't',
- 'tralbum_id' => $track->track_id
- );
- $track_data = $this->apiGet('mobile/22/tralbum_details', $query_data);
-
- $this->items[] = $this->buildTralbumItem($track_data);
- }
- } else {
- $this->items[] = $this->buildTralbumItem($tralbum_data);
- }
- }
- break;
- }
- }
-
- private function buildTralbumItem($tralbum_data){
- $band_data = $tralbum_data->band;
-
- // Format title like: ARTIST - ALBUM/TRACK (OPTIONAL RELEASER)
- // Format artist/author like: ARTIST (OPTIONAL RELEASER)
- //
- // If the album/track is released under a label/a band other than the artist
- // themselves, append that releaser name to the title and artist/author.
- //
- // This sadly doesn't always work right for individual tracks as the artist
- // of the track is always set to the releaser.
- $artist = $tralbum_data->tralbum_artist;
- $full_title = $artist . ' - ' . $tralbum_data->title;
- $full_artist = $artist;
- if (isset($tralbum_data->label)) {
- $full_title .= ' (' . $tralbum_data->label . ')';
- $full_artist .= ' (' . $tralbum_data->label . ')';
- } elseif ($band_data->name !== $artist) {
- $full_title .= ' (' . $band_data->name . ')';
- $full_artist .= ' (' . $band_data->name . ')';
- }
-
- $small_img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_300PX);
- $img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_700PX);
-
- $item = array(
- 'uri' => $tralbum_data->bandcamp_url,
- 'author' => $full_artist,
- 'title' => $full_title,
- 'enclosures' => array($img),
- 'timestamp' => $tralbum_data->release_date
- );
-
- $item['categories'] = array();
- foreach ($tralbum_data->tags as $tag) {
- $item['categories'][] = $tag->norm_name;
- }
-
- // Give articles a unique UID depending on its track list
- // Releases should then show up as new articles when tracks are added
- if ($this->getInput('type') === 'changes') {
- $item['uid'] = "bandcamp/$band_data->band_id/$tralbum_data->id/";
- foreach ($tralbum_data->tracks as $track) {
- $item['uid'] .= $track->track_id;
- }
- }
-
- $item['content'] = "<img src='$small_img' /><br/>$full_title<br/>";
- if ($tralbum_data->type === 'a') {
- $item['content'] .= '<ol>';
- foreach ($tralbum_data->tracks as $track) {
- $item['content'] .= "<li>$track->title</li>";
- }
- $item['content'] .= '</ol>';
- }
- if (!empty($tralbum_data->about)) {
- $item['content'] .= '<p>'
- . nl2br($tralbum_data->about)
- . '</p>';
- }
-
- return $item;
- }
-
- private function buildRequestJson(){
- $requestJson = array(
- '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 apiGet($endpoint, $query_data) {
- $url = self::URI . 'api/' . $endpoint . '?' . http_build_query($query_data);
- $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;
- }
-
- 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;
- }
-
- return parent::getName();
- }
-
- public function detectParameters($url) {
- $params = array();
-
- // By tag
- $regex = '/^(https?:\/\/)?bandcamp\.com\/tag\/([^\/.&?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['tag'] = urldecode($matches[2]);
- return $params;
- }
-
- // By band
- $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['band'] = urldecode($matches[2]);
- return $params;
- }
-
- // By album
- $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['band'] = urldecode($matches[2]);
- $params['album'] = urldecode($matches[3]);
- return $params;
- }
-
- return null;
- }
+
+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;
+
+ 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 = [
+ 'Content-Type: application/json',
+ 'Content-Length: ' . strlen($data)
+ ];
+ $opts = [
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_POSTFIELDS => $data
+ ];
+ $content = getContents($url, $header, $opts);
+
+ $json = json_decode($content);
+
+ 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;
+
+ $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 = [
+ '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);
+ }
+
+ $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 = [];
+ 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];
+
+ // 'a' or 't' for albums and individual tracks respectively
+ $tralbum_type = substr($album_basic_data->item_type, 0, 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 = [
+ 'band_id' => $band_id,
+ 'tralbum_type' => 'a',
+ 'tralbum_id' => $album_id
+ ];
+ $tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
+
+ break;
+ }
+
+ foreach ($tralbums as $tralbum_data) {
+ if ($tralbum_data->type === 'a' && $this->getInput('type') === 'tracks') {
+ foreach ($tralbum_data->tracks as $track) {
+ $query_data = [
+ 'band_id' => $band_id,
+ 'tralbum_type' => 't',
+ 'tralbum_id' => $track->track_id
+ ];
+ $track_data = $this->apiGet('mobile/22/tralbum_details', $query_data);
+
+ $this->items[] = $this->buildTralbumItem($track_data);
+ }
+ } else {
+ $this->items[] = $this->buildTralbumItem($tralbum_data);
+ }
+ }
+ break;
+ }
+ }
+
+ private function buildTralbumItem($tralbum_data)
+ {
+ $band_data = $tralbum_data->band;
+
+ // Format title like: ARTIST - ALBUM/TRACK (OPTIONAL RELEASER)
+ // Format artist/author like: ARTIST (OPTIONAL RELEASER)
+ //
+ // If the album/track is released under a label/a band other than the artist
+ // themselves, append that releaser name to the title and artist/author.
+ //
+ // This sadly doesn't always work right for individual tracks as the artist
+ // of the track is always set to the releaser.
+ $artist = $tralbum_data->tralbum_artist;
+ $full_title = $artist . ' - ' . $tralbum_data->title;
+ $full_artist = $artist;
+ if (isset($tralbum_data->label)) {
+ $full_title .= ' (' . $tralbum_data->label . ')';
+ $full_artist .= ' (' . $tralbum_data->label . ')';
+ } elseif ($band_data->name !== $artist) {
+ $full_title .= ' (' . $band_data->name . ')';
+ $full_artist .= ' (' . $band_data->name . ')';
+ }
+
+ $small_img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_300PX);
+ $img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_700PX);
+
+ $item = [
+ 'uri' => $tralbum_data->bandcamp_url,
+ 'author' => $full_artist,
+ 'title' => $full_title,
+ 'enclosures' => [$img],
+ 'timestamp' => $tralbum_data->release_date
+ ];
+
+ $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;
+ }
+ }
+
+ $item['content'] = "<img src='$small_img' /><br/>$full_title<br/>";
+ if ($tralbum_data->type === 'a') {
+ $item['content'] .= '<ol>';
+ foreach ($tralbum_data->tracks as $track) {
+ $item['content'] .= "<li>$track->title</li>";
+ }
+ $item['content'] .= '</ol>';
+ }
+ if (!empty($tralbum_data->about)) {
+ $item['content'] .= '<p>'
+ . nl2br($tralbum_data->about)
+ . '</p>';
+ }
+
+ return $item;
+ }
+
+ private function buildRequestJson()
+ {
+ $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 apiGet($endpoint, $query_data)
+ {
+ $url = self::URI . 'api/' . $endpoint . '?' . http_build_query($query_data);
+ $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;
+ }
+
+ 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;
+ }
+
+ return parent::getName();
+ }
+
+ 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 band
+ $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['band'] = urldecode($matches[2]);
+ return $params;
+ }
+
+ // By album
+ $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['band'] = urldecode($matches[2]);
+ $params['album'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ return null;
+ }
}
diff --git a/bridges/BandcampDailyBridge.php b/bridges/BandcampDailyBridge.php
index 827cf9cd..57299a17 100644
--- a/bridges/BandcampDailyBridge.php
+++ b/bridges/BandcampDailyBridge.php
@@ -1,159 +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(
- 'best-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(
- 'genres-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(
- 'franchises-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
-
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI())
- or returnServerError('Could not request: ' . $this->getURI());
-
- $html = defaultLinkTo($html, self::URI);
-
- $articles = $html->find('articles-list', 0);
-
- foreach($articles->find('div.list-article') as $index => $article) {
- $item = array();
-
- $articlePath = $article->find('a.title', 0)->href;
-
- $articlePageHtml = getSimpleHTMLDOMCached($articlePath, 3600)
- or returnServerError('Could not request: ' . $articlePath);
-
- $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;
-
- if ($articlePageHtml->find('meta[property="article:tag"]', 0)) {
- $item['categories'][] = $articlePageHtml->find('meta[property="article:tag"]', 0)->content;
- }
-
- $item['enclosures'][] = $articlePageHtml->find('meta[name="twitter:image"]', 0)->content;
-
- $this->items[] = $item;
-
- if (count($this->items) >= 10) {
- break;
- }
- }
- }
-
- 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();
- }
- }
-
- 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($contentKey)] . ' - Bandcamp Daily';
- default:
- return parent::getName();
- }
- }
+
+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',
+ ],
+ ]
+ ];
+
+ const CACHE_TIMEOUT = 3600; // 1 hour
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request: ' . $this->getURI());
+
+ $html = defaultLinkTo($html, self::URI);
+
+ $articles = $html->find('articles-list', 0);
+
+ foreach ($articles->find('div.list-article') as $index => $article) {
+ $item = [];
+
+ $articlePath = $article->find('a.title', 0)->href;
+
+ $articlePageHtml = getSimpleHTMLDOMCached($articlePath, 3600)
+ or returnServerError('Could not request: ' . $articlePath);
+
+ $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;
+
+ if ($articlePageHtml->find('meta[property="article:tag"]', 0)) {
+ $item['categories'][] = $articlePageHtml->find('meta[property="article:tag"]', 0)->content;
+ }
+
+ $item['enclosures'][] = $articlePageHtml->find('meta[name="twitter:image"]', 0)->content;
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10) {
+ break;
+ }
+ }
+ }
+
+ 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();
+ }
+ }
+
+ 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($contentKey)] . ' - Bandcamp Daily';
+ default:
+ return parent::getName();
+ }
+ }
}
diff --git a/bridges/BastaBridge.php b/bridges/BastaBridge.php
index b8174c60..4c0df273 100644
--- a/bridges/BastaBridge.php
+++ b/bridges/BastaBridge.php
@@ -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++;
+ }
+ }
+ }
}
diff --git a/bridges/BinanceBridge.php b/bridges/BinanceBridge.php
index 573a2172..73dbf0b9 100644
--- a/bridges/BinanceBridge.php
+++ b/bridges/BinanceBridge.php
@@ -1,41 +1,45 @@
<?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';
- }
-
- public function collectData() {
- $html = getSimpleHTMLDOM(self::URI)
- or returnServerError('Could not fetch Binance blog data.');
-
- $appData = $html->find('script[id="__APP_DATA"]');
- $appDataJson = json_decode($appData[0]->innertext);
-
- foreach($appDataJson->pageData->redux->blogList->blogList as $element) {
-
- $date = $element->postTime;
- $abstract = $element->brief;
- $uri = self::URI . '/' . $element->lang . '/blog/' . $element->idStr;
- $title = $element->title;
- $content = $element->content;
-
- $item = array();
- $item['title'] = $title;
- $item['uri'] = $uri;
- $item['timestamp'] = substr($date, 0, -3);
- $item['author'] = 'Binance';
- $item['content'] = $content;
-
- $this->items[] = $item;
-
- if (count($this->items) >= 10)
- break;
- }
- }
+
+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';
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not fetch Binance blog data.');
+
+ $appData = $html->find('script[id="__APP_DATA"]');
+ $appDataJson = json_decode($appData[0]->innertext);
+
+ foreach ($appDataJson->pageData->redux->blogList->blogList as $element) {
+ $date = $element->postTime;
+ $abstract = $element->brief;
+ $uri = self::URI . '/' . $element->lang . '/blog/' . $element->idStr;
+ $title = $element->title;
+ $content = $element->content;
+
+ $item = [];
+ $item['title'] = $title;
+ $item['uri'] = $uri;
+ $item['timestamp'] = substr($date, 0, -3);
+ $item['author'] = 'Binance';
+ $item['content'] = $content;
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10) {
+ break;
+ }
+ }
+ }
}
diff --git a/bridges/BlaguesDeMerdeBridge.php b/bridges/BlaguesDeMerdeBridge.php
index 9b776407..cc0f485e 100644
--- a/bridges/BlaguesDeMerdeBridge.php
+++ b/bridges/BlaguesDeMerdeBridge.php
@@ -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;
+ }
+ }
}
diff --git a/bridges/BleepingComputerBridge.php b/bridges/BleepingComputerBridge.php
index 78ec3125..c1d3d568 100644
--- a/bridges/BleepingComputerBridge.php
+++ b/bridges/BleepingComputerBridge.php
@@ -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);
+ }
}
diff --git a/bridges/BlizzardNewsBridge.php b/bridges/BlizzardNewsBridge.php
index 156dc290..3930e0a4 100644
--- a/bridges/BlizzardNewsBridge.php
+++ b/bridges/BlizzardNewsBridge.php
@@ -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;
+ }
}
diff --git a/bridges/BookMyShowBridge.php b/bridges/BookMyShowBridge.php
index 342085b4..7064df91 100644
--- a/bridges/BookMyShowBridge.php
+++ b/bridges/BookMyShowBridge.php
@@ -1,1288 +1,1298 @@
<?php
-class BookMyShowBridge extends BridgeAbstract {
-
- const MAINTAINER = 'captn3m0';
- const NAME = 'BookMyShow Bridge';
- const URI = 'https://in.bookmyshow.com';
- const MOVIES_IMAGE_BASE_FORMAT = 'https://in.bmscdn.com/iedb/movies/images/mobile/thumbnail/large/%s.jpg';
- const DESCRIPTION = 'Returns the latest events on BookMyShow';
-
- const TIMEZONE = 'Asia/Kolkata';
-
- const PLAYS = 'PL';
- const EVENTS = 'CT';
- const MOVIES = 'MT';
-
- const CATEGORIES = [
- self::PLAYS => 'Plays',
- self::EVENTS => 'Events',
- self::MOVIES => 'Movies',
- ];
-
- const CITIES = [
- // Most popular cities
- 'Mumbai' => 'MUMBAI',
- 'National Capital Region (NCR)' => 'NCR',
- 'Bengaluru' => 'BANG',
- 'Hyderabad' => 'HYD',
- 'Ahmedabad' => 'AHD',
- 'Chandigarh' => 'CHD',
- 'Chennai' => 'CHEN',
- 'Pune' => 'PUNE',
- 'Kolkata' => 'KOLK',
- 'Kochi' => 'KOCH',
-
- // Less common cities
- 'Aalo' => 'AALU',
- 'Abohar' => 'ABOR',
- 'Abu Road' => 'ABRD',
- 'Acharapakkam' => 'ACHA',
- 'Adilabad' => 'ADIL',
- 'Agar Malwa' => 'AGOR',
- 'Agartala' => 'AGAR',
- 'Agra' => 'AGRA',
- 'Ahmedgarh' => 'AHMG',
- 'Ahmednagar' => 'AHMED',
- 'Aizawl' => 'AIZW',
- 'Ajmer' => 'AJMER',
- 'Akaltara' => 'AKAL',
- 'Akividu' => 'AKVD',
- 'Akola' => 'AKOL',
- 'Alangudi' => 'ALNI',
- 'Alappuzha' => 'ALPZ',
- 'Alathur' => 'ALAR',
- 'Alibaug' => 'ALBG',
- 'Aligarh' => 'ALI',
- 'Allagadda' => 'ALGD',
- 'Almora' => 'ALMO',
- 'Alwar' => 'ALWR',
- 'Amadalavalasa' => 'ADAM',
- 'Amalapuram' => 'AMAP',
- 'Amaravathi' => 'AVTI',
- 'Ambala' => 'AMB',
- 'Ambikapur' => 'AMBI',
- 'Ambur' => 'AMBR',
- 'Amgaon' => 'AMGN',
- 'Amravati' => 'AMRA',
- 'Amritsar' => 'AMRI',
- 'Anakapalle' => 'ANKP',
- 'Anand' => 'AND',
- 'Anantapalli' => 'ANTT',
- 'Anantapur' => 'ANAN',
- 'Anchal' => 'ANHL',
- 'Angadipuram' => 'ANDM',
- 'Angamaly' => 'ANGA',
- 'Angara' => 'ANGR',
- 'Angul' => 'ANGL',
- 'Anjad' => 'ANJA',
- 'Anjar' => 'ANJR',
- 'Anklav' => 'ANKV',
- 'Ankleshwar' => 'ANKL',
- 'Annigeri' => 'ANGI',
- 'Arakkonam' => 'ARAK',
- 'Arambagh' => 'AMBH',
- 'Aranthangi' => 'ARNT',
- 'Ariyalur' => 'ARIY',
- 'Arni' => 'ARNI',
- 'Arsikere' => 'ARSI',
- 'Aruppukottai' => 'ARUP',
- 'Asansol' => 'ASANSOL',
- 'Ashoknagar (West Bengal)' => 'ASNA',
- 'Ashoknagar' => 'AKMP',
- 'Aswaraopeta' => 'ASWA',
- 'Atpadi' => 'ATPA',
- 'Attili' => 'ATLI',
- 'Aurangabad (Bihar)' => 'AUBI',
- 'Aurangabad (West Bengal)' => 'AURW',
- 'Aurangabad' => 'AURA',
- 'Avinashi' => 'AVII',
- 'Azamgarh' => 'AZMG',
- 'B. Kothakota' => 'BKOT',
- 'Badaun' => 'BADN',
- 'Baddi' => 'BADD',
- 'Badnawar' => 'BADR',
- 'Bagbahara' => 'BBHA',
- 'Bagha Purana' => 'BAPU',
- 'Bagru' => 'BAGU',
- 'Bahadurgarh' => 'BAHD',
- 'Bahraich' => 'BHRH',
- 'Baihar' => 'BIAH',
- 'Baikunthpur' => 'BKTH',
- 'Baindur' => 'BAND',
- 'Bakhrahat' => 'BART',
- 'Balaghat' => 'BLGT',
- 'Balangir' => 'BALG',
- 'Balasore' => 'BLSR',
- 'Balijipeta' => 'BLIJ',
- 'Balod' => 'BALD',
- 'Baloda Bazar' => 'BBCH',
- 'Balotra' => 'BALO',
- 'Balrampur' => 'BLUR',
- 'Balurghat' => 'BALU',
- 'Bangarpet' => 'BAGT',
- 'Banswada' => 'BNSA',
- 'Banswara' => 'BANS',
- 'Bantumilli' => 'BANT',
- 'Barabanki' => 'BARK',
- 'Baramati' => 'BARA',
- 'Baraut' => 'BARL',
- 'Bardoli' => 'BRDL',
- 'Bareilly' => 'BARE',
- 'Bargarh' => 'BARG',
- 'Baripada' => 'BARI',
- 'Barmer' => 'BARM',
- 'Barnala' => 'BAR',
- 'Barshi' => 'BRHI',
- 'Barwani' => 'BRWN',
- 'Basna' => 'BASN',
- 'Basti' => 'BAST',
- 'Bathinda' => 'BHAT',
- 'Batlagundu' => 'BTGD',
- 'Beawar' => 'BEAW',
- 'Beed' => 'BEED',
- 'Belagavi (Belgaum)' => 'BELG',
- 'Bellampalli' => 'BELL',
- 'Bellary' => 'BLRY',
- 'Belur' => 'BELU',
- 'Bemetara' => 'BMTA',
- 'Berachampa' => 'BRAC',
- 'Berhampore' => 'BEHA',
- 'Berhampur' => 'BERP',
- 'Bestavaripeta' => 'BEST',
- 'Betul' => 'BETU',
- 'Bhadrachalam' => 'BHDR',
- 'Bhadrak' => 'BHAD',
- 'Bhadravati' => 'BDVT',
- 'Bhainsa' => 'BHAN',
- 'Bhandara' => 'BHAA',
- 'Bharamasagara' => 'BASA',
- 'Bharuch' => 'BHAR',
- 'Bhatapara' => 'BTAP',
- 'Bhatkal' => 'BAKL',
- 'Bhattiprolu' => 'BATT',
- 'Bhavnagar' => 'BHNG',
- 'Bhilai' => 'BHILAI',
- 'Bhilwara' => 'BHIL',
- 'Bhimadole' => 'BMDE',
- 'Bhimavaram' => 'BHIM',
- 'Bhiwadi' => 'BHWD',
- 'Bhiwani' => 'BHWN',
- 'Bhopal' => 'BHOP',
- 'Bhubaneswar' => 'BHUB',
- 'Bhuj' => 'BHUJ',
- 'Bhuntar' => 'BHUN',
- 'Bhupalpalle' => 'BHUP',
- 'Bhusawal' => 'BHUS',
- 'Biaora' => 'BIAR',
- 'Bidar' => 'BIDR',
- 'Bijnor' => 'BIJ',
- 'Bijoynagar' => 'BIJO',
- 'Bikaner' => 'BIK',
- 'Bilara' => 'BILR',
- 'Bilaspur (Himachal Pradesh)' => 'BIPS',
- 'Bilaspur' => 'BILA',
- 'Bilimora' => 'BILI',
- 'Biraul' => 'BIRL',
- 'Bishrampur' => 'BSRM',
- 'Bodinayakanur' => 'BODI',
- 'Boisar' => 'BOIS',
- 'Bokaro' => 'BOKA',
- 'Bolpur' => 'BLPR',
- 'Bommidi' => 'BOMM',
- 'Bongaigaon' => 'BONG',
- 'Bongaon' => 'BONI',
- 'Borsad' => 'BORM',
- 'Brahmapur' => 'KHUB',
- 'Brahmapuri' => 'BHMP',
- 'Brajrajnagar' => 'BJNG',
- 'Bulandshahr' => 'BULA',
- 'Buldana' => 'BULD',
- 'Bundu' => 'BUND',
- 'Burdwan' => 'BURD',
- 'Burhanpur' => 'BRHP',
- 'Byadagi' => 'BYAD',
- 'Chagallu' => 'CHAG',
- 'Challakere' => 'CHLA',
- 'Challapalli' => 'CHAP',
- 'Champa' => 'CHAM',
- 'Chanchal' => 'CCWC',
- 'Chandausi' => 'CHDN',
- 'Chandragiri' => 'CHAD',
- 'Chandrakona' => 'CKNA',
- 'Chandrapur' => 'CHAN',
- 'Changanassery' => 'CNSY',
- 'Channagiri' => 'CHGI',
- 'Channarayapatna' => 'CHNN',
- 'Chaygaon' => 'CHOG',
- 'Cheepurupalli' => 'CHEE',
- 'Chendrapinni' => 'CNPI',
- 'Chengannur' => 'CHEG',
- 'Chennur' => 'CHNU',
- 'Cherial' => 'CHRY',
- 'Cheyyar' => 'CHEY',
- 'Chhibramau' => 'CHHI',
- 'Chhindwara' => 'CHIN',
- 'Chickmagaluru' => 'CHKA',
- 'Chidambaram' => 'CHID',
- 'Chikkaballapur' => 'CHIK',
- 'Chikodi' => 'CHOK',
- 'Chinturu' => 'CHTN',
- 'Chirala' => 'CHIR',
- 'Chitradurga' => 'CHIT',
- 'Chittoor' => 'CHTT',
- 'Chodavaram' => 'CDVM',
- 'Chotila' => 'CHOT',
- 'Coimbatore' => 'COIM',
- 'Cooch Behar' => 'COBE',
- 'Cuddalore' => 'CUDD',
- 'Cuttack' => 'CUTT',
- 'Dabra' => 'DABR',
- 'Dahanu' => 'DHAU',
- 'Dahegam' => 'DHGM',
- 'Dahod' => 'DAHO',
- 'Dakshin Barasat' => 'DAKS',
- 'Dalli Rajhara' => 'DALL',
- 'Daman' => 'DAMA',
- 'Damoh' => 'DAMO',
- 'Darjeeling' => 'DARJ',
- 'Darsi' => 'DARS',
- 'Dasuya' => 'DASU',
- 'Dausa' => 'DAUS',
- 'Davanagere' => 'DAVA',
- 'Davuluru' => 'DVLR',
- 'Deesa' => 'DEES',
- 'Dehradun' => 'DEH',
- 'Deoghar' => 'DOGH',
- 'Devadurga' => 'DEVD',
- 'Devarakonda' => 'DEVK',
- 'Devgad' => 'DEGA',
- 'Dewas' => 'DEWAS',
- 'Dhampur' => 'DHPR',
- 'Dhamtari' => 'DHMT',
- 'Dhanbad' => 'DHAN',
- 'Dhar' => 'DARH',
- 'Dharamsala' => 'DMSL',
- 'Dharapuram' => 'DHAR',
- 'Dharmapuri' => 'DMPI',
- 'Dharmavaram' => 'DDMA',
- 'Dharwad' => 'DHAW',
- 'Dhenkanal' => 'DNAL',
- 'Dhoraji' => 'DHOR',
- 'Dhule' => 'DHLE',
- 'Dhuri' => 'DHRI',
- 'Dibrugarh' => 'DIB',
- 'Digras' => 'DIGR',
- 'Dimapur' => 'DMPR',
- 'Dindigul' => 'DIND',
- 'Doddaballapura' => 'DDBP',
- 'Domkal' => 'DMKL',
- 'Dongargarh' => 'DONG',
- 'Doraha' => 'DORH',
- 'Durg' => 'DURG',
- 'Durgapur' => 'DURGA',
- 'Edappal' => 'EDPL',
- 'Edlapadu' => 'EDLP',
- 'Eluru' => 'ELRU',
- 'Erattupetta' => 'ERAT',
- 'Ernakulam' => 'ERNK',
- 'Erode' => 'EROD',
- 'Etawah' => 'ETWH',
- 'Ettumanoor' => 'ETTU',
- 'Faizabad' => 'FAZA',
- 'Falna' => 'FALN',
- 'Faridkot' => 'DKOT',
- 'Fatehgarh Sahib' => 'FASA',
- 'Fatehpur' => 'FATE',
- 'Fatehpur(Rajasthan)' => 'FATR',
- 'Firozpur' => 'FRZR',
- 'G.Mamidada' => 'GMAD',
- 'Gadag' => 'GADG',
- 'Gadarwara' => 'GDWR',
- 'Gadchiroli' => 'GDRO',
- 'Gajendragarh' => 'GJGH',
- 'Gajwel' => 'GAJW',
- 'Ganapavaram' => 'GANP',
- 'Gandhidham' => 'GDHAM',
- 'Gandhinagar' => 'GNAGAR',
- 'Gangavati' => 'GAVT',
- 'Gangoh' => 'GANZ',
- 'Gangtok' => 'GANG',
- 'Ganjbasoda' => 'GANJ',
- 'Garla' => 'GALA',
- 'Gauribidanur' => 'GAUR',
- 'Gaya' => 'GAYA',
- 'Gingee' => 'GING',
- 'Goa' => 'GOA',
- 'Gobichettipalayam' => 'GOBI',
- 'Godavarikhani' => 'GDVK',
- 'Godhra' => 'GODH',
- 'Gokak' => 'GKGK',
- 'Gokavaram' => 'GOKM',
- 'Golaghat' => 'GHT',
- 'Gollaprolu' => 'GOLL',
- 'Gonda' => 'GOND',
- 'Gondia' => 'GNDA',
- 'Gopalganj' => 'GOPG',
- 'Gorakhpur' => 'GRKP',
- 'Gorantla' => 'GORA',
- 'Gotegaon' => 'GTGN',
- 'Gownipalli' => 'GOWP',
- 'Gudivada' => 'GUDI',
- 'Gudiyatham' => 'GDTM',
- 'Gudur' => 'GUDR',
- 'Gulaothi' => 'GULL',
- 'Guledgudda' => 'GULD',
- 'Gummadidala' => 'GUMM',
- 'Guna' => 'GUNA',
- 'Guntakal' => 'GUNL',
- 'Guntur' => 'GUNT',
- 'Gurazala' => 'GURZ',
- 'Guwahati' => 'GUW',
- 'Gwalior' => 'GWAL',
- 'Habra' => 'HARR',
- 'Hagaribommanahalli' => 'HHGG',
- 'Hajipur' => 'HAJI',
- 'Haldia' => 'HLDI',
- 'Haldwani' => 'HALD',
- 'Haliya' => 'HALI',
- 'Hampi' => 'HMPI',
- 'Hardoi' => 'HRDI',
- 'Haridwar' => 'HRDR',
- 'Harihar' => 'HRRR',
- 'Haripad' => 'HRPD',
- 'Harugeri' => 'HARU',
- 'Hasanpur' => 'HANS',
- 'Hazaribagh' => 'HAZA',
- 'Himmatnagar' => 'HIMM',
- 'Hindaun City' => 'HIND',
- 'Hisar' => 'HISR',
- 'Honnali' => 'HONV',
- 'Honnavara' => 'HNVR',
- 'Hooghly' => 'HOOG',
- 'Hoshiarpur' => 'HOSH',
- 'Hoskote' => 'HOKT',
- 'Hospet' => 'HOSP',
- 'Hosur' => 'HSUR',
- 'Howrah' => 'HWRH',
- 'Hubballi (Hubli)' => 'HUBL',
- 'Huvinahadagali' => 'HULI',
- 'Ichalkaranji' => 'ICHL',
- 'Ichchapuram' => 'ICPR',
- 'Idappadi' => 'IDPI',
- 'Idar' => 'IDAR',
- 'Indapur' => 'INDA',
- 'Indi' => 'IIND',
- 'Indore' => 'IND',
- 'Irinjalakuda' => 'IRNK',
- 'Itanagar' => 'ITNG',
- 'Itarsi' => 'ITAR',
- 'Jabalpur' => 'JABL',
- 'Jadcherla' => 'JADC',
- 'Jagalur' => 'JAGA',
- 'Jagatdal' => 'JGDL',
- 'Jagdalpur' => 'JAGD',
- 'Jaggampeta' => 'JAGG',
- 'Jaggayyapeta' => 'JGGY',
- 'Jagtial' => 'JGTL',
- 'Jaipur' => 'JAIP',
- 'Jaisalmer' => 'JSMR',
- 'Jajpur Road' => 'JAJP',
- 'Jalakandapuram' => 'JAKA',
- 'Jalalabad' => 'JLAB',
- 'Jalandhar' => 'JALA',
- 'Jalgaon' => 'JALG',
- 'Jalna' => 'JALN',
- 'Jalpaiguri' => 'JPG',
- 'Jami' => 'JAMI',
- 'Jamkhed' => 'JAMK',
- 'Jammalamadugu' => 'JAMD',
- 'Jammu' => 'JAMM',
- 'Jamnagar' => 'JAM',
- 'Jamner' => 'JAMN',
- 'Jamshedpur' => 'JMDP',
- 'Jangaon' => 'JNGN',
- 'Jangareddy Gudem' => 'JANG',
- 'Janjgir' => 'JANR',
- 'Jasdan' => 'JASD',
- 'Jaunpur' => 'JANP',
- 'Jehanabad' => 'JEHA',
- 'Jetpur' => 'JETP',
- 'Jewar' => 'JEWR',
- 'Jeypore' => 'JEYP',
- 'Jhabua' => 'JHAB',
- 'Jhajjar' => 'JHAJ',
- 'Jhansi' => 'JNSI',
- 'Jharsuguda' => 'JRSG',
- 'Jiaganj' => 'JAGJ',
- 'Jind' => 'JIND',
- 'Jodhpur' => 'JODH',
- 'Jorhat' => 'JORT',
- 'Junagadh' => 'JUGH',
- 'Kadapa' => 'KDPA',
- 'Kadi' => 'KADI',
- 'Kaikaluru' => 'KAIK',
- 'Kaithal' => 'KAIT',
- 'Kakarapalli' => 'KAAP',
- 'Kakinada' => 'KAKI',
- 'Kalaburagi (Gulbarga)' => 'GULB',
- 'Kalimpong' => 'KALI',
- 'Kallakurichi' => 'KALL',
- 'Kalol (Panchmahal)' => 'PANH',
- 'Kalwakurthy' => 'KALW',
- 'Kalyani' => 'KALY',
- 'Kamanaickenpalayam' => 'KPLA',
- 'Kamareddy' => 'KMRD',
- 'Kamavarapukota' => 'KPKT',
- 'Kambainallur' => 'KAMR',
- 'Kamptee' => 'KAMP',
- 'Kanakapura' => 'KAKP',
- 'Kanchikacherla' => 'KNCH',
- 'Kanchipuram' => 'KNPM',
- 'Kandukur' => 'KAND',
- 'Kangayam' => 'KGKM',
- 'Kangra' => 'KANG',
- 'Kanichar' => 'KANC',
- 'Kanigiri' => 'KANI',
- 'Kanipakam' => 'KAAM',
- 'Kanjirappally' => 'KNNJ',
- 'Kanker' => 'KANK',
- 'Kannauj' => 'KANJ',
- 'Kannur' => 'KANN',
- 'Kanpur' => 'KANP',
- 'Kanyakumari' => 'KAKM',
- 'Karad' => 'KARD',
- 'Karaikal' => 'KARA',
- 'Karanja Lad' => 'KLAD',
- 'Kareli' => 'KARE',
- 'Karimangalam' => 'KARI',
- 'Karimganj' => 'KRNJ',
- 'Karimnagar' => 'KARIM',
- 'Karjat' => 'KART',
- 'Karkala' => 'KARK',
- 'Karnal' => 'KARN',
- 'Karunagapally' => 'KARG',
- 'Karur' => 'KARU',
- 'Karwar' => 'KWAR',
- 'Kasdol' => 'KASD',
- 'Kasgunj' => 'KASG',
- 'Kashipur' => 'KASH',
- 'Kasibugga' => 'KSBG',
- 'Kathipudi' => 'KATP',
- 'Kathua' => 'KATH',
- 'Katihar' => 'KATI',
- 'Kattappana' => 'AWCK',
- 'Kaveripattinam' => 'KANM',
- 'Kekri' => 'KEKR',
- 'Keonjhar' => 'KNJH',
- 'Kesinga' => 'KEGA',
- 'Khachrod' => 'KHCU',
- 'Khajipet' => 'KHAJ',
- 'Khalilabad' => 'KHBD',
- 'Khamgaon' => 'KHMG',
- 'Khammam' => 'KHAM',
- 'Khandwa' => 'KHDW',
- 'Khanna' => 'KHAN',
- 'Kharagpur' => 'KGPR',
- 'Kharsia' => 'KHAS',
- 'Khed' => 'KHED',
- 'Khopoli' => 'KHOP',
- 'Khurja' => 'KHUR',
- 'Kichha' => 'KCHA',
- 'Kishanganj' => 'KSGJ',
- 'Kodad' => 'KODA',
- 'Kodagu (Coorg)' => 'COOR',
- 'Kodakara' => 'KDKR',
- 'Kodungallur' => 'KODU',
- 'Kokrajhar' => 'KKJR',
- 'Kolar' => 'OLAR',
- 'Kolhapur' => 'KOLH',
- 'Kollam' => 'KOLM',
- 'Kollengode' => 'KOLE',
- 'Komarapalayam' => 'KOMA',
- 'Kondagaon' => 'KNGN',
- 'Kondlahalli' => 'KNAI',
- 'Korba' => 'KRBA',
- 'Kosamba' => 'KOSA',
- 'Kota (AP)' => 'KOAN',
- 'Kota' => 'KOTA',
- 'Kothagudem' => 'KTGM',
- 'Kothamangalam' => 'KTMM',
- 'Kotkapura' => 'KOTK',
- 'Kotpad' => 'KTPD',
- 'Kotputli' => 'KPLI',
- 'Kottayam' => 'KTYM',
- 'Kovur (Nellore)' => 'KOVR',
- 'Kovvur' => 'KOVU',
- 'Koyyalagudem' => 'KOEM',
- 'Kozhikode' => 'KOZH',
- 'Kozhinjampara' => 'KOZA',
- 'Krishnagiri' => 'KRHN',
- 'Krishnanagar' => 'KNWB',
- 'Krosuru' => 'KRSR',
- 'Kruthivennu' => 'KRTH',
- 'Kuchaman City' => 'KHCY',
- 'Kukshi' => 'KUKS',
- 'Kulithalai' => 'KULI',
- 'Kullu' => 'KULU',
- 'Kumbakonam' => 'KUMB',
- 'Kunkuri' => 'KKRI',
- 'Kurnool' => 'KURN',
- 'Kurukshetra' => 'KURU',
- 'Kutch' => 'KTCH',
- 'Lakhimpur Kheri' => 'LKPK',
- 'Lakhimpur' => 'LAHA',
- 'Lakkavaram' => 'LRAM',
- 'Lakshmeshwara' => 'LKSH',
- 'Latur' => 'LAT',
- 'Leh' => 'LEHL',
- 'Lingasugur' => 'LING',
- 'Lohardaga' => 'LOHA',
- 'Lonavala' => 'LNVL',
- 'Loni' => 'LONI',
- 'Lucknow' => 'LUCK',
- 'Ludhiana' => 'LUDH',
- 'Macherla' => 'MACH',
- 'Machilipatnam' => 'MAPM',
- 'Madanapalle' => 'MDNP',
- 'Maddur' => 'MADD',
- 'Madhavaram' => 'MDHA',
- 'Madhepura' => 'MHEA',
- 'Madhira' => 'MADR',
- 'Madurai' => 'MADU',
- 'Magadi' => 'MAGA',
- 'Mahabubabad' => 'MAHA',
- 'Mahad' => 'MHAD',
- 'Mahbubnagar' => 'MAHB',
- 'Maheshwar' => 'MAHE',
- 'Mahishadal' => 'MMAI',
- 'Mahudha' => 'MAHU',
- 'Malebennur' => 'MEBN',
- 'Malegaon' => 'MALE',
- 'Malerkotla' => 'MALR',
- 'Mall' => 'MAAL',
- 'Malout' => 'MALO',
- 'Mamallapuram' => 'MMLL',
- 'Manali' => 'MANA',
- 'Manapparai' => 'MAPI',
- 'Manawar' => 'MANW',
- 'Mancherial' => 'MANC',
- 'Mandapeta' => 'MAND',
- 'Mandi Gobindgarh' => 'MBBH',
- 'Mandla' => 'MADL',
- 'Mandsaur' => 'MNDS',
- 'Mandya' => 'MND',
- 'Manendragarh' => 'MANE',
- 'Mangalagiri' => 'MGLR',
- 'Mangaldoi' => 'MANG',
- 'Mangaluru (Mangalore)' => 'MLR',
- 'Manikonda (AP)' => 'MNAP',
- 'Manipal' => 'MANI',
- 'Manjeri' => 'MAJR',
- 'Mannargudi' => 'MANB',
- 'Mannarkkad' => 'MKKA',
- 'Mansa' => 'MNSA',
- 'Manuguru' => 'MNGU',
- 'Maraimalai Nagar' => 'MMNR',
- 'Markapur' => 'MARK',
- 'Marripeda' => 'MARR',
- 'Marthandam' => 'MRDM',
- 'Mathura' => 'MATH',
- 'Mattannur' => 'MATT',
- 'Mavellikara' => 'MVLR',
- 'Medak' => 'MDAK',
- 'Medarametla' => 'MDRM',
- 'Meerut' => 'MERT',
- 'Mehsana' => 'MEHS',
- 'Memari' => 'MMRR',
- 'Metpally' => 'METT',
- 'Mettuppalayam' => 'MTPM',
- 'Miryalaguda' => 'MRGD',
- 'Mirzapur' => 'MIZP',
- 'Moga' => 'MOGA',
- 'Mohali' => 'MOHL',
- 'Molakalmuru' => 'MOLA',
- 'Moodbidri' => 'MOOD',
- 'Moradabad' => 'MORA',
- 'Moranhat' => 'MORH',
- 'Morbi' => 'MOBI',
- 'Morena' => 'MRMP',
- 'Motihari' => 'MOTI',
- 'Moyna' => 'MAYN',
- 'Muddebihal' => 'MUDD',
- 'Mudhol' => 'MUDL',
- 'Mughalsarai' => 'MGSI',
- 'Mukkam' => 'MUKM',
- 'Muktsar' => 'MKST',
- 'Mullanpur' => 'MULL',
- 'Mummidivaram' => 'MUMM',
- 'Mundakayam' => 'MUAM',
- 'Mundra' => 'MUDA',
- 'MUNNAR' => 'MUNN',
- 'Muradnagar' => 'MRDG',
- 'Murtizapur' => 'MUUR',
- 'Musiri' => 'MUSI',
- 'Mussoorie' => 'MSS',
- 'Muvattupuzha' => 'MUVA',
- 'Muzaffarnagar' => 'MUZ',
- 'Muzaffarpur' => 'MUZA',
- 'Mydukur' => 'MYDU',
- 'Mysuru (Mysore)' => 'MYS',
- 'Nabadwip' => 'NABB',
- 'Nadiad' => 'NADI',
- 'Nagaon' => 'NAAM',
- 'Nagapattinam' => 'NGPT',
- 'Nagari' => 'NAGI',
- 'Nagarkurnool' => 'NGKL',
- 'Nagda' => 'NAGD',
- 'Nagercoil' => 'NAGE',
- 'Nagothane' => 'NAGO',
- 'Nagpur' => 'NAGP',
- 'Naihati' => 'NHTA',
- 'Nainital' => 'NAIN',
- 'Nakhatrana' => 'NKHT',
- 'Nalgonda' => 'NALK',
- 'Namakkal' => 'NMKL',
- 'Namchi' => 'NAMI',
- 'Nanded' => 'NAND',
- 'Nandigama' => 'NDGM',
- 'Nandurbar' => 'NDNB',
- 'Nandyal' => 'NADY',
- 'Nanjanagudu' => 'NJGU',
- 'Nanpara' => 'NANP',
- 'Narasannapeta' => 'NRPT',
- 'Narasaraopet' => 'NSPT',
- 'Narayankhed' => 'NARY',
- 'Narayanpur' => 'NRYA',
- 'Nargund' => 'NRGD',
- 'Narnaul' => 'NARN',
- 'Narsampet' => 'NASP',
- 'Narsapur' => 'NARP',
- 'Narsipatnam' => 'NARS',
- 'Nashik' => 'NASK',
- 'Nathdwara' => 'NATW',
- 'Navsari' => 'NVSR',
- 'Nawalgarh' => 'NANA',
- 'Nawanshahr' => 'NAVN',
- 'Nawapara' => 'NAWA',
- 'Nazira' => 'NZRA',
- 'Nedumkandam' => 'NEDU',
- 'Neemuch' => 'NMCH',
- 'Nellimarla' => 'NLEM',
- 'Ner Parsopant' => 'NERP',
- 'New Tehri' => 'TEHR',
- 'Neyveli' => 'NYVL',
- 'Nidadavolu' => 'NDVD',
- 'Nilagiri' => 'NIGA',
- 'Nimbahera' => 'NIPA',
- 'Nipani' => 'NIPN',
- 'Nizamabad' => 'NIZA',
- 'Nokha' => 'NKHA',
- 'Nuzvid' => 'NZVD',
- 'Nyamathi' => 'NYNT',
- 'Ongole' => 'ONGL',
- 'Ooty' => 'OOTY',
- 'Osmanabad' => 'OSMA',
- 'Ottapalam' => 'OTTP',
- 'Padrauna' => 'PADR',
- 'Pakala' => 'PAKA',
- 'Pala' => 'PALL',
- 'Palakkad' => 'PLKK',
- 'Palakollu' => 'PLKL',
- 'Palakonda' => 'PALK',
- 'Palampur' => 'PALM',
- 'Palanpur' => 'PALN',
- 'Palasa' => 'PALS',
- 'Palghar' => 'PALG',
- 'Pali' => 'PAAL',
- 'Pallipalayam' => 'PLLI',
- 'Palwal' => 'PLWL',
- 'Palwancha' => 'PLWA',
- 'Pamarru' => 'PAMA',
- 'Panchkula' => 'PNCH',
- 'Pandalam' => 'PADM',
- 'Pandharpur' => 'PNDH',
- 'Panipat' => 'PAN',
- 'Panruti' => 'PANT',
- 'Papanasam' => 'PAPA',
- 'Paralakhemundi' => 'PRKM',
- 'Paratwada' => 'PARA',
- 'Parbhani' => 'PARB',
- 'Parchur' => 'PARC',
- 'Parigi (Telangana)' => 'PARI',
- 'Parvathipuram' => 'PRVT',
- 'Patan' => 'PATA',
- 'Pathalgaon' => 'PAHT',
- 'Pathanamthitta' => 'PTNM',
- 'Pathankot' => 'PATH',
- 'Pathsala' => 'PATS',
- 'Patiala' => 'PATI',
- 'Patna' => 'PATN',
- 'Pattambi' => 'PTMB',
- 'Pattukkottai' => 'PATU',
- 'Payakaraopeta' => 'PATE',
- 'Payyanur' => 'PAYY',
- 'Pedanandipadu' => 'PEDN',
- 'Peddapalli' => 'PEDA',
- 'Peddapuram' => 'PEDP',
- 'Pen' => 'PEN',
- 'Pendra' => 'PEND',
- 'Pennagaram' => 'PENM',
- 'Penuganchiprolu' => 'PENU',
- 'Penugonda' => 'PDDG',
- 'Perambalur' => 'PERA',
- 'Peringottukurissi' => 'PERN',
- 'Perinthalmanna' => 'PNTM',
- 'Phagwara' => 'PHAG',
- 'Phalodi' => 'PHLD',
- 'Phaltan' => 'PHAL',
- 'Pileru' => 'PLRU',
- 'Pipariya' => 'PIPY',
- 'Pithampur' => 'PITH',
- 'Podili' => 'PODI',
- 'Polavaram' => 'PLAB',
- 'Pollachi' => 'POLL',
- 'Pondicherry' => 'POND',
- 'Ponduru' => 'PONU',
- 'Ponnani' => 'PONN',
- 'Porumamilla' => 'PORU',
- 'Pratapgarh (Rajasthan)' => 'PTRT',
- 'Pratapgarh (UP)' => 'PRAT',
- 'Prathipadu' => 'PRTH',
- 'Prayagraj (Allahabad)' => 'ALLH',
- 'Proddatur' => 'PROD',
- 'Pulluvila' => 'PULA',
- 'Pulpally' => 'PULP',
- 'Punalur' => 'PUNA',
- 'Punganur' => 'PGNR',
- 'Purnea' => 'PURN',
- 'Purulia' => 'PURU',
- 'Pusad' => 'PUSD',
- 'Pusapatirega' => 'PREG',
- 'Puttur' => 'PUTT',
- 'Raebareli' => 'RAEB',
- 'Rahimatpur' => 'RAHI',
- 'Raibag' => 'RAIB',
- 'Raigad' => 'RAI',
- 'Raigarh' => 'RAIG',
- 'Railway Koduru' => 'RLKD',
- 'Raipur' => 'RAIPUR',
- 'Raisinghnagar' => 'RSNG',
- 'Rajamahendravaram (Rajahmundry)' => 'RJMU',
- 'Rajapalayam' => 'RAYM',
- 'Rajkot' => 'RAJK',
- 'Rajnandgaon' => 'RAJA',
- 'Rajpipla' => 'RJPA',
- 'Rajpur' => 'RAJP',
- 'Rajpura' => 'RARA',
- 'Rajula' => 'RJLA',
- 'Ramanagara' => 'RANG',
- 'Ramayampet' => 'RAMP',
- 'Ramgarhwa' => 'RGHA',
- 'Ramnagar' => 'RAMN',
- 'Rampur' => 'RAMU',
- 'Ranaghat' => 'RANA',
- 'Ranchi' => 'RANC',
- 'Ranebennur' => 'RANE',
- 'Rangia' => 'RAAA',
- 'Raniganj' => 'RNGJ',
- 'Ranipet' => 'RANI',
- 'Ratlam' => 'RATL',
- 'Ratnagiri (Odisha)' => 'RATO',
- 'Ratnagiri' => 'RATN',
- 'Ravulapalem' => 'RVPL',
- 'Raxaul' => 'RAXA',
- 'Rayachoti' => 'RYCT',
- 'Rayavaram' => 'RAYA',
- 'Renukoot' => 'RENU',
- 'Repalle' => 'REPA',
- 'Rewa' => 'RWAA',
- 'Rewari' => 'REWA',
- 'Rishikesh' => 'RKES',
- 'Rishra' => 'RSRA',
- 'Rohtak' => 'ROH',
- 'Rourkela' => 'RKOR',
- 'Routhulapudi' => 'ROUT',
- 'Rudrapur' => 'RUDP',
- 'Rupnagar' => 'RUPN',
- 'Sadasivpet' => 'SADA',
- 'Safidon' => 'SAFI',
- 'Sagar' => 'SAMP',
- 'Saharanpur' => 'SAHA',
- 'Sakleshpur' => 'SASA',
- 'Sakti' => 'SAKT',
- 'Salem' => 'SALM',
- 'Saligrama' => 'SGMA',
- 'Salihundam' => 'SAHM',
- 'Salur' => 'SALU',
- 'Samalkota' => 'SAMA',
- 'Sambalpur' => 'SAMB',
- 'Sambhal' => 'SAML',
- 'Samsi' => 'SAMS',
- 'Sanawad' => 'SNWD',
- 'Sangamner' => 'SMNE',
- 'Sangareddy' => 'SARE',
- 'Sangaria' => 'SAGR',
- 'Sangli' => 'SANG',
- 'Sangola' => 'SNGO',
- 'Santhebennur' => 'STHB',
- 'Saraipali' => 'SPAL',
- 'Sarangarh' => 'SARH',
- 'Sarangpur' => 'SARA',
- 'Sardulgarh' => 'SARD',
- 'Sarnath' => 'SART',
- 'Sarni' => 'SARN',
- 'Sasaram' => 'SARM',
- 'Satara' => 'SATA',
- 'Sathyamangalam' => 'STHY',
- 'Satna' => 'SATN',
- 'Sattenapalle' => 'SATL',
- 'Secunderabad' => 'SCBD',
- 'Seethanagaram' => 'SEET',
- 'Sehore' => 'SEHO',
- 'Semiliguda' => 'SIMI',
- 'Sendhwa' => 'SEND',
- 'Seoni Malwa' => 'SEMA',
- 'Seoni' => 'SEON',
- 'Shadnagar' => 'SHAD',
- 'Shahada' => 'SHHA',
- 'Shahdol' => 'SHAH',
- 'Shahjahanpur' => 'SHJH',
- 'Shajapur' => 'SJUR',
- 'Shankarampet' => 'SHAN',
- 'Shankarpally' => 'SKRP',
- 'Sheorinarayan' => 'SHEO',
- 'Shikaripur' => 'SHKR',
- 'Shillong' => 'SHLG',
- 'Shimla' => 'SMLA',
- 'Shirali' => 'SHIR',
- 'Shivamogga' => 'SHIA',
- 'Shivpuri' => 'SHIV',
- 'Shoranur' => 'SHNR',
- 'Shrirampur' => 'SHUR',
- 'Siddipet' => 'SDDP',
- 'Sidlaghatta' => 'SIDL',
- 'Sikar' => 'SIKR',
- 'Silchar' => 'SIL',
- 'Siliguri' => 'SILI',
- 'Silvassa' => 'SILV',
- 'Sindhanur' => 'SIND',
- 'Sindhudurg' => 'SNDH',
- 'Sinnar' => 'SINA',
- 'Sircilla' => 'SIRC',
- 'Sirohi' => 'SIRO',
- 'Sirsi' => 'SRSI',
- 'Siruguppa' => 'SPPA',
- 'Sitamarhi' => 'SIMA',
- 'Sitapur' => 'SITA',
- 'Sivakasi' => 'SIV',
- 'Sivasagar' => 'SVSG',
- 'Solan' => 'SCO',
- 'Solapur' => 'SOLA',
- 'Sompeta' => 'SOMA',
- 'Songadh' => 'SONG',
- 'Sonipat' => 'RAIH',
- 'Sonkatch' => 'SONH',
- 'Sri Ganganagar' => 'SRIG',
- 'Srikakulam' => 'SRKL',
- 'Srinagar' => 'SRNG',
- 'Srivaikuntam' => 'SRTA',
- 'Srivilliputhur' => 'SRIV',
- 'Station Ghanpur' => 'STGH',
- 'Sultanpur' => 'SLUT',
- 'Sulthan Bathery' => 'SULY',
- 'Sundargarh' => 'SUND',
- 'Surajpur' => 'SURA',
- 'Surat' => 'SURT',
- 'Surendranagar' => 'SRDN',
- 'Suryapet' => 'SURY',
- 'Tadepalligudem' => 'TADP',
- 'Tallapudi' => 'TTPP',
- 'Tallarevu' => 'TALL',
- 'Talwandi Bhai' => 'TALW',
- 'Tamluk' => 'TMLU',
- 'Tanda' => 'TNDA',
- 'Tandur' => 'TAND',
- 'Tangutur' => 'TANG',
- 'Tanuku' => 'TANK',
- 'Tatipaka' => 'TATI',
- 'Tenali' => 'TENA',
- 'Tenkasi' => 'TENK',
- 'Tezpur' => 'TEZP',
- 'Thalassery' => 'THAY',
- 'Thalayolaparambu' => 'THAL',
- 'Thamarassery' => 'TMRY',
- 'Thanipadi' => 'THPD',
- 'Thanjavur' => 'TANJ',
- 'Tharad' => 'THRD',
- 'Theni' => 'THEN',
- 'Thirubuvanai' => 'THRU',
- 'Thiruthuraipoondi' => 'THND',
- 'Thiruttani' => 'THTN',
- 'Thiruvalla' => 'THVL',
- 'Thiruvarur' => 'THVR',
- 'Thodupuzha' => 'THOD',
- 'Thorrur' => 'THOR',
- 'Thottiyam' => 'THYM',
- 'Thrissur' => 'THSR',
- 'Thullur' => 'THUL',
- 'Thuraiyur' => 'THYR',
- 'Tilda Neora' => 'TNO',
- 'Tindivanam' => 'TNVM',
- 'Tinsukia' => 'TINS',
- 'Tiptur' => 'TIPT',
- 'Tiruchirappalli' => 'TRII',
- 'Tirukoilur' => 'TRKR',
- 'Tirunelveli' => 'TIRV',
- 'Tirupati' => 'TIRU',
- 'Tirupattur' => 'TRPR',
- 'Tirupur' => 'TIRP',
- 'Tirur' => 'TRUR',
- 'Tiruvannamalai' => 'TVNM',
- 'Titagarh' => 'TTGH',
- 'Trichy' => 'TRIC',
- 'Trivandrum' => 'TRIV',
- 'Tumakuru (Tumkur)' => 'TUMK',
- 'Tuticorin' => 'TTCN',
- 'Udaipur' => 'UDAI',
- 'Udaynarayanpur' => 'UDAY',
- 'Udgir' => 'UDGR',
- 'Udumalpet' => 'UDMP',
- 'Udupi' => 'UDUP',
- 'Ujjain' => 'UJJN',
- 'Ulundurpet' => 'ULPT',
- 'Umbergaon' => 'UMER',
- 'Una' => 'BEEL',
- 'Uthamapalayam' => 'UTHM',
- 'Vadakara' => 'VDKR',
- 'Vadakkencherry' => 'VDCY',
- 'Vadalur' => 'VADA',
- 'Vadanappally' => 'VADN',
- 'Vadodara' => 'VAD',
- 'Valigonda' => 'VALI',
- 'Valluru' => 'VALL',
- 'Valsad' => 'VLSD',
- 'Vaniyambadi' => 'VANI',
- 'Vapi' => 'VAPI',
- 'Varadiyam' => 'VRYM',
- 'Varanasi' => 'VAR',
- 'Varkala' => 'VKAL',
- 'Vatsavai' => 'VAST',
- 'Vazhapadi' => 'VAZH',
- 'Veeraghattam' => 'VEER',
- 'Velangi' => 'VELG',
- 'Velanthavalam' => 'VELM',
- 'Vellakoil' => 'VELI',
- 'Vellore' => 'VELL',
- 'Vempalli' => 'VAIM',
- 'Vemulawada' => 'VERU',
- 'Venkatapuram' => 'VNKT',
- 'Veraval' => 'VRAL',
- 'Vetapalem' => 'VLEM',
- 'Vijayapura (Bengaluru Rural)' => 'VIJP',
- 'Vijayapura (Bijapur)' => 'VJPR',
- 'Vijayarai' => 'VRAI',
- 'Vijayawada' => 'VIJA',
- 'Vikarabad' => 'VKBD',
- 'Vikasnagar' => 'VKNG',
- 'Vikravandi' => 'VIVI',
- 'Villupuram' => 'VILL',
- 'Virudhachalam' => 'VIDM',
- 'Visnagar' => 'VISN',
- 'Vizag (Visakhapatnam)' => 'VIZA',
- 'Vizianagaram' => 'VIZI',
- 'Vuyyuru' => 'VYUR',
- 'Wai' => 'WAIP',
- 'Wanaparthy' => 'WANA',
- 'Wani' => 'WANI',
- 'Warangal' => 'WAR',
- 'Wardha' => 'WARD',
- 'Warora' => 'WRRA',
- 'Wyra' => 'WWAR',
- 'Yadagirigutta' => 'YADG',
- 'Yamunanagar' => 'YAMU',
- 'Yavatmal' => 'YAVA',
- 'Yelagiri' => 'YLGA',
- 'Yelburga' => 'YELB',
- 'Yellamanchili' => 'YLMN',
- 'Yellandu' => 'YRLL',
- 'Yemmiganur' => 'YEMM',
- 'Zaheerabad' => 'ZAGE',
- 'Zirakpur' => 'ZIRK',
- ];
-
- const PARAMETERS = [
- [
- 'city' => [
- 'name' => 'City',
- 'type' => 'list',
- 'defaultValue' => 'MUMBAI',
- 'values' => self::CITIES,
- ],
-
- 'category' => [
- 'name' => 'Category',
- 'type' => 'list',
- 'defaultValue' => self::MOVIES,
- 'values' => [
- 'Plays' => self::PLAYS,
- 'Events' => self::EVENTS,
- 'Movies' => self::MOVIES,
- ],
- ],
- 'language' => [
- 'name' => 'Language',
- 'type' => 'list',
- 'defaultValue' => 'all',
- 'values' => [
- 'All' => 'all',
- 'Kannada' => 'Kannada',
- 'English' => 'English',
- 'Hindi' => 'Hindi',
- 'Telugu' => 'Telugu',
- 'Tamil' => 'Tamil',
- 'Malayalam' => 'Malayalam',
- 'Gujarati' => 'Gujarati',
- 'Assamese' => 'Assamese',
- ]
- ],
- 'include_online' => [
- 'name' => 'Include Online Events',
- 'type' => 'checkbox',
- 'defaultValue' => false,
- 'title' => 'Whether to include Online Events (applies only in case of "Events" category)'
- ],
- ]
- ];
-
- // Headers used in the generated table for Events/Plays
- // Left is the BMS API Key, and right is the rendered version
- const TABLE_HEADERS = [
- 'Genre' => 'Genre',
- 'Language' => 'Language',
- 'Length' => 'Length',
- 'EventIsGlobal' => 'Global Event',
- 'MinPrice' => 'Minimum Price',
- // This doesn't seem to be used anywhere
- // 'IsSuperstarExclusiveEvent' => 'SuperStar Exclusive',
- 'EventSoldOut' => 'Sold Out',
- ];
-
- // Picked from EventGroup entry for movies
- // Left is BMS API Ke, and right is the rendered version
- const MOVIE_TABLE_HEADERS = [
- 'Duration' => 'Screentime',
- 'EventCensor' => 'Rating',
- ];
-
- /* Common line that we want to edit out */
- const SYNOPSIS_REGEX = '/If you [\w\s,]+synopsis\@bookmyshow\.com/';
-
- // Picked from the ChildEvents entries inside a Event Group
- // for Movies
- // Left is BMS API Key, right is rendered version
- const INNER_MOVIE_HEADERS = [
- 'EventLanguage' => 'Language',
- 'EventDimension' => 'Formats',
- 'EventIsAtmosEnabled' => 'Dolby Atmos',
- 'IsMovieClubEnabled' => 'Movie Club'
- ];
-
- // Primary URL for fetching information
- // The city information is passed via a cookie
- const URL_PREFIX = 'https://in.bookmyshow.com/serv/getData?cmd=QUICKBOOK&type=';
-
- public function collectData(){
- $city = $this->getInput('city');
- $category = $this->getInput('category');
-
- $url = $this->makeUrl($category);
- $headers = $this->makeHeaders($city);
-
- $data = json_decode(getContents($url, $headers), true);
-
- if ($category == self::MOVIES) {
- $data = $data['moviesData']['BookMyShow']['arrEvents'];
- } else {
- $data = $data['data']['BookMyShow']['arrEvent'];
- }
-
- foreach ($data as $event) {
- $item = $this->generateEventData($event, $category);
- if ($item and $this->matchesFilters($category, $event)) {
- $this->items[] = $item;
- }
- }
-
- usort($this->items, function($a, $b) {
- return $b['timestamp'] - $a['timestamp'];
- });
-
- $this->items = array_slice($this->items, 0, 15);
- }
-
- private function makeUrl($category){
- return self::URL_PREFIX . $category;
- }
-
- private function getDatesHtml($dates){
- $tz = new DateTimeZone(self::TIMEZONE);
- $firstDate = DateTime::createFromFormat('Ymd', $dates[0]['ShowDateCode'], $tz)
- ->format('D, d M Y');
- if (count($dates) == 1) {
- return "<p>Date: $firstDate</p>";
- }
- $lastDateIndex = count($dates) - 1;
- $lastDate = DateTime::createFromFormat('Ymd', $dates[$lastDateIndex]['ShowDateCode'])
- ->format('D, d M Y');
- return "<p>Dates: $firstDate - $lastDate</p>";
- }
-
- /**
- * Given an event array, generates corresponding HTML entry
- * @param array $event
- * @see https://gist.github.com/captn3m0/6dbd539ca67579d22d6f90fab710ccd2 Sample JSON data for various events
- */
- private function generateEventHtml($event, $category){
- $html = $this->getDatesHtml($event['arrDates']);
- switch ($category) {
- case self::MOVIES:
- $html .= $this->generateMovieHtml($event);
- break;
- default:
- $html .= $this->generateStandardHtml($event);
- }
-
- $html .= $this->generateVenueHtml($event['arrVenues']);
- return $html;
- }
-
- /**
- * Generates a simple Venue HTML, even for multiple venues
- * spread across multiple dates as a description list.
- */
- private function generateVenueHtml($venues){
- $html = '<h3>Venues</h3><table><thead><tr><th>Venue</th><th>Directions</th></tr></thead><tbody>';
-
- foreach ($venues as $i => $venueData) {
- $venueName = $venueData['VenueName'];
- $address = $venueData['VenueAddress'];
- $lat = $venueData['VenueLatitude'];
- $lon = $venueData['VenueLongitude'];
-
- $directions = $this->generateDirectionsHtml($lat, $lon, $venueName);
- $html .= "<tr><td>$venueName</td><td>$address<br>$directions</td></tr>";
- }
-
- return "$html</tbody></table>";
- }
-
- /**
- * Generates a simple Table with event Data
- * @todo Add support for jsonGenre as a tags row
- */
- private function generateEventDetailsTable($event, $headers = self::TABLE_HEADERS){
- $table = '';
- foreach ($headers as $key => $header) {
- if ($header == 'Language') {
- $this->languages = [$event[$key]];
- }
-
- if ($event[$key] == 'Y') {
- $value = 'Yes';
- } else if ($event[$key] == 'N') {
- $value = 'No';
- } else {
- $value = $event[$key];
- }
-
- $table .= <<<EOT
+
+class BookMyShowBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'captn3m0';
+ const NAME = 'BookMyShow Bridge';
+ const URI = 'https://in.bookmyshow.com';
+ const MOVIES_IMAGE_BASE_FORMAT = 'https://in.bmscdn.com/iedb/movies/images/mobile/thumbnail/large/%s.jpg';
+ const DESCRIPTION = 'Returns the latest events on BookMyShow';
+
+ const TIMEZONE = 'Asia/Kolkata';
+
+ const PLAYS = 'PL';
+ const EVENTS = 'CT';
+ const MOVIES = 'MT';
+
+ const CATEGORIES = [
+ self::PLAYS => 'Plays',
+ self::EVENTS => 'Events',
+ self::MOVIES => 'Movies',
+ ];
+
+ const CITIES = [
+ // Most popular cities
+ 'Mumbai' => 'MUMBAI',
+ 'National Capital Region (NCR)' => 'NCR',
+ 'Bengaluru' => 'BANG',
+ 'Hyderabad' => 'HYD',
+ 'Ahmedabad' => 'AHD',
+ 'Chandigarh' => 'CHD',
+ 'Chennai' => 'CHEN',
+ 'Pune' => 'PUNE',
+ 'Kolkata' => 'KOLK',
+ 'Kochi' => 'KOCH',
+
+ // Less common cities
+ 'Aalo' => 'AALU',
+ 'Abohar' => 'ABOR',
+ 'Abu Road' => 'ABRD',
+ 'Acharapakkam' => 'ACHA',
+ 'Adilabad' => 'ADIL',
+ 'Agar Malwa' => 'AGOR',
+ 'Agartala' => 'AGAR',
+ 'Agra' => 'AGRA',
+ 'Ahmedgarh' => 'AHMG',
+ 'Ahmednagar' => 'AHMED',
+ 'Aizawl' => 'AIZW',
+ 'Ajmer' => 'AJMER',
+ 'Akaltara' => 'AKAL',
+ 'Akividu' => 'AKVD',
+ 'Akola' => 'AKOL',
+ 'Alangudi' => 'ALNI',
+ 'Alappuzha' => 'ALPZ',
+ 'Alathur' => 'ALAR',
+ 'Alibaug' => 'ALBG',
+ 'Aligarh' => 'ALI',
+ 'Allagadda' => 'ALGD',
+ 'Almora' => 'ALMO',
+ 'Alwar' => 'ALWR',
+ 'Amadalavalasa' => 'ADAM',
+ 'Amalapuram' => 'AMAP',
+ 'Amaravathi' => 'AVTI',
+ 'Ambala' => 'AMB',
+ 'Ambikapur' => 'AMBI',
+ 'Ambur' => 'AMBR',
+ 'Amgaon' => 'AMGN',
+ 'Amravati' => 'AMRA',
+ 'Amritsar' => 'AMRI',
+ 'Anakapalle' => 'ANKP',
+ 'Anand' => 'AND',
+ 'Anantapalli' => 'ANTT',
+ 'Anantapur' => 'ANAN',
+ 'Anchal' => 'ANHL',
+ 'Angadipuram' => 'ANDM',
+ 'Angamaly' => 'ANGA',
+ 'Angara' => 'ANGR',
+ 'Angul' => 'ANGL',
+ 'Anjad' => 'ANJA',
+ 'Anjar' => 'ANJR',
+ 'Anklav' => 'ANKV',
+ 'Ankleshwar' => 'ANKL',
+ 'Annigeri' => 'ANGI',
+ 'Arakkonam' => 'ARAK',
+ 'Arambagh' => 'AMBH',
+ 'Aranthangi' => 'ARNT',
+ 'Ariyalur' => 'ARIY',
+ 'Arni' => 'ARNI',
+ 'Arsikere' => 'ARSI',
+ 'Aruppukottai' => 'ARUP',
+ 'Asansol' => 'ASANSOL',
+ 'Ashoknagar (West Bengal)' => 'ASNA',
+ 'Ashoknagar' => 'AKMP',
+ 'Aswaraopeta' => 'ASWA',
+ 'Atpadi' => 'ATPA',
+ 'Attili' => 'ATLI',
+ 'Aurangabad (Bihar)' => 'AUBI',
+ 'Aurangabad (West Bengal)' => 'AURW',
+ 'Aurangabad' => 'AURA',
+ 'Avinashi' => 'AVII',
+ 'Azamgarh' => 'AZMG',
+ 'B. Kothakota' => 'BKOT',
+ 'Badaun' => 'BADN',
+ 'Baddi' => 'BADD',
+ 'Badnawar' => 'BADR',
+ 'Bagbahara' => 'BBHA',
+ 'Bagha Purana' => 'BAPU',
+ 'Bagru' => 'BAGU',
+ 'Bahadurgarh' => 'BAHD',
+ 'Bahraich' => 'BHRH',
+ 'Baihar' => 'BIAH',
+ 'Baikunthpur' => 'BKTH',
+ 'Baindur' => 'BAND',
+ 'Bakhrahat' => 'BART',
+ 'Balaghat' => 'BLGT',
+ 'Balangir' => 'BALG',
+ 'Balasore' => 'BLSR',
+ 'Balijipeta' => 'BLIJ',
+ 'Balod' => 'BALD',
+ 'Baloda Bazar' => 'BBCH',
+ 'Balotra' => 'BALO',
+ 'Balrampur' => 'BLUR',
+ 'Balurghat' => 'BALU',
+ 'Bangarpet' => 'BAGT',
+ 'Banswada' => 'BNSA',
+ 'Banswara' => 'BANS',
+ 'Bantumilli' => 'BANT',
+ 'Barabanki' => 'BARK',
+ 'Baramati' => 'BARA',
+ 'Baraut' => 'BARL',
+ 'Bardoli' => 'BRDL',
+ 'Bareilly' => 'BARE',
+ 'Bargarh' => 'BARG',
+ 'Baripada' => 'BARI',
+ 'Barmer' => 'BARM',
+ 'Barnala' => 'BAR',
+ 'Barshi' => 'BRHI',
+ 'Barwani' => 'BRWN',
+ 'Basna' => 'BASN',
+ 'Basti' => 'BAST',
+ 'Bathinda' => 'BHAT',
+ 'Batlagundu' => 'BTGD',
+ 'Beawar' => 'BEAW',
+ 'Beed' => 'BEED',
+ 'Belagavi (Belgaum)' => 'BELG',
+ 'Bellampalli' => 'BELL',
+ 'Bellary' => 'BLRY',
+ 'Belur' => 'BELU',
+ 'Bemetara' => 'BMTA',
+ 'Berachampa' => 'BRAC',
+ 'Berhampore' => 'BEHA',
+ 'Berhampur' => 'BERP',
+ 'Bestavaripeta' => 'BEST',
+ 'Betul' => 'BETU',
+ 'Bhadrachalam' => 'BHDR',
+ 'Bhadrak' => 'BHAD',
+ 'Bhadravati' => 'BDVT',
+ 'Bhainsa' => 'BHAN',
+ 'Bhandara' => 'BHAA',
+ 'Bharamasagara' => 'BASA',
+ 'Bharuch' => 'BHAR',
+ 'Bhatapara' => 'BTAP',
+ 'Bhatkal' => 'BAKL',
+ 'Bhattiprolu' => 'BATT',
+ 'Bhavnagar' => 'BHNG',
+ 'Bhilai' => 'BHILAI',
+ 'Bhilwara' => 'BHIL',
+ 'Bhimadole' => 'BMDE',
+ 'Bhimavaram' => 'BHIM',
+ 'Bhiwadi' => 'BHWD',
+ 'Bhiwani' => 'BHWN',
+ 'Bhopal' => 'BHOP',
+ 'Bhubaneswar' => 'BHUB',
+ 'Bhuj' => 'BHUJ',
+ 'Bhuntar' => 'BHUN',
+ 'Bhupalpalle' => 'BHUP',
+ 'Bhusawal' => 'BHUS',
+ 'Biaora' => 'BIAR',
+ 'Bidar' => 'BIDR',
+ 'Bijnor' => 'BIJ',
+ 'Bijoynagar' => 'BIJO',
+ 'Bikaner' => 'BIK',
+ 'Bilara' => 'BILR',
+ 'Bilaspur (Himachal Pradesh)' => 'BIPS',
+ 'Bilaspur' => 'BILA',
+ 'Bilimora' => 'BILI',
+ 'Biraul' => 'BIRL',
+ 'Bishrampur' => 'BSRM',
+ 'Bodinayakanur' => 'BODI',
+ 'Boisar' => 'BOIS',
+ 'Bokaro' => 'BOKA',
+ 'Bolpur' => 'BLPR',
+ 'Bommidi' => 'BOMM',
+ 'Bongaigaon' => 'BONG',
+ 'Bongaon' => 'BONI',
+ 'Borsad' => 'BORM',
+ 'Brahmapur' => 'KHUB',
+ 'Brahmapuri' => 'BHMP',
+ 'Brajrajnagar' => 'BJNG',
+ 'Bulandshahr' => 'BULA',
+ 'Buldana' => 'BULD',
+ 'Bundu' => 'BUND',
+ 'Burdwan' => 'BURD',
+ 'Burhanpur' => 'BRHP',
+ 'Byadagi' => 'BYAD',
+ 'Chagallu' => 'CHAG',
+ 'Challakere' => 'CHLA',
+ 'Challapalli' => 'CHAP',
+ 'Champa' => 'CHAM',
+ 'Chanchal' => 'CCWC',
+ 'Chandausi' => 'CHDN',
+ 'Chandragiri' => 'CHAD',
+ 'Chandrakona' => 'CKNA',
+ 'Chandrapur' => 'CHAN',
+ 'Changanassery' => 'CNSY',
+ 'Channagiri' => 'CHGI',
+ 'Channarayapatna' => 'CHNN',
+ 'Chaygaon' => 'CHOG',
+ 'Cheepurupalli' => 'CHEE',
+ 'Chendrapinni' => 'CNPI',
+ 'Chengannur' => 'CHEG',
+ 'Chennur' => 'CHNU',
+ 'Cherial' => 'CHRY',
+ 'Cheyyar' => 'CHEY',
+ 'Chhibramau' => 'CHHI',
+ 'Chhindwara' => 'CHIN',
+ 'Chickmagaluru' => 'CHKA',
+ 'Chidambaram' => 'CHID',
+ 'Chikkaballapur' => 'CHIK',
+ 'Chikodi' => 'CHOK',
+ 'Chinturu' => 'CHTN',
+ 'Chirala' => 'CHIR',
+ 'Chitradurga' => 'CHIT',
+ 'Chittoor' => 'CHTT',
+ 'Chodavaram' => 'CDVM',
+ 'Chotila' => 'CHOT',
+ 'Coimbatore' => 'COIM',
+ 'Cooch Behar' => 'COBE',
+ 'Cuddalore' => 'CUDD',
+ 'Cuttack' => 'CUTT',
+ 'Dabra' => 'DABR',
+ 'Dahanu' => 'DHAU',
+ 'Dahegam' => 'DHGM',
+ 'Dahod' => 'DAHO',
+ 'Dakshin Barasat' => 'DAKS',
+ 'Dalli Rajhara' => 'DALL',
+ 'Daman' => 'DAMA',
+ 'Damoh' => 'DAMO',
+ 'Darjeeling' => 'DARJ',
+ 'Darsi' => 'DARS',
+ 'Dasuya' => 'DASU',
+ 'Dausa' => 'DAUS',
+ 'Davanagere' => 'DAVA',
+ 'Davuluru' => 'DVLR',
+ 'Deesa' => 'DEES',
+ 'Dehradun' => 'DEH',
+ 'Deoghar' => 'DOGH',
+ 'Devadurga' => 'DEVD',
+ 'Devarakonda' => 'DEVK',
+ 'Devgad' => 'DEGA',
+ 'Dewas' => 'DEWAS',
+ 'Dhampur' => 'DHPR',
+ 'Dhamtari' => 'DHMT',
+ 'Dhanbad' => 'DHAN',
+ 'Dhar' => 'DARH',
+ 'Dharamsala' => 'DMSL',
+ 'Dharapuram' => 'DHAR',
+ 'Dharmapuri' => 'DMPI',
+ 'Dharmavaram' => 'DDMA',
+ 'Dharwad' => 'DHAW',
+ 'Dhenkanal' => 'DNAL',
+ 'Dhoraji' => 'DHOR',
+ 'Dhule' => 'DHLE',
+ 'Dhuri' => 'DHRI',
+ 'Dibrugarh' => 'DIB',
+ 'Digras' => 'DIGR',
+ 'Dimapur' => 'DMPR',
+ 'Dindigul' => 'DIND',
+ 'Doddaballapura' => 'DDBP',
+ 'Domkal' => 'DMKL',
+ 'Dongargarh' => 'DONG',
+ 'Doraha' => 'DORH',
+ 'Durg' => 'DURG',
+ 'Durgapur' => 'DURGA',
+ 'Edappal' => 'EDPL',
+ 'Edlapadu' => 'EDLP',
+ 'Eluru' => 'ELRU',
+ 'Erattupetta' => 'ERAT',
+ 'Ernakulam' => 'ERNK',
+ 'Erode' => 'EROD',
+ 'Etawah' => 'ETWH',
+ 'Ettumanoor' => 'ETTU',
+ 'Faizabad' => 'FAZA',
+ 'Falna' => 'FALN',
+ 'Faridkot' => 'DKOT',
+ 'Fatehgarh Sahib' => 'FASA',
+ 'Fatehpur' => 'FATE',
+ 'Fatehpur(Rajasthan)' => 'FATR',
+ 'Firozpur' => 'FRZR',
+ 'G.Mamidada' => 'GMAD',
+ 'Gadag' => 'GADG',
+ 'Gadarwara' => 'GDWR',
+ 'Gadchiroli' => 'GDRO',
+ 'Gajendragarh' => 'GJGH',
+ 'Gajwel' => 'GAJW',
+ 'Ganapavaram' => 'GANP',
+ 'Gandhidham' => 'GDHAM',
+ 'Gandhinagar' => 'GNAGAR',
+ 'Gangavati' => 'GAVT',
+ 'Gangoh' => 'GANZ',
+ 'Gangtok' => 'GANG',
+ 'Ganjbasoda' => 'GANJ',
+ 'Garla' => 'GALA',
+ 'Gauribidanur' => 'GAUR',
+ 'Gaya' => 'GAYA',
+ 'Gingee' => 'GING',
+ 'Goa' => 'GOA',
+ 'Gobichettipalayam' => 'GOBI',
+ 'Godavarikhani' => 'GDVK',
+ 'Godhra' => 'GODH',
+ 'Gokak' => 'GKGK',
+ 'Gokavaram' => 'GOKM',
+ 'Golaghat' => 'GHT',
+ 'Gollaprolu' => 'GOLL',
+ 'Gonda' => 'GOND',
+ 'Gondia' => 'GNDA',
+ 'Gopalganj' => 'GOPG',
+ 'Gorakhpur' => 'GRKP',
+ 'Gorantla' => 'GORA',
+ 'Gotegaon' => 'GTGN',
+ 'Gownipalli' => 'GOWP',
+ 'Gudivada' => 'GUDI',
+ 'Gudiyatham' => 'GDTM',
+ 'Gudur' => 'GUDR',
+ 'Gulaothi' => 'GULL',
+ 'Guledgudda' => 'GULD',
+ 'Gummadidala' => 'GUMM',
+ 'Guna' => 'GUNA',
+ 'Guntakal' => 'GUNL',
+ 'Guntur' => 'GUNT',
+ 'Gurazala' => 'GURZ',
+ 'Guwahati' => 'GUW',
+ 'Gwalior' => 'GWAL',
+ 'Habra' => 'HARR',
+ 'Hagaribommanahalli' => 'HHGG',
+ 'Hajipur' => 'HAJI',
+ 'Haldia' => 'HLDI',
+ 'Haldwani' => 'HALD',
+ 'Haliya' => 'HALI',
+ 'Hampi' => 'HMPI',
+ 'Hardoi' => 'HRDI',
+ 'Haridwar' => 'HRDR',
+ 'Harihar' => 'HRRR',
+ 'Haripad' => 'HRPD',
+ 'Harugeri' => 'HARU',
+ 'Hasanpur' => 'HANS',
+ 'Hazaribagh' => 'HAZA',
+ 'Himmatnagar' => 'HIMM',
+ 'Hindaun City' => 'HIND',
+ 'Hisar' => 'HISR',
+ 'Honnali' => 'HONV',
+ 'Honnavara' => 'HNVR',
+ 'Hooghly' => 'HOOG',
+ 'Hoshiarpur' => 'HOSH',
+ 'Hoskote' => 'HOKT',
+ 'Hospet' => 'HOSP',
+ 'Hosur' => 'HSUR',
+ 'Howrah' => 'HWRH',
+ 'Hubballi (Hubli)' => 'HUBL',
+ 'Huvinahadagali' => 'HULI',
+ 'Ichalkaranji' => 'ICHL',
+ 'Ichchapuram' => 'ICPR',
+ 'Idappadi' => 'IDPI',
+ 'Idar' => 'IDAR',
+ 'Indapur' => 'INDA',
+ 'Indi' => 'IIND',
+ 'Indore' => 'IND',
+ 'Irinjalakuda' => 'IRNK',
+ 'Itanagar' => 'ITNG',
+ 'Itarsi' => 'ITAR',
+ 'Jabalpur' => 'JABL',
+ 'Jadcherla' => 'JADC',
+ 'Jagalur' => 'JAGA',
+ 'Jagatdal' => 'JGDL',
+ 'Jagdalpur' => 'JAGD',
+ 'Jaggampeta' => 'JAGG',
+ 'Jaggayyapeta' => 'JGGY',
+ 'Jagtial' => 'JGTL',
+ 'Jaipur' => 'JAIP',
+ 'Jaisalmer' => 'JSMR',
+ 'Jajpur Road' => 'JAJP',
+ 'Jalakandapuram' => 'JAKA',
+ 'Jalalabad' => 'JLAB',
+ 'Jalandhar' => 'JALA',
+ 'Jalgaon' => 'JALG',
+ 'Jalna' => 'JALN',
+ 'Jalpaiguri' => 'JPG',
+ 'Jami' => 'JAMI',
+ 'Jamkhed' => 'JAMK',
+ 'Jammalamadugu' => 'JAMD',
+ 'Jammu' => 'JAMM',
+ 'Jamnagar' => 'JAM',
+ 'Jamner' => 'JAMN',
+ 'Jamshedpur' => 'JMDP',
+ 'Jangaon' => 'JNGN',
+ 'Jangareddy Gudem' => 'JANG',
+ 'Janjgir' => 'JANR',
+ 'Jasdan' => 'JASD',
+ 'Jaunpur' => 'JANP',
+ 'Jehanabad' => 'JEHA',
+ 'Jetpur' => 'JETP',
+ 'Jewar' => 'JEWR',
+ 'Jeypore' => 'JEYP',
+ 'Jhabua' => 'JHAB',
+ 'Jhajjar' => 'JHAJ',
+ 'Jhansi' => 'JNSI',
+ 'Jharsuguda' => 'JRSG',
+ 'Jiaganj' => 'JAGJ',
+ 'Jind' => 'JIND',
+ 'Jodhpur' => 'JODH',
+ 'Jorhat' => 'JORT',
+ 'Junagadh' => 'JUGH',
+ 'Kadapa' => 'KDPA',
+ 'Kadi' => 'KADI',
+ 'Kaikaluru' => 'KAIK',
+ 'Kaithal' => 'KAIT',
+ 'Kakarapalli' => 'KAAP',
+ 'Kakinada' => 'KAKI',
+ 'Kalaburagi (Gulbarga)' => 'GULB',
+ 'Kalimpong' => 'KALI',
+ 'Kallakurichi' => 'KALL',
+ 'Kalol (Panchmahal)' => 'PANH',
+ 'Kalwakurthy' => 'KALW',
+ 'Kalyani' => 'KALY',
+ 'Kamanaickenpalayam' => 'KPLA',
+ 'Kamareddy' => 'KMRD',
+ 'Kamavarapukota' => 'KPKT',
+ 'Kambainallur' => 'KAMR',
+ 'Kamptee' => 'KAMP',
+ 'Kanakapura' => 'KAKP',
+ 'Kanchikacherla' => 'KNCH',
+ 'Kanchipuram' => 'KNPM',
+ 'Kandukur' => 'KAND',
+ 'Kangayam' => 'KGKM',
+ 'Kangra' => 'KANG',
+ 'Kanichar' => 'KANC',
+ 'Kanigiri' => 'KANI',
+ 'Kanipakam' => 'KAAM',
+ 'Kanjirappally' => 'KNNJ',
+ 'Kanker' => 'KANK',
+ 'Kannauj' => 'KANJ',
+ 'Kannur' => 'KANN',
+ 'Kanpur' => 'KANP',
+ 'Kanyakumari' => 'KAKM',
+ 'Karad' => 'KARD',
+ 'Karaikal' => 'KARA',
+ 'Karanja Lad' => 'KLAD',
+ 'Kareli' => 'KARE',
+ 'Karimangalam' => 'KARI',
+ 'Karimganj' => 'KRNJ',
+ 'Karimnagar' => 'KARIM',
+ 'Karjat' => 'KART',
+ 'Karkala' => 'KARK',
+ 'Karnal' => 'KARN',
+ 'Karunagapally' => 'KARG',
+ 'Karur' => 'KARU',
+ 'Karwar' => 'KWAR',
+ 'Kasdol' => 'KASD',
+ 'Kasgunj' => 'KASG',
+ 'Kashipur' => 'KASH',
+ 'Kasibugga' => 'KSBG',
+ 'Kathipudi' => 'KATP',
+ 'Kathua' => 'KATH',
+ 'Katihar' => 'KATI',
+ 'Kattappana' => 'AWCK',
+ 'Kaveripattinam' => 'KANM',
+ 'Kekri' => 'KEKR',
+ 'Keonjhar' => 'KNJH',
+ 'Kesinga' => 'KEGA',
+ 'Khachrod' => 'KHCU',
+ 'Khajipet' => 'KHAJ',
+ 'Khalilabad' => 'KHBD',
+ 'Khamgaon' => 'KHMG',
+ 'Khammam' => 'KHAM',
+ 'Khandwa' => 'KHDW',
+ 'Khanna' => 'KHAN',
+ 'Kharagpur' => 'KGPR',
+ 'Kharsia' => 'KHAS',
+ 'Khed' => 'KHED',
+ 'Khopoli' => 'KHOP',
+ 'Khurja' => 'KHUR',
+ 'Kichha' => 'KCHA',
+ 'Kishanganj' => 'KSGJ',
+ 'Kodad' => 'KODA',
+ 'Kodagu (Coorg)' => 'COOR',
+ 'Kodakara' => 'KDKR',
+ 'Kodungallur' => 'KODU',
+ 'Kokrajhar' => 'KKJR',
+ 'Kolar' => 'OLAR',
+ 'Kolhapur' => 'KOLH',
+ 'Kollam' => 'KOLM',
+ 'Kollengode' => 'KOLE',
+ 'Komarapalayam' => 'KOMA',
+ 'Kondagaon' => 'KNGN',
+ 'Kondlahalli' => 'KNAI',
+ 'Korba' => 'KRBA',
+ 'Kosamba' => 'KOSA',
+ 'Kota (AP)' => 'KOAN',
+ 'Kota' => 'KOTA',
+ 'Kothagudem' => 'KTGM',
+ 'Kothamangalam' => 'KTMM',
+ 'Kotkapura' => 'KOTK',
+ 'Kotpad' => 'KTPD',
+ 'Kotputli' => 'KPLI',
+ 'Kottayam' => 'KTYM',
+ 'Kovur (Nellore)' => 'KOVR',
+ 'Kovvur' => 'KOVU',
+ 'Koyyalagudem' => 'KOEM',
+ 'Kozhikode' => 'KOZH',
+ 'Kozhinjampara' => 'KOZA',
+ 'Krishnagiri' => 'KRHN',
+ 'Krishnanagar' => 'KNWB',
+ 'Krosuru' => 'KRSR',
+ 'Kruthivennu' => 'KRTH',
+ 'Kuchaman City' => 'KHCY',
+ 'Kukshi' => 'KUKS',
+ 'Kulithalai' => 'KULI',
+ 'Kullu' => 'KULU',
+ 'Kumbakonam' => 'KUMB',
+ 'Kunkuri' => 'KKRI',
+ 'Kurnool' => 'KURN',
+ 'Kurukshetra' => 'KURU',
+ 'Kutch' => 'KTCH',
+ 'Lakhimpur Kheri' => 'LKPK',
+ 'Lakhimpur' => 'LAHA',
+ 'Lakkavaram' => 'LRAM',
+ 'Lakshmeshwara' => 'LKSH',
+ 'Latur' => 'LAT',
+ 'Leh' => 'LEHL',
+ 'Lingasugur' => 'LING',
+ 'Lohardaga' => 'LOHA',
+ 'Lonavala' => 'LNVL',
+ 'Loni' => 'LONI',
+ 'Lucknow' => 'LUCK',
+ 'Ludhiana' => 'LUDH',
+ 'Macherla' => 'MACH',
+ 'Machilipatnam' => 'MAPM',
+ 'Madanapalle' => 'MDNP',
+ 'Maddur' => 'MADD',
+ 'Madhavaram' => 'MDHA',
+ 'Madhepura' => 'MHEA',
+ 'Madhira' => 'MADR',
+ 'Madurai' => 'MADU',
+ 'Magadi' => 'MAGA',
+ 'Mahabubabad' => 'MAHA',
+ 'Mahad' => 'MHAD',
+ 'Mahbubnagar' => 'MAHB',
+ 'Maheshwar' => 'MAHE',
+ 'Mahishadal' => 'MMAI',
+ 'Mahudha' => 'MAHU',
+ 'Malebennur' => 'MEBN',
+ 'Malegaon' => 'MALE',
+ 'Malerkotla' => 'MALR',
+ 'Mall' => 'MAAL',
+ 'Malout' => 'MALO',
+ 'Mamallapuram' => 'MMLL',
+ 'Manali' => 'MANA',
+ 'Manapparai' => 'MAPI',
+ 'Manawar' => 'MANW',
+ 'Mancherial' => 'MANC',
+ 'Mandapeta' => 'MAND',
+ 'Mandi Gobindgarh' => 'MBBH',
+ 'Mandla' => 'MADL',
+ 'Mandsaur' => 'MNDS',
+ 'Mandya' => 'MND',
+ 'Manendragarh' => 'MANE',
+ 'Mangalagiri' => 'MGLR',
+ 'Mangaldoi' => 'MANG',
+ 'Mangaluru (Mangalore)' => 'MLR',
+ 'Manikonda (AP)' => 'MNAP',
+ 'Manipal' => 'MANI',
+ 'Manjeri' => 'MAJR',
+ 'Mannargudi' => 'MANB',
+ 'Mannarkkad' => 'MKKA',
+ 'Mansa' => 'MNSA',
+ 'Manuguru' => 'MNGU',
+ 'Maraimalai Nagar' => 'MMNR',
+ 'Markapur' => 'MARK',
+ 'Marripeda' => 'MARR',
+ 'Marthandam' => 'MRDM',
+ 'Mathura' => 'MATH',
+ 'Mattannur' => 'MATT',
+ 'Mavellikara' => 'MVLR',
+ 'Medak' => 'MDAK',
+ 'Medarametla' => 'MDRM',
+ 'Meerut' => 'MERT',
+ 'Mehsana' => 'MEHS',
+ 'Memari' => 'MMRR',
+ 'Metpally' => 'METT',
+ 'Mettuppalayam' => 'MTPM',
+ 'Miryalaguda' => 'MRGD',
+ 'Mirzapur' => 'MIZP',
+ 'Moga' => 'MOGA',
+ 'Mohali' => 'MOHL',
+ 'Molakalmuru' => 'MOLA',
+ 'Moodbidri' => 'MOOD',
+ 'Moradabad' => 'MORA',
+ 'Moranhat' => 'MORH',
+ 'Morbi' => 'MOBI',
+ 'Morena' => 'MRMP',
+ 'Motihari' => 'MOTI',
+ 'Moyna' => 'MAYN',
+ 'Muddebihal' => 'MUDD',
+ 'Mudhol' => 'MUDL',
+ 'Mughalsarai' => 'MGSI',
+ 'Mukkam' => 'MUKM',
+ 'Muktsar' => 'MKST',
+ 'Mullanpur' => 'MULL',
+ 'Mummidivaram' => 'MUMM',
+ 'Mundakayam' => 'MUAM',
+ 'Mundra' => 'MUDA',
+ 'MUNNAR' => 'MUNN',
+ 'Muradnagar' => 'MRDG',
+ 'Murtizapur' => 'MUUR',
+ 'Musiri' => 'MUSI',
+ 'Mussoorie' => 'MSS',
+ 'Muvattupuzha' => 'MUVA',
+ 'Muzaffarnagar' => 'MUZ',
+ 'Muzaffarpur' => 'MUZA',
+ 'Mydukur' => 'MYDU',
+ 'Mysuru (Mysore)' => 'MYS',
+ 'Nabadwip' => 'NABB',
+ 'Nadiad' => 'NADI',
+ 'Nagaon' => 'NAAM',
+ 'Nagapattinam' => 'NGPT',
+ 'Nagari' => 'NAGI',
+ 'Nagarkurnool' => 'NGKL',
+ 'Nagda' => 'NAGD',
+ 'Nagercoil' => 'NAGE',
+ 'Nagothane' => 'NAGO',
+ 'Nagpur' => 'NAGP',
+ 'Naihati' => 'NHTA',
+ 'Nainital' => 'NAIN',
+ 'Nakhatrana' => 'NKHT',
+ 'Nalgonda' => 'NALK',
+ 'Namakkal' => 'NMKL',
+ 'Namchi' => 'NAMI',
+ 'Nanded' => 'NAND',
+ 'Nandigama' => 'NDGM',
+ 'Nandurbar' => 'NDNB',
+ 'Nandyal' => 'NADY',
+ 'Nanjanagudu' => 'NJGU',
+ 'Nanpara' => 'NANP',
+ 'Narasannapeta' => 'NRPT',
+ 'Narasaraopet' => 'NSPT',
+ 'Narayankhed' => 'NARY',
+ 'Narayanpur' => 'NRYA',
+ 'Nargund' => 'NRGD',
+ 'Narnaul' => 'NARN',
+ 'Narsampet' => 'NASP',
+ 'Narsapur' => 'NARP',
+ 'Narsipatnam' => 'NARS',
+ 'Nashik' => 'NASK',
+ 'Nathdwara' => 'NATW',
+ 'Navsari' => 'NVSR',
+ 'Nawalgarh' => 'NANA',
+ 'Nawanshahr' => 'NAVN',
+ 'Nawapara' => 'NAWA',
+ 'Nazira' => 'NZRA',
+ 'Nedumkandam' => 'NEDU',
+ 'Neemuch' => 'NMCH',
+ 'Nellimarla' => 'NLEM',
+ 'Ner Parsopant' => 'NERP',
+ 'New Tehri' => 'TEHR',
+ 'Neyveli' => 'NYVL',
+ 'Nidadavolu' => 'NDVD',
+ 'Nilagiri' => 'NIGA',
+ 'Nimbahera' => 'NIPA',
+ 'Nipani' => 'NIPN',
+ 'Nizamabad' => 'NIZA',
+ 'Nokha' => 'NKHA',
+ 'Nuzvid' => 'NZVD',
+ 'Nyamathi' => 'NYNT',
+ 'Ongole' => 'ONGL',
+ 'Ooty' => 'OOTY',
+ 'Osmanabad' => 'OSMA',
+ 'Ottapalam' => 'OTTP',
+ 'Padrauna' => 'PADR',
+ 'Pakala' => 'PAKA',
+ 'Pala' => 'PALL',
+ 'Palakkad' => 'PLKK',
+ 'Palakollu' => 'PLKL',
+ 'Palakonda' => 'PALK',
+ 'Palampur' => 'PALM',
+ 'Palanpur' => 'PALN',
+ 'Palasa' => 'PALS',
+ 'Palghar' => 'PALG',
+ 'Pali' => 'PAAL',
+ 'Pallipalayam' => 'PLLI',
+ 'Palwal' => 'PLWL',
+ 'Palwancha' => 'PLWA',
+ 'Pamarru' => 'PAMA',
+ 'Panchkula' => 'PNCH',
+ 'Pandalam' => 'PADM',
+ 'Pandharpur' => 'PNDH',
+ 'Panipat' => 'PAN',
+ 'Panruti' => 'PANT',
+ 'Papanasam' => 'PAPA',
+ 'Paralakhemundi' => 'PRKM',
+ 'Paratwada' => 'PARA',
+ 'Parbhani' => 'PARB',
+ 'Parchur' => 'PARC',
+ 'Parigi (Telangana)' => 'PARI',
+ 'Parvathipuram' => 'PRVT',
+ 'Patan' => 'PATA',
+ 'Pathalgaon' => 'PAHT',
+ 'Pathanamthitta' => 'PTNM',
+ 'Pathankot' => 'PATH',
+ 'Pathsala' => 'PATS',
+ 'Patiala' => 'PATI',
+ 'Patna' => 'PATN',
+ 'Pattambi' => 'PTMB',
+ 'Pattukkottai' => 'PATU',
+ 'Payakaraopeta' => 'PATE',
+ 'Payyanur' => 'PAYY',
+ 'Pedanandipadu' => 'PEDN',
+ 'Peddapalli' => 'PEDA',
+ 'Peddapuram' => 'PEDP',
+ 'Pen' => 'PEN',
+ 'Pendra' => 'PEND',
+ 'Pennagaram' => 'PENM',
+ 'Penuganchiprolu' => 'PENU',
+ 'Penugonda' => 'PDDG',
+ 'Perambalur' => 'PERA',
+ 'Peringottukurissi' => 'PERN',
+ 'Perinthalmanna' => 'PNTM',
+ 'Phagwara' => 'PHAG',
+ 'Phalodi' => 'PHLD',
+ 'Phaltan' => 'PHAL',
+ 'Pileru' => 'PLRU',
+ 'Pipariya' => 'PIPY',
+ 'Pithampur' => 'PITH',
+ 'Podili' => 'PODI',
+ 'Polavaram' => 'PLAB',
+ 'Pollachi' => 'POLL',
+ 'Pondicherry' => 'POND',
+ 'Ponduru' => 'PONU',
+ 'Ponnani' => 'PONN',
+ 'Porumamilla' => 'PORU',
+ 'Pratapgarh (Rajasthan)' => 'PTRT',
+ 'Pratapgarh (UP)' => 'PRAT',
+ 'Prathipadu' => 'PRTH',
+ 'Prayagraj (Allahabad)' => 'ALLH',
+ 'Proddatur' => 'PROD',
+ 'Pulluvila' => 'PULA',
+ 'Pulpally' => 'PULP',
+ 'Punalur' => 'PUNA',
+ 'Punganur' => 'PGNR',
+ 'Purnea' => 'PURN',
+ 'Purulia' => 'PURU',
+ 'Pusad' => 'PUSD',
+ 'Pusapatirega' => 'PREG',
+ 'Puttur' => 'PUTT',
+ 'Raebareli' => 'RAEB',
+ 'Rahimatpur' => 'RAHI',
+ 'Raibag' => 'RAIB',
+ 'Raigad' => 'RAI',
+ 'Raigarh' => 'RAIG',
+ 'Railway Koduru' => 'RLKD',
+ 'Raipur' => 'RAIPUR',
+ 'Raisinghnagar' => 'RSNG',
+ 'Rajamahendravaram (Rajahmundry)' => 'RJMU',
+ 'Rajapalayam' => 'RAYM',
+ 'Rajkot' => 'RAJK',
+ 'Rajnandgaon' => 'RAJA',
+ 'Rajpipla' => 'RJPA',
+ 'Rajpur' => 'RAJP',
+ 'Rajpura' => 'RARA',
+ 'Rajula' => 'RJLA',
+ 'Ramanagara' => 'RANG',
+ 'Ramayampet' => 'RAMP',
+ 'Ramgarhwa' => 'RGHA',
+ 'Ramnagar' => 'RAMN',
+ 'Rampur' => 'RAMU',
+ 'Ranaghat' => 'RANA',
+ 'Ranchi' => 'RANC',
+ 'Ranebennur' => 'RANE',
+ 'Rangia' => 'RAAA',
+ 'Raniganj' => 'RNGJ',
+ 'Ranipet' => 'RANI',
+ 'Ratlam' => 'RATL',
+ 'Ratnagiri (Odisha)' => 'RATO',
+ 'Ratnagiri' => 'RATN',
+ 'Ravulapalem' => 'RVPL',
+ 'Raxaul' => 'RAXA',
+ 'Rayachoti' => 'RYCT',
+ 'Rayavaram' => 'RAYA',
+ 'Renukoot' => 'RENU',
+ 'Repalle' => 'REPA',
+ 'Rewa' => 'RWAA',
+ 'Rewari' => 'REWA',
+ 'Rishikesh' => 'RKES',
+ 'Rishra' => 'RSRA',
+ 'Rohtak' => 'ROH',
+ 'Rourkela' => 'RKOR',
+ 'Routhulapudi' => 'ROUT',
+ 'Rudrapur' => 'RUDP',
+ 'Rupnagar' => 'RUPN',
+ 'Sadasivpet' => 'SADA',
+ 'Safidon' => 'SAFI',
+ 'Sagar' => 'SAMP',
+ 'Saharanpur' => 'SAHA',
+ 'Sakleshpur' => 'SASA',
+ 'Sakti' => 'SAKT',
+ 'Salem' => 'SALM',
+ 'Saligrama' => 'SGMA',
+ 'Salihundam' => 'SAHM',
+ 'Salur' => 'SALU',
+ 'Samalkota' => 'SAMA',
+ 'Sambalpur' => 'SAMB',
+ 'Sambhal' => 'SAML',
+ 'Samsi' => 'SAMS',
+ 'Sanawad' => 'SNWD',
+ 'Sangamner' => 'SMNE',
+ 'Sangareddy' => 'SARE',
+ 'Sangaria' => 'SAGR',
+ 'Sangli' => 'SANG',
+ 'Sangola' => 'SNGO',
+ 'Santhebennur' => 'STHB',
+ 'Saraipali' => 'SPAL',
+ 'Sarangarh' => 'SARH',
+ 'Sarangpur' => 'SARA',
+ 'Sardulgarh' => 'SARD',
+ 'Sarnath' => 'SART',
+ 'Sarni' => 'SARN',
+ 'Sasaram' => 'SARM',
+ 'Satara' => 'SATA',
+ 'Sathyamangalam' => 'STHY',
+ 'Satna' => 'SATN',
+ 'Sattenapalle' => 'SATL',
+ 'Secunderabad' => 'SCBD',
+ 'Seethanagaram' => 'SEET',
+ 'Sehore' => 'SEHO',
+ 'Semiliguda' => 'SIMI',
+ 'Sendhwa' => 'SEND',
+ 'Seoni Malwa' => 'SEMA',
+ 'Seoni' => 'SEON',
+ 'Shadnagar' => 'SHAD',
+ 'Shahada' => 'SHHA',
+ 'Shahdol' => 'SHAH',
+ 'Shahjahanpur' => 'SHJH',
+ 'Shajapur' => 'SJUR',
+ 'Shankarampet' => 'SHAN',
+ 'Shankarpally' => 'SKRP',
+ 'Sheorinarayan' => 'SHEO',
+ 'Shikaripur' => 'SHKR',
+ 'Shillong' => 'SHLG',
+ 'Shimla' => 'SMLA',
+ 'Shirali' => 'SHIR',
+ 'Shivamogga' => 'SHIA',
+ 'Shivpuri' => 'SHIV',
+ 'Shoranur' => 'SHNR',
+ 'Shrirampur' => 'SHUR',
+ 'Siddipet' => 'SDDP',
+ 'Sidlaghatta' => 'SIDL',
+ 'Sikar' => 'SIKR',
+ 'Silchar' => 'SIL',
+ 'Siliguri' => 'SILI',
+ 'Silvassa' => 'SILV',
+ 'Sindhanur' => 'SIND',
+ 'Sindhudurg' => 'SNDH',
+ 'Sinnar' => 'SINA',
+ 'Sircilla' => 'SIRC',
+ 'Sirohi' => 'SIRO',
+ 'Sirsi' => 'SRSI',
+ 'Siruguppa' => 'SPPA',
+ 'Sitamarhi' => 'SIMA',
+ 'Sitapur' => 'SITA',
+ 'Sivakasi' => 'SIV',
+ 'Sivasagar' => 'SVSG',
+ 'Solan' => 'SCO',
+ 'Solapur' => 'SOLA',
+ 'Sompeta' => 'SOMA',
+ 'Songadh' => 'SONG',
+ 'Sonipat' => 'RAIH',
+ 'Sonkatch' => 'SONH',
+ 'Sri Ganganagar' => 'SRIG',
+ 'Srikakulam' => 'SRKL',
+ 'Srinagar' => 'SRNG',
+ 'Srivaikuntam' => 'SRTA',
+ 'Srivilliputhur' => 'SRIV',
+ 'Station Ghanpur' => 'STGH',
+ 'Sultanpur' => 'SLUT',
+ 'Sulthan Bathery' => 'SULY',
+ 'Sundargarh' => 'SUND',
+ 'Surajpur' => 'SURA',
+ 'Surat' => 'SURT',
+ 'Surendranagar' => 'SRDN',
+ 'Suryapet' => 'SURY',
+ 'Tadepalligudem' => 'TADP',
+ 'Tallapudi' => 'TTPP',
+ 'Tallarevu' => 'TALL',
+ 'Talwandi Bhai' => 'TALW',
+ 'Tamluk' => 'TMLU',
+ 'Tanda' => 'TNDA',
+ 'Tandur' => 'TAND',
+ 'Tangutur' => 'TANG',
+ 'Tanuku' => 'TANK',
+ 'Tatipaka' => 'TATI',
+ 'Tenali' => 'TENA',
+ 'Tenkasi' => 'TENK',
+ 'Tezpur' => 'TEZP',
+ 'Thalassery' => 'THAY',
+ 'Thalayolaparambu' => 'THAL',
+ 'Thamarassery' => 'TMRY',
+ 'Thanipadi' => 'THPD',
+ 'Thanjavur' => 'TANJ',
+ 'Tharad' => 'THRD',
+ 'Theni' => 'THEN',
+ 'Thirubuvanai' => 'THRU',
+ 'Thiruthuraipoondi' => 'THND',
+ 'Thiruttani' => 'THTN',
+ 'Thiruvalla' => 'THVL',
+ 'Thiruvarur' => 'THVR',
+ 'Thodupuzha' => 'THOD',
+ 'Thorrur' => 'THOR',
+ 'Thottiyam' => 'THYM',
+ 'Thrissur' => 'THSR',
+ 'Thullur' => 'THUL',
+ 'Thuraiyur' => 'THYR',
+ 'Tilda Neora' => 'TNO',
+ 'Tindivanam' => 'TNVM',
+ 'Tinsukia' => 'TINS',
+ 'Tiptur' => 'TIPT',
+ 'Tiruchirappalli' => 'TRII',
+ 'Tirukoilur' => 'TRKR',
+ 'Tirunelveli' => 'TIRV',
+ 'Tirupati' => 'TIRU',
+ 'Tirupattur' => 'TRPR',
+ 'Tirupur' => 'TIRP',
+ 'Tirur' => 'TRUR',
+ 'Tiruvannamalai' => 'TVNM',
+ 'Titagarh' => 'TTGH',
+ 'Trichy' => 'TRIC',
+ 'Trivandrum' => 'TRIV',
+ 'Tumakuru (Tumkur)' => 'TUMK',
+ 'Tuticorin' => 'TTCN',
+ 'Udaipur' => 'UDAI',
+ 'Udaynarayanpur' => 'UDAY',
+ 'Udgir' => 'UDGR',
+ 'Udumalpet' => 'UDMP',
+ 'Udupi' => 'UDUP',
+ 'Ujjain' => 'UJJN',
+ 'Ulundurpet' => 'ULPT',
+ 'Umbergaon' => 'UMER',
+ 'Una' => 'BEEL',
+ 'Uthamapalayam' => 'UTHM',
+ 'Vadakara' => 'VDKR',
+ 'Vadakkencherry' => 'VDCY',
+ 'Vadalur' => 'VADA',
+ 'Vadanappally' => 'VADN',
+ 'Vadodara' => 'VAD',
+ 'Valigonda' => 'VALI',
+ 'Valluru' => 'VALL',
+ 'Valsad' => 'VLSD',
+ 'Vaniyambadi' => 'VANI',
+ 'Vapi' => 'VAPI',
+ 'Varadiyam' => 'VRYM',
+ 'Varanasi' => 'VAR',
+ 'Varkala' => 'VKAL',
+ 'Vatsavai' => 'VAST',
+ 'Vazhapadi' => 'VAZH',
+ 'Veeraghattam' => 'VEER',
+ 'Velangi' => 'VELG',
+ 'Velanthavalam' => 'VELM',
+ 'Vellakoil' => 'VELI',
+ 'Vellore' => 'VELL',
+ 'Vempalli' => 'VAIM',
+ 'Vemulawada' => 'VERU',
+ 'Venkatapuram' => 'VNKT',
+ 'Veraval' => 'VRAL',
+ 'Vetapalem' => 'VLEM',
+ 'Vijayapura (Bengaluru Rural)' => 'VIJP',
+ 'Vijayapura (Bijapur)' => 'VJPR',
+ 'Vijayarai' => 'VRAI',
+ 'Vijayawada' => 'VIJA',
+ 'Vikarabad' => 'VKBD',
+ 'Vikasnagar' => 'VKNG',
+ 'Vikravandi' => 'VIVI',
+ 'Villupuram' => 'VILL',
+ 'Virudhachalam' => 'VIDM',
+ 'Visnagar' => 'VISN',
+ 'Vizag (Visakhapatnam)' => 'VIZA',
+ 'Vizianagaram' => 'VIZI',
+ 'Vuyyuru' => 'VYUR',
+ 'Wai' => 'WAIP',
+ 'Wanaparthy' => 'WANA',
+ 'Wani' => 'WANI',
+ 'Warangal' => 'WAR',
+ 'Wardha' => 'WARD',
+ 'Warora' => 'WRRA',
+ 'Wyra' => 'WWAR',
+ 'Yadagirigutta' => 'YADG',
+ 'Yamunanagar' => 'YAMU',
+ 'Yavatmal' => 'YAVA',
+ 'Yelagiri' => 'YLGA',
+ 'Yelburga' => 'YELB',
+ 'Yellamanchili' => 'YLMN',
+ 'Yellandu' => 'YRLL',
+ 'Yemmiganur' => 'YEMM',
+ 'Zaheerabad' => 'ZAGE',
+ 'Zirakpur' => 'ZIRK',
+ ];
+
+ const PARAMETERS = [
+ [
+ 'city' => [
+ 'name' => 'City',
+ 'type' => 'list',
+ 'defaultValue' => 'MUMBAI',
+ 'values' => self::CITIES,
+ ],
+
+ 'category' => [
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'defaultValue' => self::MOVIES,
+ 'values' => [
+ 'Plays' => self::PLAYS,
+ 'Events' => self::EVENTS,
+ 'Movies' => self::MOVIES,
+ ],
+ ],
+ 'language' => [
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'defaultValue' => 'all',
+ 'values' => [
+ 'All' => 'all',
+ 'Kannada' => 'Kannada',
+ 'English' => 'English',
+ 'Hindi' => 'Hindi',
+ 'Telugu' => 'Telugu',
+ 'Tamil' => 'Tamil',
+ 'Malayalam' => 'Malayalam',
+ 'Gujarati' => 'Gujarati',
+ 'Assamese' => 'Assamese',
+ ]
+ ],
+ 'include_online' => [
+ 'name' => 'Include Online Events',
+ 'type' => 'checkbox',
+ 'defaultValue' => false,
+ 'title' => 'Whether to include Online Events (applies only in case of "Events" category)'
+ ],
+ ]
+ ];
+
+ // Headers used in the generated table for Events/Plays
+ // Left is the BMS API Key, and right is the rendered version
+ const TABLE_HEADERS = [
+ 'Genre' => 'Genre',
+ 'Language' => 'Language',
+ 'Length' => 'Length',
+ 'EventIsGlobal' => 'Global Event',
+ 'MinPrice' => 'Minimum Price',
+ // This doesn't seem to be used anywhere
+ // 'IsSuperstarExclusiveEvent' => 'SuperStar Exclusive',
+ 'EventSoldOut' => 'Sold Out',
+ ];
+
+ // Picked from EventGroup entry for movies
+ // Left is BMS API Ke, and right is the rendered version
+ const MOVIE_TABLE_HEADERS = [
+ 'Duration' => 'Screentime',
+ 'EventCensor' => 'Rating',
+ ];
+
+ /* Common line that we want to edit out */
+ const SYNOPSIS_REGEX = '/If you [\w\s,]+synopsis\@bookmyshow\.com/';
+
+ // Picked from the ChildEvents entries inside a Event Group
+ // for Movies
+ // Left is BMS API Key, right is rendered version
+ const INNER_MOVIE_HEADERS = [
+ 'EventLanguage' => 'Language',
+ 'EventDimension' => 'Formats',
+ 'EventIsAtmosEnabled' => 'Dolby Atmos',
+ 'IsMovieClubEnabled' => 'Movie Club'
+ ];
+
+ // Primary URL for fetching information
+ // The city information is passed via a cookie
+ const URL_PREFIX = 'https://in.bookmyshow.com/serv/getData?cmd=QUICKBOOK&type=';
+
+ public function collectData()
+ {
+ $city = $this->getInput('city');
+ $category = $this->getInput('category');
+
+ $url = $this->makeUrl($category);
+ $headers = $this->makeHeaders($city);
+
+ $data = json_decode(getContents($url, $headers), true);
+
+ if ($category == self::MOVIES) {
+ $data = $data['moviesData']['BookMyShow']['arrEvents'];
+ } else {
+ $data = $data['data']['BookMyShow']['arrEvent'];
+ }
+
+ foreach ($data as $event) {
+ $item = $this->generateEventData($event, $category);
+ if ($item and $this->matchesFilters($category, $event)) {
+ $this->items[] = $item;
+ }
+ }
+
+ usort($this->items, function ($a, $b) {
+ return $b['timestamp'] - $a['timestamp'];
+ });
+
+ $this->items = array_slice($this->items, 0, 15);
+ }
+
+ private function makeUrl($category)
+ {
+ return self::URL_PREFIX . $category;
+ }
+
+ private function getDatesHtml($dates)
+ {
+ $tz = new DateTimeZone(self::TIMEZONE);
+ $firstDate = DateTime::createFromFormat('Ymd', $dates[0]['ShowDateCode'], $tz)
+ ->format('D, d M Y');
+ if (count($dates) == 1) {
+ return "<p>Date: $firstDate</p>";
+ }
+ $lastDateIndex = count($dates) - 1;
+ $lastDate = DateTime::createFromFormat('Ymd', $dates[$lastDateIndex]['ShowDateCode'])
+ ->format('D, d M Y');
+ return "<p>Dates: $firstDate - $lastDate</p>";
+ }
+
+ /**
+ * Given an event array, generates corresponding HTML entry
+ * @param array $event
+ * @see https://gist.github.com/captn3m0/6dbd539ca67579d22d6f90fab710ccd2 Sample JSON data for various events
+ */
+ private function generateEventHtml($event, $category)
+ {
+ $html = $this->getDatesHtml($event['arrDates']);
+ switch ($category) {
+ case self::MOVIES:
+ $html .= $this->generateMovieHtml($event);
+ break;
+ default:
+ $html .= $this->generateStandardHtml($event);
+ }
+
+ $html .= $this->generateVenueHtml($event['arrVenues']);
+ return $html;
+ }
+
+ /**
+ * Generates a simple Venue HTML, even for multiple venues
+ * spread across multiple dates as a description list.
+ */
+ private function generateVenueHtml($venues)
+ {
+ $html = '<h3>Venues</h3><table><thead><tr><th>Venue</th><th>Directions</th></tr></thead><tbody>';
+
+ foreach ($venues as $i => $venueData) {
+ $venueName = $venueData['VenueName'];
+ $address = $venueData['VenueAddress'];
+ $lat = $venueData['VenueLatitude'];
+ $lon = $venueData['VenueLongitude'];
+
+ $directions = $this->generateDirectionsHtml($lat, $lon, $venueName);
+ $html .= "<tr><td>$venueName</td><td>$address<br>$directions</td></tr>";
+ }
+
+ return "$html</tbody></table>";
+ }
+
+ /**
+ * Generates a simple Table with event Data
+ * @todo Add support for jsonGenre as a tags row
+ */
+ private function generateEventDetailsTable($event, $headers = self::TABLE_HEADERS)
+ {
+ $table = '';
+ foreach ($headers as $key => $header) {
+ if ($header == 'Language') {
+ $this->languages = [$event[$key]];
+ }
+
+ if ($event[$key] == 'Y') {
+ $value = 'Yes';
+ } elseif ($event[$key] == 'N') {
+ $value = 'No';
+ } else {
+ $value = $event[$key];
+ }
+
+ $table .= <<<EOT
<tr>
<th>$header</th>
<td>$value</td>
</tr>
EOT;
- }
+ }
- return "<table>$table</table>";
- }
+ return "<table>$table</table>";
+ }
- private function generateStandardHtml($event){
- $table = $this->generateEventDetailsTable($event);
+ private function generateStandardHtml($event)
+ {
+ $table = $this->generateEventDetailsTable($event);
- $imgsrc = $event['BannerURL'];
+ $imgsrc = $event['BannerURL'];
- return <<<EOT
+ return <<<EOT
<img title="Event Banner URL" src="$imgsrc"></img>
<br>
$table
<br>
More Details are available on the <a href="${event['FShareURL']}">BookMyShow website</a>.
EOT;
- }
-
- /**
- * Converts some movie details from child entries, such as language
- * into a single row item, either as a list, or as a Y/N
- */
- private function generateInnerMovieDetails($data){
- // Show list of languages and list of formats
- $headers = ['EventLanguage', 'EventDimension'];
- // if any of these has a Y for any of the screenings, mark it as YES
- $booleanHeaders = [
- 'EventIsAtmosEnabled', 'IsMovieClubEnabled'
- ];
-
- $items = [];
-
- // Throw values inside $items[$headerName]
- foreach ($data as $row) {
- foreach ($headers as $header) {
- $items[$header][] = $row[$header];
- }
- foreach ($booleanHeaders as $header) {
- $items[$header][] = $row[$header];
- }
- }
-
- // Remove duplicate values (if all screenings are 2D for eg)
- foreach ($headers as $header) {
- $items[$header] = array_unique($items[$header]);
-
- if ($header == 'EventLanguage') {
- $this->languages = $items[$header];
- }
- }
-
- $html = '';
-
- // Generate a list for first kind of entries
- foreach ($headers as $header) {
- $html .= self::INNER_MOVIE_HEADERS[$header] . ': ' . join(', ', $items[$header]) . '<br>';
- }
-
- // Put a yes for the boolean entries
- foreach ($booleanHeaders as $header) {
- if(in_array('Y', $items[$header])) {
- $html .= self::INNER_MOVIE_HEADERS[$header] . ': Yes<br>';
- }
- }
-
- return $html;
- }
-
- private function generateMovieHtml($eventGroup){
- $data = $eventGroup['ChildEvents'][0];
- $table = $this->generateEventDetailsTable($data, self::MOVIE_TABLE_HEADERS);
-
- $imgsrc = sprintf(self::MOVIES_IMAGE_BASE_FORMAT, $data['EventImageCode']);
-
- $url = $this->generateMovieUrl($eventGroup);
-
- $innerHtml = $this->generateInnerMovieDetails($eventGroup['ChildEvents']);
-
- $synopsis = preg_replace(self::SYNOPSIS_REGEX, '', $data['EventSynopsis']);
-
- return <<<EOT
+ }
+
+ /**
+ * Converts some movie details from child entries, such as language
+ * into a single row item, either as a list, or as a Y/N
+ */
+ private function generateInnerMovieDetails($data)
+ {
+ // Show list of languages and list of formats
+ $headers = ['EventLanguage', 'EventDimension'];
+ // if any of these has a Y for any of the screenings, mark it as YES
+ $booleanHeaders = [
+ 'EventIsAtmosEnabled', 'IsMovieClubEnabled'
+ ];
+
+ $items = [];
+
+ // Throw values inside $items[$headerName]
+ foreach ($data as $row) {
+ foreach ($headers as $header) {
+ $items[$header][] = $row[$header];
+ }
+ foreach ($booleanHeaders as $header) {
+ $items[$header][] = $row[$header];
+ }
+ }
+
+ // Remove duplicate values (if all screenings are 2D for eg)
+ foreach ($headers as $header) {
+ $items[$header] = array_unique($items[$header]);
+
+ if ($header == 'EventLanguage') {
+ $this->languages = $items[$header];
+ }
+ }
+
+ $html = '';
+
+ // Generate a list for first kind of entries
+ foreach ($headers as $header) {
+ $html .= self::INNER_MOVIE_HEADERS[$header] . ': ' . join(', ', $items[$header]) . '<br>';
+ }
+
+ // Put a yes for the boolean entries
+ foreach ($booleanHeaders as $header) {
+ if (in_array('Y', $items[$header])) {
+ $html .= self::INNER_MOVIE_HEADERS[$header] . ': Yes<br>';
+ }
+ }
+
+ return $html;
+ }
+
+ private function generateMovieHtml($eventGroup)
+ {
+ $data = $eventGroup['ChildEvents'][0];
+ $table = $this->generateEventDetailsTable($data, self::MOVIE_TABLE_HEADERS);
+
+ $imgsrc = sprintf(self::MOVIES_IMAGE_BASE_FORMAT, $data['EventImageCode']);
+
+ $url = $this->generateMovieUrl($eventGroup);
+
+ $innerHtml = $this->generateInnerMovieDetails($eventGroup['ChildEvents']);
+
+ $synopsis = preg_replace(self::SYNOPSIS_REGEX, '', $data['EventSynopsis']);
+
+ return <<<EOT
<img title="Movie Poster" src="$imgsrc"></img>
<div>$table</div>
<p>$innerHtml</p>
@@ -1290,169 +1300,179 @@ EOT;
More Details are available on the <a href="$url">BookMyShow website</a> and a trailer is available
<a href="${data['EventTrailerURL']}" title="Trailer URL">here</a>
EOT;
-
- }
-
- /**
- * Generates a canonical movie URL
- */
- private function generateMovieUrl($eventGroup){
- return self::URI . '/movies/' . $eventGroup['EventURLTitle'] . '/' . $eventGroup['EventCode'];
- }
-
- private function generateMoviesData($eventGroup){
- // Additional data picked up from the first Child Event
- $data = $eventGroup['ChildEvents'][0];
- $date = new DateTime($data['EventDate']);
-
- return [
- 'uri' => $this->generateMovieUrl($eventGroup),
- 'title' => $eventGroup['EventTitle'],
- 'timestamp' => $date->format('U'),
- 'author' => 'BookMyShow',
- 'content' => $this->generateMovieHtml($eventGroup),
- 'enclosures' => [
- sprintf(self::MOVIES_IMAGE_BASE_FORMAT, $data['EventImageCode']),
- ],
- // Sample Input = |ADVENTURE|ANIMATION|COMEDY|
- // Sample Output = ['Adventure', 'Animation', 'Comedy']
- 'categories' => array_filter(
- explode('|', ucwords(strtolower($eventGroup['EventGrpGenre']), '|'))
- ),
- 'uid' => $eventGroup['EventGroup']
- ];
- }
-
- private function generateEventData($event, $category){
- if($category == self::MOVIES) {
- return $this->generateMoviesData($event);
- }
-
- return $this->generateGenericEventData($event, $category);
- }
-
- /**
- * Takes an event data as array and returns data for RSS Post
- */
- private function generateGenericEventData($event, $category){
- $datetime = $event['Event_dtmCreated'];
- if (empty($datetime)) {
- return null;
- }
- $date = new DateTime($event['Event_dtmCreated']);
-
- return [
- 'uri' => $event['FShareURL'],
- 'title' => $event['EventTitle'],
- 'timestamp' => $date->format('U'),
- 'author' => 'BookMyShow',
- 'content' => $this->generateEventHtml($event, $category),
- 'enclosures' => [
- $event['BannerURL'],
- ],
- 'categories' => array_merge(
- [self::CATEGORIES[$category]],
- $event['GenreArray']
- ),
- 'uid' => $event['EventGroupCode'],
- ];
- }
-
- /**
- * Check if this is an online event. We can't rely on
- * EventIsWebView, since that is set to Y for everything
- */
- private function isEventOnline($event){
- if (isset($event['arrVenues']) && count($event['arrVenues']) === 1) {
- if (preg_match('/(Online|Zoom)/i', $event['arrVenues'][0]['VenueName'])) {
- return true;
- }
- }
-
- return false;
- }
-
- private function matchesLanguage(){
- if ($this->getInput('language') !== 'all') {
- $language = $this->getInput('language');
- return in_array($language, $this->languages);
- }
- return true;
- }
-
- private function matchesOnline($event){
- if ($this->getInput('include_online')) {
- return true;
- }
- return (!$this->isEventOnline($event));
- }
-
- /**
- * Currently only checks if the language filter matches
- */
- private function matchesFilters($category, $event){
- return $this->matchesLanguage() and $this->matchesOnline($event);
- }
-
- /**
- * Generates the RSS Feed title
- */
- public function getName(){
- $city = $this->getInput('city');
- $category = $this->getInput('category');
- if(!is_null($city) and !is_null($category)) {
- $categoryName = self::CATEGORIES[$category];
- $cityNames = array_flip(self::CITIES);
- $cityName = $cityNames[$city];
- if ($this->getInput('language') !== 'null') {
- $l = ucwords($this->getInput('language'));
- // Sample: English Movies in Delhi
- return "BookMyShow: $l $categoryName in $cityName";
- }
- return "BookMyShow: $categoryName in $cityName";
- }
-
- return parent::getName();
- }
-
- /**
- * Returns
- * @param string $city City Code
- * @return array list of headers
- */
- private function makeHeaders($city){
- $uniqid = uniqid();
- $rgn = urlencode("|Code=$city|");
- return [
- "Cookie: bmsId=$uniqid; Rgn=$rgn;"
- ];
- }
-
- /**
- * Generates various URLs as per https://tools.ietf.org/html/rfc5870
- * and other standards
- */
- private function generateDirectionsHtml($lat, $long, $address = ''){
- $address = urlencode($address);
-
- $links = [
- 'Apple Maps' => 'http://maps.apple.com/maps?q=%s,%s"',
- 'Google Maps' => 'http://maps.google.com/maps?ll=%s,%s',
- // 'Google Maps (Android)' => 'geo:%s,%s?q=%s',
- // 'Google Maps (iOS)' => 'comgooglemaps://?center=%s,%s&zoom=12&views=traffic',
- 'OpenStreetMap' => 'https://www.openstreetmap.org/?mlat=%s&mlon=%s&zoom=12',
- 'GeoURI' => 'geo:%s,%s?q=%s',
- ];
-
- $html = '';
-
- foreach ($links as $app => $str) {
- $url = sprintf($str, $lat, $long, $address);
- $locations[] = "<a href='$url' title='$app'>$app</a>";
- }
-
- $html .= implode(', ', $locations) . '</span>';
-
- return $html;
- }
+ }
+
+ /**
+ * Generates a canonical movie URL
+ */
+ private function generateMovieUrl($eventGroup)
+ {
+ return self::URI . '/movies/' . $eventGroup['EventURLTitle'] . '/' . $eventGroup['EventCode'];
+ }
+
+ private function generateMoviesData($eventGroup)
+ {
+ // Additional data picked up from the first Child Event
+ $data = $eventGroup['ChildEvents'][0];
+ $date = new DateTime($data['EventDate']);
+
+ return [
+ 'uri' => $this->generateMovieUrl($eventGroup),
+ 'title' => $eventGroup['EventTitle'],
+ 'timestamp' => $date->format('U'),
+ 'author' => 'BookMyShow',
+ 'content' => $this->generateMovieHtml($eventGroup),
+ 'enclosures' => [
+ sprintf(self::MOVIES_IMAGE_BASE_FORMAT, $data['EventImageCode']),
+ ],
+ // Sample Input = |ADVENTURE|ANIMATION|COMEDY|
+ // Sample Output = ['Adventure', 'Animation', 'Comedy']
+ 'categories' => array_filter(
+ explode('|', ucwords(strtolower($eventGroup['EventGrpGenre']), '|'))
+ ),
+ 'uid' => $eventGroup['EventGroup']
+ ];
+ }
+
+ private function generateEventData($event, $category)
+ {
+ if ($category == self::MOVIES) {
+ return $this->generateMoviesData($event);
+ }
+
+ return $this->generateGenericEventData($event, $category);
+ }
+
+ /**
+ * Takes an event data as array and returns data for RSS Post
+ */
+ private function generateGenericEventData($event, $category)
+ {
+ $datetime = $event['Event_dtmCreated'];
+ if (empty($datetime)) {
+ return null;
+ }
+ $date = new DateTime($event['Event_dtmCreated']);
+
+ return [
+ 'uri' => $event['FShareURL'],
+ 'title' => $event['EventTitle'],
+ 'timestamp' => $date->format('U'),
+ 'author' => 'BookMyShow',
+ 'content' => $this->generateEventHtml($event, $category),
+ 'enclosures' => [
+ $event['BannerURL'],
+ ],
+ 'categories' => array_merge(
+ [self::CATEGORIES[$category]],
+ $event['GenreArray']
+ ),
+ 'uid' => $event['EventGroupCode'],
+ ];
+ }
+
+ /**
+ * Check if this is an online event. We can't rely on
+ * EventIsWebView, since that is set to Y for everything
+ */
+ private function isEventOnline($event)
+ {
+ if (isset($event['arrVenues']) && count($event['arrVenues']) === 1) {
+ if (preg_match('/(Online|Zoom)/i', $event['arrVenues'][0]['VenueName'])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function matchesLanguage()
+ {
+ if ($this->getInput('language') !== 'all') {
+ $language = $this->getInput('language');
+ return in_array($language, $this->languages);
+ }
+ return true;
+ }
+
+ private function matchesOnline($event)
+ {
+ if ($this->getInput('include_online')) {
+ return true;
+ }
+ return (!$this->isEventOnline($event));
+ }
+
+ /**
+ * Currently only checks if the language filter matches
+ */
+ private function matchesFilters($category, $event)
+ {
+ return $this->matchesLanguage() and $this->matchesOnline($event);
+ }
+
+ /**
+ * Generates the RSS Feed title
+ */
+ public function getName()
+ {
+ $city = $this->getInput('city');
+ $category = $this->getInput('category');
+ if (!is_null($city) and !is_null($category)) {
+ $categoryName = self::CATEGORIES[$category];
+ $cityNames = array_flip(self::CITIES);
+ $cityName = $cityNames[$city];
+ if ($this->getInput('language') !== 'null') {
+ $l = ucwords($this->getInput('language'));
+ // Sample: English Movies in Delhi
+ return "BookMyShow: $l $categoryName in $cityName";
+ }
+ return "BookMyShow: $categoryName in $cityName";
+ }
+
+ return parent::getName();
+ }
+
+ /**
+ * Returns
+ * @param string $city City Code
+ * @return array list of headers
+ */
+ private function makeHeaders($city)
+ {
+ $uniqid = uniqid();
+ $rgn = urlencode("|Code=$city|");
+ return [
+ "Cookie: bmsId=$uniqid; Rgn=$rgn;"
+ ];
+ }
+
+ /**
+ * Generates various URLs as per https://tools.ietf.org/html/rfc5870
+ * and other standards
+ */
+ private function generateDirectionsHtml($lat, $long, $address = '')
+ {
+ $address = urlencode($address);
+
+ $links = [
+ 'Apple Maps' => 'http://maps.apple.com/maps?q=%s,%s"',
+ 'Google Maps' => 'http://maps.google.com/maps?ll=%s,%s',
+ // 'Google Maps (Android)' => 'geo:%s,%s?q=%s',
+ // 'Google Maps (iOS)' => 'comgooglemaps://?center=%s,%s&zoom=12&views=traffic',
+ 'OpenStreetMap' => 'https://www.openstreetmap.org/?mlat=%s&mlon=%s&zoom=12',
+ 'GeoURI' => 'geo:%s,%s?q=%s',
+ ];
+
+ $html = '';
+
+ foreach ($links as $app => $str) {
+ $url = sprintf($str, $lat, $long, $address);
+ $locations[] = "<a href='$url' title='$app'>$app</a>";
+ }
+
+ $html .= implode(', ', $locations) . '</span>';
+
+ return $html;
+ }
}
diff --git a/bridges/BooruprojectBridge.php b/bridges/BooruprojectBridge.php
index 9917da7e..761fd084 100644
--- a/bridges/BooruprojectBridge.php
+++ b/bridges/BooruprojectBridge.php
@@ -1,71 +1,77 @@
<?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();
+ }
}
diff --git a/bridges/BrutBridge.php b/bridges/BrutBridge.php
index d53b5c6d..c482c247 100644
--- a/bridges/BrutBridge.php
+++ b/bridges/BrutBridge.php
@@ -1,73 +1,75 @@
<?php
-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 = 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
-
- private $jsonRegex = '/window\.__PRELOADED_STATE__ = ((?:.*)});/';
-
- public function collectData() {
-
- $html = getSimpleHTMLDOM($this->getURI());
-
- $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 $li) {
- $item = array();
-
- $videoPath = self::URI . $li->children(0)->href;
- $videoPageHtml = getSimpleHTMLDOMCached($videoPath, 3600);
-
- $json = $this->extractJson($videoPageHtml);
- $id = array_keys((array) $json->media->index)[0];
-
- $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;
-
- $description = $json->media->index->$id->description;
- $article = '';
-
- if (is_null($json->media->index->$id->media->seo_article) === false) {
- $article = markdownToHtml($json->media->index->$id->media->seo_article);
- }
-
- $item['content'] = <<<EOD
+
+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',
+ ]
+ ]
+ ];
+
+ const CACHE_TIMEOUT = 1800; // 30 mins
+
+ private $jsonRegex = '/window\.__PRELOADED_STATE__ = ((?:.*)});/';
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $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 $li) {
+ $item = [];
+
+ $videoPath = self::URI . $li->children(0)->href;
+ $videoPageHtml = getSimpleHTMLDOMCached($videoPath, 3600);
+
+ $json = $this->extractJson($videoPageHtml);
+ $id = array_keys((array) $json->media->index)[0];
+
+ $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;
+
+ $description = $json->media->index->$id->description;
+ $article = '';
+
+ if (is_null($json->media->index->$id->media->seo_article) === false) {
+ $article = markdownToHtml($json->media->index->$id->media->seo_article);
+ }
+
+ $item['content'] = <<<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>
@@ -75,53 +77,53 @@ class BrutBridge extends BridgeAbstract {
{$article}
EOD;
- $this->items[] = $item;
-
- if (count($this->items) >= 10) {
- break;
- }
- }
- }
-
- 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) >= 10) {
+ break;
+ }
+ }
+ }
- public function getName() {
+ public function getURI()
+ {
+ if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
+ return self::URI . '/' . $this->getInput('edition') . '/' . $this->getInput('category');
+ }
- if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
- $parameters = $this->getParameters();
+ return parent::getURI();
+ }
- $editionValues = array_flip($parameters[0]['edition']['values']);
- $categoryValues = array_flip($parameters[0]['category']['values']);
+ public function getName()
+ {
+ if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
+ $parameters = $this->getParameters();
- return $categoryValues[$this->getInput('category')] . ' - ' .
- $editionValues[$this->getInput('edition')] . ' - Brut.';
- }
+ $editionValues = array_flip($parameters[0]['edition']['values']);
+ $categoryValues = array_flip($parameters[0]['category']['values']);
- return parent::getName();
- }
+ return $categoryValues[$this->getInput('category')] . ' - ' .
+ $editionValues[$this->getInput('edition')] . ' - Brut.';
+ }
- /**
- * Extract JSON from page
- */
- private function extractJson($html) {
+ return parent::getName();
+ }
- if (!preg_match($this->jsonRegex, $html, $parts)) {
- returnServerError('Failed to extract data from page');
- }
+ /**
+ * Extract JSON from page
+ */
+ private function extractJson($html)
+ {
+ if (!preg_match($this->jsonRegex, $html, $parts)) {
+ returnServerError('Failed to extract data from page');
+ }
- $data = json_decode($parts[1]);
+ $data = json_decode($parts[1]);
- if ($data === false) {
- returnServerError('Failed to decode extracted data');
- }
+ if ($data === false) {
+ returnServerError('Failed to decode extracted data');
+ }
- return $data;
- }
+ return $data;
+ }
}
diff --git a/bridges/BugzillaBridge.php b/bridges/BugzillaBridge.php
index 16ce4a28..a7fc75b8 100644
--- a/bridges/BugzillaBridge.php
+++ b/bridges/BugzillaBridge.php
@@ -1,180 +1,190 @@
<?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 = array(
- 'global' => array(
- 'instance' => array(
- 'name' => 'Instance URL',
- 'required' => true,
- 'exampleValue' => 'https://bugzilla.mozilla.org'
- )
- ),
- 'Bug comments' => array(
- 'id' => array(
- 'name' => 'Bug tracking ID',
- 'type' => 'number',
- 'required' => true,
- 'title' => 'Insert bug tracking ID',
- 'exampleValue' => 121241
- ),
- 'limit' => array(
- 'name' => 'Number of comments to return',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Specify number of comments to return',
- 'defaultValue' => -1
- ),
- 'skiptags' => array(
- 'name' => 'Skip offtopic comments',
- 'type' => 'checkbox',
- 'title' => 'Excludes comments tagged as advocacy, metoo, or offtopic from the feed'
- )
- )
- );
-
- const SKIPPED_ACTIVITY = array(
- 'cc' => true,
- 'comment_tag' => true
- );
-
- const SKIPPED_TAGS = array('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 = json_decode(getContents($url . '?include_fields=summary'), true);
- $this->title = 'Bug ' . $this->bugid . ' - ' .
- $json['bugs'][0]['summary'] . ' - ' .
- // Remove https://
- substr($this->instance, 8);
- }
-
- protected function collectComments($url) {
- $json = json_decode(getContents($url), true);
-
- // 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 = array();
- 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'] = array($this->instance . '/attachment.cgi?id=' . $comment['attachment_id']);
- }
- $this->items[] = $item;
- }
- }
-
- protected function collectUpdates($url) {
- $json = json_decode(getContents($url), true);
-
- // 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 = array();
- $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')) {
- 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 = json_decode(getContents($url), true);
- 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;
- }
+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 = json_decode(getContents($url . '?include_fields=summary'), true);
+ $this->title = 'Bug ' . $this->bugid . ' - ' .
+ $json['bugs'][0]['summary'] . ' - ' .
+ // Remove https://
+ substr($this->instance, 8);
+ }
+
+ protected function collectComments($url)
+ {
+ $json = json_decode(getContents($url), true);
+
+ // 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 = json_decode(getContents($url), true);
+
+ // 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')) {
+ 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 = json_decode(getContents($url), true);
+ 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;
+ }
}
diff --git a/bridges/BukowskisBridge.php b/bridges/BukowskisBridge.php
index 7b7c36bf..14889889 100644
--- a/bridges/BukowskisBridge.php
+++ b/bridges/BukowskisBridge.php
@@ -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();
+ }
}
diff --git a/bridges/BundesbankBridge.php b/bridges/BundesbankBridge.php
index dab7893c..4335cb69 100644
--- a/bridges/BundesbankBridge.php
+++ b/bridges/BundesbankBridge.php
@@ -1,84 +1,88 @@
<?php
-class BundesbankBridge extends BridgeAbstract {
- const PARAM_LANG = 'lang';
-
- 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 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
- )
- )
- )
- );
-
- 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';
- }
-
- return parent::getURI();
- }
-
- public function collectData() {
-
- $html = getSimpleHTMLDOM($this->getURI());
-
- $html = defaultLinkTo($html, $this->getURI());
-
- foreach($html->find('ul.resultlist li') as $study) {
- $item = array();
-
- $item['uri'] = $study->find('.teasable__link', 0)->href;
-
- // Get title without child elements (i.e. subtitle)
- $title = $study->find('.teasable__title div.h2', 0);
-
- foreach($title->children as &$child) {
- $child->outertext = '';
- }
-
- $item['title'] = $title->innertext;
-
- // 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>';
- }
-
- $item['content'] .= '<p>' . $study->find('.teasable__text', 0)->plaintext . '</p>';
-
- $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'] = array(
- $study->find('.teasable__image img', 0)->src
- );
- }
-
- $this->items[] = $item;
- }
-
- }
+class BundesbankBridge extends BridgeAbstract
+{
+ const PARAM_LANG = 'lang';
+
+ 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 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 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();
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $html = defaultLinkTo($html, $this->getURI());
+
+ foreach ($html->find('ul.resultlist li') as $study) {
+ $item = [];
+
+ $item['uri'] = $study->find('.teasable__link', 0)->href;
+
+ // Get title without child elements (i.e. subtitle)
+ $title = $study->find('.teasable__title div.h2', 0);
+
+ foreach ($title->children as &$child) {
+ $child->outertext = '';
+ }
+
+ $item['title'] = $title->innertext;
+
+ // 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>';
+ }
+
+ $item['content'] .= '<p>' . $study->find('.teasable__text', 0)->plaintext . '</p>';
+
+ $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
+ ];
+ }
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/BundestagParteispendenBridge.php b/bridges/BundestagParteispendenBridge.php
index 1af24e01..cdf398e8 100644
--- a/bridges/BundestagParteispendenBridge.php
+++ b/bridges/BundestagParteispendenBridge.php
@@ -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.');
-
- // 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;
-
- // 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.');
-
- 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;
-
- $item = array();
-
- // | 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);
-
- $content = sprintf(self::CONTENT_TEMPLATE, $party, $amount, $donor, $date);
-
- $item = array(
- '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 parse the date
- $dateTime = DateTime::createFromFormat('d.m.Y', $date);
- if ($dateTime !== false)
- $item['timestamp'] = $dateTime->getTimestamp();
-
- return $item;
- }
+ // 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.');
+
+ $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);
+
+ $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;
+ }
+ }
+ }
+
+ 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 = [];
+
+ // | 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);
+
+ $content = sprintf(self::CONTENT_TEMPLATE, $party, $amount, $donor, $date);
+
+ $item = [
+ 'title' => $party . ': ' . $amount,
+ 'content' => $content,
+ 'uid' => sha1($content),
+ ];
+
+ // 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();
+ }
+
+ return $item;
+ }
}
diff --git a/bridges/CBCEditorsBlogBridge.php b/bridges/CBCEditorsBlogBridge.php
index c7feb344..a9c0a4dc 100644
--- a/bridges/CBCEditorsBlogBridge.php
+++ b/bridges/CBCEditorsBlogBridge.php
@@ -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;
+ }
+ }
+ }
}
diff --git a/bridges/CNETBridge.php b/bridges/CNETBridge.php
index 27946f25..34442abd 100644
--- a/bridges/CNETBridge.php
+++ b/bridges/CNETBridge.php
@@ -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;
+ }
+ }
+ }
}
diff --git a/bridges/CNETFranceBridge.php b/bridges/CNETFranceBridge.php
index 9195d1b4..724564fa 100644
--- a/bridges/CNETFranceBridge.php
+++ b/bridges/CNETFranceBridge.php
@@ -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'
- )
- )
- );
-
- private $bannedTitle = array();
- private $bannedURL = array();
-
- public function collectData()
- {
- $title = $this->getInput('title');
- $url = $this->getInput('url');
-
- if ($title !== null) {
- $this->bannedTitle = explode(';', $title);
- }
-
- if ($url !== null) {
- $this->bannedURL = explode(';', $url);
- }
-
- $this->collectExpandableDatas('https://www.cnetfrance.fr/feeds/rss/news/');
- }
-
- protected function parseItem($feedItem)
- {
- $item = parent::parseItem($feedItem);
-
- foreach ($this->bannedTitle as $term) {
- if (preg_match('/' . $term . '/mi', $item['title']) === 1) {
- return null;
- }
- }
-
- foreach ($this->bannedURL as $term) {
- if (preg_match('/' . $term . '/mi', $item['uri']) === 1) {
- return null;
- }
- }
-
- return $item;
- }
+ 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 = [];
+ private $bannedURL = [];
+
+ public function collectData()
+ {
+ $title = $this->getInput('title');
+ $url = $this->getInput('url');
+
+ if ($title !== null) {
+ $this->bannedTitle = explode(';', $title);
+ }
+
+ if ($url !== null) {
+ $this->bannedURL = explode(';', $url);
+ }
+
+ $this->collectExpandableDatas('https://www.cnetfrance.fr/feeds/rss/news/');
+ }
+
+ protected function parseItem($feedItem)
+ {
+ $item = parent::parseItem($feedItem);
+
+ foreach ($this->bannedTitle as $term) {
+ if (preg_match('/' . $term . '/mi', $item['title']) === 1) {
+ return null;
+ }
+ }
+
+ foreach ($this->bannedURL as $term) {
+ if (preg_match('/' . $term . '/mi', $item['uri']) === 1) {
+ return null;
+ }
+ }
+
+ return $item;
+ }
}
diff --git a/bridges/CVEDetailsBridge.php b/bridges/CVEDetailsBridge.php
index 18da49bd..38b37bb7 100644
--- a/bridges/CVEDetailsBridge.php
+++ b/bridges/CVEDetailsBridge.php
@@ -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';
-
- 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
- ),
- ));
-
- 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 $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);
-
- $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;
- }
- }
-
- // Build the name of the feed.
- public function getName() {
- if ($this->getInput('vendor_id') == '') {
- return self::NAME;
- }
-
- if ($this->html == null) {
- $this->fetchContent();
- }
-
- $name = 'CVE Vulnerabilities for ' . $this->vendor;
- if ($this->product != '') {
- $name .= '/' . $this->product;
- }
-
- return $name;
- }
-
- // 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();
-
- $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;
-
- $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,
- );
-
- // We only want to fetch the latest 10 CVEs
- if (count($this->items) >= 10) {
- break;
- }
- }
- }
+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 = [[
+ // 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 = '';
+
+ // 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;
+ }
+
+ // 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;
+
+ $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;
+ }
+
+ if ($this->html == null) {
+ $this->fetchContent();
+ }
+
+ $name = 'CVE Vulnerabilities for ' . $this->vendor;
+ if ($this->product != '') {
+ $name .= '/' . $this->product;
+ }
+
+ return $name;
+ }
+
+ // 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 = [$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;
+ }
+
+ // The CVE number itself
+ $title = $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;
+ }
+ }
+ }
}
diff --git a/bridges/CachetBridge.php b/bridges/CachetBridge.php
index 75b18017..355e7926 100644
--- a/bridges/CachetBridge.php
+++ b/bridges/CachetBridge.php
@@ -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;
+ }
+ }
}
diff --git a/bridges/CarThrottleBridge.php b/bridges/CarThrottleBridge.php
index 86dafd9e..95641573 100644
--- a/bridges/CarThrottleBridge.php
+++ b/bridges/CarThrottleBridge.php
@@ -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;
+ }
}
diff --git a/bridges/CastorusBridge.php b/bridges/CastorusBridge.php
index 9dc878f7..a0a1454e 100644
--- a/bridges/CastorusBridge.php
+++ b/bridges/CastorusBridge.php
@@ -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'
- )
- )
- );
-
- // Extracts the title from an actitiy
- private function extractActivityTitle($activity){
- $title = $activity->find('a', 0);
-
- if(!$title)
- returnServerError('Cannot find title!');
-
- return trim($title->plaintext);
- }
-
- // Extracts the url from an actitiy
- private function extractActivityUrl($activity){
- $url = $activity->find('a', 0);
-
- if(!$url)
- returnServerError('Cannot find url!');
-
- return self::URI . $url->href;
- }
-
- // 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('*');
-
- if(!$nodes)
- returnServerError('Cannot find nodes!');
-
- foreach($nodes as $node) {
- $node->outertext = '';
- }
-
- return strtotime($activity->innertext);
- }
-
- // Extracts the price change
- private function extractActivityPrice($activity){
- $price = $activity->find('span', 1);
-
- if(!$price)
- returnServerError('Cannot find price!');
-
- return $price->innertext;
- }
-
- public function collectData(){
- $zip_filter = trim($this->getInput('zip'));
- $city_filter = trim($this->getInput('city'));
-
- $html = getSimpleHTMLDOM(self::URI);
-
- if(!$html)
- returnServerError('Could not load data from ' . self::URI . '!');
-
- $activities = $html->find('div#activite > li');
-
- if(!$activities)
- returnServerError('Failed to find activities!');
-
- foreach($activities as $activity) {
- $item = array();
-
- $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($zip_filter)
- && !(substr($item['title'], 0, strlen($zip_filter)) === $zip_filter)) {
- continue; // Skip this item
- }
-
- if(isset($city_filter)
- && !(substr($item['title'], strpos($item['title'], ' ') + 1, strlen($city_filter)) === $city_filter)) {
- continue; // Skip this item
- }
-
- $this->items[] = $item;
- }
- }
+
+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 = [
+ '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'
+ ]
+ ]
+ ];
+
+ // Extracts the title from an actitiy
+ private function extractActivityTitle($activity)
+ {
+ $title = $activity->find('a', 0);
+
+ if (!$title) {
+ returnServerError('Cannot find title!');
+ }
+
+ return trim($title->plaintext);
+ }
+
+ // Extracts the url from an actitiy
+ private function extractActivityUrl($activity)
+ {
+ $url = $activity->find('a', 0);
+
+ if (!$url) {
+ returnServerError('Cannot find url!');
+ }
+
+ return self::URI . $url->href;
+ }
+
+ // 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('*');
+
+ if (!$nodes) {
+ returnServerError('Cannot find nodes!');
+ }
+
+ foreach ($nodes as $node) {
+ $node->outertext = '';
+ }
+
+ return strtotime($activity->innertext);
+ }
+
+ // Extracts the price change
+ private function extractActivityPrice($activity)
+ {
+ $price = $activity->find('span', 1);
+
+ if (!$price) {
+ returnServerError('Cannot find price!');
+ }
+
+ return $price->innertext;
+ }
+
+ public function collectData()
+ {
+ $zip_filter = trim($this->getInput('zip'));
+ $city_filter = trim($this->getInput('city'));
+
+ $html = getSimpleHTMLDOM(self::URI);
+
+ if (!$html) {
+ returnServerError('Could not load data from ' . self::URI . '!');
+ }
+
+ $activities = $html->find('div#activite > li');
+
+ if (!$activities) {
+ returnServerError('Failed to find activities!');
+ }
+
+ foreach ($activities as $activity) {
+ $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($zip_filter)
+ && !(substr($item['title'], 0, strlen($zip_filter)) === $zip_filter)
+ ) {
+ continue; // Skip this item
+ }
+
+ if (
+ isset($city_filter)
+ && !(substr($item['title'], strpos($item['title'], ' ') + 1, strlen($city_filter)) === $city_filter)
+ ) {
+ continue; // Skip this item
+ }
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/CdactionBridge.php b/bridges/CdactionBridge.php
index 6712faf6..a73a1b4f 100644
--- a/bridges/CdactionBridge.php
+++ b/bridges/CdactionBridge.php
@@ -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;
+ }
+ }
}
diff --git a/bridges/CeskaTelevizeBridge.php b/bridges/CeskaTelevizeBridge.php
index c3be1dc3..99b9b868 100644
--- a/bridges/CeskaTelevizeBridge.php
+++ b/bridges/CeskaTelevizeBridge.php
@@ -1,83 +1,88 @@
<?php
-class CeskaTelevizeBridge extends BridgeAbstract {
-
- const NAME = 'Česká televize Bridge';
- const URI = 'https://www.ceskatelevize.cz';
- const CACHE_TIMEOUT = 3600;
- const DESCRIPTION = 'Return newest videos';
- const MAINTAINER = 'kolarcz';
-
- const PARAMETERS = array(
- array(
- 'url' => array(
- 'name' => 'url to the show',
- 'required' => true,
- 'exampleValue' => 'https://www.ceskatelevize.cz/porady/1097181328-udalosti/'
- )
- )
- );
-
- private function fixChars($text) {
- return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
- }
-
- private function getUploadTimeFromString($string) {
- if (strpos($string, 'dnes') !== false) {
- return strtotime('today');
- } elseif (strpos($string, 'včera') !== false) {
- return strtotime('yesterday');
- } elseif (!preg_match('/(\d+).\s(\d+).(\s(\d+))?/', $string, $match)) {
- returnServerError('Could not get date from Česká televize string');
- }
-
- $date = sprintf('%04d-%02d-%02d', isset($match[3]) ? $match[3] : date('Y'), $match[2], $match[1]);
- return strtotime($date);
- }
-
- public function collectData() {
- $url = $this->getInput('url');
-
- $validUrl = '/^(https:\/\/www\.ceskatelevize\.cz\/porady\/\d+-[a-z0-9-]+\/)(bonus\/)?$/';
- if (!preg_match($validUrl, $url, $match)) {
- returnServerError('Invalid url');
- }
-
- $category = isset($match[4]) ? $match[4] : 'nove';
- $fixedUrl = "{$match[1]}dily/{$category}/";
-
- $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})";
- }
-
- 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 = 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;
- }
- }
-
- public function getURI() {
- return isset($this->feedUri) ? $this->feedUri : parent::getURI();
- }
-
- public function getName() {
- return isset($this->feedName) ? $this->feedName : parent::getName();
- }
+class CeskaTelevizeBridge extends BridgeAbstract
+{
+ const NAME = 'Česká televize Bridge';
+ const URI = 'https://www.ceskatelevize.cz';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'Return newest videos';
+ const MAINTAINER = 'kolarcz';
+
+ const PARAMETERS = [
+ [
+ 'url' => [
+ '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 getUploadTimeFromString($string)
+ {
+ if (strpos($string, 'dnes') !== false) {
+ return strtotime('today');
+ } elseif (strpos($string, 'včera') !== false) {
+ return strtotime('yesterday');
+ } elseif (!preg_match('/(\d+).\s(\d+).(\s(\d+))?/', $string, $match)) {
+ returnServerError('Could not get date from Česká televize string');
+ }
+
+ $date = sprintf('%04d-%02d-%02d', isset($match[3]) ? $match[3] : date('Y'), $match[2], $match[1]);
+ return strtotime($date);
+ }
+
+ public function collectData()
+ {
+ $url = $this->getInput('url');
+
+ $validUrl = '/^(https:\/\/www\.ceskatelevize\.cz\/porady\/\d+-[a-z0-9-]+\/)(bonus\/)?$/';
+ if (!preg_match($validUrl, $url, $match)) {
+ returnServerError('Invalid url');
+ }
+
+ $category = isset($match[4]) ? $match[4] : 'nove';
+ $fixedUrl = "{$match[1]}dily/{$category}/";
+
+ $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})";
+ }
+
+ 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)
+ ];
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ return isset($this->feedUri) ? $this->feedUri : parent::getURI();
+ }
+
+ public function getName()
+ {
+ return isset($this->feedName) ? $this->feedName : parent::getName();
+ }
}
diff --git a/bridges/CodebergBridge.php b/bridges/CodebergBridge.php
index d8a40525..b9a2b4c9 100644
--- a/bridges/CodebergBridge.php
+++ b/bridges/CodebergBridge.php
@@ -1,346 +1,359 @@
<?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;
-
- 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'
- )
- );
-
- private $defaultBranch = 'main';
- private $issueTitle = '';
-
- 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]+)/';
-
- public function detectParameters($url) {
- $params = array();
-
- // 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;
- }
-
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI());
-
- $html = defaultLinkTo($html, $this->getURI());
-
- 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);
- }
- }
-
- public function getName() {
- switch($this->queriedContext) {
- case 'Commits':
- if ($this->getBranch() === $this->defaultBranch) {
- return $this->getRepo() . ' Commits';
- }
-
- 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();
- }
- }
-
- 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();
- }
- }
-
- private function getBranch() {
- if ($this->getInput('branch')) {
- return $this->getInput('branch');
- }
-
- return $this->defaultBranch;
- }
-
- private function getRepo() {
- return $this->getInput('username') . '/' . $this->getInput('repo');
- }
-
- /**
- * Extract commits
- */
- private function extractCommits($html) {
- $table = $html->find('table#commits-table', 0);
- $tbody = $table->find('tbody.commit-list', 0);
-
- foreach ($tbody->find('tr') as $tr) {
- $item = array();
-
- $message = $tr->find('td.message', 0);
-
- $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;
-
- if ($message->find('pre.commit-body', 0)) {
- $message->find('pre.commit-body', 0)->style = '';
-
- $item['content'] = $message->find('pre.commit-body', 0);
- } else {
- $item['content'] = '<blockquote>' . $item['title'] . '</blockquote>';
- }
-
- $this->items[] = $item;
- }
- }
-
- /**
- * Extract issues
- */
- private function extractIssues($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 issue page
- $issuePage = getSimpleHTMLDOMCached($item['uri'], 3600);
- $issuePage = defaultLinkTo($issuePage, self::URI);
-
- $item['content'] = $issuePage->find('div.timeline-item.comment.first', 0)->find('div.render-content.markup', 0);
-
- foreach ($li->find('a.ui.label') as $label) {
- $item['categories'][] = $label->plaintext;
- }
-
- $this->items[] = $item;
- }
- }
-
- /**
- * Extract issue comments
- */
- private function extractIssueComments($html) {
- $this->issueTitle = $html->find('span#issue-title', 0)->plaintext
- . ' (' . $html->find('span.index', 0)->plaintext . ')';
-
- foreach ($html->find('div.timeline-item.comment') as $div) {
- $item = array();
-
- if ($div->class === 'timeline-item comment merge box') {
- continue;
- }
-
- $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);
-
- if ($div->find('div.dropzone-attachments', 0)) {
- $item['content'] .= $div->find('div.dropzone-attachments', 0);
- }
-
- $item['author'] = $div->find('a.author', 0)->innertext;
- $item['timestamp'] = $div->find('span.time-since', 0)->title;
-
- $this->items[] = $item;
- }
- }
- /**
- * Extract pulls
- */
- private function extractPulls($html) {
- $div = $html->find('div.issue.list', 0);
+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' => [],
+ '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 CACHE_TIMEOUT = 1800;
+
+ 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 $defaultBranch = 'main';
+ private $issueTitle = '';
+
+ 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]+)/';
+
+ 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;
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $html = defaultLinkTo($html, $this->getURI());
+
+ 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);
+ }
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Commits':
+ if ($this->getBranch() === $this->defaultBranch) {
+ return $this->getRepo() . ' Commits';
+ }
+
+ 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();
+ }
+ }
+
+ 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();
+ }
+ }
+
+ private function getBranch()
+ {
+ if ($this->getInput('branch')) {
+ return $this->getInput('branch');
+ }
+
+ return $this->defaultBranch;
+ }
+
+ private function getRepo()
+ {
+ return $this->getInput('username') . '/' . $this->getInput('repo');
+ }
+
+ /**
+ * Extract commits
+ */
+ private function extractCommits($html)
+ {
+ $table = $html->find('table#commits-table', 0);
+ $tbody = $table->find('tbody.commit-list', 0);
+
+ foreach ($tbody->find('tr') as $tr) {
+ $item = [];
+
+ $message = $tr->find('td.message', 0);
+
+ $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;
+
+ if ($message->find('pre.commit-body', 0)) {
+ $message->find('pre.commit-body', 0)->style = '';
+
+ $item['content'] = $message->find('pre.commit-body', 0);
+ } else {
+ $item['content'] = '<blockquote>' . $item['title'] . '</blockquote>';
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Extract issues
+ */
+ private function extractIssues($html)
+ {
+ $div = $html->find('div.issue.list', 0);
+
+ foreach ($div->find('li.item') as $li) {
+ $item = [];
+
+ $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 issue page
+ $issuePage = getSimpleHTMLDOMCached($item['uri'], 3600);
+ $issuePage = defaultLinkTo($issuePage, self::URI);
+
+ $item['content'] = $issuePage->find('div.timeline-item.comment.first', 0)->find('div.render-content.markup', 0);
+
+ foreach ($li->find('a.ui.label') as $label) {
+ $item['categories'][] = $label->plaintext;
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Extract issue comments
+ */
+ private function extractIssueComments($html)
+ {
+ $this->issueTitle = $html->find('span#issue-title', 0)->plaintext
+ . ' (' . $html->find('span.index', 0)->plaintext . ')';
+
+ foreach ($html->find('div.timeline-item.comment') as $div) {
+ $item = [];
+
+ if ($div->class === 'timeline-item comment merge box') {
+ continue;
+ }
+
+ $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);
+
+ if ($div->find('div.dropzone-attachments', 0)) {
+ $item['content'] .= $div->find('div.dropzone-attachments', 0);
+ }
+
+ $item['author'] = $div->find('a.author', 0)->innertext;
+ $item['timestamp'] = $div->find('span.time-since', 0)->title;
+
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Extract pulls
+ */
+ private function extractPulls($html)
+ {
+ $div = $html->find('div.issue.list', 0);
+
+ foreach ($div->find('li.item') as $li) {
+ $item = [];
+
+ $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;
- foreach ($div->find('li.item') as $li) {
- $item = array();
+ // 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;
+ }
- $number = trim($li->find('a.index,ml-0.mr-2', 0)->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 = [];
+ $item['title'] = $li->find('h4', 0)->plaintext;
+ $item['uri'] = $li->find('h4', 0)->find('a', 0)->href;
- $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;
+ $tag = $this->stripSvg($li->find('span.tag', 0));
+ $commit = $this->stripSvg($li->find('span.commit', 0));
+ $downloads = $this->extractDownloads($li->find('details.download', 0));
- // 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 +361,59 @@ 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 = '';
+ /**
+ * Extract downloads for a releases
+ */
+ private function extractDownloads($html, $skipFirst = false)
+ {
+ $downloads = '';
- foreach ($html->find('a') as $index => $a) {
- if ($skipFirst === true && $index === 0) {
- continue;
- }
+ foreach ($html->find('a') as $index => $a) {
+ if ($skipFirst === true && $index === 0) {
+ continue;
+ }
- $downloads .= <<<HTML
+ $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;
-
- 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 = '';
- }
-
- return $html;
- }
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Strip SVG tag
+ */
+ private function stripSvg($html)
+ {
+ if ($html->find('svg', 0)) {
+ $html->find('svg', 0)->outertext = '';
+ }
+
+ return $html;
+ }
}
diff --git a/bridges/CollegeDeFranceBridge.php b/bridges/CollegeDeFranceBridge.php
index 3c241834..2a1b33d4 100644
--- a/bridges/CollegeDeFranceBridge.php
+++ b/bridges/CollegeDeFranceBridge.php
@@ -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;
+ }
+ }
}
diff --git a/bridges/ComboiosDePortugalBridge.php b/bridges/ComboiosDePortugalBridge.php
index 652ba601..7b2381b9 100644
--- a/bridges/ComboiosDePortugalBridge.php
+++ b/bridges/ComboiosDePortugalBridge.php
@@ -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;
+ }
+ }
}
diff --git a/bridges/ComicsKingdomBridge.php b/bridges/ComicsKingdomBridge.php
index 402403e0..8baf7511 100644
--- a/bridges/ComicsKingdomBridge.php
+++ b/bridges/ComicsKingdomBridge.php
@@ -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();
+ }
}
diff --git a/bridges/CommonDreamsBridge.php b/bridges/CommonDreamsBridge.php
index 22b9238d..ea21b436 100644
--- a/bridges/CommonDreamsBridge.php
+++ b/bridges/CommonDreamsBridge.php
@@ -1,26 +1,30 @@
<?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)
+ {
+ $html3 = getSimpleHTMLDOMCached($url);
+ $text = $html3->find('div[class=field--type-text-with-summary]', 0)->innertext;
+ $html3->clear();
+ unset($html3);
+ return $text;
+ }
}
diff --git a/bridges/CopieDoubleBridge.php b/bridges/CopieDoubleBridge.php
index 756ecb9e..00739a4e 100644
--- a/bridges/CopieDoubleBridge.php
+++ b/bridges/CopieDoubleBridge.php
@@ -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;
+ }
+ }
+ }
}
diff --git a/bridges/CourrierInternationalBridge.php b/bridges/CourrierInternationalBridge.php
index a490e28f..fdbe2ea6 100644
--- a/bridges/CourrierInternationalBridge.php
+++ b/bridges/CourrierInternationalBridge.php
@@ -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;
+ }
}
diff --git a/bridges/CraigslistBridge.php b/bridges/CraigslistBridge.php
index 8e677cf4..d56c770e 100644
--- a/bridges/CraigslistBridge.php
+++ b/bridges/CraigslistBridge.php
@@ -1,106 +1,110 @@
<?php
-class CraigslistBridge extends BridgeAbstract {
- 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;
+ $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;
- $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;
- }
- }
+ $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;
+ }
+ }
}
diff --git a/bridges/CrewbayBridge.php b/bridges/CrewbayBridge.php
index a3c52b9a..0ca017c2 100644
--- a/bridges/CrewbayBridge.php
+++ b/bridges/CrewbayBridge.php
@@ -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.');
-
- $annonces = $html->find('#SearchResults div.result');
- $limit = 0;
-
- foreach ($annonces as $annonce) {
- $detail = $annonce->find('.btn--profile', 0);
- $htmlDetail = getSimpleHTMLDOMCached($detail->href);
-
- 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);
- }
-
- $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 (!$found) {
- continue;
- }
- }
-
- $item = array();
-
- 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['uri'] = $detail->href;
- $images = $annonce->find('.avatar img');
- $item['enclosures'] = array(end($images)->getAttribute('src'));
-
- $content = $htmlDetail->find('.viewer-intro--info', 0)->innertext;
-
- $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;
- }
- }
-
- if (!empty($this->getInput('keyword'))) {
- $keyword = strtolower($this->getInput('keyword'));
- if (strpos(strtolower($item['title']), $keyword) === false) {
- if (strpos(strtolower($content), $keyword) === false) {
- continue;
- }
- }
- }
-
- $item['content'] = $content;
-
- $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;
- }
- }
-
- $this->items[] = $item;
- $limit += 1;
-
- if ($limit == 10) break;
- }
- }
-
- public function getURI() {
- $uri = parent::getURI();
-
- if ($this->getInput('type') == 'boats') {
- $uri .= '/boats';
- } else {
- $uri .= '/crew';
- }
-
- if ($this->getInput('status') == 'professional') {
- $uri .= '/professional';
- } else {
- $uri .= '/recreational';
- }
-
- return $uri;
- }
+
+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'
+ ]
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $url = $this->getURI();
+ $html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.');
+
+ $annonces = $html->find('#SearchResults div.result');
+ $limit = 0;
+
+ foreach ($annonces as $annonce) {
+ $detail = $annonce->find('.btn--profile', 0);
+ $htmlDetail = getSimpleHTMLDOMCached($detail->href);
+
+ 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);
+ }
+
+ $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 (!$found) {
+ continue;
+ }
+ }
+
+ $item = [];
+
+ 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['uri'] = $detail->href;
+ $images = $annonce->find('.avatar img');
+ $item['enclosures'] = [end($images)->getAttribute('src')];
+
+ $content = $htmlDetail->find('.viewer-intro--info', 0)->innertext;
+
+ $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;
+ }
+ }
+
+ if (!empty($this->getInput('keyword'))) {
+ $keyword = strtolower($this->getInput('keyword'));
+ if (strpos(strtolower($item['title']), $keyword) === false) {
+ if (strpos(strtolower($content), $keyword) === false) {
+ continue;
+ }
+ }
+ }
+
+ $item['content'] = $content;
+
+ $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;
+ }
+ }
+
+ $this->items[] = $item;
+ $limit += 1;
+
+ if ($limit == 10) {
+ break;
+ }
+ }
+ }
+
+ public function getURI()
+ {
+ $uri = parent::getURI();
+
+ if ($this->getInput('type') == 'boats') {
+ $uri .= '/boats';
+ } else {
+ $uri .= '/crew';
+ }
+
+ if ($this->getInput('status') == 'professional') {
+ $uri .= '/professional';
+ } else {
+ $uri .= '/recreational';
+ }
+
+ return $uri;
+ }
}
diff --git a/bridges/CryptomeBridge.php b/bridges/CryptomeBridge.php
index 50ab48bc..de5544ec 100644
--- a/bridges/CryptomeBridge.php
+++ b/bridges/CryptomeBridge.php
@@ -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++;
+ }
+ }
+ }
}
diff --git a/bridges/CubariBridge.php b/bridges/CubariBridge.php
index 00a2a0ba..9a08dbba 100644
--- a/bridges/CubariBridge.php
+++ b/bridges/CubariBridge.php
@@ -1,98 +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 = array(array(
- 'gist' => array(
- 'name' => 'Gist/Raw Url',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'https://raw.githubusercontent.com/kurisumx/baka/main/ikedan'
- )
- ));
-
- private $mangaTitle = '';
-
- public function getName()
- {
- if (!empty($this->mangaTitle))
- return $this->mangaTitle . ' - ' . self::NAME;
- else
- return self::NAME;
- }
-
- public function getURI()
- {
- if ($this->getInput('gist') != '')
- return self::URI . '/read/gist/' . $this->getEncodedGist();
- else
- return self::URI;
- }
-
- /**
- * The Cubari bridge.
- *
- * Cubari urls are base64 encodes of a given github raw or gist link described as below:
- * https://cubari.moe/read/gist/${bаse64.url_encode(raw/<rest of the url...>)}/
- * https://cubari.moe/read/gist/${bаse64.url_encode(gist/<rest of the url...>)}/
- * https://cubari.moe/read/gist/${gitio shortcode}
- *
- * This bridge uses just the raw/gist and generates matching cubari urls.
- */
- public function collectData()
- {
- $jsonSite = getContents($this->getInput('gist'));
- $jsonFile = json_decode($jsonSite, true);
-
- $this->mangaTitle = $jsonFile['title'];
-
- $chapters = $jsonFile['chapters'];
-
- foreach ($chapters as $chapnum => $chapter) {
- $item = $this->getItemFromChapter($chapnum, $chapter);
- $this->items[] = $item;
- }
-
- array_multisort(array_column($this->items, 'timestamp'), SORT_DESC, $this->items);
- }
-
- protected function getEncodedGist()
- {
- $url = $this->getInput('gist');
-
- preg_match('/\/([a-z]*)\.githubusercontent.com(.*)/', $url, $matches);
-
- // raw or gist is first match.
- $unencoded = $matches[1] . $matches[2];
-
- return base64_encode($unencoded);
- }
-
- private function getSanitizedHash($string)
- {
- return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string))));
- }
-
- protected function getItemFromChapter($chapnum, $chapter)
- {
- $item = array();
-
- $item['uri'] = $this->getURI() . '/' . $chapnum;
- $item['title'] = 'Chapter ' . $chapnum . ' - ' . $chapter['title'] . ' - ' . $this->mangaTitle;
- foreach ($chapter['groups'] as $key => $value)
- $item['author'] = $key;
- $item['timestamp'] = $chapter['last_updated'];
-
- $item['content'] = '<p>Manga: <a href=' . $this->getURI() . '>' . $this->mangaTitle . '</a> </p>
+ 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']);
+ $item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']);
- return $item;
- }
+ return $item;
+ }
}
diff --git a/bridges/CuriousCatBridge.php b/bridges/CuriousCatBridge.php
index 641da5d8..573c776f 100644
--- a/bridges/CuriousCatBridge.php
+++ b/bridges/CuriousCatBridge.php
@@ -1,108 +1,110 @@
<?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, true);
- 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;
- }
-
- private function ellipsisTitle($text) {
- $length = 150;
-
- if (strlen($text) > $length) {
- $text = explode('<br>', wordwrap($text, $length, '<br>'));
- return $text[0] . '...';
- }
-
- return $text;
- }
-
- private function formatUrls($content) {
-
- return preg_replace(
- '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims',
- '<a target="_blank" href="$1" target="_blank">$1</a> ',
- $content
- );
-
- }
+ return $content;
+ }
+
+ private function ellipsisTitle($text)
+ {
+ $length = 150;
+
+ if (strlen($text) > $length) {
+ $text = explode('<br>', wordwrap($text, $length, '<br>'));
+ return $text[0] . '...';
+ }
+
+ return $text;
+ }
+
+ private function formatUrls($content)
+ {
+ return preg_replace(
+ '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims',
+ '<a target="_blank" href="$1" target="_blank">$1</a> ',
+ $content
+ );
+ }
}
diff --git a/bridges/CyanideAndHappinessBridge.php b/bridges/CyanideAndHappinessBridge.php
index 41ac096a..2b54affc 100644
--- a/bridges/CyanideAndHappinessBridge.php
+++ b/bridges/CyanideAndHappinessBridge.php
@@ -1,37 +1,42 @@
<?php
-class CyanideAndHappinessBridge extends BridgeAbstract {
- const NAME = 'Cyanide & Happiness';
- const URI = 'https://explosm.net/';
- const DESCRIPTION = 'The Webcomic from Explosm.';
- const MAINTAINER = 'sal0max';
- const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours
- public function getIcon() {
- return self::URI . 'favicon-32x32.png';
- }
+class CyanideAndHappinessBridge extends BridgeAbstract
+{
+ const NAME = 'Cyanide & Happiness';
+ const URI = 'https://explosm.net/';
+ const DESCRIPTION = 'The Webcomic from Explosm.';
+ const MAINTAINER = 'sal0max';
+ const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours
- public function getURI(){
- return self::URI . 'comics/latest#comic';
- }
+ public function getIcon()
+ {
+ return self::URI . 'favicon-32x32.png';
+ }
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getUri());
+ public function getURI()
+ {
+ return self::URI . 'comics/latest#comic';
+ }
- foreach ($html->find('[class*=ComicImage]') as $element) {
- $date = $element->find('[class^=Author__Right] p', 0)->plaintext;
- $author = str_replace('by ', '', $element->find('[class^=Author__Right] p', 1)->plaintext);
- $image = $element->find('img', 0)->src;
- $link = $html->find('[rel=canonical]', 0)->href;
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getUri());
- $item = array(
- 'uid' => $link,
- 'author' => $author,
- 'title' => $date,
- 'uri' => $link . '#comic',
- 'timestamp' => str_replace('.', '-', $date) . 'T00:00:00Z',
- 'content' => "<img src=\"$image\" />"
- );
- $this->items[] = $item;
- }
- }
+ foreach ($html->find('[class*=ComicImage]') as $element) {
+ $date = $element->find('[class^=Author__Right] p', 0)->plaintext;
+ $author = str_replace('by ', '', $element->find('[class^=Author__Right] p', 1)->plaintext);
+ $image = $element->find('img', 0)->src;
+ $link = $html->find('[rel=canonical]', 0)->href;
+
+ $item = [
+ 'uid' => $link,
+ 'author' => $author,
+ 'title' => $date,
+ 'uri' => $link . '#comic',
+ 'timestamp' => str_replace('.', '-', $date) . 'T00:00:00Z',
+ 'content' => "<img src=\"$image\" />"
+ ];
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/DailymotionBridge.php b/bridges/DailymotionBridge.php
index 688c0a10..5d892954 100644
--- a/bridges/DailymotionBridge.php
+++ b/bridges/DailymotionBridge.php
@@ -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';
-
- 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,
- )
- )
- );
-
- private $feedName = '';
-
- private $apiUrl = 'https://api.dailymotion.com';
- private $apiFields = 'created_time,description,id,owner.screenname,tags,thumbnail_url,title,url';
-
- public function getIcon() {
- return 'https://static1-ssl.dmcdn.net/images/neon/favicons/android-icon-36x36.png.vf806ca4ed0deed812';
- }
-
- public function collectData() {
-
- if ($this->queriedContext === 'By username' || $this->queriedContext === 'By playlist id') {
-
- $apiJson = getContents($this->getApiUrl());
-
- $apiData = json_decode($apiJson, true);
-
- $this->feedName = $this->getPlaylistTitle($this->getInput('p'));
-
- foreach ($apiData['list'] as $apiItem) {
- $item = array();
-
- $item['uri'] = $apiItem['url'];
- $item['uid'] = $apiItem['id'];
- $item['title'] = $apiItem['title'];
- $item['timestamp'] = $apiItem['created_time'];
- $item['author'] = $apiItem['owner.screenname'];
- $item['content'] = '<p><a href="' . $apiItem['url'] . '">
+
+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 = [
+ '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 $apiUrl = 'https://api.dailymotion.com';
+ private $apiFields = 'created_time,description,id,owner.screenname,tags,thumbnail_url,title,url';
+
+ public function getIcon()
+ {
+ return 'https://static1-ssl.dmcdn.net/images/neon/favicons/android-icon-36x36.png.vf806ca4ed0deed812';
+ }
+
+ public function collectData()
+ {
+ if ($this->queriedContext === 'By username' || $this->queriedContext === 'By playlist id') {
+ $apiJson = getContents($this->getApiUrl());
+
+ $apiData = json_decode($apiJson, true);
+
+ $this->feedName = $this->getPlaylistTitle($this->getInput('p'));
+
+ foreach ($apiData['list'] as $apiItem) {
+ $item = [];
+
+ $item['uri'] = $apiItem['url'];
+ $item['uid'] = $apiItem['id'];
+ $item['title'] = $apiItem['title'];
+ $item['timestamp'] = $apiItem['created_time'];
+ $item['author'] = $apiItem['owner.screenname'];
+ $item['content'] = '<p><a href="' . $apiItem['url'] . '">
<img src="' . $apiItem['thumbnail_url'] . '"></a></p><p>' . $apiItem['description'] . '</p>';
- $item['categories'] = $apiItem['tags'];
- $item['enclosures'][] = $apiItem['thumbnail_url'];
-
- $this->items[] = $item;
- }
- }
-
- if ($this->queriedContext === 'From search results') {
-
- $html = getSimpleHTMLDOM($this->getURI());
-
- 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']);
-
- if(empty($metadata)) {
- continue;
- }
-
- $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>';
-
- $this->items[] = $item;
-
- 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'), '_');
-
- if ($this->feedName) {
- $specific = $this->feedName;
- }
-
- break;
- case 'From search results':
- $specific = $this->getInput('s');
- break;
- default: return parent::getName();
- }
-
- 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'));
-
- if(!is_null($this->getInput('pa'))) {
- $pa = $this->getInput('pa');
-
- if ($this->getInput('pa') < 1) {
- $pa = 1;
- }
-
- $uri .= '/' . $pa;
- }
- break;
- default: return parent::getURI();
- }
- return $uri;
- }
-
- private function getMetadata($id) {
- $metadata = array();
-
- $html = getSimpleHTMLDOM(self::URI . 'video/' . $id);
-
- if(!$html) {
- return $metadata;
- }
-
- $metadata['title'] = $html->find('meta[property=og:title]', 0)->getAttribute('content');
- $metadata['timestamp'] = strtotime(
- $html->find('meta[property=video:release_date]', 0)->getAttribute('content')
- );
- $metadata['thumbnailUri'] = $html->find('meta[property=og:image]', 0)->getAttribute('content');
- $metadata['uri'] = $html->find('meta[property=og:url]', 0)->getAttribute('content');
- return $metadata;
- }
-
- private function getPlaylistTitle($id) {
- $title = '';
-
- $url = self::URI . 'playlist/' . $id;
-
- $html = getSimpleHTMLDOM($url);
-
- $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;
- }
- }
+ $item['categories'] = $apiItem['tags'];
+ $item['enclosures'][] = $apiItem['thumbnail_url'];
+
+ $this->items[] = $item;
+ }
+ }
+
+ if ($this->queriedContext === 'From search results') {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ foreach ($html->find('div.media a.preview_link') as $element) {
+ $item = [];
+
+ $item['id'] = str_replace('/video/', '', strtok($element->href, '_'));
+ $metadata = $this->getMetadata($item['id']);
+
+ if (empty($metadata)) {
+ continue;
+ }
+
+ $item['uri'] = $metadata['uri'];
+ $item['title'] = $metadata['title'];
+ $item['timestamp'] = $metadata['timestamp'];
+
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $metadata['thumbnailUri']
+ . '" /></a><br><a href="'
+ . $item['uri']
+ . '">'
+ . $item['title']
+ . '</a>';
+
+ $this->items[] = $item;
+
+ 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'), '_');
+
+ if ($this->feedName) {
+ $specific = $this->feedName;
+ }
+
+ break;
+ case 'From search results':
+ $specific = $this->getInput('s');
+ break;
+ default:
+ return parent::getName();
+ }
+
+ 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'));
+
+ if (!is_null($this->getInput('pa'))) {
+ $pa = $this->getInput('pa');
+
+ if ($this->getInput('pa') < 1) {
+ $pa = 1;
+ }
+
+ $uri .= '/' . $pa;
+ }
+ break;
+ default:
+ return parent::getURI();
+ }
+ return $uri;
+ }
+
+ private function getMetadata($id)
+ {
+ $metadata = [];
+
+ $html = getSimpleHTMLDOM(self::URI . 'video/' . $id);
+
+ if (!$html) {
+ return $metadata;
+ }
+
+ $metadata['title'] = $html->find('meta[property=og:title]', 0)->getAttribute('content');
+ $metadata['timestamp'] = strtotime(
+ $html->find('meta[property=video:release_date]', 0)->getAttribute('content')
+ );
+ $metadata['thumbnailUri'] = $html->find('meta[property=og:image]', 0)->getAttribute('content');
+ $metadata['uri'] = $html->find('meta[property=og:url]', 0)->getAttribute('content');
+ return $metadata;
+ }
+
+ private function getPlaylistTitle($id)
+ {
+ $title = '';
+
+ $url = self::URI . 'playlist/' . $id;
+
+ $html = getSimpleHTMLDOM($url);
+
+ $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;
+ }
+ }
}
diff --git a/bridges/DanbooruBridge.php b/bridges/DanbooruBridge.php
index d6337f6b..3ca4476e 100644
--- a/bridges/DanbooruBridge.php
+++ b/bridges/DanbooruBridge.php
@@ -1,68 +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(
- 'type' => 'text',
- 'name' => 'tags',
- 'exampleValue' => 'cosplay',
- )
- ),
- 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);
+ }
+ }
}
diff --git a/bridges/DansTonChatBridge.php b/bridges/DansTonChatBridge.php
index 1f1115f7..9712ec9d 100644
--- a/bridges/DansTonChatBridge.php
+++ b/bridges/DansTonChatBridge.php
@@ -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;
+ }
+ }
}
diff --git a/bridges/DarkReadingBridge.php b/bridges/DarkReadingBridge.php
index 8fe242dd..6881c604 100644
--- a/bridges/DarkReadingBridge.php
+++ b/bridges/DarkReadingBridge.php
@@ -1,83 +1,90 @@
<?php
-class DarkReadingBridge extends FeedExpander {
- const MAINTAINER = 'ORelio';
- const NAME = 'Dark Reading Bridge';
- const URI = 'https://www.darkreading.com/';
- const DESCRIPTION = 'Returns the newest articles from Dark Reading';
- const PARAMETERS = array( array(
- 'feed' => array(
- 'name' => 'Feed',
- 'type' => 'list',
- 'values' => array(
- 'All Dark Reading Stories' => '000_AllArticles',
- 'Attacks/Breaches' => '644_Attacks/Breaches',
- 'Application Security' => '645_Application%20Security',
- 'Database Security' => '646_Database%20Security',
- 'Cloud' => '647_Cloud',
- 'Endpoint' => '648_Endpoint',
- 'Authentication' => '649_Authentication',
- 'Privacy' => '650_Privacy',
- 'Mobile' => '651_Mobile',
- 'Perimeter' => '652_Perimeter',
- 'Risk' => '653_Risk',
- 'Compliance' => '654_Compliance',
- 'Operations' => '655_Operations',
- 'Careers and People' => '656_Careers%20and%20People',
- 'Identity and Access Management' => '657_Identity%20and%20Access%20Management',
- 'Analytics' => '658_Analytics',
- 'Threat Intelligence' => '659_Threat%20Intelligence',
- 'Security Monitoring' => '660_Security%20Monitoring',
- 'Vulnerabilities / Threats' => '661_Vulnerabilities%20/%20Threats',
- 'Advanced Threats' => '662_Advanced%20Threats',
- 'Insider Threats' => '663_Insider%20Threats',
- 'Vulnerability Management' => '664_Vulnerability%20Management',
- )
- ),
- 'limit' => self::LIMIT,
- ));
+class DarkReadingBridge extends FeedExpander
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = 'Dark Reading Bridge';
+ const URI = 'https://www.darkreading.com/';
+ const DESCRIPTION = 'Returns the newest articles from Dark Reading';
- public function collectData(){
- $feed = $this->getInput('feed');
- $feed_splitted = explode('_', $feed);
- $feed_id = $feed_splitted[0];
- $feed_name = $feed_splitted[1];
- if(empty($feed) || !ctype_digit($feed_id) || !preg_match('/[A-Za-z%20\/]/', $feed_name)) {
- returnClientError('Invalid feed, please check the "feed" parameter.');
- }
- $feed_url = $this->getURI() . 'rss_simple.asp';
- if ($feed_id != '000') {
- $feed_url .= '?f_n=' . $feed_id . '&f_ln=' . $feed_name;
- }
- $limit = $this->getInput('limit') ?? 10;
- $this->collectExpandableDatas($feed_url, $limit);
- }
+ const PARAMETERS = [ [
+ 'feed' => [
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'values' => [
+ 'All Dark Reading Stories' => '000_AllArticles',
+ 'Attacks/Breaches' => '644_Attacks/Breaches',
+ 'Application Security' => '645_Application%20Security',
+ 'Database Security' => '646_Database%20Security',
+ 'Cloud' => '647_Cloud',
+ 'Endpoint' => '648_Endpoint',
+ 'Authentication' => '649_Authentication',
+ 'Privacy' => '650_Privacy',
+ 'Mobile' => '651_Mobile',
+ 'Perimeter' => '652_Perimeter',
+ 'Risk' => '653_Risk',
+ 'Compliance' => '654_Compliance',
+ 'Operations' => '655_Operations',
+ 'Careers and People' => '656_Careers%20and%20People',
+ 'Identity and Access Management' => '657_Identity%20and%20Access%20Management',
+ 'Analytics' => '658_Analytics',
+ 'Threat Intelligence' => '659_Threat%20Intelligence',
+ 'Security Monitoring' => '660_Security%20Monitoring',
+ 'Vulnerabilities / Threats' => '661_Vulnerabilities%20/%20Threats',
+ 'Advanced Threats' => '662_Advanced%20Threats',
+ 'Insider Threats' => '663_Insider%20Threats',
+ 'Vulnerability Management' => '664_Vulnerability%20Management',
+ ]
+ ],
+ 'limit' => self::LIMIT,
+ ]];
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- $article = getSimpleHTMLDOMCached($item['uri']);
- $item['content'] = $this->extractArticleContent($article);
- $item['enclosures'] = array(); //remove author profile picture
- $image = $article->find('meta[property="og:image"]', 0);
- if (is_object($image)) {
- $image = $image->content;
- $item['enclosures'] = array($image);
- }
- return $item;
- }
+ public function collectData()
+ {
+ $feed = $this->getInput('feed');
+ $feed_splitted = explode('_', $feed);
+ $feed_id = $feed_splitted[0];
+ $feed_name = $feed_splitted[1];
+ if (empty($feed) || !ctype_digit($feed_id) || !preg_match('/[A-Za-z%20\/]/', $feed_name)) {
+ returnClientError('Invalid feed, please check the "feed" parameter.');
+ }
+ $feed_url = $this->getURI() . 'rss_simple.asp';
+ if ($feed_id != '000') {
+ $feed_url .= '?f_n=' . $feed_id . '&f_ln=' . $feed_name;
+ }
+ $limit = $this->getInput('limit') ?? 10;
+ $this->collectExpandableDatas($feed_url, $limit);
+ }
- private function extractArticleContent($article){
- $content = $article->find('div.article-content', 0)->innertext;
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ $article = getSimpleHTMLDOMCached($item['uri']);
+ $item['content'] = $this->extractArticleContent($article);
+ $item['enclosures'] = []; //remove author profile picture
+ $image = $article->find('meta[property="og:image"]', 0);
+ if (is_object($image)) {
+ $image = $image->content;
+ $item['enclosures'] = [$image];
+ }
+ return $item;
+ }
- foreach (array(
- '<div class="divsplitter',
- '<div style="float: left; margin-right: 2px;',
- '<div class="more-insights',
- '<div id="more-insights',
- ) as $div_start) {
- $content = stripRecursiveHTMLSection($content, 'div', $div_start);
- }
+ private function extractArticleContent($article)
+ {
+ $content = $article->find('div.article-content', 0)->innertext;
- return $content;
- }
+ foreach (
+ [
+ '<div class="divsplitter',
+ '<div style="float: left; margin-right: 2px;',
+ '<div class="more-insights',
+ '<div id="more-insights',
+ ] as $div_start
+ ) {
+ $content = stripRecursiveHTMLSection($content, 'div', $div_start);
+ }
+
+ return $content;
+ }
}
diff --git a/bridges/DauphineLibereBridge.php b/bridges/DauphineLibereBridge.php
index 20c82070..82323036 100644
--- a/bridges/DauphineLibereBridge.php
+++ b/bridges/DauphineLibereBridge.php
@@ -1,57 +1,61 @@
<?php
-class DauphineLibereBridge extends FeedExpander {
- const MAINTAINER = 'qwertygc';
- const NAME = 'Dauphine Bridge';
- const URI = 'https://www.ledauphine.com/';
- const CACHE_TIMEOUT = 7200; // 2h
- const DESCRIPTION = 'Returns the newest articles.';
+class DauphineLibereBridge extends FeedExpander
+{
+ const MAINTAINER = 'qwertygc';
+ const NAME = 'Dauphine Bridge';
+ const URI = 'https://www.ledauphine.com/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Returns the newest articles.';
- const PARAMETERS = array( array(
- 'u' => array(
- 'name' => 'Catégorie de l\'article',
- 'type' => 'list',
- 'values' => array(
- 'À la une' => '',
- 'France Monde' => 'france-monde',
- 'Faits Divers' => 'faits-divers',
- 'Économie et Finance' => 'economie-et-finance',
- 'Politique' => 'politique',
- 'Sport' => 'sport',
- 'Ain' => 'ain',
- 'Alpes-de-Haute-Provence' => 'haute-provence',
- 'Hautes-Alpes' => 'hautes-alpes',
- 'Ardèche' => 'ardeche',
- 'Drôme' => 'drome',
- 'Isère Sud' => 'isere-sud',
- 'Savoie' => 'savoie',
- 'Haute-Savoie' => 'haute-savoie',
- 'Vaucluse' => 'vaucluse'
- )
- )
- ));
+ const PARAMETERS = [ [
+ 'u' => [
+ 'name' => 'Catégorie de l\'article',
+ 'type' => 'list',
+ 'values' => [
+ 'À la une' => '',
+ 'France Monde' => 'france-monde',
+ 'Faits Divers' => 'faits-divers',
+ 'Économie et Finance' => 'economie-et-finance',
+ 'Politique' => 'politique',
+ 'Sport' => 'sport',
+ 'Ain' => 'ain',
+ 'Alpes-de-Haute-Provence' => 'haute-provence',
+ 'Hautes-Alpes' => 'hautes-alpes',
+ 'Ardèche' => 'ardeche',
+ 'Drôme' => 'drome',
+ 'Isère Sud' => 'isere-sud',
+ 'Savoie' => 'savoie',
+ 'Haute-Savoie' => 'haute-savoie',
+ 'Vaucluse' => 'vaucluse'
+ ]
+ ]
+ ]];
- public function collectData(){
- $url = self::URI . 'rss';
+ public function collectData()
+ {
+ $url = self::URI . 'rss';
- if(empty($this->getInput('u'))) {
- $url = self::URI . $this->getInput('u') . '/rss';
- }
+ if (empty($this->getInput('u'))) {
+ $url = self::URI . $this->getInput('u') . '/rss';
+ }
- $this->collectExpandableDatas($url, 10);
- }
+ $this->collectExpandableDatas($url, 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){
- $html2 = getSimpleHTMLDOMCached($url);
- foreach ($html2->find('.noprint, link, script, iframe, .shareTool, .contentInfo') as $remove) {
- $remove->outertext = '';
- }
- return $html2->find('div.content', 0)->innertext;
- }
+ private function extractContent($url)
+ {
+ $html2 = getSimpleHTMLDOMCached($url);
+ foreach ($html2->find('.noprint, link, script, iframe, .shareTool, .contentInfo') as $remove) {
+ $remove->outertext = '';
+ }
+ return $html2->find('div.content', 0)->innertext;
+ }
}
diff --git a/bridges/DavesTrailerPageBridge.php b/bridges/DavesTrailerPageBridge.php
index 88e0bbb3..965f7e59 100644
--- a/bridges/DavesTrailerPageBridge.php
+++ b/bridges/DavesTrailerPageBridge.php
@@ -1,37 +1,40 @@
<?php
-class DavesTrailerPageBridge extends BridgeAbstract {
- const MAINTAINER = 'johnnygroovy';
- const NAME = 'Daves Trailer Page Bridge';
- const URI = 'https://www.davestrailerpage.co.uk/';
- const DESCRIPTION = 'Last trailers in HD thanks to Dave.';
- public function collectData(){
- $html = getSimpleHTMLDOM(static::URI)
- or returnClientError('No results for this query.');
+class DavesTrailerPageBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'johnnygroovy';
+ const NAME = 'Daves Trailer Page Bridge';
+ const URI = 'https://www.davestrailerpage.co.uk/';
+ const DESCRIPTION = 'Last trailers in HD thanks to Dave.';
- $curr_date = null;
- foreach ($html->find('tr') as $tr) {
- // If it's a date row, update the current date
- if ($tr->align == 'center') {
- $curr_date = $tr->plaintext;
- continue;
- }
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(static::URI)
+ or returnClientError('No results for this query.');
- $item = array();
+ $curr_date = null;
+ foreach ($html->find('tr') as $tr) {
+ // If it's a date row, update the current date
+ if ($tr->align == 'center') {
+ $curr_date = $tr->plaintext;
+ continue;
+ }
- // title
- $item['title'] = $tr->find('td', 0)->find('b', 0)->plaintext;
+ $item = [];
- // content
- $item['content'] = $tr->find('ul', 1);
+ // title
+ $item['title'] = $tr->find('td', 0)->find('b', 0)->plaintext;
- // uri
- $item['uri'] = $tr->find('a', 3)->getAttribute('href');
+ // content
+ $item['content'] = $tr->find('ul', 1);
- // date: parsed by FeedItem using strtotime
- $item['timestamp'] = $curr_date;
+ // uri
+ $item['uri'] = $tr->find('a', 3)->getAttribute('href');
- $this->items[] = $item;
- }
- }
+ // date: parsed by FeedItem using strtotime
+ $item['timestamp'] = $curr_date;
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/DealabsBridge.php b/bridges/DealabsBridge.php
index c2d8c9f3..6e5ba9e3 100644
--- a/bridges/DealabsBridge.php
+++ b/bridges/DealabsBridge.php
@@ -1,1965 +1,1963 @@
<?php
-class DealabsBridge extends PepperBridgeAbstract {
- const NAME = 'Dealabs Bridge';
- const URI = 'https://www.dealabs.com/';
- const DESCRIPTION = 'Affiche les Deals de Dealabs';
- const MAINTAINER = 'sysadminstory';
- const PARAMETERS = array(
- 'Recherche par Mot(s) clé(s)' => array (
- 'q' => array(
- 'name' => 'Mot(s) clé(s)',
- 'type' => 'text',
- 'exampleValue' => 'lampe',
- 'required' => true
- ),
- 'hide_expired' => array(
- 'name' => 'Masquer les éléments expirés',
- 'type' => 'checkbox',
- ),
- 'hide_local' => array(
- 'name' => 'Masquer les deals locaux',
- 'type' => 'checkbox',
- 'title' => 'Masquer les deals en magasins physiques',
- ),
- 'priceFrom' => array(
- 'name' => 'Prix minimum',
- 'type' => 'text',
- 'title' => 'Prix mnimum en euros',
- 'required' => false
- ),
- 'priceTo' => array(
- 'name' => 'Prix maximum',
- 'type' => 'text',
- 'title' => 'Prix maximum en euros',
- 'required' => false
- ),
- ),
+class DealabsBridge extends PepperBridgeAbstract
+{
+ const NAME = 'Dealabs Bridge';
+ const URI = 'https://www.dealabs.com/';
+ const DESCRIPTION = 'Affiche les Deals de Dealabs';
+ const MAINTAINER = 'sysadminstory';
+ const PARAMETERS = [
+ 'Recherche par Mot(s) clé(s)' => [
+ 'q' => [
+ 'name' => 'Mot(s) clé(s)',
+ 'type' => 'text',
+ 'exampleValue' => 'lampe',
+ 'required' => true
+ ],
+ 'hide_expired' => [
+ 'name' => 'Masquer les éléments expirés',
+ 'type' => 'checkbox',
+ ],
+ 'hide_local' => [
+ 'name' => 'Masquer les deals locaux',
+ 'type' => 'checkbox',
+ 'title' => 'Masquer les deals en magasins physiques',
+ ],
+ 'priceFrom' => [
+ 'name' => 'Prix minimum',
+ 'type' => 'text',
+ 'title' => 'Prix mnimum en euros',
+ 'required' => false
+ ],
+ 'priceTo' => [
+ 'name' => 'Prix maximum',
+ 'type' => 'text',
+ 'title' => 'Prix maximum en euros',
+ 'required' => false
+ ],
+ ],
- 'Deals par groupe' => array(
- 'group' => array(
- 'name' => 'Groupe',
- 'type' => 'list',
- 'title' => 'Groupe dont il faut afficher les deals',
- 'values' => array(
- 'Abattants WC' => 'abattants-wc',
- 'Abonnement PlayStation Plus' => 'playstation-plus',
- 'Abonnements cinéma' => 'abonnements-cinema',
- 'Abonnements de train' => 'abonnements-de-train',
- 'Abonnements internet' => 'abonnements-internet',
- 'Abonnements presse' => 'abonnements-presse',
- 'Accessoires aquarium' => 'accessoires-aquarium',
- 'Accessoires auto' => 'auto',
- 'Accessoires électroniques' => 'accessoires-gadgets',
- 'Accessoires gamers PC' => 'accessoires-gamers-pc',
- 'Accessoires gaming' => 'accessoires-gaming',
- 'Accessoires iPhone' => 'accessoires-iphone',
- 'Accessoires mode' => 'accessoires-mode',
- 'Accessoires moto' => 'moto',
- 'Accessoires Nintendo' => 'accessoires-nintendo',
- 'Accessoires PC portables' => 'accessoires-pc-portables',
- 'Accessoires photo' => 'accessoires-photo',
- 'Accessoires PlayStation' => 'accessoires-playstation',
- 'Accessoires pour barbecue' => 'accessoires-barbecue',
- 'Accessoires studio photo' => 'accessoires-studio-photo',
- 'Accessoires téléphonie' => 'accessoires-telephonie',
- 'Accessoires TV' => 'accessoires-tv',
- 'Accessoires vélo' => 'accessoires-velo',
- 'Accessoires Xbox' => 'accessoires-xbox',
- 'Acer' => 'acer',
- 'Acer Predator' => 'acer-predator',
- 'Achats / Ventes' => 'achats-ventes-echanges-estimations-dons',
- 'Achats à l&#039;étranger' => 'limport-sites-avis-questions-langues',
- 'Adaptateurs' => 'adaptateurs',
- 'Adhérents Fnac' => 'adherents-fnac',
- 'Adhésions &amp; Souscriptions' => 'adhesions-souscriptions-abonnements',
- 'adidas' => 'adidas',
- 'Adidas Gazelle' => 'adidas-gazelle',
- 'adidas Stan Smith' => 'adidas-stan-smith',
- 'adidas Superstar' => 'adidas-superstar',
- 'adidas Ultraboost' => 'adidas-ultraboost',
- 'adidas Yung-1' => 'adidas-yung-1',
- 'adidas ZX Flux' => 'adidas-zx-flux',
- 'Adoucissant' => 'adoucissant',
- 'Agendas' => 'agendas',
- 'Age of Empires' => 'age-of-empires',
- 'Age of Empires: Definitive Edition' => 'age-of-empires-definitive-edition',
- 'Alarmes' => 'alarmes',
- 'Albums photo' => 'albums-photo',
- 'Alcools' => 'alcools',
- 'Alcools forts' => 'alcools-forts',
- 'Alimentation' => 'epicerie',
- 'Alimentation bébés' => 'alimentation-bebes',
- 'Alimentation PC' => 'alimentation-pc',
- 'Alimentation sportifs' => 'alimentation-sportifs',
- 'Amazfit Bip' => 'xiaomi-amazfit-bip',
- 'Amazon Echo' => 'amazon-echo',
- 'Amazon Echo Dot' => 'amazon-echo-dot',
- 'Amazon Echo Plus' => 'amazon-echo-plus',
- 'Amazon Echo Show' => 'amazon-echo-show',
- 'Amazon Echo Show 5' => 'amazon-echo-show-5',
- 'Amazon Echo Spot' => 'amazon-echo-spot',
- 'Amazon Fire TV' => 'amazon-fire-tv',
- 'Amazon Kindle' => 'amazon-kindle',
- 'Amazon Prime' => 'amazon-prime',
- 'AMD Radeon' => 'amd-radeon',
- 'AMD Ryzen' => 'amd-ryzen',
- 'AMD Ryzen 5 5600X' => 'amd-ryzen-5-5600x',
- 'AMD Ryzen 7 5800X' => 'amd-ryzen-7-5800x',
- 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x',
- 'AMD Ryzen 9 5950X' => 'amd-ryzen-9-5950x',
- 'AMD Vega' => 'amd-vega',
- 'amiibo' => 'amiibo',
- 'Amplis (guitare/basse)' => 'amplis-guitare-basse',
- 'Amplis audio' => 'amplis',
- 'Ampoules' => 'ampoules',
- 'Ampoules à LED' => 'ampoules-a-led',
- 'Angleterre' => 'angleterre',
- 'Animal Crossing' => 'animal-crossing',
- 'Animal Crossing: New Horizons' => 'animal-crossing-new-horizons',
- 'Animaux' => 'animaux',
- 'Anker' => 'anker',
- 'Anno 1800' => 'anno-1800',
- 'Annonces officielles' => 'annonces-officielles',
- 'Anthem' => 'anthem',
- 'Anti-nuisibles' => 'anti-nuisibles',
- 'Anti-puces' => 'anti-puces',
- 'Antivirus' => 'antivirus',
- 'Antivols' => 'antivols',
- 'Apex Legends' => 'apex-legends',
- 'Appareils à raclette' => 'appareils-raclette',
- 'Appareils de musculation' => 'appareils-de-musculation',
- 'Appareils photo' => 'appareils-photo',
- 'Appareils photo Canon' => 'appareils-photo-canon',
- 'Appareils photo compacts' => 'appareils-photo-compacts',
- 'Appareils photo instantanés' => 'appareils-photo-instantanes',
- 'Appareils photo Nikon' => 'appareils-photo-nikon',
- 'Appareils photo Olympus' => 'appareils-photo-olympus',
- 'Appareils photo Panasonic' => 'appareils-photo-panasonic',
- 'Appareils photo Sony' => 'appareils-photo-sony',
- 'Apple' => 'apple',
- 'Apple AirPods' => 'apple-airpods',
- 'Apple AirPods 2' => 'apple-airpods-2',
- 'Apple AirPods Max' => 'apple-airpods-max',
- 'Apple AirPods Pro' => 'apple-airpods-pro',
- 'Apple HomePod' => 'apple-homepod',
- 'Apple HomePod Mini' => 'apple-homepod-mini',
- 'Apple TV' => 'apple-tv',
- 'Apple TV+' => 'apple-tv-plus',
- 'Apple Watch' => 'apple-watch',
- 'Apple Watch 3' => 'apple-watch-3',
- 'Apple Watch 4' => 'apple-watch-4',
- 'Apple Watch 5' => 'apple-watch-5',
- 'Apple Watch 6' => 'apple-watch-6',
- 'Apple Watch SE' => 'apple-watch-se',
- 'Applications' => 'applications',
- 'Applications Android' => 'applications-android',
- 'Applications iOS' => 'applications-ios',
- 'Appliques murales' => 'appliques-murales',
- 'Applis &amp; logiciels' => 'applis-logiciels',
- 'Après-shampooings' => 'apres-shampooings',
- 'Aquariums' => 'aquariums',
- 'Arbres à chat' => 'arbres-a-chat',
- 'Arduino' => 'arduino',
- 'Armoires &amp; placards' => 'armoires-et-placards',
- 'Articles de cuisine et d&#039;entretien' => 'articles-de-cuisine',
- 'Arts culinaires' => 'arts-culinaires',
- 'Arts de la table' => 'arts-de-la-table',
- 'ASICS' => 'asics',
- 'Asmodée' => 'asmodee',
- 'Aspirateurs' => 'aspirateurs',
- 'Aspirateurs balais' => 'aspirateurs-balais',
- 'Aspirateurs Dreame' => 'aspirateurs-xiaomi',
- 'Aspirateurs Dyson' => 'aspirateurs-dyson',
- 'Aspirateurs robot' => 'aspirateurs-robot',
- 'Aspirateurs Rowenta' => 'apsirateurs-rowenta',
- 'Aspirateurs sans sac' => 'aspirateurs-sans-sac',
- 'Assassin&#039;s Creed' => 'assassin-s-creed',
- 'Assassin&#039;s Creed: Unity' => 'assassins-creed-unity',
- 'Assassin&#039;s Creed: Valhalla' => 'assassin-s-creed-valhalla',
- 'Assassin&#039;s Creed Odyssey' => 'assassin-s-creed-odyssey',
- 'Assassin&#039;s Creed Origins' => 'assassin-s-creed-origins',
- 'Assurances' => 'assurances',
- 'Astuces pour économiser' => 'vos-astuces-pour-faire-des-economies',
- 'Asus' => 'asus',
- 'Asus ROG' => 'asus-rog',
- 'Asus ROG Phone' => 'asus-rog-phone',
- 'Asus ROG Phone 2' => 'asus-rog-phone-2',
- 'ASUS Transformer' => 'asus-transformer',
- 'Asus VivoBook' => 'asus-vivobook',
- 'Asus ZenBook' => 'asus-zenbook',
- 'Asus ZenFone 2' => 'asus-zenfone-2',
- 'Asus ZenFone 3' => 'asus-zenfone-3',
- 'Asus ZenFone 4' => 'asus-zenfone-4',
- 'Asus ZenFone 6' => 'asus-zenfone-6',
- 'Asus ZenFone GO' => 'asus-zenfone-go',
- 'Asus ZenFone Zoom' => 'asus-zenfone-zoom',
- 'Audio &amp; Hi-fi' => 'audio-et-hi-fi',
- 'Aukey' => 'aukey',
- 'Auto-Moto' => 'auto-moto',
- 'Autoradios' => 'autoradios',
- 'Azzaro Wanted' => 'azzaro-wanted',
- 'Baby foot' => 'baby-foot',
- 'BabyLiss' => 'babyliss',
- 'Babyphones' => 'babyphones',
- 'Badminton' => 'badminton',
- 'Bagagerie' => 'bagagerie',
- 'Baignoires pour bébé' => 'baignoires-pour-bebe',
- 'Bains de bouche' => 'bains-de-bouche',
- 'Balais &amp; serpillères' => 'balais-et-serpilleres',
- 'Balances connectées' => 'balances-connectees',
- 'Balançoires' => 'balancoires',
- 'Ballet &amp; danse' => 'ballet-et-danse',
- 'Ballons de football' => 'ballons-de-football',
- 'Bandes dessinées' => 'bandes-dessinees',
- 'Banques' => 'banques',
- 'Barbecue' => 'barbecue',
- 'Barbecue électrique' => 'barbecue-electrique',
- 'Barbecue Weber' => 'barbecue-weber',
- 'Barbie' => 'barbie',
- 'Barres de son' => 'barres-de-son',
- 'Barres de son Yamaha' => 'barres-de-son-yamaha',
- 'Batman Arkham' => 'batman-arkham',
- 'Batteries externes' => 'batteries-externes',
- 'Batteries voiture' => 'batteries-voiture',
- 'Batteurs' => 'batteurs-electriques',
- 'Battlefield' => 'battlefield',
- 'Battlefield 1' => 'battlefield-1',
- 'Battlefield V' => 'battlefield-5',
- 'Béaba' => 'beaba',
- 'Beats by Dre' => 'beats-by-dre',
- 'Beats Solo 3' => 'beats-solo-3',
- 'Beats Studio 3' => 'beats-studio-3',
- 'Beauté' => 'beaute',
- 'Bébés' => 'bebes-nouveaux-nes',
- 'BenQ' => 'benq',
- 'Be quiet!' => 'be-quiet',
- 'Beyerdynamic MMX 300' => 'beyerdynamic-mmx-300',
- 'Biberons' => 'biberons',
- 'Bien-être &amp; santé' => 'bien-etre-et-massages',
- 'Bières' => 'bieres',
- 'Bijoux' => 'bijoux',
- 'Bikinis' => 'bikinis',
- 'Bilans de santé &amp; dépistages' => 'bilans-de-sante-et-depistages',
- 'Billets de bus' => 'billets-de-bus',
- 'Billets de train' => 'billets-de-train',
- 'BioShock' => 'bioshock',
- 'BioShock Infinite' => 'bioshock-infinite',
- 'Bitdefender' => 'bitdefender',
- 'Blabla' => 'blabla-parlez-de-tout-et-de-rien',
- 'Black &amp; Decker' => 'black-decker',
- 'Blackberry' => 'blackberry',
- 'Black Desert Online' => 'black-desert-online',
- 'Blédina' => 'bledina',
- 'Blenders' => 'blenders',
- 'Bleu de Chanel' => 'bleu-de-chanel',
- 'Blousons de moto' => 'blousons-de-moto',
- 'Blu-Ray' => 'blu-ray',
- 'Bodys pour bébé' => 'bodys-pour-bebe',
- 'Boissons' => 'boissons',
- 'Boîtes à outils' => 'boites-a-outils',
- 'Boîtiers PC' => 'boitiers-pc',
- 'Boîtiers TV' => 'boitiers-tv',
- 'Bonbons' => 'bonbons',
- 'Bonnets' => 'bonnets',
- 'Bonnets de bain' => 'bonnets-de-bain',
- 'Borderlands' => 'borderlands',
- 'Borderlands 3' => 'borderlands-3',
- 'Bosch' => 'bosch',
- 'Bose' => 'bose',
- 'Bose Headphones 700' => 'bose-headphones-700',
- 'Bose Home Speaker 500' => 'bose-home-speaker-500',
- 'Bose QuietComfort' => 'bose-quietcomfort',
- 'Bose QuietComfort 35 II' => 'bose-quietcomfort-35ii',
- 'Bose SoundLink' => 'bose-soundlink',
- 'Bose SoundTouch' => 'bose-soundtouch',
- 'Bottes' => 'bottes',
- 'Bottes de moto' => 'bottes-de-moto',
- 'Bottes de neige' => 'bottes-neige',
- 'Bottes de pluie' => 'bottes-pluie',
- 'Bottes femme' => 'bottes-femme',
- 'Bottes homme' => 'bottes-homme',
- 'Bougies &amp; bougeoirs' => 'bougies-et-bougeoirs',
- 'Box beauté' => 'box-beaute',
- 'Bracelet fitness' => 'bracelet-fitness',
- 'Brandt' => 'brandt',
- 'Braun Series 3' => 'braun-series-3',
- 'Braun Series 5' => 'braun-series-5',
- 'Braun Series 7' => 'braun-series-7',
- 'Braun Series 9' => 'braun-series-9',
- 'Braun Silk Épil' => 'braun-silk-epil',
- 'Brita' => 'brita',
- 'Brosses à dents' => 'brosses-a-dents',
- 'Brosses à dents électriques' => 'brosses-a-dents-electriques',
- 'Brosses à dents électriques Oral-B' => 'brosses-a-dents-electriques-oral-b',
- 'Brosses pour animaux' => 'brosses-pour-animaux',
- 'Cable management' => 'cable-management',
- 'Câbles' => 'cables',
- 'Câbles Ethernet' => 'cables-ethernet',
- 'Câbles HDMI' => 'cables-hdmi',
- 'Câbles Jack' => 'cables-jack',
- 'Câbles USB' => 'cables-usb',
- 'Cadeaux' => 'cadeaux',
- 'Cadres' => 'cadres',
- 'Cadres de vélo' => 'cadres-de-velo',
- 'Café' => 'cafe',
- 'Café en dosettes' => 'cafe-en-dosettes',
- 'Café en grain' => 'cafe-en-grain',
- 'Cafetières' => 'cafetieres',
- 'Cafetières expresso' => 'cafetieres-expresso',
- 'Cafetières filtre' => 'cafetieres-filtre',
- 'Cafetières italiennes' => 'cafetieres-italiennes',
- 'Cahiers' => 'cahiers',
- 'Caissons de basses' => 'caissons-de-basses',
- 'Calendrier de l&#039;Avent Lego' => 'calendriers-avent-lego',
- 'Calendriers' => 'calendriers',
- 'Calendriers de l&#039;Avent' => 'calendriers-avent',
- 'Call of Duty' => 'call-of-duty',
- 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war',
- 'Call of Duty: Black Ops III' => 'call-of-duty-black-ops-3',
- 'Call of Duty: Black Ops IIII' => 'call-of-duty-black-ops-4',
- 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare',
- 'Call of Duty: Modern Warfare' => 'call-of-duty-modern-warfare',
- 'Call of Duty: WW2' => 'call-of-duty-ww2',
- 'Calor' => 'calor',
- 'Caméras' => 'cameras',
- 'Caméras IP' => 'cameras-ip',
- 'Caméras sportives' => 'cameras-sportives',
- 'Camping' => 'camping',
- 'Canapés' => 'canape',
- 'Canon' => 'canon',
- 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker',
- 'Caravanes' => 'caravanes',
- 'Carburant' => 'carburant',
- 'Cartables' => 'cartables',
- 'Cartes &amp; programmes de fidélité' => 'cartes-et-programmes-de-fidelite',
- 'Cartes bancaires' => 'cartes-bancaires',
- 'Cartes de développement' => 'cartes-developpement',
- 'Cartes graphiques' => 'cartes-graphiques',
- 'Cartes mémoire' => 'cartes-memoire',
- 'Cartes mères' => 'cartes-meres',
- 'Cartes postales' => 'cartes-postales',
- 'Cartes prépayées Playstation Store' => 'playstation-store',
- 'Cartes SD' => 'cartes-sd',
- 'Cartes son' => 'cartes-son',
- 'Casio' => 'casio',
- 'Casque sans fil Xbox' => 'casque-sans-fil-xbox',
- 'Casques Apple' => 'casques-apple',
- 'Casques à réduction de bruit' => 'casque-reduction-active-bruit',
- 'Casques audio' => 'casques-audio',
- 'Casques Bose' => 'casques-bose',
- 'Casques de moto' => 'casques-de-moto',
- 'Casques de vélo' => 'casques-de-velo',
- 'Casques Jabra' => 'casques-jabra',
- 'Casques Samsung' => 'casques-samsung',
- 'Casques sans fil' => 'casques-sans-fil',
- 'Casques Sennheiser' => 'casques-sennheiser',
- 'Casques Sony' => 'casques-sony',
- 'Casques VR' => 'vr',
- 'Casquettes' => 'casquettes',
- 'Casseroles' => 'casseroles',
- 'Catit' => 'catit',
- 'Caves à vin' => 'caves-a-vin',
- 'CD &amp; vinyles' => 'cd-vinyles',
- 'CDAV' => 'cdav',
- 'Ceintures' => 'ceintures',
- 'Centrales vapeur' => 'centrales-vapeur',
- 'Chaînes hi-fi' => 'chaines-hi-fi',
- 'Chaises' => 'chaises',
- 'Chaises hautes' => 'chaises-hautes',
- 'Chambre' => 'chambre',
- 'Champagne' => 'champagne',
- 'Chapeaux' => 'chapeaux',
- 'Chapeaux &amp; casquettes' => 'chapeaux-casquettes',
- 'Chargeurs' => 'chargeurs',
- 'Chargeurs allume-cigare' => 'chargeurs-allume-cigare',
- 'Chargeurs de piles' => 'chargeurs-de-piles',
- 'Chargeurs sans fil' => 'chargeurs-sans-fil',
- 'Chasse' => 'chasse',
- 'Chatières' => 'chatieres',
- 'Chats' => 'chats',
- 'Chauffage' => 'chauffage',
- 'Chaussettes &amp; collants' => 'chaussettes-et-collants',
- 'Chaussons' => 'chaussons',
- 'Chaussures' => 'chaussures',
- 'Chaussures adidas' => 'chaussures-adidas',
- 'Chaussures de football' => 'chaussures-de-football',
- 'Chaussures de randonnée' => 'chaussures-de-randonnee',
- 'Chaussures de ski' => 'chaussures-de-ski',
- 'Chaussures de ville' => 'chaussures-de-ville',
- 'Chaussures New Balance' => 'chaussures-new-balance',
- 'Chaussures Nike' => 'chaussures-nike',
- 'Chaussures pour enfants' => 'chaussures-enfants',
- 'Chaussures pour femme' => 'chaussures-femme',
- 'Chaussures pour homme' => 'chaussures-homme',
- 'Chaussures Puma' => 'chaussures-puma',
- 'Chaussures Reebok' => 'chaussures-reebok',
- 'Chaussures running' => 'chaussures-de-running',
- 'Chelsea boots' => 'chelsea-boots',
- 'Chemises' => 'chemises',
- 'Chiens' => 'chiens',
- 'Chocolat' => 'chocolat',
- 'Chuck Taylor' => 'chuck-taylor',
- 'Cinéma' => 'cinema',
- 'Cire dépilatoire' => 'cire-depilatoire',
- 'Cirque &amp; arts de rue' => 'cirque-et-arts-de-rue',
- 'Citytrips' => 'citytrips',
- 'Civilization' => 'civilization',
- 'Civilization VI' => 'civilization-vi',
- 'CK One' => 'ck-one',
- 'Clarks' => 'clarks',
- 'Claviers' => 'claviers',
- 'Claviers (musique)' => 'claviers-musique',
- 'Claviers gamer' => 'claviers-gamer',
- 'Claviers Logitech' => 'claviers-logitech',
- 'Claviers mécaniques' => 'claviers-mecaniques',
- 'Claviers sans fil' => 'claviers-sans-fil',
- 'Clés USB' => 'cles-usb',
- 'Climatisation' => 'climatisation',
- 'Climatiseurs' => 'climatiseurs',
- 'Cocottes' => 'cocottes',
- 'Coffrets de livres' => 'coffrets-de-livres',
- 'Coffrets DVD' => 'coffrets-dvd',
- 'Coffrets maquillage' => 'coffrets-maquillage',
- 'Colliers &amp; laisses' => 'colliers-et-laisses',
- 'Compléments alimentaires' => 'complements-alimentaires',
- 'Composteurs' => 'composteurs',
- 'Concerts' => 'concerts',
- 'Concours' => 'concours',
- 'Congélateurs' => 'congelateurs',
- 'Connectiques' => 'connectiques',
- 'Console Google Stadia' => 'google-stadia',
- 'Console Nintendo Classic Mini' => 'nintendo-classic-mini',
- 'Console Nintendo Classic Mini: SNES' => 'nintendo-classic-mini-snes',
- 'Console Nintendo Switch' => 'nintendo-switch',
- 'Console Nintendo Switch Lite' => 'nintendo-switch-lite',
- 'Console PS4' => 'playstation-4',
- 'Console PS4 Pro' => 'playstation-4-pro',
- 'Console PS5' => 'playstation-5',
- 'Consoles' => 'consoles',
- 'Consoles &amp; jeux vidéo' => 'consoles-jeux-video',
- 'Console Sega Mega Drive Mini' => 'sega-mega-drive-mini',
- 'Console Xbox One S' => 'xbox-one-s',
- 'Console Xbox One X' => 'xbox-one-x',
- 'Console Xbox Series S' => 'xbox-series-s',
- 'Console Xbox Series X' => 'xbox-series-x',
- 'Consommables imprimantes' => 'consommables-imprimantes',
- 'Converse' => 'converse',
- 'Coques iPhone' => 'coques-iphone',
- 'Corsair Void PRO' => 'corsair-void-pro',
- 'Costumes' => 'costumes',
- 'Costumes &amp; déguisements' => 'costumes-et-deguisements',
- 'Couches' => 'couches',
- 'Couettes' => 'couettes',
- 'Coupes menstruelles' => 'coupes-menstruelles',
- 'Cours &amp; formations' => 'cours-et-formations',
- 'Courses hippiques' => 'courses-hippiques',
- 'Couteaux de cuisine' => 'couteaux-de-cuisine',
- 'Couture' => 'couture',
- 'Couverts' => 'couverts',
- 'Couverts pour bébés' => 'couverts-pour-bebes',
- 'Covoiturage' => 'covoiturage',
- 'Crash Team Racing Nitro-Fueled' => 'crash-team-racing-nitro-fueled',
- 'Cravates' => 'cravates',
- 'Crédits' => 'credits',
- 'Crèmes hydratantes' => 'cremes-hydratantes',
- 'Crèmes solaires' => 'cremes-solaires',
- 'Croisières' => 'croisieres',
- 'Croquettes pour chat' => 'croquettes-pour-chat',
- 'Croquettes pour chien' => 'croquettes-pour-chien',
- 'Cuiseurs à riz' => 'cuiseur-riz',
- 'Cuisinières' => 'cuisinieres',
- 'Culottes menstruelles' => 'culottes-menstruelles',
- 'Culture &amp; divertissement' => 'culture-divertissement',
- 'Cyberpunk 2077' => 'cyberpunk-2077',
- 'Cyclisme' => 'cyclisme',
- 'Cyclisme &amp; sports urbains' => 'cyclisme-sports-urbains',
- 'Darksiders' => 'darksiders',
- 'Dashcams' => 'dashcams',
- 'DDR3' => 'ddr3',
- 'DDR4' => 'ddr4',
- 'Dead Rising' => 'dead-rising',
- 'Death Stranding' => 'death-stranding',
- 'Décoration' => 'decoration',
- 'Décorations de Noël' => 'decoration-noel',
- 'Deebot' => 'ecovacs-deebot',
- 'Deezer' => 'deezer',
- 'Dell' => 'dell',
- 'Dell XPS' => 'dell-xps',
- 'Delsey' => 'delsey',
- 'Demandes de deals' => 'les-demandes-de-deals',
- 'Denon' => 'denon',
- 'Dentifrices' => 'dentifrices',
- 'Déodorants' => 'deodorants',
- 'Désherbants' => 'desherbants',
- 'Déshumidificateurs' => 'deshumidificateurs',
- 'Désinfectant' => 'desinfectants',
- 'Désodorisants &amp; parfums d&#039;intérieur' => 'desodorisants-et-parfums-d-interieur',
- 'Destiny' => 'destiny',
- 'Destiny 2' => 'destiny-2',
- 'Détecteurs de fumée' => 'detecteurs-de-fumee',
- 'Detroit: Become Human' => 'detroit-become-human',
- 'Deus Ex' => 'deus-ex',
- 'Deus Ex: Mankind Divided' => 'deus-ex-mankind-divided',
- 'Devil May Cry 5' => 'devil-may-cry-5',
- 'Dishonored' => 'dishonored',
- 'Dishonored 2' => 'dishonored-2',
- 'Disney+' => 'disney-plus',
- 'Disneyland Paris' => 'disneyland-paris',
- 'Disques durs (internes)' => 'hdd',
- 'Disques durs externes' => 'disques-durs-externes',
- 'Divers' => 'divers',
- 'DJI' => 'dji',
- 'DJI Mavic Air 2' => 'dji-mavic-air-2',
- 'DJI Mavic Mini' => 'dji-mavic-mini',
- 'Dolce Gusto' => 'dolce-gusto',
- 'Domotique' => 'smart-home',
- 'Doom Eternal' => 'doom-eternal',
- 'Dosettes Dolce Gusto' => 'dosettes-dolce-guste',
- 'Dosettes Nespresso' => 'dosettes-nespresso',
- 'Dosettes Senseo' => 'dosettes-senseo',
- 'Dosettes Tassimo' => 'dosettes-tassimo',
- 'Dr. Martens' => 'dr-martens',
- 'Dragon Age' => 'dragon-age',
- 'Dragon Ball' => 'dragon-ball',
- 'Dragon Ball FighterZ' => 'dragon-ball-fighterz',
- 'Dragon Ball Z: Kakarot' => 'dragon-ball-z-kakarot',
- 'Dragon Quest' => 'dragon-quest',
- 'Dragon Quest Builders' => 'dragon-quest-builders',
- 'Dragon Quest Builders 2' => 'dragon-quest-builders-2',
- 'Draisiennes' => 'draisiennes',
- 'Draps &amp; housses' => 'draps-et-housses',
- 'Dreame V10' => 'xiaomi-dreame-v10',
- 'Dreame V11' => 'xiaomi-dreame-v11',
- 'Drones' => 'drones',
- 'Durex' => 'durex',
- 'DVD' => 'dvd',
- 'Dying Light' => 'dying-light',
- 'Dying Light 2' => 'dying-light-2',
- 'Dyson' => 'dyson',
- 'Dyson V10' => 'dyson-v10',
- 'Dyson V11' => 'dyson-v11',
- 'Eastpak' => 'eastpak',
- 'Ebooks' => 'ebooks',
- 'Écharpes &amp; foulards' => 'echarpes-et-foulards',
- 'Éclairage intelligent' => 'smart-light',
- 'Écouteurs' => 'ecouteurs',
- 'Écouteurs sans fil' => 'ecouteurs-sans-fil',
- 'Écouteurs sport' => 'ecouteurs-sport',
- 'Ecovacs' => 'ecovacs',
- 'Ecovacs Deebot 900' => 'ecovacs-deebot-900',
- 'Ecovacs Deebot OZMO 930' => 'ecovacs-deebot-ozmo-930',
- 'Écrans' => 'ecrans',
- 'Écrans 4K / UHD' => 'ecrans-4k-uhd',
- 'Écrans 21&quot; et moins' => 'ecrans-21-pouces-et-moins',
- 'Écrans 24&quot;' => 'ecrans-24-pouces',
- 'Écrans 27&quot;' => 'ecrans-27-pouces',
- 'Écrans 29&quot; et plus' => 'ecrans-29-pouces-et-plus',
- 'Écrans Acer' => 'ecrans-acer',
- 'Écrans Asus' => 'ecrans-asus',
- 'Écrans BenQ' => 'ecrans-benq',
- 'Écrans Dell' => 'ecrans-dell',
- 'Écrans de projection' => 'ecrans-de-projection',
- 'Écrans FreeSync' => 'ecrans-freesync',
- 'Écrans gaming' => 'ecrans-gamer',
- 'Écrans incurvés' => 'ecrans-incurves',
- 'Écrans Philips' => 'ecrans-philips',
- 'Écrans Samsung' => 'ecrans-samsung',
- 'Électricité (matériel)' => 'electricite',
- 'Electrolux' => 'electrolux',
- 'Électroménager' => 'electromenager',
- 'Embauchoirs' => 'embauchoirs',
- 'Enceintes' => 'enceintes',
- 'Enceintes Bluetooth' => 'enceintes-bluetooth',
- 'Enceintes connectées' => 'enceintes-connectees',
- 'Enceintes portables sans fil' => 'enceintes-portables-sans-fil',
- 'Énergie' => 'energie',
- 'Engrais' => 'engrais',
- 'Épicerie &amp; courses' => 'epicerie-courses-supermarches',
- 'Épilateurs à lumière pulsée' => 'epilateurs-a-lumiere-pulsee',
- 'Épilateurs électriques' => 'epilateurs-electriques',
- 'Épilation' => 'epilation',
- 'Équipement motard' => 'equipement-motard',
- 'Équipement running' => 'equipement-running',
- 'Équipement sportif' => 'equipement-sportif',
- 'Érotisme' => 'erotisme',
- 'Escarpins' => 'escarpins',
- 'Événements sportifs' => 'evenements-sportifs',
- 'Expositions' => 'expositions',
- 'Extracteurs de jus' => 'extracteurs-de-jus',
- 'F1 2017' => 'f1-2017',
- 'F1 2019' => 'f1-2019',
- 'Facom' => 'facom',
- 'Fallout' => 'fallout',
- 'Fallout 4' => 'fallout-4',
- 'Fallout 76' => 'fallout-76',
- 'Famille &amp; enfants' => 'famille-enfants',
- 'Far Cry' => 'far-cry',
- 'Far Cry New Dawn' => 'far-cry-new-dawn',
- 'Fards à paupières' => 'fards-a-paupieres',
- 'Fast-foods' => 'fast-foods',
- 'Fauteuils' => 'fauteuils',
- 'Fauteuils gamer' => 'fauteuils-gaming',
- 'Fe' => 'fe',
- 'Fers à lisser / à friser' => 'fers-a-lisser-a-friser',
- 'Fers à repasser' => 'fers-a-repasser',
- 'Fers à souder' => 'fers-a-souder',
- 'Festivals' => 'festivals',
- 'Feutres' => 'feutres',
- 'FIFA' => 'fifa',
- 'FIFA 17' => 'fifa-17',
- 'FIFA 18' => 'fifa-18',
- 'FIFA 19' => 'fifa-19',
- 'FIFA 20' => 'fifa-20',
- 'FIFA 21' => 'fifa-21',
- 'Figurines' => 'figurines',
- 'Films &amp; Séries' => 'films',
- 'Final Fantasy' => 'final-fantasy',
- 'Final Fantasy XII' => 'final-fantasy-xii',
- 'Finances &amp; Assurances' => 'finances-assurances',
- 'fitbit' => 'fitbit',
- 'Fitness &amp; yoga' => 'fitness-yoga',
- 'Flash' => 'flash',
- 'Fluval' => 'fluval',
- 'Foires &amp; salons' => 'foires-et-salons',
- 'Fonds de teint' => 'fonds-de-teint',
- 'Football' => 'football',
- 'Forfaits de ski' => 'forfaits-ski',
- 'Forfaits mobiles' => 'forfaits-mobiles',
- 'Forfaits mobiles et internet' => 'telecommunications',
- 'For Honor' => 'for-honor',
- 'Formations premiers secours' => 'formations-premiers-secours',
- 'Formule 1' => 'formule-1',
- 'Fortnite' => 'fortnite',
- 'Fortnite: Pack Feu Obscur' => 'fortnite-pack-feu-obscur',
- 'Forza' => 'forza',
- 'Forza Horizon' => 'forza-horizon',
- 'Forza Horizon 3' => 'forza-horizon-3',
- 'Forza Horizon 4' => 'forza-horizon-4',
- 'Forza Motorsport' => 'forza-motosport',
- 'Forza Motorsport 7' => 'forza-motorsport-7',
- 'Fossil' => 'fossil',
- 'Fournitures scolaires' => 'fournitures-scolaires',
- 'Fours' => 'fours',
- 'Fours à poser' => 'fours-a-poser',
- 'Fours encastrables' => 'fours-encastrables',
- 'Friandises pour chat' => 'friandises-pour-chat',
- 'Friandises pour chien' => 'friandises-pour-chien',
- 'Friskies' => 'friskies',
- 'Friteuses' => 'friteuses',
- 'Friteuses sans huile' => 'friteuses-sans-huile',
- 'Fruits &amp; légumes' => 'fruits-et-legumes',
- 'Fujifilm' => 'fujifilm',
- 'Funko Pop' => 'funko-pop',
- 'FURminator' => 'furminator',
- 'Futuroscope' => 'futuroscope',
- 'Gamelles' => 'gamelles',
- 'Game of Thrones' => 'game-of-thrones',
- 'Gaming' => 'le-laboratoire-des-gamers',
- 'Gants' => 'gants',
- 'Gants moto' => 'gants-moto',
- 'Garmin' => 'garmin',
- 'Garmin Fenix' => 'garmin-fenix',
- 'Garmin Forerunner' => 'garmin-forerunner',
- 'Garmin Vivoactive' => 'garmin-vivoactive',
- 'Garmin Vivomove' => 'garmin-vivomove',
- 'Gâteaux &amp; biscuits' => 'gateaux-et-biscuits',
- 'Gears 5' => 'gears-5',
- 'Gel hydroalcoolique' => 'gel-hydroalcoolique',
- 'Gels douche' => 'gels-douche',
- 'Geox' => 'geox',
- 'Ghost of Tsushima' => 'ghost-of-tsushima',
- 'Gigoteuses' => 'gigoteuses',
- 'Gillette Fusion' => 'gillette-fusion',
- 'Gillette Mach3' => 'gillette-mach3',
- 'Glaces' => 'glaces',
- 'Glacières' => 'glacieres',
- 'Glisse urbaine' => 'glisse-urbaine',
- 'God of War' => 'god-of-war',
- 'Google Chromecast' => 'google-chromecast',
- 'Google Home' => 'google-home',
- 'Google Home Max' => 'google-home-max',
- 'Google Home Mini' => 'google-home-mini',
- 'Google Nest Hub' => 'google-nest-hub',
- 'Google Nest Mini' => 'google-nest-mini',
- 'Google Pixel' => 'google-pixel',
- 'Google Pixel 2' => 'google-pixel-2',
- 'Google Pixel 2 XL' => 'google-pixel-2-xl',
- 'Google Pixel 3' => 'google-pixel-3',
- 'Google Pixel 3 XL' => 'google-pixel-3-xl',
- 'Google Pixel 3a' => 'google-pixel-3a',
- 'Google Pixel 4' => 'google-pixel-4',
- 'Google Pixel 4 XL' => 'google-pixel-4xl',
- 'Google Pixel 4a' => 'google-pixel-4a',
- 'Google Pixel 5' => 'google-pixel-5',
- 'Google Pixel XL' => 'google-pixel-xl',
- 'GoPro' => 'gopro-hero',
- 'GoPro Hero 9' => 'gopro-hero-9',
- 'Gran Turismo' => 'gran-turismo',
- 'Grille-pain' => 'grille-pain',
- 'Grossesse &amp; maternité' => 'grossesse-maternite',
- 'GTA' => 'gta',
- 'GTA V' => 'gta-v',
- 'GTX 1060' => 'nvidia-geforce-gtx-1060',
- 'GTX 1070' => 'nvidia-geforce-gtx-1070',
- 'GTX 1080' => 'nvidia-geforce-gtx-1080',
- 'GTX 1080 Ti' => 'nvidia-geforce-gtx-1080-ti',
- 'GTX 1650' => 'gtx-1650',
- 'GTX 1660' => 'gtx-1660',
- 'GTX 1660 Ti' => 'gtx-1660-ti',
- 'Guerlain La Petite Robe Noire' => 'guerlain-petite-robe-noire',
- 'Guirlandes lumineuses' => 'guirlandes-lumineuses',
- 'Guitares' => 'guitares',
- 'Gyropodes' => 'gyropodes',
- 'Half Life' => 'half-life',
- 'Half Life 2' => 'half-life-2',
- 'Half Life Alyx' => 'half-life-alyx',
- 'Halloween' => 'halloween',
- 'Haltères &amp; poids' => 'halteres-et-poids',
- 'Hama' => 'hama',
- 'Hamacs' => 'hamacs',
- 'Hand spinners' => 'hand-spinners',
- 'Harnais pour chien' => 'harnais-pour-chien',
- 'Harry Potter' => 'harry-potter',
- 'Havaianas' => 'havaianas',
- 'High-Tech' => 'high-tech',
- 'High-tech &amp; informatique' => 'le-laboratoire-high-tech-informatique',
- 'Hisense' => 'hisense',
- 'Home Cinéma' => 'home-cinema',
- 'Honor' => 'honor',
- 'Honor 6X' => 'honor-6x',
- 'Honor 8' => 'honor-8',
- 'Honor 8 Pro' => 'honor-8-pro',
- 'Honor 8X' => 'honor-8x',
- 'Honor 8X Max' => 'honor-8x-max',
- 'Honor 9' => 'honor-9',
- 'Honor 10' => 'honor-10',
- 'Honor 20' => 'honor-20',
- 'Honor 20 Lite' => 'honor-20-lite',
- 'Honor 20 Pro' => 'honor-20-pro',
- 'Honor Band 5' => 'honor-band-5',
- 'Honor MagicBook' => 'honor-magicbook',
- 'Honor MagicWatch 2' => 'honor-magicwatch-2',
- 'Honor View 20' => 'honor-view-20',
- 'Horizon Zero Dawn' => 'horizon-zero-dawn',
- 'Hôtels &amp; Hébergements' => 'hotels',
- 'Hoverboards' => 'hoverboards',
- 'HTC 10' => 'htc-10',
- 'HTC Desire' => 'htc-desire',
- 'HTC One M9' => 'htc-one-m9',
- 'HTC U11' => 'htc-u11',
- 'HTC U Play' => 'htc-u-play',
- 'HTC U Ultra' => 'htc-u-ultra',
- 'HTC Vive' => 'htc-vive',
- 'Huawei' => 'huawei',
- 'Huawei FreeBuds 3' => 'huawei-freebuds-3',
- 'Huawei Mate 9' => 'huawei-mate-9',
- 'Huawei Mate 10' => 'huawei-mate-10',
- 'Huawei Mate 10 Pro' => 'huawei-mate-10-pro',
- 'Huawei Mate 20' => 'huawei-mate-20',
- 'Huawei Mate 20 Lite' => 'huawei-mate-20-lite',
- 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro',
- 'Huawei Mate 20 RS' => 'huawei-mate-20-rs',
- 'Huawei Mate 30' => 'huawei-mate-30',
- 'Huawei Mate 30 Lite' => 'huawei-mate-30-lite',
- 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro',
- 'Huawei P8 Lite' => 'huawei-p8-lite',
- 'Huawei P9 Lite' => 'huawei-p9-lite',
- 'Huawei P10' => 'huawei-p10',
- 'Huawei P10 Lite' => 'huawei-p10-lite',
- 'Huawei P10 Plus' => 'huawei-p10-plus',
- 'Huawei P20' => 'huawei-p20',
- 'Huawei P20 Lite' => 'huawei-p20-lite',
- 'Huawei P20 Pro' => 'huawei-p20-pro',
- 'Huawei P30' => 'huawei-p30',
- 'Huawei P30 Lite' => 'huawei-p30-lite',
- 'Huawei P30 Pro' => 'huawei-p30-pro',
- 'Huawei P40' => 'huawei-p40',
- 'Huawei P40 Lite' => 'huawei-p40-lite',
- 'Huawei P40 Pro' => 'huawei-p40-pro',
- 'Huawei Watch' => 'huawei-watch',
- 'Huawei Watch 2' => 'huawei-watch-2',
- 'Hubs' => 'hubs',
- 'Hugo Boss Bottled' => 'hugo-boss-bottled',
- 'Huile moteur' => 'huile-moteur',
- 'Hygiène &amp; soins' => 'hygiene-soins',
- 'Hygiène de la maison' => 'hygiene-de-la-maison',
- 'Hygiène des bébés' => 'hygiene-des-bebes',
- 'Hygiène intime' => 'hygiene-intime',
- 'iMac' => 'mac-de-bureau',
- 'iMac 2021' => 'imac-2021',
- 'Image, son, photo' => 'le-laboratoire-audiovisuel',
- 'Impressions photo' => 'impressions-photo',
- 'Imprimantes' => 'imprimantes',
- 'Imprimantes 3D' => 'imprimantes-3d',
- 'Imprimantes Brother' => 'imprimantes-brother',
- 'Imprimantes Canon' => 'imprimantes-canon',
- 'Imprimantes Epson' => 'imprimantes-epson',
- 'Imprimantes HP' => 'imprimantes-hp',
- 'Imprimantes laser' => 'imprimantes-laser',
- 'Imprimantes multifonctions' => 'imprimantes-multifonctions',
- 'Informatique' => 'informatique',
- 'Instax Mini' => 'instax-mini',
- 'Instruments de musique' => 'instruments-de-musique',
- 'Intel i5' => 'intel-i5',
- 'Intel i7' => 'intel-i7',
- 'Intel i9' => 'intel-i9',
- 'iPad' => 'apple-ipad',
- 'iPad 2019' => 'ipad-2019',
- 'iPad 2020' => 'ipad-2020',
- 'iPad Air' => 'ipad-air',
- 'iPad Air 2019' => 'ipad-air-2019',
- 'iPad Air 2020' => 'ipad-air-2020',
- 'iPad Mini' => 'apple-ipad-mini',
- 'iPad Pro' => 'apple-ipad-pro',
- 'iPad Pro 11' => 'ipad-pro-11',
- 'iPad Pro 12.9' => 'ipad-pro-12-9',
- 'iPad Pro 2020' => 'ipad-pro-2020',
- 'iPhone' => 'apple-iphone',
- 'iPhone 6' => 'apple-iphone-6',
- 'iPhone 7' => 'apple-iphone-7',
- 'iPhone 7 Plus' => 'apple-iphone-7-plus',
- 'iPhone 8' => 'apple-iphone-8',
- 'iPhone 8 Plus' => 'apple-iphone-8-plus',
- 'iPhone 11' => 'iphone-11',
- 'iPhone 11 Pro' => 'iphone-11-pro',
- 'iPhone 11 Pro Max' => 'iphone-11-pro-max',
- 'iPhone 12' => 'iphone-12',
- 'iPhone 12 Mini' => 'iphone-12-mini',
- 'iPhone 12 Pro' => 'iphone-12-pro',
- 'iPhone 12 Pro Max' => 'iphone-12-pro-max',
- 'iPhone SE' => 'apple-iphone-se',
- 'iPhone X' => 'apple-iphone-x',
- 'iPhone XR' => 'apple-iphone-xr',
- 'iPhone XS' => 'apple-iphone-xs',
- 'iPhone XS Max' => 'apple-iphone-xs-max',
- 'iRobot Roomba' => 'irobot-roomba',
- 'Isolation' => 'isolation',
- 'Jabra Elite 75t' => 'jabra-elite-75t',
- 'Jabra Elite 85h' => 'jabra-elite-85h',
- 'Jabra Elite 85t' => 'jabra-elite-85t',
- 'Jabra Elite Active 65t' => 'jabra-elite-active-65t',
- 'Jacuzzis' => 'jacuzzis',
- 'Jardin' => 'jardin',
- 'Jardin &amp; bricolage' => 'jardin-bricolage',
- 'Jardinage' => 'entretien-du-jardin',
- 'JBL' => 'jbl',
- 'JBL Charge 4' => 'jbl-charge-4',
- 'JBL Flip' => 'jbl-flip',
- 'JBL GO' => 'jbl-go',
- 'JBL Xtreme 2' => 'jbl-xtreme-2',
- 'Jeans' => 'jeans',
- 'Jets dentaires' => 'jets-dentaires',
- 'Jeux &amp; jouets' => 'jeux-jouets',
- 'Jeux &amp; sports de café' => 'jeux-sports-cafe-bar',
- 'Jeux d&#039;adresse' => 'jeux-adresse',
- 'Jeux d&#039;apprentissage' => 'jeux-d-apprentissage',
- 'Jeux d&#039;eau' => 'jeux-jouets-eau',
- 'Jeux d&#039;extérieur' => 'jeux-d-exterieur',
- 'Jeux d&#039;imitation' => 'jeux-d-imitation',
- 'Jeux de cartes et de plateau' => 'jeux-cartes-plateau-societe',
- 'Jeux de construction' => 'jeux-de-construction',
- 'Jeux de hasard &amp; paris' => 'jeux-et-paris',
- 'Jeux de société' => 'jeux-de-societe',
- 'Jeux Nintendo 3DS' => 'jeux-3ds',
- 'Jeux Nintendo Switch' => 'jeux-nintendo-switch',
- 'Jeux PC' => 'jeux-pc',
- 'Jeux PC dématérialisés' => 'jeux-pc-dematerialises',
- 'Jeux pour bébés' => 'jeux-pour-bebes',
- 'Jeux PS4' => 'jeux-playstation-4',
- 'Jeux PS4 dématérialisés' => 'jeux-ps4-dematerialises',
- 'Jeux PS5' => 'jeux-playstation-5',
- 'Jeux PS5 dématérialisés' => 'jeux-playstation-5-dematerialises',
- 'Jeux PS Plus' => 'jeux-ps-plus',
- 'Jeux vidéo' => 'jeux-video',
- 'Jeux VR' => 'jeux-vr',
- 'Jeux Wii U' => 'jeux-wii-u',
- 'Jeux Xbox One' => 'jeux-xbox-one',
- 'Jeux Xbox One dématérialisés' => 'jeux-xbox-dematerialises',
- 'Jeux Xbox Series X' => 'jeux-xbox-series-x',
- 'Jeux Xbox with Gold' => 'jeux-xbox-with-gold',
- 'Jouets' => 'jouets',
- 'Jouets pour chat' => 'jouets-pour-chat',
- 'Jouets pour chien' => 'jouets-pour-chien',
- 'Journaux numériques' => 'journaux-numeriques',
- 'Journaux papier' => 'journaux-papier',
- 'Joy-Con' => 'manettes-nintendo-switch-joy-con',
- 'Jungle Speed' => 'jungle-speed',
- 'Just Cause' => 'just-cause',
- 'Just Cause 3' => 'just-cause-3',
- 'Just Cause 4' => 'just-cause-4',
- 'Kärcher' => 'karcher',
- 'Kaspersky' => 'kaspersky',
- 'Kinder' => 'kinder',
- 'Kindle Oasis' => 'kindle-oasis',
- 'Kindle Paperwhite' => 'kindle-paperwhite',
- 'Kindle Voyage' => 'kindle-voyage',
- 'Kingdom Hearts' => 'kingdom-hearts',
- 'Kingdom Hearts 3' => 'kingdom-hearts-3',
- 'Kingston HyperX Cloud II' => 'kingston-hyperx-cloud-2',
- 'Kits premiers secours' => 'premiers-secours',
- 'Kobo' => 'kobo',
- 'Kobo Aura 2' => 'kobo-aura-2',
- 'Kobo Aura H2o' => 'kobo-aura-h2o',
- 'Kobo Aura One' => 'kobo-aura-one',
- 'L&#039;annale du destin' => 'l-annale-du-destin',
- 'L&#039;ombre de la guerre' => 'l-ombre-de-la-guerre',
- 'L&#039;ombre du Mordor' => 'l-ombre-du-mordor',
- 'Lacoste' => 'lacoste',
- 'Lampadaires' => 'lampadaires',
- 'Lampes' => 'lampes',
- 'Lampes de table' => 'lampes-de-table',
- 'Lampes solaires' => 'lampes-solaires',
- 'Lancôme La Vie est Belle' => 'lancome-la-vie-est-belle',
- 'Lapeyre' => 'lapeyre',
- 'La Terre du Milieu' => 'la-terre-du-milieu',
- 'Lavage auto' => 'lavage-auto',
- 'Lavazza' => 'lavazza',
- 'Lave-linge' => 'lave-linge',
- 'Lave-linge frontal' => 'lave-linge-frontal',
- 'Lave-linge séchant' => 'lave-linge-sechant',
- 'Lave-linge top' => 'lave-linge-top',
- 'Lave-vaisselle' => 'lave-vaisselle',
- 'Lay-Z-Spa' => 'lay-z-spa',
- 'Leasing voiture' => 'leasing-voiture',
- 'Le bâton de la vérité' => 'le-baton-de-la-verite',
- 'Lecteurs Blu-Ray' => 'lecteurs-blu-ray',
- 'Lecteurs CD' => 'lecteurs-cd',
- 'Lecteurs DVD' => 'lecteurs-dvd',
- 'Lego' => 'lego',
- 'Lego Architecture' => 'lego-architecture',
- 'Lego Batman' => 'lego-batman',
- 'Lego City' => 'lego-city',
- 'Lego Creator' => 'lego-creator',
- 'Lego Dimensions' => 'lego-dimensions',
- 'Lego Duplo' => 'lego-duplo',
- 'Lego Friends' => 'lego-friends',
- 'Lego Harry Potter' => 'lego-harry-potter',
- 'Lego Ideas' => 'lego-ideas',
- 'Lego Marvel' => 'lego-marvel',
- 'Lego Nexo Knights' => 'lego-nexo-knights',
- 'Lego Ninjago' => 'lego-ninjago',
- 'Lego Star Wars' => 'lego-star-wars',
- 'Lego Technic' => 'lego-technic',
- 'Lenovo' => 'lenovo',
- 'Lenovo IdeaPad' => 'lenovo-ideapad',
- 'Lenovo K6 Note' => 'lenovo-k6-note',
- 'Lenovo P8' => 'lenovo-p8',
- 'Lenovo Tab 3' => 'lenovo-tab-3',
- 'Lenovo Tab 4' => 'lenovo-tab-4',
- 'Lenovo ThinkPad' => 'lenovo-thinkpad',
- 'Lenovo Yoga' => 'lenovo-yoga',
- 'Lenovo Yoga Tab 3' => 'lenovo-yoga-tab-3',
- 'Lentilles de contact' => 'lentilles-de-contact',
- 'Le Seigneur des anneaux' => 'le-seigneur-des-anneaux',
- 'Les Sims' => 'les-sims',
- 'Les Sims 4' => 'les-sims-4',
- 'Lessive' => 'lessive',
- 'Levi&#039;s' => 'levi-s',
- 'LG' => 'lg',
- 'LG G4' => 'lg-g4',
- 'LG G5' => 'lg-g5',
- 'LG G6' => 'lg-g6',
- 'LG OLED TV' => 'lg-oled-tv',
- 'LG Q6' => 'lg-q6',
- 'LG Q8' => 'lg-q8',
- 'Life is Strange' => 'life-is-strange',
- 'Linge de maison' => 'linge-de-maison',
- 'Lingerie' => 'lingerie',
- 'Lingettes désinfectantes' => 'lingettes-desinfectantes',
- 'Lingettes pour bébés' => 'lingettes-pour-bebes',
- 'Liseuses' => 'liseuses',
- 'Litière pour chat' => 'litiere-pour-chat',
- 'Lits' => 'lits',
- 'Lits pour bébé' => 'lits-pour-bebe',
- 'Lits pour enfants' => 'lits-pour-enfants',
- 'Little Nightmares' => 'little-nightmares',
- 'Livraison de repas' => 'service-de-livraison-de-repas',
- 'Livres &amp; littérature' => 'livres-litterature',
- 'Livres &amp; Magazines' => 'livres',
- 'Livres audio' => 'livres-audio',
- 'Livres photo' => 'livres-photo',
- 'Location de voiture' => 'location-de-voiture',
- 'Logiciels' => 'logiciels',
- 'Logiciels de sécurité' => 'logiciels-de-securite',
- 'Logiciels Microsoft' => 'logiciels-microsoft',
- 'Logitech' => 'logitech',
- 'Logitech G502' => 'logitech-g502',
- 'Logitech G703' => 'logitech-g703',
- 'Logitech G Pro X' => 'logitech-g-pro-x',
- 'Logitech Harmony' => 'logitech-harmony',
- 'Logitech MX Master' => 'logitech-mx-master',
- 'Logitech MX Master 2S' => 'logitech-mx-master-2s',
- 'Loisirs créatifs' => 'loisirs-creatifs',
- 'Lolita Lempicka' => 'lolita-lempicka-premier-parfum',
- 'Loup-Garou' => 'loup-garou',
- 'Lubrifiants' => 'lubrifiants',
- 'Luges' => 'luges',
- 'Luigi&#039;s Mansion 3' => 'luigi-mansion-3',
- 'Luminaires' => 'luminaires',
- 'Lunettes de natation' => 'lunettes-de-natation',
- 'Lunettes de soleil' => 'lunettes-de-soleil',
- 'M&amp;M&#039;s' => 'metm-s',
- 'MacBook' => 'macbook',
- 'MacBook Air' => 'apple-macbook-air',
- 'MacBook Pro' => 'apple-macbook-pro',
- 'MacBook Pro 13' => 'macbook-pro-13',
- 'MacBook Pro 15' => 'macbook-pro-15',
- 'MacBook Pro 16' => 'macbook-pro-16',
- 'Machines à café à dosettes' => 'machines-a-cafe-a-dosettes',
- 'Machines à café en grain' => 'machines-a-cafe-en-grain',
- 'Machines à coudre' => 'machines-a-coudre',
- 'Machines à pain' => 'machines-a-pain',
- 'Machines de sport' => 'machines-sport',
- 'Machines Dolce Gusto' => 'machines-dolce-gusto',
- 'Machines Nespresso' => 'machines-nespresso',
- 'Machines Senseo' => 'machines-senseo',
- 'Machines Tassimo' => 'machines-tassimo',
- 'Mac mini' => 'mac-mini',
- 'Madden NFL 20' => 'madden-nfl-20',
- 'Magasins d&#039;usine' => 'magasins-usine',
- 'Magazines' => 'magazines',
- 'Maillots de bain' => 'maillots-de-bain',
- 'Maillots de football' => 'maillots-de-football',
- 'Maison &amp; Habitat' => 'maison-habitat',
- 'Maisons de poupées' => 'maisons-poupees',
- 'Makita' => 'makita',
- 'Manettes' => 'manettes-accessoires-consoles',
- 'Manettes DualSense' => 'manettes-playstation-5',
- 'Manettes Nintendo Switch' => 'manettes-nintendo-switch',
- 'Manettes Nintendo Switch Pro' => 'manettes-nintendo-switch-pro',
- 'Manettes PlayStation 4' => 'manettes-playstation-4',
- 'Manettes Xbox' => 'manettes-xbox',
- 'Manettes Xbox One' => 'manettes-xbox-one',
- 'Manettes Xbox One Elite' => 'manettes-xbox-one-elite',
- 'Manettes Xbox Series X' => 'manettes-xbox-series-x',
- 'Manix' => 'manix',
- 'Manteaux' => 'manteaux',
- 'Maquillage' => 'maquillage',
- 'Marchands et leurs offres' => 'vos-avisdemandes-sur-les-marchands-et-leurs-offres',
- 'Mario &amp; Sonic aux Jeux Olympiques de Tokyo 2020' => 'mario-sonic-jeux-olympiques-tokyo-2020',
- 'Mario Kart' => 'mario-kart',
- 'Marques' => 'marques',
- 'Marteaux &amp; maillets' => 'marteaux-et-maillets',
- 'Marvel&#039;s Avengers' => 'marvels-avengers',
- 'Mascara' => 'mascara',
- 'Masques cheveux' => 'masques-cheveux',
- 'Masques de protection' => 'masques-de-protection-respiratoire',
- 'Masques de ski' => 'masques-de-ski',
- 'Mass Effect' => 'mass-effect',
- 'Mass Effect: Andromeda' => 'mass-effect-andromeda',
- 'Matchs de football' => 'matchs-de-football',
- 'Matelas' => 'matelas',
- 'Matelas gonflables' => 'matelas-gonflables',
- 'Matériaux de construction' => 'materiaux-de-construction',
- 'Matériel de ski' => 'materiel-de-ski',
- 'Medion' => 'medion',
- 'Metro' => 'metro',
- 'Metro 2033' => 'metro-2033',
- 'Metro Exodus' => 'metro-exodus',
- 'Meubles pour aquarium' => 'meubles-pour-aquarium',
- 'Meubles pour chat' => 'meubles-pour-chat',
- 'Meubles salle de bain' => 'salle-de-bain',
- 'Micro-casques gaming' => 'micro-casques-gaming',
- 'Micro-ondes' => 'micro-ondes',
- 'Microphones' => 'microphones',
- 'Micro SD' => 'micro-sd',
- 'Microsoft Flight Simulator' => 'microsoft-flight-simulator',
- 'Microsoft Office' => 'microsoft-office',
- 'Microsoft Surface Book' => 'microsoft-surface-book',
- 'Microsoft Surface Pro 6' => 'microsoft-surface-pro-6',
- 'Microsoft Surface Pro 7' => 'microsoft-surface-pro-7',
- 'Miele' => 'miele',
- 'Minecraft' => 'minecraft',
- 'Mini PC' => 'mini-pc',
- 'Mini réfrigérateurs' => 'mini-refrigerateurs',
- 'Miroirs' => 'miroirs',
- 'Mixeurs &amp; Blenders' => 'mixeurs-blenders',
- 'Mixeurs plongeants' => 'mixeur-plongeant',
- 'Mobilier' => 'mobilier',
- 'Mobilier de bureau' => 'fournitures-de-bureau',
- 'Mobilier de jardin' => 'mobilier-jardin',
- 'Mobilier de salon' => 'mobilier-salon',
- 'Mobvoi Ticwatch' => 'mobvoi-ticwatch',
- 'Mode' => 'mode',
- 'Mode &amp; accessoires' => 'mode-accessoires',
- 'Mode &amp; beauté' => 'le-laboratoire-de-la-mode-beaute',
- 'Mode enfants' => 'mode-enfants',
- 'Mode femme' => 'mode-femme',
- 'Mode homme' => 'mode-homme',
- 'Modélisme' => 'modelisme',
- 'Monopoly' => 'monopoly',
- 'Montage PC' => 'montage-pc',
- 'Montre connectée Amazfit' => 'montres-connectees-amazfit',
- 'Montre connectée Garmin' => 'montres-connectees-garmin',
- 'Montre connectée Honor' => 'montres-connectees-honor',
- 'Montre connectée Samsung' => 'smartwatch-samsung',
- 'Montres' => 'montres',
- 'Montres connectées' => 'smartwatch',
- 'Mortal Kombat' => 'mortal-kombat',
- 'Mortal Kombat 11' => 'mortal-kombat-11',
- 'Moto C Plus' => 'moto-c-plus',
- 'Moto E4' => 'moto-e4',
- 'Moto G5' => 'moto-g5',
- 'Moto G5 Plus' => 'moto-g5-plus',
- 'Moto G5S' => 'moto-g5s',
- 'Moto G5S Plus' => 'moto-g5s-plus',
- 'Moto G6' => 'moto-g6',
- 'Moto G6 Play' => 'moto-g6-play',
- 'Moto G6 Plus' => 'moto-g6-plus',
- 'Moto G7 Play' => 'moto-g7-play',
- 'Moto G7 Plus' => 'moto-g7-plus',
- 'Moto G7 Power' => 'moto-g7-power',
- 'Moto M' => 'moto-m',
- 'Motorola' => 'motorola',
- 'Moto Z2' => 'moto-z2',
- 'Moto Z2 Force' => 'moto-z2-force',
- 'Moto Z2 Play' => 'moto-z2-play',
- 'Moto Z3' => 'moto-z3',
- 'Moto Z3 Play' => 'moto-z3-play',
- 'Moulinex' => 'moulinex',
- 'Mousses à raser' => 'mousses-a-raser',
- 'MSI' => 'msi',
- 'Musées' => 'musees',
- 'Musique' => 'musique',
- 'NAS' => 'nas',
- 'Natation' => 'natation',
- 'Nature &amp; sports d&#039;hiver' => 'nature-sports-hiver',
- 'Navigation' => 'navigation',
- 'NBA 2K' => 'nba-2k',
- 'NBA 2K20' => 'nba-2k20',
- 'NERF' => 'nerf',
- 'Nescafé' => 'nescafe',
- 'Nespresso' => 'nespresso',
- 'Nest Learning Thermostat' => 'nest-learning-thermostat',
- 'Nest Protect' => 'nest-protect',
- 'Netflix' => 'netflix',
- 'Nettoyeurs haute-pression' => 'nettoyeurs-haute-pression',
- 'Nettoyeurs haute pression Karcher' => 'nettoyeurs-haute-pression-karcher',
- 'Nettoyeurs vapeur' => 'nettoyeurs-vapeur',
- 'New Balance' => 'new-balance',
- 'New Balance 574' => 'new-balance-574',
- 'NHL 20' => 'nhl-20',
- 'Nike' => 'nike',
- 'Nike Air Force' => 'nike-air-force',
- 'Nike Air Jordan' => 'nike-air-jordan',
- 'Nike Air Max' => 'nike-air-max',
- 'Nike Air Max 90' => 'nike-air-max-90',
- 'Nike Air Max 200' => 'nike-air-max-200',
- 'Nike Air Max 270' => 'nike-air-max-270',
- 'Nike Air Max 720' => 'nike-air-max-720',
- 'Nike Free' => 'nike-free',
- 'Nike Huarache' => 'nike-huarache',
- 'Nike Roshe Run' => 'nike-roshe-run',
- 'Nikon' => 'nikon',
- 'Nikon D3500' => 'nikon-d3500',
- 'Ni no Kuni' => 'ni-no-kuni',
- 'Ni No Kuni: Wrath of the White Witch' => 'ni-no-kuni-wrath-white-witch',
- 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-2-revenant-kingdom',
- 'Nintendo' => 'nintendo',
- 'Nioh' => 'nioh',
- 'Nivea' => 'nivea',
- 'Nocciolata' => 'nocciolata',
- 'Nokia' => 'nokia',
- 'Nokia 5' => 'nokia-5',
- 'Nokia 6' => 'nokia-6',
- 'Nokia 8' => 'nokia-8',
- 'Nokia 9 PureView' => 'nokia-9-pureview',
- 'Nougats' => 'nougats',
- 'Nourriture pour chat' => 'nourriture-pour-chat',
- 'Nourriture pour chien' => 'nourriture-pour-chien',
- 'Nourriture pour poissons' => 'nourriture-pour-poissons',
- 'Nutella' => 'nutella',
- 'Nvidia' => 'nvidia',
- 'Nvidia GeForce' => 'nvidia-geforce',
- 'Nvidia Shield' => 'nvidia-shield',
- 'Objectifs' => 'objectifs',
- 'Objets connectés' => 'objets-connectes',
- 'Oculus Go' => 'oculus-go',
- 'Oculus Rift' => 'oculus-rift',
- 'Oiseaux' => 'oiseaux',
- 'One Piece: Pirate Warriors' => 'one-piece-pirate-warriors',
- 'OnePlus 5' => 'oneplus-5',
- 'OnePlus 5T' => 'oneplus-5t',
- 'OnePlus 6' => 'oneplus-6',
- 'OnePlus 6T' => 'oneplus-6t',
- 'OnePlus 7' => 'oneplus-7',
- 'OnePlus 7 Pro' => 'oneplus-7-pro',
- 'OnePlus 7T' => 'oneplus-7t',
- 'OnePlus 7T Pro' => 'oneplus-7t-pro',
- 'OnePlus 8' => 'oneplus-8',
- 'OnePlus 8 Pro' => 'oneplus-8-pro',
- 'OnePlus 8T' => 'oneplus-8t',
- 'OnePlus 9' => 'oneplus-9',
- 'OnePlus 9 Pro' => 'oneplus-9-pro',
- 'OnePlus Nord' => 'oneplus-nord',
- 'Onkyo' => 'onkyo',
- 'Oppo Find X2 Lite' => 'oppo-find-x2-lite',
- 'Oppo Find X2 Neo' => 'oppo-find-x2-neo',
- 'Oppo Find X2 Pro' => 'oppo-find-x2-pro',
- 'Oppo Reno' => 'oppo-reno',
- 'Optique' => 'optique',
- 'Oral-B' => 'oral-b',
- 'Ordinateurs de bureau' => 'ordinateurs-de-bureau',
- 'Ordinateurs tout-en-un' => 'pc-de-bureau-complets',
- 'Oreillers' => 'oreillers',
- 'Osram Smart+' => 'osram-smart-plus',
- 'Outillage' => 'outillage',
- 'Outils à main' => 'outils-main',
- 'Outils de jardinage' => 'outils-de-jardinage',
- 'Outils électriques' => 'outils-electriques',
- 'Overwatch' => 'overwatch',
- 'Packs clavier-souris' => 'packs-clavier-souris',
- 'Packs consoles' => 'packs-consoles',
- 'Paco Rabanne Invictus' => 'paco-rabanne-invictus',
- 'Paco Rabanne Lady Million' => 'paco-rabanne-lady-million',
- 'Paco Rabanne One Million' => 'paco-rabanne-one-million',
- 'Pain &amp; pâtisseries' => 'pain-patisseries',
- 'Pampers' => 'pampers',
- 'Panasonic' => 'panasonic',
- 'Panasonic Lumix' => 'panasonic-lumix',
- 'Panier Plus' => 'panier-plus',
- 'Pantalons' => 'pantalons',
- 'Papeterie' => 'papeterie',
- 'Papeterie et bureautique' => 'papeterie-bureautique',
- 'Papier bureautique' => 'papier-bureautique',
- 'Papier peint' => 'papier-peint',
- 'Papier toilette' => 'papier-toilette',
- 'Parapharmacie' => 'parapharmacie',
- 'Parasols' => 'parasols',
- 'Parc Astérix' => 'parc-asterix',
- 'Parcs d&#039;attraction' => 'parcs-d-attraction',
- 'Parfums' => 'parfums',
- 'Parfums femme' => 'parfums-femme',
- 'Parfums homme' => 'parfums-homme',
- 'Parkas' => 'parkas',
- 'Parrot' => 'parrot',
- 'Partitions' => 'partitions',
- 'Pâtée pour chat' => 'patee-pour-chat',
- 'Pâtée pour chien' => 'patee-pour-chien',
- 'Pâtes à tartiner' => 'pates-tartiner',
- 'Pâtisserie' => 'patisserie',
- 'PC Barebones' => 'pc-barebones',
- 'PC gamer fixe' => 'pc-gamer-complets',
- 'PC gaming' => 'pc-gaming',
- 'PC hybrides' => 'hybrides',
- 'PC Microsoft Surface' => 'pc-microsoft-surface',
- 'PC portables' => 'pc-portables',
- 'PC portables Acer' => 'pc-portables-acer',
- 'PC portables ASUS' => 'pc-portables-asus',
- 'PC portables Dell' => 'pc-portables-dell',
- 'PC portables gaming' => 'portables-gamer',
- 'PC portables Honor' => 'pc-portables-honor',
- 'PC portables HP' => 'pc-portables-hp',
- 'PC portables Lenovo' => 'pc-portables-lenovo',
- 'PC portables Lenovo Legion' => 'lenovo-legion',
- 'PC portables Xiaomi' => 'pc-portables-xiaomi',
- 'Pêche' => 'peche',
- 'Peignes &amp; brosses à cheveux' => 'peignes-et-brosses-a-cheveux',
- 'Peignoirs' => 'peignoirs',
- 'Peintures' => 'peintures',
- 'Peluches' => 'peluches',
- 'Perceuses' => 'perceuses',
- 'Périphériques PC' => 'peripheriques-pc',
- 'Persona 5' => 'persona-5',
- 'Persona 5 Royal' => 'persona-5-royal',
- 'PES' => 'pro-evolution-soccer',
- 'Pèse-personnes' => 'pese-personnes',
- 'Petites voitures' => 'petites-voitures',
- 'Pharmacie &amp; parapharmacie' => 'pharmacie-parapharmacie',
- 'Philips' => 'philips',
- 'Philips Hue' => 'philips-hue',
- 'Philips Hue E14' => 'philips-hue-e14',
- 'Philips Hue E27' => 'philips-hue-e27',
- 'Philips Hue Go' => 'philips-hue-go',
- 'Philips Hue GU10' => 'philips-hue-gu10',
- 'Philips Hue LightStrip' => 'philips-hue-lightstrip',
- 'Philips Hue Play HDMI Sync Box' => 'philips-hue-play-hdmi-sync-box',
- 'Philips Lumea' => 'philips-lumea',
- 'Philips OneBlade' => 'philips-one-blade',
- 'Philips Sonicare' => 'philips-sonicare',
- 'Photo' => 'photo',
- 'Pièces auto' => 'pieces-auto',
- 'Pièces moto' => 'pieces-moto',
- 'Pièces vélo' => 'pieces-velo',
- 'Piles' => 'piles',
- 'Piles rechargeables' => 'piles-rechargeables',
- 'Pinceaux maquillage' => 'pinceaux-maquillage',
- 'Pinces' => 'pinces',
- 'Ping-pong' => 'ping-pong',
- 'Pioneer' => 'pioneer',
- 'Piscines' => 'piscines',
- 'Pizza' => 'pizza',
- 'Places de cinéma' => 'places-de-cinema',
- 'Plafonniers' => 'plafonniers',
- 'Plancha' => 'planchas',
- 'Plantes &amp; semis' => 'plantes',
- 'Plaques de cuisson' => 'plaques-de-cuisson',
- 'Platines vinyle' => 'platines-vinyle',
- 'Plats &amp; moules' => 'plats-et-moules',
- 'PlayerUnknown&#039;s Battlegrounds' => 'playerunknown-s-battleground',
- 'Playmobil' => 'playmobil',
- 'PlayStation' => 'playstation',
- 'Pneus' => 'pneus',
- 'PocketBook' => 'pocketbook',
- 'PocketBook Touch Lux 3' => 'pocketbook-touch-lux-3',
- 'POCO F2 Pro' => 'poco-f2-pro',
- 'POCO F3' => 'poco-f3',
- 'POCO M3' => 'poco-m3',
- 'POCO X3' => 'poco-x3',
- 'POCO X3 Pro' => 'poco-x3-pro',
- 'Poêles' => 'poeles',
- 'Pokémon' => 'pokemon',
- 'Pokémon: Let&#039;s Go' => 'pokemon-letsgo',
- 'Pokémon Épée et Bouclier' => 'pokemon-epee-bouclier',
- 'Pokémon Tournament' => 'pokemon-tournament',
- 'Pokémon Ultra Sun / Moon' => 'pokemon-ultra-sun-moon',
- 'Polaroid' => 'polaroid',
- 'Polos' => 'polos',
- 'Pompes à vélo' => 'pompes-velo',
- 'Porte-bébé' => 'porte-bebe',
- 'Portefeuilles' => 'portefeuilles',
- 'Posters' => 'posters',
- 'Potager' => 'potager',
- 'Pots &amp; cache-pots' => 'pots-et-cache-pots',
- 'Poubelles' => 'poubelles',
- 'Poulaillers' => 'poulaillers',
- 'Poupées' => 'poupees',
- 'Poussettes' => 'poussettes-bebe',
- 'Présentez-vous !' => 'mieux-se-connaitre-presentez-vous',
- 'Préservatifs' => 'preservatifs',
- 'Princesse Tam-Tam' => 'princesse-tam-tam',
- 'Prises connectées' => 'prises-connectees',
- 'Processeurs' => 'processeurs',
- 'Produit pour lentilles' => 'produit-pour-lentilles',
- 'Produits de massage' => 'produits-de-massage',
- 'Produits frais' => 'produits-frais',
- 'Produits reconditionnés' => 'reconditionne',
- 'Produits vétérinaires' => 'produits-veterinaires',
- 'Programme d&#039;Entraînement Cérébral du Dr. Kawashima' => 'dr-kawashima-brain-training',
- 'Project Cars 2' => 'project-cars-2',
- 'Protection de la maison' => 'protection-de-la-maison',
- 'Protections intimes' => 'protections-intimes',
- 'Protection solaire' => 'protection-solaire',
- 'Puériculture' => 'puericulture',
- 'Pulls' => 'pulls',
- 'Puma' => 'puma',
- 'Purificateurs d&#039;air' => 'purificateurs-d-air',
- 'Purina' => 'purina',
- 'Puzzles' => 'puzzles',
- 'Pyjamas' => 'pyjamas',
- 'Pyjamas &amp; chemises de nuit' => 'pyjamas-chemises-de-nuit',
- 'Pyjamas pour bébés' => 'pyjamas-pour-bebes',
- 'Qobuz' => 'qobuz',
- 'Quiksilver' => 'quiksilver',
- 'Radiateurs' => 'radiateurs',
- 'Ralph Lauren' => 'ralph-lauren',
- 'RAM' => 'ram',
- 'Randonnée' => 'randonnee',
- 'Raquettes de ping-pong' => 'raquettes-de-ping-pong',
- 'Raquettes de tennis' => 'raquettes-de-tennis',
- 'Rasage et épilation' => 'rasage-epilation',
- 'Rasoirs Braun' => 'rasoirs-braun',
- 'Rasoirs électriques' => 'rasoirs-electriques',
- 'Rasoirs Gillette' => 'gillette',
- 'Rasoirs manuels' => 'rasoirs-manuels',
- 'Rasoirs Philips' => 'rasoirs-philips',
- 'Rasoirs Wilkinson' => 'rasoirs-wilkinson-sword',
- 'Raspberry Pi' => 'raspberry-pi',
- 'Ray-Ban' => 'ray-ban',
- 'Razer' => 'razer',
- 'Razer DeathAdder' => 'razer-deathadder',
- 'Realme 5 Pro' => 'realme-5-pro',
- 'Realme X2 Pro' => 'realme-x2-pro',
- 'Red Dead Redemption' => 'red-dead-redemption',
- 'Red Dead Redemption 2' => 'red-dead-redemption-2',
- 'Réductions étudiants &amp; jeunes' => 'reductions-etudiants-et-jeunes',
- 'Reebok' => 'reebok',
- 'Reebok Club C' => 'reebok-club-c',
- 'Réfrigérateurs' => 'refrigerateurs',
- 'Réfrigérateurs américains' => 'refrigerateurs-americains',
- 'Refroidissement PC' => 'refroidissement-pc',
- 'Réhausseurs' => 'rehausseurs',
- 'Remington' => 'remington',
- 'Repas de fête' => 'repas-fete-reveillon',
- 'Repassage' => 'repassage',
- 'Répéteurs' => 'repeteurs',
- 'Réseau' => 'reseau',
- 'Resident Evil' => 'resident-evil',
- 'Resident Evil 3' => 'resident-evil-3',
- 'Resident Evil 7' => 'resident-evil-7',
- 'Restaurants' => 'restaurants',
- 'Revêtements de sols' => 'revetements-de-sols',
- 'Revêtements muraux' => 'revetements-muraux',
- 'Rhum' => 'rhum',
- 'Richelieus' => 'richelieus',
- 'Ring Fit Adventure' => 'ring-fit-adventure',
- 'Risk' => 'risk',
- 'Robes &amp; jupes' => 'robes-et-jupes',
- 'Roborock' => 'roborock',
- 'Roborock S5 MAX' => 'roborock-s5-max',
- 'Roborock S6' => 'roborock-s6',
- 'Robots cuiseurs' => 'robots-cuiseurs',
- 'Robots ménagers' => 'robots-menagers',
- 'Robot tondeuse' => 'robot-tondeuse',
- 'ROCCAT' => 'roccat',
- 'Rollers' => 'rollers',
- 'Rouges à lèvres' => 'rouges-a-levres',
- 'Routeurs' => 'routeurs',
- 'Rowenta' => 'rowenta',
- 'Royal Canin' => 'royal-canin',
- 'RTX 2060' => 'rtx-2060',
- 'RTX 2070' => 'rtx-2070',
- 'RTX 2080' => 'rtx-2080',
- 'RTX 2080 Ti' => 'rtx-2080-ti',
- 'RTX 3070' => 'rtx-3070',
- 'RTX 3080' => 'rtx-3080',
- 'RTX 3090' => 'rtx-3090',
- 'RX 480' => 'rx-480',
- 'RX 580' => 'rx-580',
- 'RX 590' => 'radeon-rx-590',
- 'RX Vega 56' => 'rx-vega-56',
- 'RX Vega 64' => 'rx-vega-64',
- 'Sacs à déjections' => 'sacs-a-dejections',
- 'Sacs à dos' => 'sacs-a-dos',
- 'Sacs à langer' => 'sacs-a-langer',
- 'Sacs à main' => 'sacs-a-main',
- 'Sacs bandoulière' => 'sacs-bandouliere',
- 'Sacs de couchage' => 'sacs-de-couchage',
- 'Sacs de randonnée' => 'sacs-de-randonnee',
- 'Sacs de sport' => 'sacs-de-sport',
- 'Sacs de voyage' => 'sacs-de-voyage',
- 'Salle à manger' => 'salle-manger',
- 'Samsonite' => 'samsonite',
- 'Samsung' => 'samsung',
- 'Samsung Galaxy A5' => 'samsung-galaxy-a5',
- 'Samsung Galaxy A50' => 'samsung-galaxy-a50',
- 'Samsung Galaxy A51' => 'samsung-galaxy-a51',
- 'Samsung Galaxy A51 5G' => 'samsung-galaxy-a51-5g',
- 'Samsung Galaxy A70' => 'samsung-galaxy-a70',
- 'Samsung Galaxy A80' => 'samsung-galaxy-a80',
- 'Samsung Galaxy Buds' => 'samsung-galaxy-buds',
- 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus',
- 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live',
- 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro',
- 'Samsung Galaxy Fold' => 'samsung-galaxy-fold',
- 'Samsung Galaxy Note 8' => 'samsung-galaxy-note-8',
- 'Samsung Galaxy Note 9' => 'samsung-galaxy-note-9',
- 'Samsung Galaxy Note 10' => 'samsung-galaxy-note-10',
- 'Samsung Galaxy Note 10 Lite' => 'samsung-galaxy-note-10-lite',
- 'Samsung Galaxy Note 10 Plus' => 'samsung-galaxy-note-10-plus',
- 'Samsung Galaxy Note20' => 'samsung-galaxy-note-20',
- 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note-20-ultra',
- 'Samsung Galaxy S7' => 'samsung-galaxy-s7',
- 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge',
- 'Samsung Galaxy S8' => 'samsung-galaxy-s8',
- 'Samsung Galaxy S8+' => 'samsung-galaxy-s8plus',
- 'Samsung Galaxy S9' => 'samsung-galaxy-s9',
- 'Samsung Galaxy S9 Plus' => 'samsung-galaxy-s9-plus',
- 'Samsung Galaxy S10' => 'samsung-galaxy-s10',
- 'Samsung Galaxy S10 Lite' => 'samsung-galaxy-s10-lite',
- 'Samsung Galaxy S10+' => 'samsung-galaxy-s10-plus',
- 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e',
- 'Samsung Galaxy S20' => 'samsung-galaxy-s20',
- 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe',
- 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra',
- 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus',
- 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g',
- 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g',
- 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g',
- 'Samsung Galaxy Tab A' => 'samsung-galaxy-tab-a',
- 'Samsung Galaxy Tab S2' => 'samsung-galaxy-tab-s2',
- 'Samsung Galaxy Tab S3' => 'samsung-galaxy-tab-s3',
- 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4',
- 'Samsung Galaxy Tab S5e' => 'samsung-galaxy-tab-s5e',
- 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6',
- 'Samsung Galaxy Tab S7' => 'samsung-galaxy-tab-s7',
- 'Samsung Galaxy Watch' => 'samsung-galaxy-watch',
- 'Samsung Galaxy Watch3' => 'samsung-galaxy-watch-3',
- 'Samsung Galaxy Watch Active 2' => 'samsung-galaxy-watch-active2',
- 'Samsung Galaxy Z Flip' => 'galaxy-z-flip',
- 'Samsung Gear' => 'samsung-gear',
- 'Samsung Gear S3' => 'samsung-gear-s3',
- 'Samsung Gear VR' => 'samsung-gear-vr',
- 'Sandales' => 'sandales',
- 'SanDisk' => 'sandisk',
- 'Sanitaires et robinetterie' => 'sanitaires-robinetterie',
- 'Santé &amp; Cosmétiques' => 'sante-et-cosmetiques',
- 'Sapins de Noël' => 'sapins-noel',
- 'Savons' => 'savons',
- 'Scanners' => 'scanners',
- 'Scanners A3' => 'scanners-a3',
- 'Scanners A4' => 'scanners-a4',
- 'Scies' => 'scies',
- 'Scooters' => 'scooters',
- 'Seagate' => 'seagate',
- 'Sécateurs' => 'secateurs',
- 'Sèche-cheveux' => 'seche-cheveux',
- 'Sèche-linge' => 'seche-linge',
- 'Seiko' => 'seiko',
- 'Séjours' => 'sejours',
- 'Sekiro: Shadows Die Twice' => 'sekiro',
- 'Semis &amp; graines' => 'semis-et-graines',
- 'Sennheiser' => 'sennheiser',
- 'Senseo' => 'senseo',
- 'Séries TV' => 'series-tv',
- 'Service &amp; réparation auto-moto' => 'service-reparation-auto-moto',
- 'Services' => 'services-divers',
- 'Services auto' => 'services-auto',
- 'Services de livraison' => 'services-livraisons',
- 'Services moto' => 'services-moto',
- 'Services photo' => 'services-photo',
- 'Serviettes' => 'serviettes',
- 'Serviettes hygiéniques' => 'serviettes-hygieniques',
- 'Sextoys' => 'sextoys',
- 'Shadow of the Colossus' => 'shadow-of-the-colossus',
- 'Shadow of the Tomb Raider' => 'shadow-tomb-raider',
- 'Shalimar' => 'shalimar',
- 'Shampooings &amp; soins' => 'shampooings-et-soins',
- 'Shenmue' => 'shenmue',
- 'Shenmue I &amp; II' => 'shenmue-i-ii',
- 'Shenmue III' => 'shenmue-iii',
- 'Shorts' => 'shorts',
- 'Shorts de bain' => 'shorts-de-bain',
- 'Sièges auto' => 'sieges-auto',
- 'Siemens' => 'siemens',
- 'Skates &amp; longboards' => 'skates-et-longboards',
- 'Skechers' => 'sketchers',
- 'Ski' => 'ski',
- 'Skyrim' => 'skyrim',
- 'Slips &amp; boxers' => 'slips-et-boxers',
- 'Smartphones' => 'smartphones',
- 'Smartphones à moins de 100€' => 'smartphones-moins-de-100',
- 'Smartphones à moins de 200€' => 'smartphones-moins-de-200',
- 'Smartphones Android' => 'smartphones-android',
- 'Smartphones Asus' => 'smartphones-asus',
- 'Smartphones Google' => 'smartphones-google',
- 'Smartphones Honor' => 'smartphones-honor',
- 'Smartphones HTC' => 'smartphones-htc',
- 'Smartphones Huawei' => 'smartphones-huawei',
- 'Smartphones Lenovo Motorola' => 'smartphones-lenovo-motorola',
- 'Smartphones LG' => 'smartphones-lg',
- 'Smartphones Nokia' => 'smartphones-nokia',
- 'Smartphones OnePlus' => 'smartphones-oneplus',
- 'Smartphones Oppo' => 'smartphones-oppo',
- 'Smartphones Realme' => 'smartphones-realme',
- 'Smartphones Samsung' => 'smartphones-samsung',
- 'Smartphones Sony' => 'smartphones-sony',
- 'Smartphones Xiaomi' => 'smartphones-xiaomi',
- 'Smartphones ZTE' => 'smartphones-zte',
- 'Smart TV' => 'smart-tv',
- 'Sneakers' => 'sneakers',
- 'SodaStream' => 'sodastream',
- 'Sofas gonflable' => 'sofas-gonflable',
- 'Soin barbe et rasage' => 'soin-barbe-rasage',
- 'Soin de la peau' => 'soin-peau',
- 'Soin des cheveux' => 'soin-des-cheveux',
- 'Soin des ongles' => 'soin-ongles',
- 'Soins dentaires' => 'soins-dentaires',
- 'Sonos' => 'sonos',
- 'Sonos Beam' => 'sonos-beam',
- 'Sonos Move' => 'sonos-move',
- 'Sonos One' => 'sonos-one',
- 'Sonos PLAY:1' => 'sonos-play-1',
- 'Sonos PLAY:3' => 'sonos-play-3',
- 'Sonos PLAY:5' => 'sonos-play-5',
- 'Sonos PLAYBAR' => 'sonos-playbar',
- 'Sony' => 'sony',
- 'Sony PlayStation VR' => 'sony-playstation-vr',
- 'Sony Pulse 3D sans fil' => 'casque-audio-sony-pulse-3d',
- 'Sony WF-1000XM3' => 'sony-wf-1000xm3',
- 'Sony WH-1000XM3' => 'sony-wh-1000xm3',
- 'Sony WH-1000XM4' => 'sony-wh-1000xm4',
- 'Sony Xperia XA1' => 'sony-xperia-xa1',
- 'Sony Xperia X Compact' => 'sony-xperia-x-compact',
- 'Sony Xperia XZ1' => 'sony-xperia-xz1',
- 'Sony Xperia XZ1 Compact' => 'sony-xperia-xz1-compact',
- 'Sony Xperia XZ Premium' => 'sony-xperia-xz-premium',
- 'Sony Xperia Z3' => 'sony-xperia-z3',
- 'Soulcalibur' => 'soulcalibur',
- 'Souris' => 'souris',
- 'Souris gamer' => 'souris-gamer',
- 'Souris Logitech' => 'souris-logitech',
- 'Souris sans fil' => 'souris-sans-fil',
- 'Sous-vêtements' => 'sous-vetements',
- 'Sous-vêtements de sport' => 'sous-vetements-de-sport',
- 'South Park' => 'south-park',
- 'Soutiens-gorge' => 'soutiens-gorge',
- 'Spas' => 'spa',
- 'Spectacles' => 'spectacles',
- 'Spectacles &amp; Billetterie' => 'sorties',
- 'Spectacles comiques' => 'spectacles-comiques',
- 'Spectacles pour enfants' => 'spectacles-pour-enfants',
- 'Sports &amp; plein air' => 'sports-plein-air',
- 'Sports collectifs' => 'sports-collectifs',
- 'Sports nautiques' => 'sports-nautiques',
- 'Sportswear' => 'sportswear',
- 'Spotify' => 'spotify',
- 'SSD' => 'ssd',
- 'Star Wars: Jedi Fallen Order' => 'star-wars-jedi-fallen-order',
- 'Star Wars: Squadrons' => 'star-wars-squadrons',
- 'Star Wars Battlefront' => 'star-wars-battlefront',
- 'Stations météo' => 'stations-meteo',
- 'Stickers muraux' => 'stickers-muraux',
- 'Stihl' => 'stihl',
- 'Stockage externe' => 'stockage',
- 'Streaming' => 'streaming',
- 'Streaming musical' => 'streaming-musical',
- 'Streaming vidéo' => 'streaming-video',
- 'Stylos' => 'stylos',
- 'Sucettes' => 'sucettes',
- 'Super Mario' => 'super-mario',
- 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars',
- 'Super Mario Maker 2' => 'super-mario-maker-2',
- 'Super Mario Party' => 'super-mario-party',
- 'Super Smash Bros. Ultimate' => 'super-smash-bros-ultimate',
- 'Support GPS &amp; smartphone' => 'support-gps-et-smartphone',
- 'Supports TV' => 'supports-tv',
- 'Surface Pro 4' => 'surface-pro-4',
- 'Surgelés' => 'surgeles',
- 'Surveillance' => 'surveillance',
- 'Suspensions' => 'suspensions',
- 'Swatch' => 'swatch',
- 'Switch réseau' => 'switch-reseau',
- 'Systèmes d&#039;exploitation' => 'systemes-d-exploitation',
- 'Systèmes multiroom' => 'systemes-multiroom',
- 'T-shirts' => 't-shirts',
- 'Tables' => 'tables',
- 'Tables à langer' => 'tables-a-langer',
- 'Tables à repasser' => 'tables-a-repasser',
- 'Tables basses' => 'tables-basses',
- 'Tables de camping' => 'tables-de-camping',
- 'Tables de mixage' => 'tables-de-mixage',
- 'Tables de ping-pong' => 'tables-ping-pong',
- 'Tablettes' => 'tablettes',
- 'Tablettes graphiques' => 'tablettes-graphiques',
- 'Tablettes graphiques Huion' => 'huion',
- 'Tablettes graphiques Wacom' => 'wacom',
- 'Tablettes Huawei' => 'tablettes-huawei',
- 'Tablettes Lenovo' => 'tablettes-lenovo',
- 'Tablettes Microsoft Surface' => 'tablettes-microsoft-surface',
- 'Tablettes Samsung' => 'tablettes-samsung',
- 'Tablettes Xiaomi' => 'tablettes-xiaomi',
- 'Tampons' => 'tampons',
- 'Tapis' => 'tapis',
- 'Tapis de souris' => 'tapis-de-souris',
- 'Tassimo' => 'tassimo',
- 'Taxis' => 'taxis',
- 'Tefal' => 'tefal',
- 'Tekken' => 'tekken',
- 'Tekken 7' => 'tekken-7',
- 'Télécommandes' => 'telecommandes',
- 'Téléphones fixes' => 'telephones-fixes',
- 'Téléphonie' => 'telephonie',
- 'Téléviseurs' => 'televiseurs',
- 'Tentes' => 'tentes',
- 'Tentes Quechua' => 'tentes-quechua',
- 'Têtes de brosse à dents de rechange' => 'tetes-de-brosse-a-dents-de-rechange',
- 'Théâtre' => 'theatre',
- 'The Last of Us' => 'the-last-of-us',
- 'The Last of Us Part II' => 'the-last-of-us-part-2',
- 'The Legend of Zelda' => 'the-legend-of-zelda',
- 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild',
- 'The Legend of Zelda: Link&#039;s Awakening' => 'legend-of-zelda-link-s-awakening',
- 'The Legend of Zelda: Skyward Sword HD' => 'the-legend-of-zelda-skyward-sword-hd',
- 'Thermomètres' => 'thermometres',
- 'Thermomix' => 'thermomix',
- 'Thermostats connectés' => 'thermostat-connecte',
- 'Thés' => 'thes',
- 'Thés glacés' => 'thes-glaces',
- 'The Walking dead' => 'the-walking-dead',
- 'The Witcher' => 'the-witcher',
- 'The Witcher 3' => 'the-witcher-3',
- 'Time&#039;s Up!' => 'time-s-up',
- 'Tokyo Laundry' => 'tokyo-laundry',
- 'Tomb Raider' => 'tomb-raider',
- 'Tom Clancy&#039;s' => 'tom-clancy-s',
- 'Tom Clancy&#039;s Ghost Recon: Wildlands' => 'tom-clancy-s-ghost-recon-wildlands',
- 'Tom Clancy&#039;s Ghost Recon Breakpoint' => 'tom-clancy-s-ghost-recon-breakpoint',
- 'Tom Clancy&#039;s The Division' => 'tom-clancy-s-the-division',
- 'TomTom' => 'tomtom',
- 'Tondeuses' => 'tondeuses',
- 'Tondeuses à gazon' => 'tondeuses-a-gazon',
- 'Toner' => 'toner',
- 'Tongs' => 'tongs',
- 'Torchons' => 'torchons',
- 'Toshiba' => 'toshiba',
- 'Total War' => 'total-war',
- 'Total War: Warhammer' => 'total-war-warhammer',
- 'Total War: Warhammer II' => 'total-war-warhammer-ii',
- 'Tournevis' => 'tournevis-et-visseuses',
- 'TP-Link' => 'tp-link',
- 'Trains &amp; Bus' => 'trains-bus',
- 'Trampolines' => 'trampolines',
- 'Transats &amp; cosys' => 'transats-et-cosys',
- 'Transport bébé' => 'poussettes',
- 'Transport d&#039;animaux' => 'transport-d-animaux',
- 'Transports en commun' => 'transports-en-commun',
- 'Transports urbains' => 'transports-urbains',
- 'Travaux &amp; matériaux' => 'travaux-materiaux',
- 'Trépieds' => 'trepieds',
- 'Trixie' => 'trixie',
- 'Tronçonneuses' => 'tronconneuses',
- 'Tropico' => 'tropico',
- 'Tropico 6' => 'tropico-6',
- 'Trottinettes' => 'trottinettes',
- 'Trottinettes électriques' => 'trottinettes-electriques',
- 'Trottinettes électriques en libre-service' => 'location-trottinettes-electriques',
- 'Trottinettes Xiaomi' => 'trottinettes-xiaomi',
- 'TV &amp; Vidéo' => 'tv-video',
- 'TV 4K' => 'tv-4k',
- 'TV 40&#039;&#039; à 64&#039;&#039;' => 'tv-40-pouces-a-64-pouces',
- 'TV 65&#039;&#039; et plus' => 'tv-65-pouces-et-plus',
- 'TV Hisense' => 'tv-hisense',
- 'TV LG' => 'tv-lg',
- 'TV OLED' => 'tv-oled',
- 'TV Panasonic' => 'tv-panasonic',
- 'TV Philips' => 'tv-philips',
- 'TV Samsung' => 'tv-samsung',
- 'TV Samsung QLED' => 'tv-samsung-qled',
- 'TV Samsung The Frame' => 'tv-samsung-the-frame',
- 'TV Sony' => 'tv-sony',
- 'TV TCL' => 'tv-tcl',
- 'TV Toshiba' => 'tv-toshiba',
- 'TV Xiaomi' => 'tv-xiaomi',
- 'UE Boom 2' => 'ue-boom-2',
- 'UE Boom 3' => 'ue-boom-3',
- 'UE Megaboom' => 'ue-megaboom',
- 'UE Megaboom 3' => 'ue-megaboom-3',
- 'UE Wonderboom' => 'ue-wonderboom',
- 'Ultraportables' => 'ultraportables',
- 'Uncharted' => 'uncharted',
- 'Uncharted 4' => 'uncharted-4',
- 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy',
- 'Under Armour' => 'under-armour',
- 'Until Dawn' => 'until-dawn',
- 'Ustensiles de cuisine' => 'ustensiles-de-cuisine',
- 'Ustensiles de cuisson' => 'ustensiles-de-cuisson',
- 'Vacances et séjours' => 'vacances-sejours',
- 'Vaisselle' => 'vaisselle',
- 'Valises' => 'valises',
- 'Valises cabine' => 'valises-cabine',
- 'Valises rigides' => 'valises-rigides',
- 'Vans Old Skool' => 'vans-old-skool',
- 'Variétés &amp; revues' => 'varietes-et-revues',
- 'Vases' => 'vases',
- 'Veet' => 'veet',
- 'Veilleuses' => 'veilleuses',
- 'Vélos' => 'velos',
- 'Vélos d&#039;appartement' => 'velos-d-appartement',
- 'Vélos électriques' => 'velos-electriques',
- 'Ventilateurs' => 'ventilateurs',
- 'Ventirad' => 'ventirad',
- 'Vernis à ongles' => 'vernis-a-ongles',
- 'Verres' => 'verres',
- 'Vestes' => 'vestes',
- 'Vestes polaires' => 'vestes-polaires',
- 'Vêtements d&#039;été' => 'vetements-d-ete',
- 'Vêtements d&#039;hiver' => 'vetements-d-hiver',
- 'Vêtements de grossesse' => 'vetements-de-grossesse',
- 'Vêtements de montagne' => 'vetements-techniques',
- 'Vêtements de running' => 'vetements-de-running',
- 'Vêtements de ski' => 'vetements-de-ski',
- 'Vêtements de sport' => 'vetements-de-sport',
- 'Vêtements pour bébé' => 'vetements-pour-bebe',
- 'Vidéoprojecteurs' => 'projecteurs',
- 'Vidéoprojecteurs 3D' => 'videoprojecteurs-3d',
- 'Vidéoprojecteurs Acer' => 'videoprojecteurs-acer',
- 'Vidéoprojecteurs BenQ' => 'videoprojecteurs-benq',
- 'Vidéoprojecteurs Epson' => 'videoprojecteurs-epson',
- 'Vidéoprojecteurs HD' => 'videoprojecteurs-hd',
- 'Vidéoprojecteurs LG' => 'videoprojecteurs-lg',
- 'Vidéoprojecteurs Optoma' => 'videoprojecteurs-optoma',
- 'Vins' => 'vins',
- 'Visites &amp; patrimoine' => 'visites-et-patrimoine',
- 'Visseuses' => 'visseuses',
- 'VOD' => 'vod',
- 'Voitures &amp; motos' => 'voitures-motos',
- 'Voitures télécommandées' => 'voitures-telecommandees',
- 'Volants' => 'volants-de-course',
- 'Vols' => 'billets-d-avion',
- 'Voyages' => 'voyages',
- 'Voyages &amp; loisirs' => 'le-laboratoire-des-voyages-loisirs',
- 'VPN' => 'vpn',
- 'VTC' => 'vtc',
- 'VTT' => 'vtt',
- 'Wacom Cintiq' => 'cintiq',
- 'Watch Dogs' => 'watch-dogs',
- 'Watch Dogs 2' => 'watch-dogs-2',
- 'Watch Dogs: Legion' => 'watch-dogs-legion',
- 'Watercooling' => 'watercooling',
- 'WD (Western Digital)' => 'western-digital',
- 'Wearables' => 'wearables',
- 'Webcams' => 'webcams',
- 'Whey' => 'whey',
- 'Whirlpool' => 'whirlpool',
- 'Whiskas' => 'whiskas',
- 'Whisky' => 'whisky',
- 'Wiko' => 'wiko',
- 'Wilkinson Sword Hydro 5' => 'wilkinson-sword-hydro-5',
- 'Windows' => 'windows',
- 'WindScribe' => 'windscribe',
- 'Wolfenstein' => 'wolfenstein',
- 'Wolfenstein II: The New Colossus' => 'wolfenstein-ii-the-new-colossus',
- 'Xbox' => 'xbox',
- 'Xbox Game Pass' => 'xbox-game-pass',
- 'Xbox Live' => 'xbox-live',
- 'XCOM' => 'xcom',
- 'XCOM 2' => 'xcom-2',
- 'Xiaomi' => 'xiaomi',
- 'Xiaomi AirDots' => 'xiaomi-airdots',
- 'Xiaomi Black Shark' => 'xiaomi-black-shark',
- 'Xiaomi Black Shark 2' => 'xiaomi-black-shark-2',
- 'Xiaomi Mi6' => 'xiaomi-mi6',
- 'Xiaomi Mi8' => 'xiaomi-mi8',
- 'Xiaomi Mi8 Lite' => 'xiaomi-mi8-lite',
- 'Xiaomi Mi8 Pro' => 'xiaomi-mi8-pro',
- 'Xiaomi Mi8 SE' => 'xoaimi-mi8-se',
- 'Xiaomi Mi9' => 'xiaomi-mi9',
- 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite',
- 'Xiaomi Mi 9 Pro' => 'xiaomi-mi-9-pro',
- 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se',
- 'Xiaomi Mi 9T' => 'xiaomi-mi-9t',
- 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro',
- 'Xiaomi Mi 10' => 'xiaomi-mi-10',
- 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite',
- 'Xiaomi Mi 10 Pro' => 'xiaomi-mi-10-pro',
- 'Xiaomi Mi 10T' => 'xiaomi-mi-10t',
- 'Xiaomi Mi 10T Lite' => 'xiaomi-mi-10t-lite',
- 'Xiaomi Mi 10T Pro' => 'xiaomi-mi-10t-pro',
- 'Xiaomi Mi 11' => 'xiaomi-mi-11',
- 'Xiaomi Mi 11 Lite' => 'xiaomi-mi-11-lite',
- 'Xiaomi Mi A1' => 'xiaomi-mi-a1',
- 'Xiaomi Mi A2' => 'xiaomi-mi-a2',
- 'Xiaomi Mi A2 Lite' => 'xiaomi-mi-a2-lite',
- 'Xiaomi Mi Airdots Pro' => 'xiaomi-mi-airdots-pro',
- 'Xiaomi Mi Band' => 'xiaomi-mi-band',
- 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4',
- 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5',
- 'Xiaomi Mi Band 6' => 'xiaomi-mi-band-6',
- 'Xiaomi Mi Box' => 'xiaomi-mi-box',
- 'Xiaomi Mi Electric Scooter M365' => 'xiaomi-mi-electric-scooter-m365',
- 'Xiaomi Mi Max' => 'xiaomi-mi-max',
- 'Xiaomi Mi Mix' => 'xiaomi-mi-mix',
- 'Xiaomi Mi Mix 2' => 'xiaomi-mi-mix-2',
- 'Xiaomi Mi Note 10' => 'xiaomi-mi-note-10',
- 'Xiaomi Mi Note 10 Pro' => 'xiaomi-mi-note-10-pro',
- 'Xiaomi Mi Pad 3' => 'xiaomi-mi-pad-3',
- 'Xiaomi Mi Watch' => 'xiaomi-mi-watch',
- 'Xiaomi Pocophone F1' => 'xiaomi-pocophone-f1',
- 'Xiaomi Redmi 4A' => 'xiaomi-redmi-4a',
- 'Xiaomi Redmi 4X' => 'xiaomi-redmi-4x',
- 'Xiaomi Redmi 7' => 'xiaomi-redmi-7',
- 'Xiaomi Redmi 9' => 'xiaomi-redmi-9',
- 'Xiaomi Redmi AirDots' => 'xiaomi-redmi-airdots',
- 'Xiaomi Redmi Note 4' => 'xiaomi-redmi-note-4',
- 'Xiaomi Redmi Note 5' => 'xiaomi-redmi-note-5',
- 'Xiaomi Redmi Note 6' => 'xiaomi-redmi-note-6',
- 'Xiaomi Redmi Note 7' => 'xiaomi-redmi-note-7',
- 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8',
- 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro',
- 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9',
- 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro',
- 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s',
- 'Xiaomi Redmi Note 10' => 'xiaomi-redmi-note-10',
- 'Xiaomi Redmi Note 10 Pro' => 'xiaomi-redmi-10-pro',
- 'Xiaomi Smart Home' => 'xiaomi-smart-home',
- 'Yamaha' => 'yamaha',
- 'Yeelight' => 'xiaomi-yeelight',
- 'Yoshi&#039;s Crafted World' => 'yoshi-crafted-world',
- 'Zoos' => 'zoos',
- )
- ),
- 'order' => array(
- 'name' => 'Trier par',
- 'type' => 'list',
- 'title' => 'Ordre de tri des deals',
- 'values' => array(
- 'Du deal le plus Hot au moins Hot' => '-hot',
- 'Du deal le plus récent au plus ancien' => '-nouveaux',
- )
- )
- ),
- 'Surveillance Discussion' => array(
- 'url' => array(
- 'name' => 'URL de la discussion',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'URL discussion à surveiller: https://www.dealabs.com/discussions/titre-1234',
- 'exampleValue' => 'https://www.dealabs.com/discussions/jeux-steam-gratuits-gleam-woobox-etc-1071415',
- ),
+ 'Deals par groupe' => [
+ 'group' => [
+ 'name' => 'Groupe',
+ 'type' => 'list',
+ 'title' => 'Groupe dont il faut afficher les deals',
+ 'values' => [
+ 'Abattants WC' => 'abattants-wc',
+ 'Abonnement PlayStation Plus' => 'playstation-plus',
+ 'Abonnements cinéma' => 'abonnements-cinema',
+ 'Abonnements de train' => 'abonnements-de-train',
+ 'Abonnements internet' => 'abonnements-internet',
+ 'Abonnements presse' => 'abonnements-presse',
+ 'Accessoires aquarium' => 'accessoires-aquarium',
+ 'Accessoires auto' => 'auto',
+ 'Accessoires électroniques' => 'accessoires-gadgets',
+ 'Accessoires gamers PC' => 'accessoires-gamers-pc',
+ 'Accessoires gaming' => 'accessoires-gaming',
+ 'Accessoires iPhone' => 'accessoires-iphone',
+ 'Accessoires mode' => 'accessoires-mode',
+ 'Accessoires moto' => 'moto',
+ 'Accessoires Nintendo' => 'accessoires-nintendo',
+ 'Accessoires PC portables' => 'accessoires-pc-portables',
+ 'Accessoires photo' => 'accessoires-photo',
+ 'Accessoires PlayStation' => 'accessoires-playstation',
+ 'Accessoires pour barbecue' => 'accessoires-barbecue',
+ 'Accessoires studio photo' => 'accessoires-studio-photo',
+ 'Accessoires téléphonie' => 'accessoires-telephonie',
+ 'Accessoires TV' => 'accessoires-tv',
+ 'Accessoires vélo' => 'accessoires-velo',
+ 'Accessoires Xbox' => 'accessoires-xbox',
+ 'Acer' => 'acer',
+ 'Acer Predator' => 'acer-predator',
+ 'Achats / Ventes' => 'achats-ventes-echanges-estimations-dons',
+ 'Achats à l&#039;étranger' => 'limport-sites-avis-questions-langues',
+ 'Adaptateurs' => 'adaptateurs',
+ 'Adhérents Fnac' => 'adherents-fnac',
+ 'Adhésions &amp; Souscriptions' => 'adhesions-souscriptions-abonnements',
+ 'adidas' => 'adidas',
+ 'Adidas Gazelle' => 'adidas-gazelle',
+ 'adidas Stan Smith' => 'adidas-stan-smith',
+ 'adidas Superstar' => 'adidas-superstar',
+ 'adidas Ultraboost' => 'adidas-ultraboost',
+ 'adidas Yung-1' => 'adidas-yung-1',
+ 'adidas ZX Flux' => 'adidas-zx-flux',
+ 'Adoucissant' => 'adoucissant',
+ 'Agendas' => 'agendas',
+ 'Age of Empires' => 'age-of-empires',
+ 'Age of Empires: Definitive Edition' => 'age-of-empires-definitive-edition',
+ 'Alarmes' => 'alarmes',
+ 'Albums photo' => 'albums-photo',
+ 'Alcools' => 'alcools',
+ 'Alcools forts' => 'alcools-forts',
+ 'Alimentation' => 'epicerie',
+ 'Alimentation bébés' => 'alimentation-bebes',
+ 'Alimentation PC' => 'alimentation-pc',
+ 'Alimentation sportifs' => 'alimentation-sportifs',
+ 'Amazfit Bip' => 'xiaomi-amazfit-bip',
+ 'Amazon Echo' => 'amazon-echo',
+ 'Amazon Echo Dot' => 'amazon-echo-dot',
+ 'Amazon Echo Plus' => 'amazon-echo-plus',
+ 'Amazon Echo Show' => 'amazon-echo-show',
+ 'Amazon Echo Show 5' => 'amazon-echo-show-5',
+ 'Amazon Echo Spot' => 'amazon-echo-spot',
+ 'Amazon Fire TV' => 'amazon-fire-tv',
+ 'Amazon Kindle' => 'amazon-kindle',
+ 'Amazon Prime' => 'amazon-prime',
+ 'AMD Radeon' => 'amd-radeon',
+ 'AMD Ryzen' => 'amd-ryzen',
+ 'AMD Ryzen 5 5600X' => 'amd-ryzen-5-5600x',
+ 'AMD Ryzen 7 5800X' => 'amd-ryzen-7-5800x',
+ 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x',
+ 'AMD Ryzen 9 5950X' => 'amd-ryzen-9-5950x',
+ 'AMD Vega' => 'amd-vega',
+ 'amiibo' => 'amiibo',
+ 'Amplis (guitare/basse)' => 'amplis-guitare-basse',
+ 'Amplis audio' => 'amplis',
+ 'Ampoules' => 'ampoules',
+ 'Ampoules à LED' => 'ampoules-a-led',
+ 'Angleterre' => 'angleterre',
+ 'Animal Crossing' => 'animal-crossing',
+ 'Animal Crossing: New Horizons' => 'animal-crossing-new-horizons',
+ 'Animaux' => 'animaux',
+ 'Anker' => 'anker',
+ 'Anno 1800' => 'anno-1800',
+ 'Annonces officielles' => 'annonces-officielles',
+ 'Anthem' => 'anthem',
+ 'Anti-nuisibles' => 'anti-nuisibles',
+ 'Anti-puces' => 'anti-puces',
+ 'Antivirus' => 'antivirus',
+ 'Antivols' => 'antivols',
+ 'Apex Legends' => 'apex-legends',
+ 'Appareils à raclette' => 'appareils-raclette',
+ 'Appareils de musculation' => 'appareils-de-musculation',
+ 'Appareils photo' => 'appareils-photo',
+ 'Appareils photo Canon' => 'appareils-photo-canon',
+ 'Appareils photo compacts' => 'appareils-photo-compacts',
+ 'Appareils photo instantanés' => 'appareils-photo-instantanes',
+ 'Appareils photo Nikon' => 'appareils-photo-nikon',
+ 'Appareils photo Olympus' => 'appareils-photo-olympus',
+ 'Appareils photo Panasonic' => 'appareils-photo-panasonic',
+ 'Appareils photo Sony' => 'appareils-photo-sony',
+ 'Apple' => 'apple',
+ 'Apple AirPods' => 'apple-airpods',
+ 'Apple AirPods 2' => 'apple-airpods-2',
+ 'Apple AirPods Max' => 'apple-airpods-max',
+ 'Apple AirPods Pro' => 'apple-airpods-pro',
+ 'Apple HomePod' => 'apple-homepod',
+ 'Apple HomePod Mini' => 'apple-homepod-mini',
+ 'Apple TV' => 'apple-tv',
+ 'Apple TV+' => 'apple-tv-plus',
+ 'Apple Watch' => 'apple-watch',
+ 'Apple Watch 3' => 'apple-watch-3',
+ 'Apple Watch 4' => 'apple-watch-4',
+ 'Apple Watch 5' => 'apple-watch-5',
+ 'Apple Watch 6' => 'apple-watch-6',
+ 'Apple Watch SE' => 'apple-watch-se',
+ 'Applications' => 'applications',
+ 'Applications Android' => 'applications-android',
+ 'Applications iOS' => 'applications-ios',
+ 'Appliques murales' => 'appliques-murales',
+ 'Applis &amp; logiciels' => 'applis-logiciels',
+ 'Après-shampooings' => 'apres-shampooings',
+ 'Aquariums' => 'aquariums',
+ 'Arbres à chat' => 'arbres-a-chat',
+ 'Arduino' => 'arduino',
+ 'Armoires &amp; placards' => 'armoires-et-placards',
+ 'Articles de cuisine et d&#039;entretien' => 'articles-de-cuisine',
+ 'Arts culinaires' => 'arts-culinaires',
+ 'Arts de la table' => 'arts-de-la-table',
+ 'ASICS' => 'asics',
+ 'Asmodée' => 'asmodee',
+ 'Aspirateurs' => 'aspirateurs',
+ 'Aspirateurs balais' => 'aspirateurs-balais',
+ 'Aspirateurs Dreame' => 'aspirateurs-xiaomi',
+ 'Aspirateurs Dyson' => 'aspirateurs-dyson',
+ 'Aspirateurs robot' => 'aspirateurs-robot',
+ 'Aspirateurs Rowenta' => 'apsirateurs-rowenta',
+ 'Aspirateurs sans sac' => 'aspirateurs-sans-sac',
+ 'Assassin&#039;s Creed' => 'assassin-s-creed',
+ 'Assassin&#039;s Creed: Unity' => 'assassins-creed-unity',
+ 'Assassin&#039;s Creed: Valhalla' => 'assassin-s-creed-valhalla',
+ 'Assassin&#039;s Creed Odyssey' => 'assassin-s-creed-odyssey',
+ 'Assassin&#039;s Creed Origins' => 'assassin-s-creed-origins',
+ 'Assurances' => 'assurances',
+ 'Astuces pour économiser' => 'vos-astuces-pour-faire-des-economies',
+ 'Asus' => 'asus',
+ 'Asus ROG' => 'asus-rog',
+ 'Asus ROG Phone' => 'asus-rog-phone',
+ 'Asus ROG Phone 2' => 'asus-rog-phone-2',
+ 'ASUS Transformer' => 'asus-transformer',
+ 'Asus VivoBook' => 'asus-vivobook',
+ 'Asus ZenBook' => 'asus-zenbook',
+ 'Asus ZenFone 2' => 'asus-zenfone-2',
+ 'Asus ZenFone 3' => 'asus-zenfone-3',
+ 'Asus ZenFone 4' => 'asus-zenfone-4',
+ 'Asus ZenFone 6' => 'asus-zenfone-6',
+ 'Asus ZenFone GO' => 'asus-zenfone-go',
+ 'Asus ZenFone Zoom' => 'asus-zenfone-zoom',
+ 'Audio &amp; Hi-fi' => 'audio-et-hi-fi',
+ 'Aukey' => 'aukey',
+ 'Auto-Moto' => 'auto-moto',
+ 'Autoradios' => 'autoradios',
+ 'Azzaro Wanted' => 'azzaro-wanted',
+ 'Baby foot' => 'baby-foot',
+ 'BabyLiss' => 'babyliss',
+ 'Babyphones' => 'babyphones',
+ 'Badminton' => 'badminton',
+ 'Bagagerie' => 'bagagerie',
+ 'Baignoires pour bébé' => 'baignoires-pour-bebe',
+ 'Bains de bouche' => 'bains-de-bouche',
+ 'Balais &amp; serpillères' => 'balais-et-serpilleres',
+ 'Balances connectées' => 'balances-connectees',
+ 'Balançoires' => 'balancoires',
+ 'Ballet &amp; danse' => 'ballet-et-danse',
+ 'Ballons de football' => 'ballons-de-football',
+ 'Bandes dessinées' => 'bandes-dessinees',
+ 'Banques' => 'banques',
+ 'Barbecue' => 'barbecue',
+ 'Barbecue électrique' => 'barbecue-electrique',
+ 'Barbecue Weber' => 'barbecue-weber',
+ 'Barbie' => 'barbie',
+ 'Barres de son' => 'barres-de-son',
+ 'Barres de son Yamaha' => 'barres-de-son-yamaha',
+ 'Batman Arkham' => 'batman-arkham',
+ 'Batteries externes' => 'batteries-externes',
+ 'Batteries voiture' => 'batteries-voiture',
+ 'Batteurs' => 'batteurs-electriques',
+ 'Battlefield' => 'battlefield',
+ 'Battlefield 1' => 'battlefield-1',
+ 'Battlefield V' => 'battlefield-5',
+ 'Béaba' => 'beaba',
+ 'Beats by Dre' => 'beats-by-dre',
+ 'Beats Solo 3' => 'beats-solo-3',
+ 'Beats Studio 3' => 'beats-studio-3',
+ 'Beauté' => 'beaute',
+ 'Bébés' => 'bebes-nouveaux-nes',
+ 'BenQ' => 'benq',
+ 'Be quiet!' => 'be-quiet',
+ 'Beyerdynamic MMX 300' => 'beyerdynamic-mmx-300',
+ 'Biberons' => 'biberons',
+ 'Bien-être &amp; santé' => 'bien-etre-et-massages',
+ 'Bières' => 'bieres',
+ 'Bijoux' => 'bijoux',
+ 'Bikinis' => 'bikinis',
+ 'Bilans de santé &amp; dépistages' => 'bilans-de-sante-et-depistages',
+ 'Billets de bus' => 'billets-de-bus',
+ 'Billets de train' => 'billets-de-train',
+ 'BioShock' => 'bioshock',
+ 'BioShock Infinite' => 'bioshock-infinite',
+ 'Bitdefender' => 'bitdefender',
+ 'Blabla' => 'blabla-parlez-de-tout-et-de-rien',
+ 'Black &amp; Decker' => 'black-decker',
+ 'Blackberry' => 'blackberry',
+ 'Black Desert Online' => 'black-desert-online',
+ 'Blédina' => 'bledina',
+ 'Blenders' => 'blenders',
+ 'Bleu de Chanel' => 'bleu-de-chanel',
+ 'Blousons de moto' => 'blousons-de-moto',
+ 'Blu-Ray' => 'blu-ray',
+ 'Bodys pour bébé' => 'bodys-pour-bebe',
+ 'Boissons' => 'boissons',
+ 'Boîtes à outils' => 'boites-a-outils',
+ 'Boîtiers PC' => 'boitiers-pc',
+ 'Boîtiers TV' => 'boitiers-tv',
+ 'Bonbons' => 'bonbons',
+ 'Bonnets' => 'bonnets',
+ 'Bonnets de bain' => 'bonnets-de-bain',
+ 'Borderlands' => 'borderlands',
+ 'Borderlands 3' => 'borderlands-3',
+ 'Bosch' => 'bosch',
+ 'Bose' => 'bose',
+ 'Bose Headphones 700' => 'bose-headphones-700',
+ 'Bose Home Speaker 500' => 'bose-home-speaker-500',
+ 'Bose QuietComfort' => 'bose-quietcomfort',
+ 'Bose QuietComfort 35 II' => 'bose-quietcomfort-35ii',
+ 'Bose SoundLink' => 'bose-soundlink',
+ 'Bose SoundTouch' => 'bose-soundtouch',
+ 'Bottes' => 'bottes',
+ 'Bottes de moto' => 'bottes-de-moto',
+ 'Bottes de neige' => 'bottes-neige',
+ 'Bottes de pluie' => 'bottes-pluie',
+ 'Bottes femme' => 'bottes-femme',
+ 'Bottes homme' => 'bottes-homme',
+ 'Bougies &amp; bougeoirs' => 'bougies-et-bougeoirs',
+ 'Box beauté' => 'box-beaute',
+ 'Bracelet fitness' => 'bracelet-fitness',
+ 'Brandt' => 'brandt',
+ 'Braun Series 3' => 'braun-series-3',
+ 'Braun Series 5' => 'braun-series-5',
+ 'Braun Series 7' => 'braun-series-7',
+ 'Braun Series 9' => 'braun-series-9',
+ 'Braun Silk Épil' => 'braun-silk-epil',
+ 'Brita' => 'brita',
+ 'Brosses à dents' => 'brosses-a-dents',
+ 'Brosses à dents électriques' => 'brosses-a-dents-electriques',
+ 'Brosses à dents électriques Oral-B' => 'brosses-a-dents-electriques-oral-b',
+ 'Brosses pour animaux' => 'brosses-pour-animaux',
+ 'Cable management' => 'cable-management',
+ 'Câbles' => 'cables',
+ 'Câbles Ethernet' => 'cables-ethernet',
+ 'Câbles HDMI' => 'cables-hdmi',
+ 'Câbles Jack' => 'cables-jack',
+ 'Câbles USB' => 'cables-usb',
+ 'Cadeaux' => 'cadeaux',
+ 'Cadres' => 'cadres',
+ 'Cadres de vélo' => 'cadres-de-velo',
+ 'Café' => 'cafe',
+ 'Café en dosettes' => 'cafe-en-dosettes',
+ 'Café en grain' => 'cafe-en-grain',
+ 'Cafetières' => 'cafetieres',
+ 'Cafetières expresso' => 'cafetieres-expresso',
+ 'Cafetières filtre' => 'cafetieres-filtre',
+ 'Cafetières italiennes' => 'cafetieres-italiennes',
+ 'Cahiers' => 'cahiers',
+ 'Caissons de basses' => 'caissons-de-basses',
+ 'Calendrier de l&#039;Avent Lego' => 'calendriers-avent-lego',
+ 'Calendriers' => 'calendriers',
+ 'Calendriers de l&#039;Avent' => 'calendriers-avent',
+ 'Call of Duty' => 'call-of-duty',
+ 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war',
+ 'Call of Duty: Black Ops III' => 'call-of-duty-black-ops-3',
+ 'Call of Duty: Black Ops IIII' => 'call-of-duty-black-ops-4',
+ 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare',
+ 'Call of Duty: Modern Warfare' => 'call-of-duty-modern-warfare',
+ 'Call of Duty: WW2' => 'call-of-duty-ww2',
+ 'Calor' => 'calor',
+ 'Caméras' => 'cameras',
+ 'Caméras IP' => 'cameras-ip',
+ 'Caméras sportives' => 'cameras-sportives',
+ 'Camping' => 'camping',
+ 'Canapés' => 'canape',
+ 'Canon' => 'canon',
+ 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker',
+ 'Caravanes' => 'caravanes',
+ 'Carburant' => 'carburant',
+ 'Cartables' => 'cartables',
+ 'Cartes &amp; programmes de fidélité' => 'cartes-et-programmes-de-fidelite',
+ 'Cartes bancaires' => 'cartes-bancaires',
+ 'Cartes de développement' => 'cartes-developpement',
+ 'Cartes graphiques' => 'cartes-graphiques',
+ 'Cartes mémoire' => 'cartes-memoire',
+ 'Cartes mères' => 'cartes-meres',
+ 'Cartes postales' => 'cartes-postales',
+ 'Cartes prépayées Playstation Store' => 'playstation-store',
+ 'Cartes SD' => 'cartes-sd',
+ 'Cartes son' => 'cartes-son',
+ 'Casio' => 'casio',
+ 'Casque sans fil Xbox' => 'casque-sans-fil-xbox',
+ 'Casques Apple' => 'casques-apple',
+ 'Casques à réduction de bruit' => 'casque-reduction-active-bruit',
+ 'Casques audio' => 'casques-audio',
+ 'Casques Bose' => 'casques-bose',
+ 'Casques de moto' => 'casques-de-moto',
+ 'Casques de vélo' => 'casques-de-velo',
+ 'Casques Jabra' => 'casques-jabra',
+ 'Casques Samsung' => 'casques-samsung',
+ 'Casques sans fil' => 'casques-sans-fil',
+ 'Casques Sennheiser' => 'casques-sennheiser',
+ 'Casques Sony' => 'casques-sony',
+ 'Casques VR' => 'vr',
+ 'Casquettes' => 'casquettes',
+ 'Casseroles' => 'casseroles',
+ 'Catit' => 'catit',
+ 'Caves à vin' => 'caves-a-vin',
+ 'CD &amp; vinyles' => 'cd-vinyles',
+ 'CDAV' => 'cdav',
+ 'Ceintures' => 'ceintures',
+ 'Centrales vapeur' => 'centrales-vapeur',
+ 'Chaînes hi-fi' => 'chaines-hi-fi',
+ 'Chaises' => 'chaises',
+ 'Chaises hautes' => 'chaises-hautes',
+ 'Chambre' => 'chambre',
+ 'Champagne' => 'champagne',
+ 'Chapeaux' => 'chapeaux',
+ 'Chapeaux &amp; casquettes' => 'chapeaux-casquettes',
+ 'Chargeurs' => 'chargeurs',
+ 'Chargeurs allume-cigare' => 'chargeurs-allume-cigare',
+ 'Chargeurs de piles' => 'chargeurs-de-piles',
+ 'Chargeurs sans fil' => 'chargeurs-sans-fil',
+ 'Chasse' => 'chasse',
+ 'Chatières' => 'chatieres',
+ 'Chats' => 'chats',
+ 'Chauffage' => 'chauffage',
+ 'Chaussettes &amp; collants' => 'chaussettes-et-collants',
+ 'Chaussons' => 'chaussons',
+ 'Chaussures' => 'chaussures',
+ 'Chaussures adidas' => 'chaussures-adidas',
+ 'Chaussures de football' => 'chaussures-de-football',
+ 'Chaussures de randonnée' => 'chaussures-de-randonnee',
+ 'Chaussures de ski' => 'chaussures-de-ski',
+ 'Chaussures de ville' => 'chaussures-de-ville',
+ 'Chaussures New Balance' => 'chaussures-new-balance',
+ 'Chaussures Nike' => 'chaussures-nike',
+ 'Chaussures pour enfants' => 'chaussures-enfants',
+ 'Chaussures pour femme' => 'chaussures-femme',
+ 'Chaussures pour homme' => 'chaussures-homme',
+ 'Chaussures Puma' => 'chaussures-puma',
+ 'Chaussures Reebok' => 'chaussures-reebok',
+ 'Chaussures running' => 'chaussures-de-running',
+ 'Chelsea boots' => 'chelsea-boots',
+ 'Chemises' => 'chemises',
+ 'Chiens' => 'chiens',
+ 'Chocolat' => 'chocolat',
+ 'Chuck Taylor' => 'chuck-taylor',
+ 'Cinéma' => 'cinema',
+ 'Cire dépilatoire' => 'cire-depilatoire',
+ 'Cirque &amp; arts de rue' => 'cirque-et-arts-de-rue',
+ 'Citytrips' => 'citytrips',
+ 'Civilization' => 'civilization',
+ 'Civilization VI' => 'civilization-vi',
+ 'CK One' => 'ck-one',
+ 'Clarks' => 'clarks',
+ 'Claviers' => 'claviers',
+ 'Claviers (musique)' => 'claviers-musique',
+ 'Claviers gamer' => 'claviers-gamer',
+ 'Claviers Logitech' => 'claviers-logitech',
+ 'Claviers mécaniques' => 'claviers-mecaniques',
+ 'Claviers sans fil' => 'claviers-sans-fil',
+ 'Clés USB' => 'cles-usb',
+ 'Climatisation' => 'climatisation',
+ 'Climatiseurs' => 'climatiseurs',
+ 'Cocottes' => 'cocottes',
+ 'Coffrets de livres' => 'coffrets-de-livres',
+ 'Coffrets DVD' => 'coffrets-dvd',
+ 'Coffrets maquillage' => 'coffrets-maquillage',
+ 'Colliers &amp; laisses' => 'colliers-et-laisses',
+ 'Compléments alimentaires' => 'complements-alimentaires',
+ 'Composteurs' => 'composteurs',
+ 'Concerts' => 'concerts',
+ 'Concours' => 'concours',
+ 'Congélateurs' => 'congelateurs',
+ 'Connectiques' => 'connectiques',
+ 'Console Google Stadia' => 'google-stadia',
+ 'Console Nintendo Classic Mini' => 'nintendo-classic-mini',
+ 'Console Nintendo Classic Mini: SNES' => 'nintendo-classic-mini-snes',
+ 'Console Nintendo Switch' => 'nintendo-switch',
+ 'Console Nintendo Switch Lite' => 'nintendo-switch-lite',
+ 'Console PS4' => 'playstation-4',
+ 'Console PS4 Pro' => 'playstation-4-pro',
+ 'Console PS5' => 'playstation-5',
+ 'Consoles' => 'consoles',
+ 'Consoles &amp; jeux vidéo' => 'consoles-jeux-video',
+ 'Console Sega Mega Drive Mini' => 'sega-mega-drive-mini',
+ 'Console Xbox One S' => 'xbox-one-s',
+ 'Console Xbox One X' => 'xbox-one-x',
+ 'Console Xbox Series S' => 'xbox-series-s',
+ 'Console Xbox Series X' => 'xbox-series-x',
+ 'Consommables imprimantes' => 'consommables-imprimantes',
+ 'Converse' => 'converse',
+ 'Coques iPhone' => 'coques-iphone',
+ 'Corsair Void PRO' => 'corsair-void-pro',
+ 'Costumes' => 'costumes',
+ 'Costumes &amp; déguisements' => 'costumes-et-deguisements',
+ 'Couches' => 'couches',
+ 'Couettes' => 'couettes',
+ 'Coupes menstruelles' => 'coupes-menstruelles',
+ 'Cours &amp; formations' => 'cours-et-formations',
+ 'Courses hippiques' => 'courses-hippiques',
+ 'Couteaux de cuisine' => 'couteaux-de-cuisine',
+ 'Couture' => 'couture',
+ 'Couverts' => 'couverts',
+ 'Couverts pour bébés' => 'couverts-pour-bebes',
+ 'Covoiturage' => 'covoiturage',
+ 'Crash Team Racing Nitro-Fueled' => 'crash-team-racing-nitro-fueled',
+ 'Cravates' => 'cravates',
+ 'Crédits' => 'credits',
+ 'Crèmes hydratantes' => 'cremes-hydratantes',
+ 'Crèmes solaires' => 'cremes-solaires',
+ 'Croisières' => 'croisieres',
+ 'Croquettes pour chat' => 'croquettes-pour-chat',
+ 'Croquettes pour chien' => 'croquettes-pour-chien',
+ 'Cuiseurs à riz' => 'cuiseur-riz',
+ 'Cuisinières' => 'cuisinieres',
+ 'Culottes menstruelles' => 'culottes-menstruelles',
+ 'Culture &amp; divertissement' => 'culture-divertissement',
+ 'Cyberpunk 2077' => 'cyberpunk-2077',
+ 'Cyclisme' => 'cyclisme',
+ 'Cyclisme &amp; sports urbains' => 'cyclisme-sports-urbains',
+ 'Darksiders' => 'darksiders',
+ 'Dashcams' => 'dashcams',
+ 'DDR3' => 'ddr3',
+ 'DDR4' => 'ddr4',
+ 'Dead Rising' => 'dead-rising',
+ 'Death Stranding' => 'death-stranding',
+ 'Décoration' => 'decoration',
+ 'Décorations de Noël' => 'decoration-noel',
+ 'Deebot' => 'ecovacs-deebot',
+ 'Deezer' => 'deezer',
+ 'Dell' => 'dell',
+ 'Dell XPS' => 'dell-xps',
+ 'Delsey' => 'delsey',
+ 'Demandes de deals' => 'les-demandes-de-deals',
+ 'Denon' => 'denon',
+ 'Dentifrices' => 'dentifrices',
+ 'Déodorants' => 'deodorants',
+ 'Désherbants' => 'desherbants',
+ 'Déshumidificateurs' => 'deshumidificateurs',
+ 'Désinfectant' => 'desinfectants',
+ 'Désodorisants &amp; parfums d&#039;intérieur' => 'desodorisants-et-parfums-d-interieur',
+ 'Destiny' => 'destiny',
+ 'Destiny 2' => 'destiny-2',
+ 'Détecteurs de fumée' => 'detecteurs-de-fumee',
+ 'Detroit: Become Human' => 'detroit-become-human',
+ 'Deus Ex' => 'deus-ex',
+ 'Deus Ex: Mankind Divided' => 'deus-ex-mankind-divided',
+ 'Devil May Cry 5' => 'devil-may-cry-5',
+ 'Dishonored' => 'dishonored',
+ 'Dishonored 2' => 'dishonored-2',
+ 'Disney+' => 'disney-plus',
+ 'Disneyland Paris' => 'disneyland-paris',
+ 'Disques durs (internes)' => 'hdd',
+ 'Disques durs externes' => 'disques-durs-externes',
+ 'Divers' => 'divers',
+ 'DJI' => 'dji',
+ 'DJI Mavic Air 2' => 'dji-mavic-air-2',
+ 'DJI Mavic Mini' => 'dji-mavic-mini',
+ 'Dolce Gusto' => 'dolce-gusto',
+ 'Domotique' => 'smart-home',
+ 'Doom Eternal' => 'doom-eternal',
+ 'Dosettes Dolce Gusto' => 'dosettes-dolce-guste',
+ 'Dosettes Nespresso' => 'dosettes-nespresso',
+ 'Dosettes Senseo' => 'dosettes-senseo',
+ 'Dosettes Tassimo' => 'dosettes-tassimo',
+ 'Dr. Martens' => 'dr-martens',
+ 'Dragon Age' => 'dragon-age',
+ 'Dragon Ball' => 'dragon-ball',
+ 'Dragon Ball FighterZ' => 'dragon-ball-fighterz',
+ 'Dragon Ball Z: Kakarot' => 'dragon-ball-z-kakarot',
+ 'Dragon Quest' => 'dragon-quest',
+ 'Dragon Quest Builders' => 'dragon-quest-builders',
+ 'Dragon Quest Builders 2' => 'dragon-quest-builders-2',
+ 'Draisiennes' => 'draisiennes',
+ 'Draps &amp; housses' => 'draps-et-housses',
+ 'Dreame V10' => 'xiaomi-dreame-v10',
+ 'Dreame V11' => 'xiaomi-dreame-v11',
+ 'Drones' => 'drones',
+ 'Durex' => 'durex',
+ 'DVD' => 'dvd',
+ 'Dying Light' => 'dying-light',
+ 'Dying Light 2' => 'dying-light-2',
+ 'Dyson' => 'dyson',
+ 'Dyson V10' => 'dyson-v10',
+ 'Dyson V11' => 'dyson-v11',
+ 'Eastpak' => 'eastpak',
+ 'Ebooks' => 'ebooks',
+ 'Écharpes &amp; foulards' => 'echarpes-et-foulards',
+ 'Éclairage intelligent' => 'smart-light',
+ 'Écouteurs' => 'ecouteurs',
+ 'Écouteurs sans fil' => 'ecouteurs-sans-fil',
+ 'Écouteurs sport' => 'ecouteurs-sport',
+ 'Ecovacs' => 'ecovacs',
+ 'Ecovacs Deebot 900' => 'ecovacs-deebot-900',
+ 'Ecovacs Deebot OZMO 930' => 'ecovacs-deebot-ozmo-930',
+ 'Écrans' => 'ecrans',
+ 'Écrans 4K / UHD' => 'ecrans-4k-uhd',
+ 'Écrans 21&quot; et moins' => 'ecrans-21-pouces-et-moins',
+ 'Écrans 24&quot;' => 'ecrans-24-pouces',
+ 'Écrans 27&quot;' => 'ecrans-27-pouces',
+ 'Écrans 29&quot; et plus' => 'ecrans-29-pouces-et-plus',
+ 'Écrans Acer' => 'ecrans-acer',
+ 'Écrans Asus' => 'ecrans-asus',
+ 'Écrans BenQ' => 'ecrans-benq',
+ 'Écrans Dell' => 'ecrans-dell',
+ 'Écrans de projection' => 'ecrans-de-projection',
+ 'Écrans FreeSync' => 'ecrans-freesync',
+ 'Écrans gaming' => 'ecrans-gamer',
+ 'Écrans incurvés' => 'ecrans-incurves',
+ 'Écrans Philips' => 'ecrans-philips',
+ 'Écrans Samsung' => 'ecrans-samsung',
+ 'Électricité (matériel)' => 'electricite',
+ 'Electrolux' => 'electrolux',
+ 'Électroménager' => 'electromenager',
+ 'Embauchoirs' => 'embauchoirs',
+ 'Enceintes' => 'enceintes',
+ 'Enceintes Bluetooth' => 'enceintes-bluetooth',
+ 'Enceintes connectées' => 'enceintes-connectees',
+ 'Enceintes portables sans fil' => 'enceintes-portables-sans-fil',
+ 'Énergie' => 'energie',
+ 'Engrais' => 'engrais',
+ 'Épicerie &amp; courses' => 'epicerie-courses-supermarches',
+ 'Épilateurs à lumière pulsée' => 'epilateurs-a-lumiere-pulsee',
+ 'Épilateurs électriques' => 'epilateurs-electriques',
+ 'Épilation' => 'epilation',
+ 'Équipement motard' => 'equipement-motard',
+ 'Équipement running' => 'equipement-running',
+ 'Équipement sportif' => 'equipement-sportif',
+ 'Érotisme' => 'erotisme',
+ 'Escarpins' => 'escarpins',
+ 'Événements sportifs' => 'evenements-sportifs',
+ 'Expositions' => 'expositions',
+ 'Extracteurs de jus' => 'extracteurs-de-jus',
+ 'F1 2017' => 'f1-2017',
+ 'F1 2019' => 'f1-2019',
+ 'Facom' => 'facom',
+ 'Fallout' => 'fallout',
+ 'Fallout 4' => 'fallout-4',
+ 'Fallout 76' => 'fallout-76',
+ 'Famille &amp; enfants' => 'famille-enfants',
+ 'Far Cry' => 'far-cry',
+ 'Far Cry New Dawn' => 'far-cry-new-dawn',
+ 'Fards à paupières' => 'fards-a-paupieres',
+ 'Fast-foods' => 'fast-foods',
+ 'Fauteuils' => 'fauteuils',
+ 'Fauteuils gamer' => 'fauteuils-gaming',
+ 'Fe' => 'fe',
+ 'Fers à lisser / à friser' => 'fers-a-lisser-a-friser',
+ 'Fers à repasser' => 'fers-a-repasser',
+ 'Fers à souder' => 'fers-a-souder',
+ 'Festivals' => 'festivals',
+ 'Feutres' => 'feutres',
+ 'FIFA' => 'fifa',
+ 'FIFA 17' => 'fifa-17',
+ 'FIFA 18' => 'fifa-18',
+ 'FIFA 19' => 'fifa-19',
+ 'FIFA 20' => 'fifa-20',
+ 'FIFA 21' => 'fifa-21',
+ 'Figurines' => 'figurines',
+ 'Films &amp; Séries' => 'films',
+ 'Final Fantasy' => 'final-fantasy',
+ 'Final Fantasy XII' => 'final-fantasy-xii',
+ 'Finances &amp; Assurances' => 'finances-assurances',
+ 'fitbit' => 'fitbit',
+ 'Fitness &amp; yoga' => 'fitness-yoga',
+ 'Flash' => 'flash',
+ 'Fluval' => 'fluval',
+ 'Foires &amp; salons' => 'foires-et-salons',
+ 'Fonds de teint' => 'fonds-de-teint',
+ 'Football' => 'football',
+ 'Forfaits de ski' => 'forfaits-ski',
+ 'Forfaits mobiles' => 'forfaits-mobiles',
+ 'Forfaits mobiles et internet' => 'telecommunications',
+ 'For Honor' => 'for-honor',
+ 'Formations premiers secours' => 'formations-premiers-secours',
+ 'Formule 1' => 'formule-1',
+ 'Fortnite' => 'fortnite',
+ 'Fortnite: Pack Feu Obscur' => 'fortnite-pack-feu-obscur',
+ 'Forza' => 'forza',
+ 'Forza Horizon' => 'forza-horizon',
+ 'Forza Horizon 3' => 'forza-horizon-3',
+ 'Forza Horizon 4' => 'forza-horizon-4',
+ 'Forza Motorsport' => 'forza-motosport',
+ 'Forza Motorsport 7' => 'forza-motorsport-7',
+ 'Fossil' => 'fossil',
+ 'Fournitures scolaires' => 'fournitures-scolaires',
+ 'Fours' => 'fours',
+ 'Fours à poser' => 'fours-a-poser',
+ 'Fours encastrables' => 'fours-encastrables',
+ 'Friandises pour chat' => 'friandises-pour-chat',
+ 'Friandises pour chien' => 'friandises-pour-chien',
+ 'Friskies' => 'friskies',
+ 'Friteuses' => 'friteuses',
+ 'Friteuses sans huile' => 'friteuses-sans-huile',
+ 'Fruits &amp; légumes' => 'fruits-et-legumes',
+ 'Fujifilm' => 'fujifilm',
+ 'Funko Pop' => 'funko-pop',
+ 'FURminator' => 'furminator',
+ 'Futuroscope' => 'futuroscope',
+ 'Gamelles' => 'gamelles',
+ 'Game of Thrones' => 'game-of-thrones',
+ 'Gaming' => 'le-laboratoire-des-gamers',
+ 'Gants' => 'gants',
+ 'Gants moto' => 'gants-moto',
+ 'Garmin' => 'garmin',
+ 'Garmin Fenix' => 'garmin-fenix',
+ 'Garmin Forerunner' => 'garmin-forerunner',
+ 'Garmin Vivoactive' => 'garmin-vivoactive',
+ 'Garmin Vivomove' => 'garmin-vivomove',
+ 'Gâteaux &amp; biscuits' => 'gateaux-et-biscuits',
+ 'Gears 5' => 'gears-5',
+ 'Gel hydroalcoolique' => 'gel-hydroalcoolique',
+ 'Gels douche' => 'gels-douche',
+ 'Geox' => 'geox',
+ 'Ghost of Tsushima' => 'ghost-of-tsushima',
+ 'Gigoteuses' => 'gigoteuses',
+ 'Gillette Fusion' => 'gillette-fusion',
+ 'Gillette Mach3' => 'gillette-mach3',
+ 'Glaces' => 'glaces',
+ 'Glacières' => 'glacieres',
+ 'Glisse urbaine' => 'glisse-urbaine',
+ 'God of War' => 'god-of-war',
+ 'Google Chromecast' => 'google-chromecast',
+ 'Google Home' => 'google-home',
+ 'Google Home Max' => 'google-home-max',
+ 'Google Home Mini' => 'google-home-mini',
+ 'Google Nest Hub' => 'google-nest-hub',
+ 'Google Nest Mini' => 'google-nest-mini',
+ 'Google Pixel' => 'google-pixel',
+ 'Google Pixel 2' => 'google-pixel-2',
+ 'Google Pixel 2 XL' => 'google-pixel-2-xl',
+ 'Google Pixel 3' => 'google-pixel-3',
+ 'Google Pixel 3 XL' => 'google-pixel-3-xl',
+ 'Google Pixel 3a' => 'google-pixel-3a',
+ 'Google Pixel 4' => 'google-pixel-4',
+ 'Google Pixel 4 XL' => 'google-pixel-4xl',
+ 'Google Pixel 4a' => 'google-pixel-4a',
+ 'Google Pixel 5' => 'google-pixel-5',
+ 'Google Pixel XL' => 'google-pixel-xl',
+ 'GoPro' => 'gopro-hero',
+ 'GoPro Hero 9' => 'gopro-hero-9',
+ 'Gran Turismo' => 'gran-turismo',
+ 'Grille-pain' => 'grille-pain',
+ 'Grossesse &amp; maternité' => 'grossesse-maternite',
+ 'GTA' => 'gta',
+ 'GTA V' => 'gta-v',
+ 'GTX 1060' => 'nvidia-geforce-gtx-1060',
+ 'GTX 1070' => 'nvidia-geforce-gtx-1070',
+ 'GTX 1080' => 'nvidia-geforce-gtx-1080',
+ 'GTX 1080 Ti' => 'nvidia-geforce-gtx-1080-ti',
+ 'GTX 1650' => 'gtx-1650',
+ 'GTX 1660' => 'gtx-1660',
+ 'GTX 1660 Ti' => 'gtx-1660-ti',
+ 'Guerlain La Petite Robe Noire' => 'guerlain-petite-robe-noire',
+ 'Guirlandes lumineuses' => 'guirlandes-lumineuses',
+ 'Guitares' => 'guitares',
+ 'Gyropodes' => 'gyropodes',
+ 'Half Life' => 'half-life',
+ 'Half Life 2' => 'half-life-2',
+ 'Half Life Alyx' => 'half-life-alyx',
+ 'Halloween' => 'halloween',
+ 'Haltères &amp; poids' => 'halteres-et-poids',
+ 'Hama' => 'hama',
+ 'Hamacs' => 'hamacs',
+ 'Hand spinners' => 'hand-spinners',
+ 'Harnais pour chien' => 'harnais-pour-chien',
+ 'Harry Potter' => 'harry-potter',
+ 'Havaianas' => 'havaianas',
+ 'High-Tech' => 'high-tech',
+ 'High-tech &amp; informatique' => 'le-laboratoire-high-tech-informatique',
+ 'Hisense' => 'hisense',
+ 'Home Cinéma' => 'home-cinema',
+ 'Honor' => 'honor',
+ 'Honor 6X' => 'honor-6x',
+ 'Honor 8' => 'honor-8',
+ 'Honor 8 Pro' => 'honor-8-pro',
+ 'Honor 8X' => 'honor-8x',
+ 'Honor 8X Max' => 'honor-8x-max',
+ 'Honor 9' => 'honor-9',
+ 'Honor 10' => 'honor-10',
+ 'Honor 20' => 'honor-20',
+ 'Honor 20 Lite' => 'honor-20-lite',
+ 'Honor 20 Pro' => 'honor-20-pro',
+ 'Honor Band 5' => 'honor-band-5',
+ 'Honor MagicBook' => 'honor-magicbook',
+ 'Honor MagicWatch 2' => 'honor-magicwatch-2',
+ 'Honor View 20' => 'honor-view-20',
+ 'Horizon Zero Dawn' => 'horizon-zero-dawn',
+ 'Hôtels &amp; Hébergements' => 'hotels',
+ 'Hoverboards' => 'hoverboards',
+ 'HTC 10' => 'htc-10',
+ 'HTC Desire' => 'htc-desire',
+ 'HTC One M9' => 'htc-one-m9',
+ 'HTC U11' => 'htc-u11',
+ 'HTC U Play' => 'htc-u-play',
+ 'HTC U Ultra' => 'htc-u-ultra',
+ 'HTC Vive' => 'htc-vive',
+ 'Huawei' => 'huawei',
+ 'Huawei FreeBuds 3' => 'huawei-freebuds-3',
+ 'Huawei Mate 9' => 'huawei-mate-9',
+ 'Huawei Mate 10' => 'huawei-mate-10',
+ 'Huawei Mate 10 Pro' => 'huawei-mate-10-pro',
+ 'Huawei Mate 20' => 'huawei-mate-20',
+ 'Huawei Mate 20 Lite' => 'huawei-mate-20-lite',
+ 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro',
+ 'Huawei Mate 20 RS' => 'huawei-mate-20-rs',
+ 'Huawei Mate 30' => 'huawei-mate-30',
+ 'Huawei Mate 30 Lite' => 'huawei-mate-30-lite',
+ 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro',
+ 'Huawei P8 Lite' => 'huawei-p8-lite',
+ 'Huawei P9 Lite' => 'huawei-p9-lite',
+ 'Huawei P10' => 'huawei-p10',
+ 'Huawei P10 Lite' => 'huawei-p10-lite',
+ 'Huawei P10 Plus' => 'huawei-p10-plus',
+ 'Huawei P20' => 'huawei-p20',
+ 'Huawei P20 Lite' => 'huawei-p20-lite',
+ 'Huawei P20 Pro' => 'huawei-p20-pro',
+ 'Huawei P30' => 'huawei-p30',
+ 'Huawei P30 Lite' => 'huawei-p30-lite',
+ 'Huawei P30 Pro' => 'huawei-p30-pro',
+ 'Huawei P40' => 'huawei-p40',
+ 'Huawei P40 Lite' => 'huawei-p40-lite',
+ 'Huawei P40 Pro' => 'huawei-p40-pro',
+ 'Huawei Watch' => 'huawei-watch',
+ 'Huawei Watch 2' => 'huawei-watch-2',
+ 'Hubs' => 'hubs',
+ 'Hugo Boss Bottled' => 'hugo-boss-bottled',
+ 'Huile moteur' => 'huile-moteur',
+ 'Hygiène &amp; soins' => 'hygiene-soins',
+ 'Hygiène de la maison' => 'hygiene-de-la-maison',
+ 'Hygiène des bébés' => 'hygiene-des-bebes',
+ 'Hygiène intime' => 'hygiene-intime',
+ 'iMac' => 'mac-de-bureau',
+ 'iMac 2021' => 'imac-2021',
+ 'Image, son, photo' => 'le-laboratoire-audiovisuel',
+ 'Impressions photo' => 'impressions-photo',
+ 'Imprimantes' => 'imprimantes',
+ 'Imprimantes 3D' => 'imprimantes-3d',
+ 'Imprimantes Brother' => 'imprimantes-brother',
+ 'Imprimantes Canon' => 'imprimantes-canon',
+ 'Imprimantes Epson' => 'imprimantes-epson',
+ 'Imprimantes HP' => 'imprimantes-hp',
+ 'Imprimantes laser' => 'imprimantes-laser',
+ 'Imprimantes multifonctions' => 'imprimantes-multifonctions',
+ 'Informatique' => 'informatique',
+ 'Instax Mini' => 'instax-mini',
+ 'Instruments de musique' => 'instruments-de-musique',
+ 'Intel i5' => 'intel-i5',
+ 'Intel i7' => 'intel-i7',
+ 'Intel i9' => 'intel-i9',
+ 'iPad' => 'apple-ipad',
+ 'iPad 2019' => 'ipad-2019',
+ 'iPad 2020' => 'ipad-2020',
+ 'iPad Air' => 'ipad-air',
+ 'iPad Air 2019' => 'ipad-air-2019',
+ 'iPad Air 2020' => 'ipad-air-2020',
+ 'iPad Mini' => 'apple-ipad-mini',
+ 'iPad Pro' => 'apple-ipad-pro',
+ 'iPad Pro 11' => 'ipad-pro-11',
+ 'iPad Pro 12.9' => 'ipad-pro-12-9',
+ 'iPad Pro 2020' => 'ipad-pro-2020',
+ 'iPhone' => 'apple-iphone',
+ 'iPhone 6' => 'apple-iphone-6',
+ 'iPhone 7' => 'apple-iphone-7',
+ 'iPhone 7 Plus' => 'apple-iphone-7-plus',
+ 'iPhone 8' => 'apple-iphone-8',
+ 'iPhone 8 Plus' => 'apple-iphone-8-plus',
+ 'iPhone 11' => 'iphone-11',
+ 'iPhone 11 Pro' => 'iphone-11-pro',
+ 'iPhone 11 Pro Max' => 'iphone-11-pro-max',
+ 'iPhone 12' => 'iphone-12',
+ 'iPhone 12 Mini' => 'iphone-12-mini',
+ 'iPhone 12 Pro' => 'iphone-12-pro',
+ 'iPhone 12 Pro Max' => 'iphone-12-pro-max',
+ 'iPhone SE' => 'apple-iphone-se',
+ 'iPhone X' => 'apple-iphone-x',
+ 'iPhone XR' => 'apple-iphone-xr',
+ 'iPhone XS' => 'apple-iphone-xs',
+ 'iPhone XS Max' => 'apple-iphone-xs-max',
+ 'iRobot Roomba' => 'irobot-roomba',
+ 'Isolation' => 'isolation',
+ 'Jabra Elite 75t' => 'jabra-elite-75t',
+ 'Jabra Elite 85h' => 'jabra-elite-85h',
+ 'Jabra Elite 85t' => 'jabra-elite-85t',
+ 'Jabra Elite Active 65t' => 'jabra-elite-active-65t',
+ 'Jacuzzis' => 'jacuzzis',
+ 'Jardin' => 'jardin',
+ 'Jardin &amp; bricolage' => 'jardin-bricolage',
+ 'Jardinage' => 'entretien-du-jardin',
+ 'JBL' => 'jbl',
+ 'JBL Charge 4' => 'jbl-charge-4',
+ 'JBL Flip' => 'jbl-flip',
+ 'JBL GO' => 'jbl-go',
+ 'JBL Xtreme 2' => 'jbl-xtreme-2',
+ 'Jeans' => 'jeans',
+ 'Jets dentaires' => 'jets-dentaires',
+ 'Jeux &amp; jouets' => 'jeux-jouets',
+ 'Jeux &amp; sports de café' => 'jeux-sports-cafe-bar',
+ 'Jeux d&#039;adresse' => 'jeux-adresse',
+ 'Jeux d&#039;apprentissage' => 'jeux-d-apprentissage',
+ 'Jeux d&#039;eau' => 'jeux-jouets-eau',
+ 'Jeux d&#039;extérieur' => 'jeux-d-exterieur',
+ 'Jeux d&#039;imitation' => 'jeux-d-imitation',
+ 'Jeux de cartes et de plateau' => 'jeux-cartes-plateau-societe',
+ 'Jeux de construction' => 'jeux-de-construction',
+ 'Jeux de hasard &amp; paris' => 'jeux-et-paris',
+ 'Jeux de société' => 'jeux-de-societe',
+ 'Jeux Nintendo 3DS' => 'jeux-3ds',
+ 'Jeux Nintendo Switch' => 'jeux-nintendo-switch',
+ 'Jeux PC' => 'jeux-pc',
+ 'Jeux PC dématérialisés' => 'jeux-pc-dematerialises',
+ 'Jeux pour bébés' => 'jeux-pour-bebes',
+ 'Jeux PS4' => 'jeux-playstation-4',
+ 'Jeux PS4 dématérialisés' => 'jeux-ps4-dematerialises',
+ 'Jeux PS5' => 'jeux-playstation-5',
+ 'Jeux PS5 dématérialisés' => 'jeux-playstation-5-dematerialises',
+ 'Jeux PS Plus' => 'jeux-ps-plus',
+ 'Jeux vidéo' => 'jeux-video',
+ 'Jeux VR' => 'jeux-vr',
+ 'Jeux Wii U' => 'jeux-wii-u',
+ 'Jeux Xbox One' => 'jeux-xbox-one',
+ 'Jeux Xbox One dématérialisés' => 'jeux-xbox-dematerialises',
+ 'Jeux Xbox Series X' => 'jeux-xbox-series-x',
+ 'Jeux Xbox with Gold' => 'jeux-xbox-with-gold',
+ 'Jouets' => 'jouets',
+ 'Jouets pour chat' => 'jouets-pour-chat',
+ 'Jouets pour chien' => 'jouets-pour-chien',
+ 'Journaux numériques' => 'journaux-numeriques',
+ 'Journaux papier' => 'journaux-papier',
+ 'Joy-Con' => 'manettes-nintendo-switch-joy-con',
+ 'Jungle Speed' => 'jungle-speed',
+ 'Just Cause' => 'just-cause',
+ 'Just Cause 3' => 'just-cause-3',
+ 'Just Cause 4' => 'just-cause-4',
+ 'Kärcher' => 'karcher',
+ 'Kaspersky' => 'kaspersky',
+ 'Kinder' => 'kinder',
+ 'Kindle Oasis' => 'kindle-oasis',
+ 'Kindle Paperwhite' => 'kindle-paperwhite',
+ 'Kindle Voyage' => 'kindle-voyage',
+ 'Kingdom Hearts' => 'kingdom-hearts',
+ 'Kingdom Hearts 3' => 'kingdom-hearts-3',
+ 'Kingston HyperX Cloud II' => 'kingston-hyperx-cloud-2',
+ 'Kits premiers secours' => 'premiers-secours',
+ 'Kobo' => 'kobo',
+ 'Kobo Aura 2' => 'kobo-aura-2',
+ 'Kobo Aura H2o' => 'kobo-aura-h2o',
+ 'Kobo Aura One' => 'kobo-aura-one',
+ 'L&#039;annale du destin' => 'l-annale-du-destin',
+ 'L&#039;ombre de la guerre' => 'l-ombre-de-la-guerre',
+ 'L&#039;ombre du Mordor' => 'l-ombre-du-mordor',
+ 'Lacoste' => 'lacoste',
+ 'Lampadaires' => 'lampadaires',
+ 'Lampes' => 'lampes',
+ 'Lampes de table' => 'lampes-de-table',
+ 'Lampes solaires' => 'lampes-solaires',
+ 'Lancôme La Vie est Belle' => 'lancome-la-vie-est-belle',
+ 'Lapeyre' => 'lapeyre',
+ 'La Terre du Milieu' => 'la-terre-du-milieu',
+ 'Lavage auto' => 'lavage-auto',
+ 'Lavazza' => 'lavazza',
+ 'Lave-linge' => 'lave-linge',
+ 'Lave-linge frontal' => 'lave-linge-frontal',
+ 'Lave-linge séchant' => 'lave-linge-sechant',
+ 'Lave-linge top' => 'lave-linge-top',
+ 'Lave-vaisselle' => 'lave-vaisselle',
+ 'Lay-Z-Spa' => 'lay-z-spa',
+ 'Leasing voiture' => 'leasing-voiture',
+ 'Le bâton de la vérité' => 'le-baton-de-la-verite',
+ 'Lecteurs Blu-Ray' => 'lecteurs-blu-ray',
+ 'Lecteurs CD' => 'lecteurs-cd',
+ 'Lecteurs DVD' => 'lecteurs-dvd',
+ 'Lego' => 'lego',
+ 'Lego Architecture' => 'lego-architecture',
+ 'Lego Batman' => 'lego-batman',
+ 'Lego City' => 'lego-city',
+ 'Lego Creator' => 'lego-creator',
+ 'Lego Dimensions' => 'lego-dimensions',
+ 'Lego Duplo' => 'lego-duplo',
+ 'Lego Friends' => 'lego-friends',
+ 'Lego Harry Potter' => 'lego-harry-potter',
+ 'Lego Ideas' => 'lego-ideas',
+ 'Lego Marvel' => 'lego-marvel',
+ 'Lego Nexo Knights' => 'lego-nexo-knights',
+ 'Lego Ninjago' => 'lego-ninjago',
+ 'Lego Star Wars' => 'lego-star-wars',
+ 'Lego Technic' => 'lego-technic',
+ 'Lenovo' => 'lenovo',
+ 'Lenovo IdeaPad' => 'lenovo-ideapad',
+ 'Lenovo K6 Note' => 'lenovo-k6-note',
+ 'Lenovo P8' => 'lenovo-p8',
+ 'Lenovo Tab 3' => 'lenovo-tab-3',
+ 'Lenovo Tab 4' => 'lenovo-tab-4',
+ 'Lenovo ThinkPad' => 'lenovo-thinkpad',
+ 'Lenovo Yoga' => 'lenovo-yoga',
+ 'Lenovo Yoga Tab 3' => 'lenovo-yoga-tab-3',
+ 'Lentilles de contact' => 'lentilles-de-contact',
+ 'Le Seigneur des anneaux' => 'le-seigneur-des-anneaux',
+ 'Les Sims' => 'les-sims',
+ 'Les Sims 4' => 'les-sims-4',
+ 'Lessive' => 'lessive',
+ 'Levi&#039;s' => 'levi-s',
+ 'LG' => 'lg',
+ 'LG G4' => 'lg-g4',
+ 'LG G5' => 'lg-g5',
+ 'LG G6' => 'lg-g6',
+ 'LG OLED TV' => 'lg-oled-tv',
+ 'LG Q6' => 'lg-q6',
+ 'LG Q8' => 'lg-q8',
+ 'Life is Strange' => 'life-is-strange',
+ 'Linge de maison' => 'linge-de-maison',
+ 'Lingerie' => 'lingerie',
+ 'Lingettes désinfectantes' => 'lingettes-desinfectantes',
+ 'Lingettes pour bébés' => 'lingettes-pour-bebes',
+ 'Liseuses' => 'liseuses',
+ 'Litière pour chat' => 'litiere-pour-chat',
+ 'Lits' => 'lits',
+ 'Lits pour bébé' => 'lits-pour-bebe',
+ 'Lits pour enfants' => 'lits-pour-enfants',
+ 'Little Nightmares' => 'little-nightmares',
+ 'Livraison de repas' => 'service-de-livraison-de-repas',
+ 'Livres &amp; littérature' => 'livres-litterature',
+ 'Livres &amp; Magazines' => 'livres',
+ 'Livres audio' => 'livres-audio',
+ 'Livres photo' => 'livres-photo',
+ 'Location de voiture' => 'location-de-voiture',
+ 'Logiciels' => 'logiciels',
+ 'Logiciels de sécurité' => 'logiciels-de-securite',
+ 'Logiciels Microsoft' => 'logiciels-microsoft',
+ 'Logitech' => 'logitech',
+ 'Logitech G502' => 'logitech-g502',
+ 'Logitech G703' => 'logitech-g703',
+ 'Logitech G Pro X' => 'logitech-g-pro-x',
+ 'Logitech Harmony' => 'logitech-harmony',
+ 'Logitech MX Master' => 'logitech-mx-master',
+ 'Logitech MX Master 2S' => 'logitech-mx-master-2s',
+ 'Loisirs créatifs' => 'loisirs-creatifs',
+ 'Lolita Lempicka' => 'lolita-lempicka-premier-parfum',
+ 'Loup-Garou' => 'loup-garou',
+ 'Lubrifiants' => 'lubrifiants',
+ 'Luges' => 'luges',
+ 'Luigi&#039;s Mansion 3' => 'luigi-mansion-3',
+ 'Luminaires' => 'luminaires',
+ 'Lunettes de natation' => 'lunettes-de-natation',
+ 'Lunettes de soleil' => 'lunettes-de-soleil',
+ 'M&amp;M&#039;s' => 'metm-s',
+ 'MacBook' => 'macbook',
+ 'MacBook Air' => 'apple-macbook-air',
+ 'MacBook Pro' => 'apple-macbook-pro',
+ 'MacBook Pro 13' => 'macbook-pro-13',
+ 'MacBook Pro 15' => 'macbook-pro-15',
+ 'MacBook Pro 16' => 'macbook-pro-16',
+ 'Machines à café à dosettes' => 'machines-a-cafe-a-dosettes',
+ 'Machines à café en grain' => 'machines-a-cafe-en-grain',
+ 'Machines à coudre' => 'machines-a-coudre',
+ 'Machines à pain' => 'machines-a-pain',
+ 'Machines de sport' => 'machines-sport',
+ 'Machines Dolce Gusto' => 'machines-dolce-gusto',
+ 'Machines Nespresso' => 'machines-nespresso',
+ 'Machines Senseo' => 'machines-senseo',
+ 'Machines Tassimo' => 'machines-tassimo',
+ 'Mac mini' => 'mac-mini',
+ 'Madden NFL 20' => 'madden-nfl-20',
+ 'Magasins d&#039;usine' => 'magasins-usine',
+ 'Magazines' => 'magazines',
+ 'Maillots de bain' => 'maillots-de-bain',
+ 'Maillots de football' => 'maillots-de-football',
+ 'Maison &amp; Habitat' => 'maison-habitat',
+ 'Maisons de poupées' => 'maisons-poupees',
+ 'Makita' => 'makita',
+ 'Manettes' => 'manettes-accessoires-consoles',
+ 'Manettes DualSense' => 'manettes-playstation-5',
+ 'Manettes Nintendo Switch' => 'manettes-nintendo-switch',
+ 'Manettes Nintendo Switch Pro' => 'manettes-nintendo-switch-pro',
+ 'Manettes PlayStation 4' => 'manettes-playstation-4',
+ 'Manettes Xbox' => 'manettes-xbox',
+ 'Manettes Xbox One' => 'manettes-xbox-one',
+ 'Manettes Xbox One Elite' => 'manettes-xbox-one-elite',
+ 'Manettes Xbox Series X' => 'manettes-xbox-series-x',
+ 'Manix' => 'manix',
+ 'Manteaux' => 'manteaux',
+ 'Maquillage' => 'maquillage',
+ 'Marchands et leurs offres' => 'vos-avisdemandes-sur-les-marchands-et-leurs-offres',
+ 'Mario &amp; Sonic aux Jeux Olympiques de Tokyo 2020' => 'mario-sonic-jeux-olympiques-tokyo-2020',
+ 'Mario Kart' => 'mario-kart',
+ 'Marques' => 'marques',
+ 'Marteaux &amp; maillets' => 'marteaux-et-maillets',
+ 'Marvel&#039;s Avengers' => 'marvels-avengers',
+ 'Mascara' => 'mascara',
+ 'Masques cheveux' => 'masques-cheveux',
+ 'Masques de protection' => 'masques-de-protection-respiratoire',
+ 'Masques de ski' => 'masques-de-ski',
+ 'Mass Effect' => 'mass-effect',
+ 'Mass Effect: Andromeda' => 'mass-effect-andromeda',
+ 'Matchs de football' => 'matchs-de-football',
+ 'Matelas' => 'matelas',
+ 'Matelas gonflables' => 'matelas-gonflables',
+ 'Matériaux de construction' => 'materiaux-de-construction',
+ 'Matériel de ski' => 'materiel-de-ski',
+ 'Medion' => 'medion',
+ 'Metro' => 'metro',
+ 'Metro 2033' => 'metro-2033',
+ 'Metro Exodus' => 'metro-exodus',
+ 'Meubles pour aquarium' => 'meubles-pour-aquarium',
+ 'Meubles pour chat' => 'meubles-pour-chat',
+ 'Meubles salle de bain' => 'salle-de-bain',
+ 'Micro-casques gaming' => 'micro-casques-gaming',
+ 'Micro-ondes' => 'micro-ondes',
+ 'Microphones' => 'microphones',
+ 'Micro SD' => 'micro-sd',
+ 'Microsoft Flight Simulator' => 'microsoft-flight-simulator',
+ 'Microsoft Office' => 'microsoft-office',
+ 'Microsoft Surface Book' => 'microsoft-surface-book',
+ 'Microsoft Surface Pro 6' => 'microsoft-surface-pro-6',
+ 'Microsoft Surface Pro 7' => 'microsoft-surface-pro-7',
+ 'Miele' => 'miele',
+ 'Minecraft' => 'minecraft',
+ 'Mini PC' => 'mini-pc',
+ 'Mini réfrigérateurs' => 'mini-refrigerateurs',
+ 'Miroirs' => 'miroirs',
+ 'Mixeurs &amp; Blenders' => 'mixeurs-blenders',
+ 'Mixeurs plongeants' => 'mixeur-plongeant',
+ 'Mobilier' => 'mobilier',
+ 'Mobilier de bureau' => 'fournitures-de-bureau',
+ 'Mobilier de jardin' => 'mobilier-jardin',
+ 'Mobilier de salon' => 'mobilier-salon',
+ 'Mobvoi Ticwatch' => 'mobvoi-ticwatch',
+ 'Mode' => 'mode',
+ 'Mode &amp; accessoires' => 'mode-accessoires',
+ 'Mode &amp; beauté' => 'le-laboratoire-de-la-mode-beaute',
+ 'Mode enfants' => 'mode-enfants',
+ 'Mode femme' => 'mode-femme',
+ 'Mode homme' => 'mode-homme',
+ 'Modélisme' => 'modelisme',
+ 'Monopoly' => 'monopoly',
+ 'Montage PC' => 'montage-pc',
+ 'Montre connectée Amazfit' => 'montres-connectees-amazfit',
+ 'Montre connectée Garmin' => 'montres-connectees-garmin',
+ 'Montre connectée Honor' => 'montres-connectees-honor',
+ 'Montre connectée Samsung' => 'smartwatch-samsung',
+ 'Montres' => 'montres',
+ 'Montres connectées' => 'smartwatch',
+ 'Mortal Kombat' => 'mortal-kombat',
+ 'Mortal Kombat 11' => 'mortal-kombat-11',
+ 'Moto C Plus' => 'moto-c-plus',
+ 'Moto E4' => 'moto-e4',
+ 'Moto G5' => 'moto-g5',
+ 'Moto G5 Plus' => 'moto-g5-plus',
+ 'Moto G5S' => 'moto-g5s',
+ 'Moto G5S Plus' => 'moto-g5s-plus',
+ 'Moto G6' => 'moto-g6',
+ 'Moto G6 Play' => 'moto-g6-play',
+ 'Moto G6 Plus' => 'moto-g6-plus',
+ 'Moto G7 Play' => 'moto-g7-play',
+ 'Moto G7 Plus' => 'moto-g7-plus',
+ 'Moto G7 Power' => 'moto-g7-power',
+ 'Moto M' => 'moto-m',
+ 'Motorola' => 'motorola',
+ 'Moto Z2' => 'moto-z2',
+ 'Moto Z2 Force' => 'moto-z2-force',
+ 'Moto Z2 Play' => 'moto-z2-play',
+ 'Moto Z3' => 'moto-z3',
+ 'Moto Z3 Play' => 'moto-z3-play',
+ 'Moulinex' => 'moulinex',
+ 'Mousses à raser' => 'mousses-a-raser',
+ 'MSI' => 'msi',
+ 'Musées' => 'musees',
+ 'Musique' => 'musique',
+ 'NAS' => 'nas',
+ 'Natation' => 'natation',
+ 'Nature &amp; sports d&#039;hiver' => 'nature-sports-hiver',
+ 'Navigation' => 'navigation',
+ 'NBA 2K' => 'nba-2k',
+ 'NBA 2K20' => 'nba-2k20',
+ 'NERF' => 'nerf',
+ 'Nescafé' => 'nescafe',
+ 'Nespresso' => 'nespresso',
+ 'Nest Learning Thermostat' => 'nest-learning-thermostat',
+ 'Nest Protect' => 'nest-protect',
+ 'Netflix' => 'netflix',
+ 'Nettoyeurs haute-pression' => 'nettoyeurs-haute-pression',
+ 'Nettoyeurs haute pression Karcher' => 'nettoyeurs-haute-pression-karcher',
+ 'Nettoyeurs vapeur' => 'nettoyeurs-vapeur',
+ 'New Balance' => 'new-balance',
+ 'New Balance 574' => 'new-balance-574',
+ 'NHL 20' => 'nhl-20',
+ 'Nike' => 'nike',
+ 'Nike Air Force' => 'nike-air-force',
+ 'Nike Air Jordan' => 'nike-air-jordan',
+ 'Nike Air Max' => 'nike-air-max',
+ 'Nike Air Max 90' => 'nike-air-max-90',
+ 'Nike Air Max 200' => 'nike-air-max-200',
+ 'Nike Air Max 270' => 'nike-air-max-270',
+ 'Nike Air Max 720' => 'nike-air-max-720',
+ 'Nike Free' => 'nike-free',
+ 'Nike Huarache' => 'nike-huarache',
+ 'Nike Roshe Run' => 'nike-roshe-run',
+ 'Nikon' => 'nikon',
+ 'Nikon D3500' => 'nikon-d3500',
+ 'Ni no Kuni' => 'ni-no-kuni',
+ 'Ni No Kuni: Wrath of the White Witch' => 'ni-no-kuni-wrath-white-witch',
+ 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-2-revenant-kingdom',
+ 'Nintendo' => 'nintendo',
+ 'Nioh' => 'nioh',
+ 'Nivea' => 'nivea',
+ 'Nocciolata' => 'nocciolata',
+ 'Nokia' => 'nokia',
+ 'Nokia 5' => 'nokia-5',
+ 'Nokia 6' => 'nokia-6',
+ 'Nokia 8' => 'nokia-8',
+ 'Nokia 9 PureView' => 'nokia-9-pureview',
+ 'Nougats' => 'nougats',
+ 'Nourriture pour chat' => 'nourriture-pour-chat',
+ 'Nourriture pour chien' => 'nourriture-pour-chien',
+ 'Nourriture pour poissons' => 'nourriture-pour-poissons',
+ 'Nutella' => 'nutella',
+ 'Nvidia' => 'nvidia',
+ 'Nvidia GeForce' => 'nvidia-geforce',
+ 'Nvidia Shield' => 'nvidia-shield',
+ 'Objectifs' => 'objectifs',
+ 'Objets connectés' => 'objets-connectes',
+ 'Oculus Go' => 'oculus-go',
+ 'Oculus Rift' => 'oculus-rift',
+ 'Oiseaux' => 'oiseaux',
+ 'One Piece: Pirate Warriors' => 'one-piece-pirate-warriors',
+ 'OnePlus 5' => 'oneplus-5',
+ 'OnePlus 5T' => 'oneplus-5t',
+ 'OnePlus 6' => 'oneplus-6',
+ 'OnePlus 6T' => 'oneplus-6t',
+ 'OnePlus 7' => 'oneplus-7',
+ 'OnePlus 7 Pro' => 'oneplus-7-pro',
+ 'OnePlus 7T' => 'oneplus-7t',
+ 'OnePlus 7T Pro' => 'oneplus-7t-pro',
+ 'OnePlus 8' => 'oneplus-8',
+ 'OnePlus 8 Pro' => 'oneplus-8-pro',
+ 'OnePlus 8T' => 'oneplus-8t',
+ 'OnePlus 9' => 'oneplus-9',
+ 'OnePlus 9 Pro' => 'oneplus-9-pro',
+ 'OnePlus Nord' => 'oneplus-nord',
+ 'Onkyo' => 'onkyo',
+ 'Oppo Find X2 Lite' => 'oppo-find-x2-lite',
+ 'Oppo Find X2 Neo' => 'oppo-find-x2-neo',
+ 'Oppo Find X2 Pro' => 'oppo-find-x2-pro',
+ 'Oppo Reno' => 'oppo-reno',
+ 'Optique' => 'optique',
+ 'Oral-B' => 'oral-b',
+ 'Ordinateurs de bureau' => 'ordinateurs-de-bureau',
+ 'Ordinateurs tout-en-un' => 'pc-de-bureau-complets',
+ 'Oreillers' => 'oreillers',
+ 'Osram Smart+' => 'osram-smart-plus',
+ 'Outillage' => 'outillage',
+ 'Outils à main' => 'outils-main',
+ 'Outils de jardinage' => 'outils-de-jardinage',
+ 'Outils électriques' => 'outils-electriques',
+ 'Overwatch' => 'overwatch',
+ 'Packs clavier-souris' => 'packs-clavier-souris',
+ 'Packs consoles' => 'packs-consoles',
+ 'Paco Rabanne Invictus' => 'paco-rabanne-invictus',
+ 'Paco Rabanne Lady Million' => 'paco-rabanne-lady-million',
+ 'Paco Rabanne One Million' => 'paco-rabanne-one-million',
+ 'Pain &amp; pâtisseries' => 'pain-patisseries',
+ 'Pampers' => 'pampers',
+ 'Panasonic' => 'panasonic',
+ 'Panasonic Lumix' => 'panasonic-lumix',
+ 'Panier Plus' => 'panier-plus',
+ 'Pantalons' => 'pantalons',
+ 'Papeterie' => 'papeterie',
+ 'Papeterie et bureautique' => 'papeterie-bureautique',
+ 'Papier bureautique' => 'papier-bureautique',
+ 'Papier peint' => 'papier-peint',
+ 'Papier toilette' => 'papier-toilette',
+ 'Parapharmacie' => 'parapharmacie',
+ 'Parasols' => 'parasols',
+ 'Parc Astérix' => 'parc-asterix',
+ 'Parcs d&#039;attraction' => 'parcs-d-attraction',
+ 'Parfums' => 'parfums',
+ 'Parfums femme' => 'parfums-femme',
+ 'Parfums homme' => 'parfums-homme',
+ 'Parkas' => 'parkas',
+ 'Parrot' => 'parrot',
+ 'Partitions' => 'partitions',
+ 'Pâtée pour chat' => 'patee-pour-chat',
+ 'Pâtée pour chien' => 'patee-pour-chien',
+ 'Pâtes à tartiner' => 'pates-tartiner',
+ 'Pâtisserie' => 'patisserie',
+ 'PC Barebones' => 'pc-barebones',
+ 'PC gamer fixe' => 'pc-gamer-complets',
+ 'PC gaming' => 'pc-gaming',
+ 'PC hybrides' => 'hybrides',
+ 'PC Microsoft Surface' => 'pc-microsoft-surface',
+ 'PC portables' => 'pc-portables',
+ 'PC portables Acer' => 'pc-portables-acer',
+ 'PC portables ASUS' => 'pc-portables-asus',
+ 'PC portables Dell' => 'pc-portables-dell',
+ 'PC portables gaming' => 'portables-gamer',
+ 'PC portables Honor' => 'pc-portables-honor',
+ 'PC portables HP' => 'pc-portables-hp',
+ 'PC portables Lenovo' => 'pc-portables-lenovo',
+ 'PC portables Lenovo Legion' => 'lenovo-legion',
+ 'PC portables Xiaomi' => 'pc-portables-xiaomi',
+ 'Pêche' => 'peche',
+ 'Peignes &amp; brosses à cheveux' => 'peignes-et-brosses-a-cheveux',
+ 'Peignoirs' => 'peignoirs',
+ 'Peintures' => 'peintures',
+ 'Peluches' => 'peluches',
+ 'Perceuses' => 'perceuses',
+ 'Périphériques PC' => 'peripheriques-pc',
+ 'Persona 5' => 'persona-5',
+ 'Persona 5 Royal' => 'persona-5-royal',
+ 'PES' => 'pro-evolution-soccer',
+ 'Pèse-personnes' => 'pese-personnes',
+ 'Petites voitures' => 'petites-voitures',
+ 'Pharmacie &amp; parapharmacie' => 'pharmacie-parapharmacie',
+ 'Philips' => 'philips',
+ 'Philips Hue' => 'philips-hue',
+ 'Philips Hue E14' => 'philips-hue-e14',
+ 'Philips Hue E27' => 'philips-hue-e27',
+ 'Philips Hue Go' => 'philips-hue-go',
+ 'Philips Hue GU10' => 'philips-hue-gu10',
+ 'Philips Hue LightStrip' => 'philips-hue-lightstrip',
+ 'Philips Hue Play HDMI Sync Box' => 'philips-hue-play-hdmi-sync-box',
+ 'Philips Lumea' => 'philips-lumea',
+ 'Philips OneBlade' => 'philips-one-blade',
+ 'Philips Sonicare' => 'philips-sonicare',
+ 'Photo' => 'photo',
+ 'Pièces auto' => 'pieces-auto',
+ 'Pièces moto' => 'pieces-moto',
+ 'Pièces vélo' => 'pieces-velo',
+ 'Piles' => 'piles',
+ 'Piles rechargeables' => 'piles-rechargeables',
+ 'Pinceaux maquillage' => 'pinceaux-maquillage',
+ 'Pinces' => 'pinces',
+ 'Ping-pong' => 'ping-pong',
+ 'Pioneer' => 'pioneer',
+ 'Piscines' => 'piscines',
+ 'Pizza' => 'pizza',
+ 'Places de cinéma' => 'places-de-cinema',
+ 'Plafonniers' => 'plafonniers',
+ 'Plancha' => 'planchas',
+ 'Plantes &amp; semis' => 'plantes',
+ 'Plaques de cuisson' => 'plaques-de-cuisson',
+ 'Platines vinyle' => 'platines-vinyle',
+ 'Plats &amp; moules' => 'plats-et-moules',
+ 'PlayerUnknown&#039;s Battlegrounds' => 'playerunknown-s-battleground',
+ 'Playmobil' => 'playmobil',
+ 'PlayStation' => 'playstation',
+ 'Pneus' => 'pneus',
+ 'PocketBook' => 'pocketbook',
+ 'PocketBook Touch Lux 3' => 'pocketbook-touch-lux-3',
+ 'POCO F2 Pro' => 'poco-f2-pro',
+ 'POCO F3' => 'poco-f3',
+ 'POCO M3' => 'poco-m3',
+ 'POCO X3' => 'poco-x3',
+ 'POCO X3 Pro' => 'poco-x3-pro',
+ 'Poêles' => 'poeles',
+ 'Pokémon' => 'pokemon',
+ 'Pokémon: Let&#039;s Go' => 'pokemon-letsgo',
+ 'Pokémon Épée et Bouclier' => 'pokemon-epee-bouclier',
+ 'Pokémon Tournament' => 'pokemon-tournament',
+ 'Pokémon Ultra Sun / Moon' => 'pokemon-ultra-sun-moon',
+ 'Polaroid' => 'polaroid',
+ 'Polos' => 'polos',
+ 'Pompes à vélo' => 'pompes-velo',
+ 'Porte-bébé' => 'porte-bebe',
+ 'Portefeuilles' => 'portefeuilles',
+ 'Posters' => 'posters',
+ 'Potager' => 'potager',
+ 'Pots &amp; cache-pots' => 'pots-et-cache-pots',
+ 'Poubelles' => 'poubelles',
+ 'Poulaillers' => 'poulaillers',
+ 'Poupées' => 'poupees',
+ 'Poussettes' => 'poussettes-bebe',
+ 'Présentez-vous !' => 'mieux-se-connaitre-presentez-vous',
+ 'Préservatifs' => 'preservatifs',
+ 'Princesse Tam-Tam' => 'princesse-tam-tam',
+ 'Prises connectées' => 'prises-connectees',
+ 'Processeurs' => 'processeurs',
+ 'Produit pour lentilles' => 'produit-pour-lentilles',
+ 'Produits de massage' => 'produits-de-massage',
+ 'Produits frais' => 'produits-frais',
+ 'Produits reconditionnés' => 'reconditionne',
+ 'Produits vétérinaires' => 'produits-veterinaires',
+ 'Programme d&#039;Entraînement Cérébral du Dr. Kawashima' => 'dr-kawashima-brain-training',
+ 'Project Cars 2' => 'project-cars-2',
+ 'Protection de la maison' => 'protection-de-la-maison',
+ 'Protections intimes' => 'protections-intimes',
+ 'Protection solaire' => 'protection-solaire',
+ 'Puériculture' => 'puericulture',
+ 'Pulls' => 'pulls',
+ 'Puma' => 'puma',
+ 'Purificateurs d&#039;air' => 'purificateurs-d-air',
+ 'Purina' => 'purina',
+ 'Puzzles' => 'puzzles',
+ 'Pyjamas' => 'pyjamas',
+ 'Pyjamas &amp; chemises de nuit' => 'pyjamas-chemises-de-nuit',
+ 'Pyjamas pour bébés' => 'pyjamas-pour-bebes',
+ 'Qobuz' => 'qobuz',
+ 'Quiksilver' => 'quiksilver',
+ 'Radiateurs' => 'radiateurs',
+ 'Ralph Lauren' => 'ralph-lauren',
+ 'RAM' => 'ram',
+ 'Randonnée' => 'randonnee',
+ 'Raquettes de ping-pong' => 'raquettes-de-ping-pong',
+ 'Raquettes de tennis' => 'raquettes-de-tennis',
+ 'Rasage et épilation' => 'rasage-epilation',
+ 'Rasoirs Braun' => 'rasoirs-braun',
+ 'Rasoirs électriques' => 'rasoirs-electriques',
+ 'Rasoirs Gillette' => 'gillette',
+ 'Rasoirs manuels' => 'rasoirs-manuels',
+ 'Rasoirs Philips' => 'rasoirs-philips',
+ 'Rasoirs Wilkinson' => 'rasoirs-wilkinson-sword',
+ 'Raspberry Pi' => 'raspberry-pi',
+ 'Ray-Ban' => 'ray-ban',
+ 'Razer' => 'razer',
+ 'Razer DeathAdder' => 'razer-deathadder',
+ 'Realme 5 Pro' => 'realme-5-pro',
+ 'Realme X2 Pro' => 'realme-x2-pro',
+ 'Red Dead Redemption' => 'red-dead-redemption',
+ 'Red Dead Redemption 2' => 'red-dead-redemption-2',
+ 'Réductions étudiants &amp; jeunes' => 'reductions-etudiants-et-jeunes',
+ 'Reebok' => 'reebok',
+ 'Reebok Club C' => 'reebok-club-c',
+ 'Réfrigérateurs' => 'refrigerateurs',
+ 'Réfrigérateurs américains' => 'refrigerateurs-americains',
+ 'Refroidissement PC' => 'refroidissement-pc',
+ 'Réhausseurs' => 'rehausseurs',
+ 'Remington' => 'remington',
+ 'Repas de fête' => 'repas-fete-reveillon',
+ 'Repassage' => 'repassage',
+ 'Répéteurs' => 'repeteurs',
+ 'Réseau' => 'reseau',
+ 'Resident Evil' => 'resident-evil',
+ 'Resident Evil 3' => 'resident-evil-3',
+ 'Resident Evil 7' => 'resident-evil-7',
+ 'Restaurants' => 'restaurants',
+ 'Revêtements de sols' => 'revetements-de-sols',
+ 'Revêtements muraux' => 'revetements-muraux',
+ 'Rhum' => 'rhum',
+ 'Richelieus' => 'richelieus',
+ 'Ring Fit Adventure' => 'ring-fit-adventure',
+ 'Risk' => 'risk',
+ 'Robes &amp; jupes' => 'robes-et-jupes',
+ 'Roborock' => 'roborock',
+ 'Roborock S5 MAX' => 'roborock-s5-max',
+ 'Roborock S6' => 'roborock-s6',
+ 'Robots cuiseurs' => 'robots-cuiseurs',
+ 'Robots ménagers' => 'robots-menagers',
+ 'Robot tondeuse' => 'robot-tondeuse',
+ 'ROCCAT' => 'roccat',
+ 'Rollers' => 'rollers',
+ 'Rouges à lèvres' => 'rouges-a-levres',
+ 'Routeurs' => 'routeurs',
+ 'Rowenta' => 'rowenta',
+ 'Royal Canin' => 'royal-canin',
+ 'RTX 2060' => 'rtx-2060',
+ 'RTX 2070' => 'rtx-2070',
+ 'RTX 2080' => 'rtx-2080',
+ 'RTX 2080 Ti' => 'rtx-2080-ti',
+ 'RTX 3070' => 'rtx-3070',
+ 'RTX 3080' => 'rtx-3080',
+ 'RTX 3090' => 'rtx-3090',
+ 'RX 480' => 'rx-480',
+ 'RX 580' => 'rx-580',
+ 'RX 590' => 'radeon-rx-590',
+ 'RX Vega 56' => 'rx-vega-56',
+ 'RX Vega 64' => 'rx-vega-64',
+ 'Sacs à déjections' => 'sacs-a-dejections',
+ 'Sacs à dos' => 'sacs-a-dos',
+ 'Sacs à langer' => 'sacs-a-langer',
+ 'Sacs à main' => 'sacs-a-main',
+ 'Sacs bandoulière' => 'sacs-bandouliere',
+ 'Sacs de couchage' => 'sacs-de-couchage',
+ 'Sacs de randonnée' => 'sacs-de-randonnee',
+ 'Sacs de sport' => 'sacs-de-sport',
+ 'Sacs de voyage' => 'sacs-de-voyage',
+ 'Salle à manger' => 'salle-manger',
+ 'Samsonite' => 'samsonite',
+ 'Samsung' => 'samsung',
+ 'Samsung Galaxy A5' => 'samsung-galaxy-a5',
+ 'Samsung Galaxy A50' => 'samsung-galaxy-a50',
+ 'Samsung Galaxy A51' => 'samsung-galaxy-a51',
+ 'Samsung Galaxy A51 5G' => 'samsung-galaxy-a51-5g',
+ 'Samsung Galaxy A70' => 'samsung-galaxy-a70',
+ 'Samsung Galaxy A80' => 'samsung-galaxy-a80',
+ 'Samsung Galaxy Buds' => 'samsung-galaxy-buds',
+ 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus',
+ 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live',
+ 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro',
+ 'Samsung Galaxy Fold' => 'samsung-galaxy-fold',
+ 'Samsung Galaxy Note 8' => 'samsung-galaxy-note-8',
+ 'Samsung Galaxy Note 9' => 'samsung-galaxy-note-9',
+ 'Samsung Galaxy Note 10' => 'samsung-galaxy-note-10',
+ 'Samsung Galaxy Note 10 Lite' => 'samsung-galaxy-note-10-lite',
+ 'Samsung Galaxy Note 10 Plus' => 'samsung-galaxy-note-10-plus',
+ 'Samsung Galaxy Note20' => 'samsung-galaxy-note-20',
+ 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note-20-ultra',
+ 'Samsung Galaxy S7' => 'samsung-galaxy-s7',
+ 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge',
+ 'Samsung Galaxy S8' => 'samsung-galaxy-s8',
+ 'Samsung Galaxy S8+' => 'samsung-galaxy-s8plus',
+ 'Samsung Galaxy S9' => 'samsung-galaxy-s9',
+ 'Samsung Galaxy S9 Plus' => 'samsung-galaxy-s9-plus',
+ 'Samsung Galaxy S10' => 'samsung-galaxy-s10',
+ 'Samsung Galaxy S10 Lite' => 'samsung-galaxy-s10-lite',
+ 'Samsung Galaxy S10+' => 'samsung-galaxy-s10-plus',
+ 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e',
+ 'Samsung Galaxy S20' => 'samsung-galaxy-s20',
+ 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe',
+ 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra',
+ 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus',
+ 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g',
+ 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g',
+ 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g',
+ 'Samsung Galaxy Tab A' => 'samsung-galaxy-tab-a',
+ 'Samsung Galaxy Tab S2' => 'samsung-galaxy-tab-s2',
+ 'Samsung Galaxy Tab S3' => 'samsung-galaxy-tab-s3',
+ 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4',
+ 'Samsung Galaxy Tab S5e' => 'samsung-galaxy-tab-s5e',
+ 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6',
+ 'Samsung Galaxy Tab S7' => 'samsung-galaxy-tab-s7',
+ 'Samsung Galaxy Watch' => 'samsung-galaxy-watch',
+ 'Samsung Galaxy Watch3' => 'samsung-galaxy-watch-3',
+ 'Samsung Galaxy Watch Active 2' => 'samsung-galaxy-watch-active2',
+ 'Samsung Galaxy Z Flip' => 'galaxy-z-flip',
+ 'Samsung Gear' => 'samsung-gear',
+ 'Samsung Gear S3' => 'samsung-gear-s3',
+ 'Samsung Gear VR' => 'samsung-gear-vr',
+ 'Sandales' => 'sandales',
+ 'SanDisk' => 'sandisk',
+ 'Sanitaires et robinetterie' => 'sanitaires-robinetterie',
+ 'Santé &amp; Cosmétiques' => 'sante-et-cosmetiques',
+ 'Sapins de Noël' => 'sapins-noel',
+ 'Savons' => 'savons',
+ 'Scanners' => 'scanners',
+ 'Scanners A3' => 'scanners-a3',
+ 'Scanners A4' => 'scanners-a4',
+ 'Scies' => 'scies',
+ 'Scooters' => 'scooters',
+ 'Seagate' => 'seagate',
+ 'Sécateurs' => 'secateurs',
+ 'Sèche-cheveux' => 'seche-cheveux',
+ 'Sèche-linge' => 'seche-linge',
+ 'Seiko' => 'seiko',
+ 'Séjours' => 'sejours',
+ 'Sekiro: Shadows Die Twice' => 'sekiro',
+ 'Semis &amp; graines' => 'semis-et-graines',
+ 'Sennheiser' => 'sennheiser',
+ 'Senseo' => 'senseo',
+ 'Séries TV' => 'series-tv',
+ 'Service &amp; réparation auto-moto' => 'service-reparation-auto-moto',
+ 'Services' => 'services-divers',
+ 'Services auto' => 'services-auto',
+ 'Services de livraison' => 'services-livraisons',
+ 'Services moto' => 'services-moto',
+ 'Services photo' => 'services-photo',
+ 'Serviettes' => 'serviettes',
+ 'Serviettes hygiéniques' => 'serviettes-hygieniques',
+ 'Sextoys' => 'sextoys',
+ 'Shadow of the Colossus' => 'shadow-of-the-colossus',
+ 'Shadow of the Tomb Raider' => 'shadow-tomb-raider',
+ 'Shalimar' => 'shalimar',
+ 'Shampooings &amp; soins' => 'shampooings-et-soins',
+ 'Shenmue' => 'shenmue',
+ 'Shenmue I &amp; II' => 'shenmue-i-ii',
+ 'Shenmue III' => 'shenmue-iii',
+ 'Shorts' => 'shorts',
+ 'Shorts de bain' => 'shorts-de-bain',
+ 'Sièges auto' => 'sieges-auto',
+ 'Siemens' => 'siemens',
+ 'Skates &amp; longboards' => 'skates-et-longboards',
+ 'Skechers' => 'sketchers',
+ 'Ski' => 'ski',
+ 'Skyrim' => 'skyrim',
+ 'Slips &amp; boxers' => 'slips-et-boxers',
+ 'Smartphones' => 'smartphones',
+ 'Smartphones à moins de 100€' => 'smartphones-moins-de-100',
+ 'Smartphones à moins de 200€' => 'smartphones-moins-de-200',
+ 'Smartphones Android' => 'smartphones-android',
+ 'Smartphones Asus' => 'smartphones-asus',
+ 'Smartphones Google' => 'smartphones-google',
+ 'Smartphones Honor' => 'smartphones-honor',
+ 'Smartphones HTC' => 'smartphones-htc',
+ 'Smartphones Huawei' => 'smartphones-huawei',
+ 'Smartphones Lenovo Motorola' => 'smartphones-lenovo-motorola',
+ 'Smartphones LG' => 'smartphones-lg',
+ 'Smartphones Nokia' => 'smartphones-nokia',
+ 'Smartphones OnePlus' => 'smartphones-oneplus',
+ 'Smartphones Oppo' => 'smartphones-oppo',
+ 'Smartphones Realme' => 'smartphones-realme',
+ 'Smartphones Samsung' => 'smartphones-samsung',
+ 'Smartphones Sony' => 'smartphones-sony',
+ 'Smartphones Xiaomi' => 'smartphones-xiaomi',
+ 'Smartphones ZTE' => 'smartphones-zte',
+ 'Smart TV' => 'smart-tv',
+ 'Sneakers' => 'sneakers',
+ 'SodaStream' => 'sodastream',
+ 'Sofas gonflable' => 'sofas-gonflable',
+ 'Soin barbe et rasage' => 'soin-barbe-rasage',
+ 'Soin de la peau' => 'soin-peau',
+ 'Soin des cheveux' => 'soin-des-cheveux',
+ 'Soin des ongles' => 'soin-ongles',
+ 'Soins dentaires' => 'soins-dentaires',
+ 'Sonos' => 'sonos',
+ 'Sonos Beam' => 'sonos-beam',
+ 'Sonos Move' => 'sonos-move',
+ 'Sonos One' => 'sonos-one',
+ 'Sonos PLAY:1' => 'sonos-play-1',
+ 'Sonos PLAY:3' => 'sonos-play-3',
+ 'Sonos PLAY:5' => 'sonos-play-5',
+ 'Sonos PLAYBAR' => 'sonos-playbar',
+ 'Sony' => 'sony',
+ 'Sony PlayStation VR' => 'sony-playstation-vr',
+ 'Sony Pulse 3D sans fil' => 'casque-audio-sony-pulse-3d',
+ 'Sony WF-1000XM3' => 'sony-wf-1000xm3',
+ 'Sony WH-1000XM3' => 'sony-wh-1000xm3',
+ 'Sony WH-1000XM4' => 'sony-wh-1000xm4',
+ 'Sony Xperia XA1' => 'sony-xperia-xa1',
+ 'Sony Xperia X Compact' => 'sony-xperia-x-compact',
+ 'Sony Xperia XZ1' => 'sony-xperia-xz1',
+ 'Sony Xperia XZ1 Compact' => 'sony-xperia-xz1-compact',
+ 'Sony Xperia XZ Premium' => 'sony-xperia-xz-premium',
+ 'Sony Xperia Z3' => 'sony-xperia-z3',
+ 'Soulcalibur' => 'soulcalibur',
+ 'Souris' => 'souris',
+ 'Souris gamer' => 'souris-gamer',
+ 'Souris Logitech' => 'souris-logitech',
+ 'Souris sans fil' => 'souris-sans-fil',
+ 'Sous-vêtements' => 'sous-vetements',
+ 'Sous-vêtements de sport' => 'sous-vetements-de-sport',
+ 'South Park' => 'south-park',
+ 'Soutiens-gorge' => 'soutiens-gorge',
+ 'Spas' => 'spa',
+ 'Spectacles' => 'spectacles',
+ 'Spectacles &amp; Billetterie' => 'sorties',
+ 'Spectacles comiques' => 'spectacles-comiques',
+ 'Spectacles pour enfants' => 'spectacles-pour-enfants',
+ 'Sports &amp; plein air' => 'sports-plein-air',
+ 'Sports collectifs' => 'sports-collectifs',
+ 'Sports nautiques' => 'sports-nautiques',
+ 'Sportswear' => 'sportswear',
+ 'Spotify' => 'spotify',
+ 'SSD' => 'ssd',
+ 'Star Wars: Jedi Fallen Order' => 'star-wars-jedi-fallen-order',
+ 'Star Wars: Squadrons' => 'star-wars-squadrons',
+ 'Star Wars Battlefront' => 'star-wars-battlefront',
+ 'Stations météo' => 'stations-meteo',
+ 'Stickers muraux' => 'stickers-muraux',
+ 'Stihl' => 'stihl',
+ 'Stockage externe' => 'stockage',
+ 'Streaming' => 'streaming',
+ 'Streaming musical' => 'streaming-musical',
+ 'Streaming vidéo' => 'streaming-video',
+ 'Stylos' => 'stylos',
+ 'Sucettes' => 'sucettes',
+ 'Super Mario' => 'super-mario',
+ 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars',
+ 'Super Mario Maker 2' => 'super-mario-maker-2',
+ 'Super Mario Party' => 'super-mario-party',
+ 'Super Smash Bros. Ultimate' => 'super-smash-bros-ultimate',
+ 'Support GPS &amp; smartphone' => 'support-gps-et-smartphone',
+ 'Supports TV' => 'supports-tv',
+ 'Surface Pro 4' => 'surface-pro-4',
+ 'Surgelés' => 'surgeles',
+ 'Surveillance' => 'surveillance',
+ 'Suspensions' => 'suspensions',
+ 'Swatch' => 'swatch',
+ 'Switch réseau' => 'switch-reseau',
+ 'Systèmes d&#039;exploitation' => 'systemes-d-exploitation',
+ 'Systèmes multiroom' => 'systemes-multiroom',
+ 'T-shirts' => 't-shirts',
+ 'Tables' => 'tables',
+ 'Tables à langer' => 'tables-a-langer',
+ 'Tables à repasser' => 'tables-a-repasser',
+ 'Tables basses' => 'tables-basses',
+ 'Tables de camping' => 'tables-de-camping',
+ 'Tables de mixage' => 'tables-de-mixage',
+ 'Tables de ping-pong' => 'tables-ping-pong',
+ 'Tablettes' => 'tablettes',
+ 'Tablettes graphiques' => 'tablettes-graphiques',
+ 'Tablettes graphiques Huion' => 'huion',
+ 'Tablettes graphiques Wacom' => 'wacom',
+ 'Tablettes Huawei' => 'tablettes-huawei',
+ 'Tablettes Lenovo' => 'tablettes-lenovo',
+ 'Tablettes Microsoft Surface' => 'tablettes-microsoft-surface',
+ 'Tablettes Samsung' => 'tablettes-samsung',
+ 'Tablettes Xiaomi' => 'tablettes-xiaomi',
+ 'Tampons' => 'tampons',
+ 'Tapis' => 'tapis',
+ 'Tapis de souris' => 'tapis-de-souris',
+ 'Tassimo' => 'tassimo',
+ 'Taxis' => 'taxis',
+ 'Tefal' => 'tefal',
+ 'Tekken' => 'tekken',
+ 'Tekken 7' => 'tekken-7',
+ 'Télécommandes' => 'telecommandes',
+ 'Téléphones fixes' => 'telephones-fixes',
+ 'Téléphonie' => 'telephonie',
+ 'Téléviseurs' => 'televiseurs',
+ 'Tentes' => 'tentes',
+ 'Tentes Quechua' => 'tentes-quechua',
+ 'Têtes de brosse à dents de rechange' => 'tetes-de-brosse-a-dents-de-rechange',
+ 'Théâtre' => 'theatre',
+ 'The Last of Us' => 'the-last-of-us',
+ 'The Last of Us Part II' => 'the-last-of-us-part-2',
+ 'The Legend of Zelda' => 'the-legend-of-zelda',
+ 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild',
+ 'The Legend of Zelda: Link&#039;s Awakening' => 'legend-of-zelda-link-s-awakening',
+ 'The Legend of Zelda: Skyward Sword HD' => 'the-legend-of-zelda-skyward-sword-hd',
+ 'Thermomètres' => 'thermometres',
+ 'Thermomix' => 'thermomix',
+ 'Thermostats connectés' => 'thermostat-connecte',
+ 'Thés' => 'thes',
+ 'Thés glacés' => 'thes-glaces',
+ 'The Walking dead' => 'the-walking-dead',
+ 'The Witcher' => 'the-witcher',
+ 'The Witcher 3' => 'the-witcher-3',
+ 'Time&#039;s Up!' => 'time-s-up',
+ 'Tokyo Laundry' => 'tokyo-laundry',
+ 'Tomb Raider' => 'tomb-raider',
+ 'Tom Clancy&#039;s' => 'tom-clancy-s',
+ 'Tom Clancy&#039;s Ghost Recon: Wildlands' => 'tom-clancy-s-ghost-recon-wildlands',
+ 'Tom Clancy&#039;s Ghost Recon Breakpoint' => 'tom-clancy-s-ghost-recon-breakpoint',
+ 'Tom Clancy&#039;s The Division' => 'tom-clancy-s-the-division',
+ 'TomTom' => 'tomtom',
+ 'Tondeuses' => 'tondeuses',
+ 'Tondeuses à gazon' => 'tondeuses-a-gazon',
+ 'Toner' => 'toner',
+ 'Tongs' => 'tongs',
+ 'Torchons' => 'torchons',
+ 'Toshiba' => 'toshiba',
+ 'Total War' => 'total-war',
+ 'Total War: Warhammer' => 'total-war-warhammer',
+ 'Total War: Warhammer II' => 'total-war-warhammer-ii',
+ 'Tournevis' => 'tournevis-et-visseuses',
+ 'TP-Link' => 'tp-link',
+ 'Trains &amp; Bus' => 'trains-bus',
+ 'Trampolines' => 'trampolines',
+ 'Transats &amp; cosys' => 'transats-et-cosys',
+ 'Transport bébé' => 'poussettes',
+ 'Transport d&#039;animaux' => 'transport-d-animaux',
+ 'Transports en commun' => 'transports-en-commun',
+ 'Transports urbains' => 'transports-urbains',
+ 'Travaux &amp; matériaux' => 'travaux-materiaux',
+ 'Trépieds' => 'trepieds',
+ 'Trixie' => 'trixie',
+ 'Tronçonneuses' => 'tronconneuses',
+ 'Tropico' => 'tropico',
+ 'Tropico 6' => 'tropico-6',
+ 'Trottinettes' => 'trottinettes',
+ 'Trottinettes électriques' => 'trottinettes-electriques',
+ 'Trottinettes électriques en libre-service' => 'location-trottinettes-electriques',
+ 'Trottinettes Xiaomi' => 'trottinettes-xiaomi',
+ 'TV &amp; Vidéo' => 'tv-video',
+ 'TV 4K' => 'tv-4k',
+ 'TV 40&#039;&#039; à 64&#039;&#039;' => 'tv-40-pouces-a-64-pouces',
+ 'TV 65&#039;&#039; et plus' => 'tv-65-pouces-et-plus',
+ 'TV Hisense' => 'tv-hisense',
+ 'TV LG' => 'tv-lg',
+ 'TV OLED' => 'tv-oled',
+ 'TV Panasonic' => 'tv-panasonic',
+ 'TV Philips' => 'tv-philips',
+ 'TV Samsung' => 'tv-samsung',
+ 'TV Samsung QLED' => 'tv-samsung-qled',
+ 'TV Samsung The Frame' => 'tv-samsung-the-frame',
+ 'TV Sony' => 'tv-sony',
+ 'TV TCL' => 'tv-tcl',
+ 'TV Toshiba' => 'tv-toshiba',
+ 'TV Xiaomi' => 'tv-xiaomi',
+ 'UE Boom 2' => 'ue-boom-2',
+ 'UE Boom 3' => 'ue-boom-3',
+ 'UE Megaboom' => 'ue-megaboom',
+ 'UE Megaboom 3' => 'ue-megaboom-3',
+ 'UE Wonderboom' => 'ue-wonderboom',
+ 'Ultraportables' => 'ultraportables',
+ 'Uncharted' => 'uncharted',
+ 'Uncharted 4' => 'uncharted-4',
+ 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy',
+ 'Under Armour' => 'under-armour',
+ 'Until Dawn' => 'until-dawn',
+ 'Ustensiles de cuisine' => 'ustensiles-de-cuisine',
+ 'Ustensiles de cuisson' => 'ustensiles-de-cuisson',
+ 'Vacances et séjours' => 'vacances-sejours',
+ 'Vaisselle' => 'vaisselle',
+ 'Valises' => 'valises',
+ 'Valises cabine' => 'valises-cabine',
+ 'Valises rigides' => 'valises-rigides',
+ 'Vans Old Skool' => 'vans-old-skool',
+ 'Variétés &amp; revues' => 'varietes-et-revues',
+ 'Vases' => 'vases',
+ 'Veet' => 'veet',
+ 'Veilleuses' => 'veilleuses',
+ 'Vélos' => 'velos',
+ 'Vélos d&#039;appartement' => 'velos-d-appartement',
+ 'Vélos électriques' => 'velos-electriques',
+ 'Ventilateurs' => 'ventilateurs',
+ 'Ventirad' => 'ventirad',
+ 'Vernis à ongles' => 'vernis-a-ongles',
+ 'Verres' => 'verres',
+ 'Vestes' => 'vestes',
+ 'Vestes polaires' => 'vestes-polaires',
+ 'Vêtements d&#039;été' => 'vetements-d-ete',
+ 'Vêtements d&#039;hiver' => 'vetements-d-hiver',
+ 'Vêtements de grossesse' => 'vetements-de-grossesse',
+ 'Vêtements de montagne' => 'vetements-techniques',
+ 'Vêtements de running' => 'vetements-de-running',
+ 'Vêtements de ski' => 'vetements-de-ski',
+ 'Vêtements de sport' => 'vetements-de-sport',
+ 'Vêtements pour bébé' => 'vetements-pour-bebe',
+ 'Vidéoprojecteurs' => 'projecteurs',
+ 'Vidéoprojecteurs 3D' => 'videoprojecteurs-3d',
+ 'Vidéoprojecteurs Acer' => 'videoprojecteurs-acer',
+ 'Vidéoprojecteurs BenQ' => 'videoprojecteurs-benq',
+ 'Vidéoprojecteurs Epson' => 'videoprojecteurs-epson',
+ 'Vidéoprojecteurs HD' => 'videoprojecteurs-hd',
+ 'Vidéoprojecteurs LG' => 'videoprojecteurs-lg',
+ 'Vidéoprojecteurs Optoma' => 'videoprojecteurs-optoma',
+ 'Vins' => 'vins',
+ 'Visites &amp; patrimoine' => 'visites-et-patrimoine',
+ 'Visseuses' => 'visseuses',
+ 'VOD' => 'vod',
+ 'Voitures &amp; motos' => 'voitures-motos',
+ 'Voitures télécommandées' => 'voitures-telecommandees',
+ 'Volants' => 'volants-de-course',
+ 'Vols' => 'billets-d-avion',
+ 'Voyages' => 'voyages',
+ 'Voyages &amp; loisirs' => 'le-laboratoire-des-voyages-loisirs',
+ 'VPN' => 'vpn',
+ 'VTC' => 'vtc',
+ 'VTT' => 'vtt',
+ 'Wacom Cintiq' => 'cintiq',
+ 'Watch Dogs' => 'watch-dogs',
+ 'Watch Dogs 2' => 'watch-dogs-2',
+ 'Watch Dogs: Legion' => 'watch-dogs-legion',
+ 'Watercooling' => 'watercooling',
+ 'WD (Western Digital)' => 'western-digital',
+ 'Wearables' => 'wearables',
+ 'Webcams' => 'webcams',
+ 'Whey' => 'whey',
+ 'Whirlpool' => 'whirlpool',
+ 'Whiskas' => 'whiskas',
+ 'Whisky' => 'whisky',
+ 'Wiko' => 'wiko',
+ 'Wilkinson Sword Hydro 5' => 'wilkinson-sword-hydro-5',
+ 'Windows' => 'windows',
+ 'WindScribe' => 'windscribe',
+ 'Wolfenstein' => 'wolfenstein',
+ 'Wolfenstein II: The New Colossus' => 'wolfenstein-ii-the-new-colossus',
+ 'Xbox' => 'xbox',
+ 'Xbox Game Pass' => 'xbox-game-pass',
+ 'Xbox Live' => 'xbox-live',
+ 'XCOM' => 'xcom',
+ 'XCOM 2' => 'xcom-2',
+ 'Xiaomi' => 'xiaomi',
+ 'Xiaomi AirDots' => 'xiaomi-airdots',
+ 'Xiaomi Black Shark' => 'xiaomi-black-shark',
+ 'Xiaomi Black Shark 2' => 'xiaomi-black-shark-2',
+ 'Xiaomi Mi6' => 'xiaomi-mi6',
+ 'Xiaomi Mi8' => 'xiaomi-mi8',
+ 'Xiaomi Mi8 Lite' => 'xiaomi-mi8-lite',
+ 'Xiaomi Mi8 Pro' => 'xiaomi-mi8-pro',
+ 'Xiaomi Mi8 SE' => 'xoaimi-mi8-se',
+ 'Xiaomi Mi9' => 'xiaomi-mi9',
+ 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite',
+ 'Xiaomi Mi 9 Pro' => 'xiaomi-mi-9-pro',
+ 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se',
+ 'Xiaomi Mi 9T' => 'xiaomi-mi-9t',
+ 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro',
+ 'Xiaomi Mi 10' => 'xiaomi-mi-10',
+ 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite',
+ 'Xiaomi Mi 10 Pro' => 'xiaomi-mi-10-pro',
+ 'Xiaomi Mi 10T' => 'xiaomi-mi-10t',
+ 'Xiaomi Mi 10T Lite' => 'xiaomi-mi-10t-lite',
+ 'Xiaomi Mi 10T Pro' => 'xiaomi-mi-10t-pro',
+ 'Xiaomi Mi 11' => 'xiaomi-mi-11',
+ 'Xiaomi Mi 11 Lite' => 'xiaomi-mi-11-lite',
+ 'Xiaomi Mi A1' => 'xiaomi-mi-a1',
+ 'Xiaomi Mi A2' => 'xiaomi-mi-a2',
+ 'Xiaomi Mi A2 Lite' => 'xiaomi-mi-a2-lite',
+ 'Xiaomi Mi Airdots Pro' => 'xiaomi-mi-airdots-pro',
+ 'Xiaomi Mi Band' => 'xiaomi-mi-band',
+ 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4',
+ 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5',
+ 'Xiaomi Mi Band 6' => 'xiaomi-mi-band-6',
+ 'Xiaomi Mi Box' => 'xiaomi-mi-box',
+ 'Xiaomi Mi Electric Scooter M365' => 'xiaomi-mi-electric-scooter-m365',
+ 'Xiaomi Mi Max' => 'xiaomi-mi-max',
+ 'Xiaomi Mi Mix' => 'xiaomi-mi-mix',
+ 'Xiaomi Mi Mix 2' => 'xiaomi-mi-mix-2',
+ 'Xiaomi Mi Note 10' => 'xiaomi-mi-note-10',
+ 'Xiaomi Mi Note 10 Pro' => 'xiaomi-mi-note-10-pro',
+ 'Xiaomi Mi Pad 3' => 'xiaomi-mi-pad-3',
+ 'Xiaomi Mi Watch' => 'xiaomi-mi-watch',
+ 'Xiaomi Pocophone F1' => 'xiaomi-pocophone-f1',
+ 'Xiaomi Redmi 4A' => 'xiaomi-redmi-4a',
+ 'Xiaomi Redmi 4X' => 'xiaomi-redmi-4x',
+ 'Xiaomi Redmi 7' => 'xiaomi-redmi-7',
+ 'Xiaomi Redmi 9' => 'xiaomi-redmi-9',
+ 'Xiaomi Redmi AirDots' => 'xiaomi-redmi-airdots',
+ 'Xiaomi Redmi Note 4' => 'xiaomi-redmi-note-4',
+ 'Xiaomi Redmi Note 5' => 'xiaomi-redmi-note-5',
+ 'Xiaomi Redmi Note 6' => 'xiaomi-redmi-note-6',
+ 'Xiaomi Redmi Note 7' => 'xiaomi-redmi-note-7',
+ 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8',
+ 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro',
+ 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9',
+ 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro',
+ 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s',
+ 'Xiaomi Redmi Note 10' => 'xiaomi-redmi-note-10',
+ 'Xiaomi Redmi Note 10 Pro' => 'xiaomi-redmi-10-pro',
+ 'Xiaomi Smart Home' => 'xiaomi-smart-home',
+ 'Yamaha' => 'yamaha',
+ 'Yeelight' => 'xiaomi-yeelight',
+ 'Yoshi&#039;s Crafted World' => 'yoshi-crafted-world',
+ 'Zoos' => 'zoos',
+ ]
+ ],
+ 'order' => [
+ 'name' => 'Trier par',
+ 'type' => 'list',
+ 'title' => 'Ordre de tri des deals',
+ 'values' => [
+ 'Du deal le plus Hot au moins Hot' => '-hot',
+ 'Du deal le plus récent au plus ancien' => '-nouveaux',
+ ]
+ ]
+ ],
+ 'Surveillance Discussion' => [
+ 'url' => [
+ 'name' => 'URL de la discussion',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'URL discussion à surveiller: https://www.dealabs.com/discussions/titre-1234',
+ 'exampleValue' => 'https://www.dealabs.com/discussions/jeux-steam-gratuits-gleam-woobox-etc-1071415',
+ ],
- 'only_with_url' => array(
- 'name' => 'Exclure les commentaires sans URL',
- 'type' => 'checkbox',
- 'title' => 'Exclure les commentaires ne contenant pas d\'URL dans le flux',
- 'defaultValue' => false,
- )
+ 'only_with_url' => [
+ 'name' => 'Exclure les commentaires sans URL',
+ 'type' => 'checkbox',
+ 'title' => 'Exclure les commentaires ne contenant pas d\'URL dans le flux',
+ 'defaultValue' => false,
+ ]
- )
-
- );
-
- public $lang = array(
- 'bridge-uri' => SELF::URI,
- 'bridge-name' => SELF::NAME,
- 'context-keyword' => 'Recherche par Mot(s) clé(s)',
- 'context-group' => 'Deals par groupe',
- 'context-talk' => 'Surveillance Discussion',
- 'uri-group' => 'groupe/',
- 'request-error' => 'Impossible de joindre Dealabs',
- 'thread-error' => 'Impossible de déterminer l\'ID de la discussion. Vérifiez l\'URL que vous avez entré',
- 'no-results' => 'Il n&#039;y a rien à afficher pour le moment :(',
- 'relative-date-indicator' => array(
- 'il y a',
- ),
- 'price' => 'Prix',
- 'shipping' => 'Livraison',
- 'origin' => 'Origine',
- 'discount' => 'Réduction',
- 'title-keyword' => 'Recherche',
- 'title-group' => 'Groupe',
- 'title-talk' => 'Surveillance Discussion',
- 'local-months' => array(
- 'janvier',
- 'février',
- 'mars',
- 'avril',
- 'mai',
- 'juin',
- 'juillet',
- 'août',
- 'septembre',
- 'octobre',
- 'novembre',
- 'décembre'
- ),
- 'local-time-relative' => array(
- 'il y a ',
- 'min',
- 'h',
- 'jour',
- 'jours',
- 'mois',
- 'ans',
- 'et '
- ),
- 'date-prefixes' => array(
- 'Actualisé ',
- ),
- 'relative-date-alt-prefixes' => array(
- 'Actualisé ',
- ),
- 'relative-date-ignore-suffix' => array(
- ),
-
- 'localdeal' => array(
- 'Local',
- 'Pays d\'expédition'
- ),
- );
+ ]
+ ];
+ public $lang = [
+ 'bridge-uri' => self::URI,
+ 'bridge-name' => self::NAME,
+ 'context-keyword' => 'Recherche par Mot(s) clé(s)',
+ 'context-group' => 'Deals par groupe',
+ 'context-talk' => 'Surveillance Discussion',
+ 'uri-group' => 'groupe/',
+ 'request-error' => 'Impossible de joindre Dealabs',
+ 'thread-error' => 'Impossible de déterminer l\'ID de la discussion. Vérifiez l\'URL que vous avez entré',
+ 'no-results' => 'Il n&#039;y a rien à afficher pour le moment :(',
+ 'relative-date-indicator' => [
+ 'il y a',
+ ],
+ 'price' => 'Prix',
+ 'shipping' => 'Livraison',
+ 'origin' => 'Origine',
+ 'discount' => 'Réduction',
+ 'title-keyword' => 'Recherche',
+ 'title-group' => 'Groupe',
+ 'title-talk' => 'Surveillance Discussion',
+ 'local-months' => [
+ 'janvier',
+ 'février',
+ 'mars',
+ 'avril',
+ 'mai',
+ 'juin',
+ 'juillet',
+ 'août',
+ 'septembre',
+ 'octobre',
+ 'novembre',
+ 'décembre'
+ ],
+ 'local-time-relative' => [
+ 'il y a ',
+ 'min',
+ 'h',
+ 'jour',
+ 'jours',
+ 'mois',
+ 'ans',
+ 'et '
+ ],
+ 'date-prefixes' => [
+ 'Actualisé ',
+ ],
+ 'relative-date-alt-prefixes' => [
+ 'Actualisé ',
+ ],
+ 'relative-date-ignore-suffix' => [
+ ],
+ 'localdeal' => [
+ 'Local',
+ 'Pays d\'expédition'
+ ],
+ ];
}
diff --git a/bridges/DemoBridge.php b/bridges/DemoBridge.php
index f48b4510..06ec4e1e 100644
--- a/bridges/DemoBridge.php
+++ b/bridges/DemoBridge.php
@@ -1,46 +1,47 @@
<?php
-class DemoBridge extends BridgeAbstract {
- const MAINTAINER = 'teromene';
- const NAME = 'DemoBridge';
- const URI = 'http://github.com/rss-bridge/rss-bridge';
- const DESCRIPTION = 'Bridge used for demos';
+class DemoBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'teromene';
+ const NAME = 'DemoBridge';
+ const URI = 'http://github.com/rss-bridge/rss-bridge';
+ const DESCRIPTION = 'Bridge used for demos';
- const PARAMETERS = array(
- 'testCheckbox' => array(
- 'testCheckbox' => array(
- 'type' => 'checkbox',
- 'name' => 'test des checkbox'
- )
- ),
- 'testList' => array(
- 'testList' => array(
- 'type' => 'list',
- 'name' => 'test des listes',
- 'values' => array(
- 'Test' => 'test',
- 'Test 2' => 'test2'
- )
- )
- ),
- 'testNumber' => array(
- 'testNumber' => array(
- 'type' => 'number',
- 'name' => 'test des numéros',
- 'exampleValue' => '1515632'
- )
- )
- );
+ const PARAMETERS = [
+ 'testCheckbox' => [
+ 'testCheckbox' => [
+ 'type' => 'checkbox',
+ 'name' => 'test des checkbox'
+ ]
+ ],
+ 'testList' => [
+ 'testList' => [
+ 'type' => 'list',
+ 'name' => 'test des listes',
+ 'values' => [
+ 'Test' => 'test',
+ 'Test 2' => 'test2'
+ ]
+ ]
+ ],
+ 'testNumber' => [
+ 'testNumber' => [
+ 'type' => 'number',
+ 'name' => 'test des numéros',
+ 'exampleValue' => '1515632'
+ ]
+ ]
+ ];
- public function collectData(){
+ public function collectData()
+ {
+ $item = [];
+ $item['author'] = 'Me!';
+ $item['title'] = 'Test';
+ $item['content'] = 'Awesome content !';
+ $item['id'] = 'Lalala';
+ $item['uri'] = 'http://example.com/test';
- $item = array();
- $item['author'] = 'Me!';
- $item['title'] = 'Test';
- $item['content'] = 'Awesome content !';
- $item['id'] = 'Lalala';
- $item['uri'] = 'http://example.com/test';
-
- $this->items[] = $item;
- }
+ $this->items[] = $item;
+ }
}
diff --git a/bridges/DerpibooruBridge.php b/bridges/DerpibooruBridge.php
index 8fb2777c..e06e0eff 100644
--- a/bridges/DerpibooruBridge.php
+++ b/bridges/DerpibooruBridge.php
@@ -1,116 +1,122 @@
<?php
-class DerpibooruBridge extends BridgeAbstract {
- const NAME = 'Derpibooru Bridge';
- const URI = 'https://derpibooru.org/';
- const DESCRIPTION = 'Returns newest images from a Derpibooru search';
- const CACHE_TIMEOUT = 300; // 5min
- const MAINTAINER = 'Roliga';
- const PARAMETERS = array(
- array(
- 'f' => array(
- 'name' => 'Filter',
- 'type' => 'list',
- 'values' => array(
- 'Everything' => 56027,
- '18+ R34' => 37432,
- 'Legacy Default' => 37431,
- '18+ Dark' => 37429,
- 'Maximum Spoilers' => 37430,
- 'Default' => 100073
- ),
- 'defaultValue' => 56027
+class DerpibooruBridge extends BridgeAbstract
+{
+ const NAME = 'Derpibooru Bridge';
+ const URI = 'https://derpibooru.org/';
+ const DESCRIPTION = 'Returns newest images from a Derpibooru search';
+ const CACHE_TIMEOUT = 300; // 5min
+ const MAINTAINER = 'Roliga';
- ),
- 'q' => array(
- 'name' => 'Query',
- 'required' => true,
- 'exampleValue' => 'dog',
- )
- )
- );
+ const PARAMETERS = [
+ [
+ 'f' => [
+ 'name' => 'Filter',
+ 'type' => 'list',
+ 'values' => [
+ 'Everything' => 56027,
+ '18+ R34' => 37432,
+ 'Legacy Default' => 37431,
+ '18+ Dark' => 37429,
+ 'Maximum Spoilers' => 37430,
+ 'Default' => 100073
+ ],
+ 'defaultValue' => 56027
- public function detectParameters($url){
- $params = array();
+ ],
+ 'q' => [
+ 'name' => 'Query',
+ 'required' => true,
+ 'exampleValue' => 'dog',
+ ]
+ ]
+ ];
- // Search page e.g. https://derpibooru.org/search?q=cute
- $regex = '/^(https?:\/\/)?(www\.)?derpibooru.org\/search.+q=([^\/&?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['q'] = urldecode($matches[3]);
- return $params;
- }
+ public function detectParameters($url)
+ {
+ $params = [];
- // Tag page, e.g. https://derpibooru.org/tags/artist-colon-devinian
- $regex = '/^(https?:\/\/)?(www\.)?derpibooru.org\/tags\/([^\/&?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['q'] = str_replace('-colon-', ':', urldecode($matches[3]));
- return $params;
- }
+ // Search page e.g. https://derpibooru.org/search?q=cute
+ $regex = '/^(https?:\/\/)?(www\.)?derpibooru.org\/search.+q=([^\/&?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['q'] = urldecode($matches[3]);
+ return $params;
+ }
- return null;
- }
+ // Tag page, e.g. https://derpibooru.org/tags/artist-colon-devinian
+ $regex = '/^(https?:\/\/)?(www\.)?derpibooru.org\/tags\/([^\/&?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['q'] = str_replace('-colon-', ':', urldecode($matches[3]));
+ return $params;
+ }
- public function getName(){
- if(!is_null($this->getInput('q'))) {
- return 'Derpibooru search for: '
- . $this->getInput('q');
- } else {
- return parent::getName();
- }
- }
+ return null;
+ }
- public function getURI(){
- if(!is_null($this->getInput('f')) && !is_null($this->getInput('q'))) {
- return self::URI
- . 'search?filter_id='
- . urlencode($this->getInput('f'))
- . '&q='
- . urlencode($this->getInput('q'));
- } else {
- return parent::getURI();
- }
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('q'))) {
+ return 'Derpibooru search for: '
+ . $this->getInput('q');
+ } else {
+ return parent::getName();
+ }
+ }
- public function collectData(){
- $queryJson = json_decode(getContents(
- self::URI
- . 'api/v1/json/search/images?filter_id='
- . urlencode($this->getInput('f'))
- . '&q='
- . urlencode($this->getInput('q'))
- ));
+ public function getURI()
+ {
+ if (!is_null($this->getInput('f')) && !is_null($this->getInput('q'))) {
+ return self::URI
+ . 'search?filter_id='
+ . urlencode($this->getInput('f'))
+ . '&q='
+ . urlencode($this->getInput('q'));
+ } else {
+ return parent::getURI();
+ }
+ }
- foreach($queryJson->images as $post) {
- $item = array();
+ public function collectData()
+ {
+ $queryJson = json_decode(getContents(
+ self::URI
+ . 'api/v1/json/search/images?filter_id='
+ . urlencode($this->getInput('f'))
+ . '&q='
+ . urlencode($this->getInput('q'))
+ ));
- $postUri = self::URI . $post->id;
+ foreach ($queryJson->images as $post) {
+ $item = [];
- $item['uri'] = $postUri;
- $item['title'] = $post->name;
- $item['timestamp'] = strtotime($post->created_at);
- $item['author'] = $post->uploader;
- $item['enclosures'] = array($post->view_url);
- $item['categories'] = $post->tags;
+ $postUri = self::URI . $post->id;
- $item['content'] = '<p><a href="' // image preview
- . $postUri
- . '"><img src="'
- . $post->representations->medium
- . '"></a></p><p>' // description
- . $post->description
- . '</p><p><b>Size:</b> ' // image size
- . $post->width
- . 'x'
- . $post->height;
- // source link
- if ($post->source_url != null) {
- $item['content'] .= '<br><b>Source:</b> <a href="'
- . $post->source_url
- . '">'
- . $post->source_url
- . '</a></p>';
- };
- $this->items[] = $item;
- }
- }
+ $item['uri'] = $postUri;
+ $item['title'] = $post->name;
+ $item['timestamp'] = strtotime($post->created_at);
+ $item['author'] = $post->uploader;
+ $item['enclosures'] = [$post->view_url];
+ $item['categories'] = $post->tags;
+
+ $item['content'] = '<p><a href="' // image preview
+ . $postUri
+ . '"><img src="'
+ . $post->representations->medium
+ . '"></a></p><p>' // description
+ . $post->description
+ . '</p><p><b>Size:</b> ' // image size
+ . $post->width
+ . 'x'
+ . $post->height;
+ // source link
+ if ($post->source_url != null) {
+ $item['content'] .= '<br><b>Source:</b> <a href="'
+ . $post->source_url
+ . '">'
+ . $post->source_url
+ . '</a></p>';
+ };
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/DesoutterBridge.php b/bridges/DesoutterBridge.php
index e594240d..5331ff35 100644
--- a/bridges/DesoutterBridge.php
+++ b/bridges/DesoutterBridge.php
@@ -1,243 +1,250 @@
<?php
-class DesoutterBridge extends BridgeAbstract {
-
- const CATEGORY_NEWS = 'News & Events';
- const CATEGORY_INDUSTRY = 'Industry 4.0 News';
-
- const NAME = 'Desoutter Bridge';
- const URI = 'https://www.desouttertools.com';
- const DESCRIPTION = 'Returns feeds for news from Desoutter';
- const MAINTAINER = 'logmanoriginal';
- const CACHE_TIMEOUT = 86400; // 24 hours
-
- const PARAMETERS = array(
- self::CATEGORY_NEWS => array(
- 'news_lang' => array(
- 'name' => 'Language',
- 'type' => 'list',
- 'title' => 'Select your language',
- 'defaultValue' => 'https://www.desouttertools.com/about-desoutter/news-events',
- 'values' => array(
- 'Corporate'
- => 'https://www.desouttertools.com/about-desoutter/news-events',
- 'Česko'
- => 'https://www.desouttertools.cz/o-desoutter/aktuality-udalsoti',
- 'Deutschland'
- => 'https://www.desoutter.de/ueber-desoutter/news-events',
- 'España'
- => 'https://www.desouttertools.es/sobre-desoutter/noticias-eventos',
- 'México'
- => 'https://www.desouttertools.mx/acerca-desoutter/noticias-eventos',
- 'France'
- => 'https://www.desouttertools.fr/a-propos-de-desoutter/actualites-evenements',
- 'Magyarország'
- => 'https://www.desouttertools.hu/a-desoutter-vallalatrol/hirek-esemenyek',
- 'Italia'
- => 'https://www.desouttertools.it/su-desoutter/news-eventi',
- '日本'
- => 'https://www.desouttertools.jp/desotanituite/niyusu-ibento',
- '대한민국'
- => 'https://www.desouttertools.co.kr/desoteoe-daehaeseo/nyuseu-mic-ibenteu',
- 'Polska'
- => 'https://www.desouttertools.pl/o-desoutter/aktualnosci-wydarzenia',
- 'Brasil'
- => 'https://www.desouttertools.com.br/sobre-desoutter/noti%C2%ADcias-eventos',
- 'Portugal'
- => 'https://www.desouttertools.pt/sobre-desoutter/notIcias-eventos',
- 'România'
- => 'https://www.desouttertools.ro/despre-desoutter/noutati-evenimente',
- 'Российская Федерация'
- => 'https://www.desouttertools.com.ru/o-desoutter/novosti-mieropriiatiia',
- 'Slovensko'
- => 'https://www.desouttertools.sk/o-spolocnosti-desoutter/novinky-udalosti',
- 'Slovenija'
- => 'https://www.desouttertools.si/o-druzbi-desoutter/novice-dogodki',
- 'Sverige'
- => 'https://www.desouttertools.se/om-desoutter/nyheter-evenemang',
- 'Türkiye'
- => 'https://www.desoutter.com.tr/desoutter-hakkinda/haberler-etkinlikler',
- '中国'
- => 'https://www.desouttertools.com.cn/guan-yu-ma-tou/xin-wen-he-huo-dong',
- )
- ),
- ),
- self::CATEGORY_INDUSTRY => array(
- 'industry_lang' => array(
- 'name' => 'Language',
- 'type' => 'list',
- 'title' => 'Select your language',
- 'defaultValue' => 'Corporate',
- 'values' => array(
- 'Corporate'
- => 'https://www.desouttertools.com/industry-4-0/news',
- 'Česko'
- => 'https://www.desouttertools.cz/prumysl-4-0/novinky',
- 'Deutschland'
- => 'https://www.desoutter.de/industrie-4-0/news',
- 'España'
- => 'https://www.desouttertools.es/industria-4-0/noticias',
- 'México'
- => 'https://www.desouttertools.mx/industria-4-0/noticias',
- 'France'
- => 'https://www.desouttertools.fr/industrie-4-0/actualites',
- 'Magyarország'
- => 'https://www.desouttertools.hu/industry-4-0/hirek',
- 'Italia'
- => 'https://www.desouttertools.it/industry-4-0/news',
- '日本'
- => 'https://www.desouttertools.jp/industry-4-0/news',
- '대한민국'
- => 'https://www.desouttertools.co.kr/industry-4-0/news',
- 'Polska'
- => 'https://www.desouttertools.pl/przemysl-4-0/wiadomosci',
- 'Brasil'
- => 'https://www.desouttertools.com.br/industria-4-0/noticias',
- 'Portugal'
- => 'https://www.desouttertools.pt/industria-4-0/noticias',
- 'România'
- => 'https://www.desouttertools.ro/industry-4-0/noutati',
- 'Российская Федерация'
- => 'https://www.desouttertools.com.ru/industry-4-0/news',
- 'Slovensko'
- => 'https://www.desouttertools.sk/priemysel-4-0/novinky',
- 'Slovenija'
- => 'https://www.desouttertools.si/industrija-4-0/novice',
- 'Sverige'
- => 'https://www.desouttertools.se/industri-4-0/nyheter',
- 'Türkiye'
- => 'https://www.desoutter.com.tr/endustri-4-0/haberler',
- '中国'
- => 'https://www.desouttertools.com.cn/industry-4-0/news',
- )
- ),
- ),
- 'global' => array(
- 'full' => array(
- 'name' => 'Load full articles',
- 'type' => 'checkbox',
- 'title' => 'Enable to load the full article for each item'
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => true,
- 'defaultValue' => 3,
- 'title' => "Maximum number of items to return in the feed.\n0 = unlimited"
- )
- )
- );
-
- private $title;
-
- public function getURI() {
- switch($this->queriedContext) {
- case self::CATEGORY_NEWS:
- return $this->getInput('news_lang') ?: parent::getURI();
- case self::CATEGORY_INDUSTRY:
- return $this->getInput('industry_lang') ?: parent::getURI();
- }
-
- return parent::getURI();
- }
-
- public function getName() {
- return isset($this->title) ? $this->title . ' - ' . parent::getName() : parent::getName();
- }
-
- public function collectData() {
-
- // Uncomment to generate list of languages automtically (dev mode)
- /*
- switch($this->queriedContext) {
- case self::CATEGORY_NEWS:
- $this->extractNewsLanguages(); die;
- case self::CATEGORY_INDUSTRY:
- $this->extractIndustryLanguages(); die;
- }
- */
-
- $html = getSimpleHTMLDOM($this->getURI());
-
- $html = defaultLinkTo($html, $this->getURI());
-
- $this->title = html_entity_decode($html->find('title', 0)->plaintext, ENT_QUOTES);
-
- $limit = $this->getInput('limit') ?: 0;
-
- foreach($html->find('article') as $article) {
- $item = array();
-
- $item['uri'] = $article->find('a', 0)->href;
- $item['title'] = $article->find('a[title]', 0)->title;
-
- if($this->getInput('full')) {
- $item['content'] = $this->getFullNewsArticle($item['uri']);
- } else {
- $item['content'] = $article->find('div.tile-body p', 0)->plaintext;
- }
-
- $this->items[] = $item;
-
- if ($limit > 0 && count($this->items) >= $limit) break;
- }
-
- }
-
- private function getFullNewsArticle($uri) {
- $html = getSimpleHTMLDOMCached($uri);
-
- $html = defaultLinkTo($html, $this->getURI());
-
- return $html->find('section.article', 0);
- }
-
- /**
- * Generates a HTML page with a PHP formatted array of languages,
- * pointing to the corresponding news pages. Implementation is based
- * on the 'Corporate' site.
- * @return void
- */
- private function extractNewsLanguages() {
- $html = getSimpleHTMLDOMCached('https://www.desouttertools.com/about-desoutter/news-events');
-
- $html = defaultLinkTo($html, static::URI);
-
- $items = $html->find('ul[class="dropdown-menu"] li');
-
- $list = "\t'Corporate'\n\t=> 'https://www.desouttertools.com/about-desoutter/news-events',\n";
- foreach($items as $item) {
- $lang = trim($item->plaintext);
- $uri = $item->find('a', 0)->href;
+class DesoutterBridge extends BridgeAbstract
+{
+ const CATEGORY_NEWS = 'News & Events';
+ const CATEGORY_INDUSTRY = 'Industry 4.0 News';
+
+ const NAME = 'Desoutter Bridge';
+ const URI = 'https://www.desouttertools.com';
+ const DESCRIPTION = 'Returns feeds for news from Desoutter';
+ const MAINTAINER = 'logmanoriginal';
+ const CACHE_TIMEOUT = 86400; // 24 hours
+
+ const PARAMETERS = [
+ self::CATEGORY_NEWS => [
+ 'news_lang' => [
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'title' => 'Select your language',
+ 'defaultValue' => 'https://www.desouttertools.com/about-desoutter/news-events',
+ 'values' => [
+ 'Corporate'
+ => 'https://www.desouttertools.com/about-desoutter/news-events',
+ 'Česko'
+ => 'https://www.desouttertools.cz/o-desoutter/aktuality-udalsoti',
+ 'Deutschland'
+ => 'https://www.desoutter.de/ueber-desoutter/news-events',
+ 'España'
+ => 'https://www.desouttertools.es/sobre-desoutter/noticias-eventos',
+ 'México'
+ => 'https://www.desouttertools.mx/acerca-desoutter/noticias-eventos',
+ 'France'
+ => 'https://www.desouttertools.fr/a-propos-de-desoutter/actualites-evenements',
+ 'Magyarország'
+ => 'https://www.desouttertools.hu/a-desoutter-vallalatrol/hirek-esemenyek',
+ 'Italia'
+ => 'https://www.desouttertools.it/su-desoutter/news-eventi',
+ '日本'
+ => 'https://www.desouttertools.jp/desotanituite/niyusu-ibento',
+ '대한민국'
+ => 'https://www.desouttertools.co.kr/desoteoe-daehaeseo/nyuseu-mic-ibenteu',
+ 'Polska'
+ => 'https://www.desouttertools.pl/o-desoutter/aktualnosci-wydarzenia',
+ 'Brasil'
+ => 'https://www.desouttertools.com.br/sobre-desoutter/noti%C2%ADcias-eventos',
+ 'Portugal'
+ => 'https://www.desouttertools.pt/sobre-desoutter/notIcias-eventos',
+ 'România'
+ => 'https://www.desouttertools.ro/despre-desoutter/noutati-evenimente',
+ 'Российская Федерация'
+ => 'https://www.desouttertools.com.ru/o-desoutter/novosti-mieropriiatiia',
+ 'Slovensko'
+ => 'https://www.desouttertools.sk/o-spolocnosti-desoutter/novinky-udalosti',
+ 'Slovenija'
+ => 'https://www.desouttertools.si/o-druzbi-desoutter/novice-dogodki',
+ 'Sverige'
+ => 'https://www.desouttertools.se/om-desoutter/nyheter-evenemang',
+ 'Türkiye'
+ => 'https://www.desoutter.com.tr/desoutter-hakkinda/haberler-etkinlikler',
+ '中国'
+ => 'https://www.desouttertools.com.cn/guan-yu-ma-tou/xin-wen-he-huo-dong',
+ ]
+ ],
+ ],
+ self::CATEGORY_INDUSTRY => [
+ 'industry_lang' => [
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'title' => 'Select your language',
+ 'defaultValue' => 'Corporate',
+ 'values' => [
+ 'Corporate'
+ => 'https://www.desouttertools.com/industry-4-0/news',
+ 'Česko'
+ => 'https://www.desouttertools.cz/prumysl-4-0/novinky',
+ 'Deutschland'
+ => 'https://www.desoutter.de/industrie-4-0/news',
+ 'España'
+ => 'https://www.desouttertools.es/industria-4-0/noticias',
+ 'México'
+ => 'https://www.desouttertools.mx/industria-4-0/noticias',
+ 'France'
+ => 'https://www.desouttertools.fr/industrie-4-0/actualites',
+ 'Magyarország'
+ => 'https://www.desouttertools.hu/industry-4-0/hirek',
+ 'Italia'
+ => 'https://www.desouttertools.it/industry-4-0/news',
+ '日本'
+ => 'https://www.desouttertools.jp/industry-4-0/news',
+ '대한민국'
+ => 'https://www.desouttertools.co.kr/industry-4-0/news',
+ 'Polska'
+ => 'https://www.desouttertools.pl/przemysl-4-0/wiadomosci',
+ 'Brasil'
+ => 'https://www.desouttertools.com.br/industria-4-0/noticias',
+ 'Portugal'
+ => 'https://www.desouttertools.pt/industria-4-0/noticias',
+ 'România'
+ => 'https://www.desouttertools.ro/industry-4-0/noutati',
+ 'Российская Федерация'
+ => 'https://www.desouttertools.com.ru/industry-4-0/news',
+ 'Slovensko'
+ => 'https://www.desouttertools.sk/priemysel-4-0/novinky',
+ 'Slovenija'
+ => 'https://www.desouttertools.si/industrija-4-0/novice',
+ 'Sverige'
+ => 'https://www.desouttertools.se/industri-4-0/nyheter',
+ 'Türkiye'
+ => 'https://www.desoutter.com.tr/endustri-4-0/haberler',
+ '中国'
+ => 'https://www.desouttertools.com.cn/industry-4-0/news',
+ ]
+ ],
+ ],
+ 'global' => [
+ 'full' => [
+ 'name' => 'Load full articles',
+ 'type' => 'checkbox',
+ 'title' => 'Enable to load the full article for each item'
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => true,
+ 'defaultValue' => 3,
+ 'title' => "Maximum number of items to return in the feed.\n0 = unlimited"
+ ]
+ ]
+ ];
+
+ private $title;
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case self::CATEGORY_NEWS:
+ return $this->getInput('news_lang') ?: parent::getURI();
+ case self::CATEGORY_INDUSTRY:
+ return $this->getInput('industry_lang') ?: parent::getURI();
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ return isset($this->title) ? $this->title . ' - ' . parent::getName() : parent::getName();
+ }
+
+ public function collectData()
+ {
+ // Uncomment to generate list of languages automtically (dev mode)
+ /*
+ switch($this->queriedContext) {
+ case self::CATEGORY_NEWS:
+ $this->extractNewsLanguages(); die;
+ case self::CATEGORY_INDUSTRY:
+ $this->extractIndustryLanguages(); die;
+ }
+ */
+
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $html = defaultLinkTo($html, $this->getURI());
+
+ $this->title = html_entity_decode($html->find('title', 0)->plaintext, ENT_QUOTES);
+
+ $limit = $this->getInput('limit') ?: 0;
+
+ foreach ($html->find('article') as $article) {
+ $item = [];
+
+ $item['uri'] = $article->find('a', 0)->href;
+ $item['title'] = $article->find('a[title]', 0)->title;
+
+ if ($this->getInput('full')) {
+ $item['content'] = $this->getFullNewsArticle($item['uri']);
+ } else {
+ $item['content'] = $article->find('div.tile-body p', 0)->plaintext;
+ }
+
+ $this->items[] = $item;
+
+ if ($limit > 0 && count($this->items) >= $limit) {
+ break;
+ }
+ }
+ }
+
+ private function getFullNewsArticle($uri)
+ {
+ $html = getSimpleHTMLDOMCached($uri);
+
+ $html = defaultLinkTo($html, $this->getURI());
+
+ return $html->find('section.article', 0);
+ }
+
+ /**
+ * Generates a HTML page with a PHP formatted array of languages,
+ * pointing to the corresponding news pages. Implementation is based
+ * on the 'Corporate' site.
+ * @return void
+ */
+ private function extractNewsLanguages()
+ {
+ $html = getSimpleHTMLDOMCached('https://www.desouttertools.com/about-desoutter/news-events');
+
+ $html = defaultLinkTo($html, static::URI);
+
+ $items = $html->find('ul[class="dropdown-menu"] li');
+
+ $list = "\t'Corporate'\n\t=> 'https://www.desouttertools.com/about-desoutter/news-events',\n";
+
+ foreach ($items as $item) {
+ $lang = trim($item->plaintext);
+ $uri = $item->find('a', 0)->href;
+
+ $list .= "\t'{$lang}'\n\t=> '{$uri}',\n";
+ }
+
+ echo $list;
+ }
- $list .= "\t'{$lang}'\n\t=> '{$uri}',\n";
- }
+ /**
+ * Generates a HTML page with a PHP formatted array of languages,
+ * pointing to the corresponding news pages. Implementation is based
+ * on the 'Corporate' site.
+ * @return void
+ */
+ private function extractIndustryLanguages()
+ {
+ $html = getSimpleHTMLDOMCached('https://www.desouttertools.com/industry-4-0/news');
- echo $list;
- }
+ $html = defaultLinkTo($html, static::URI);
- /**
- * Generates a HTML page with a PHP formatted array of languages,
- * pointing to the corresponding news pages. Implementation is based
- * on the 'Corporate' site.
- * @return void
- */
- private function extractIndustryLanguages() {
- $html = getSimpleHTMLDOMCached('https://www.desouttertools.com/industry-4-0/news');
+ $items = $html->find('ul[class="dropdown-menu"] li');
- $html = defaultLinkTo($html, static::URI);
+ $list = "\t'Corporate'\n\t=> 'https://www.desouttertools.com/industry-4-0/news',\n";
- $items = $html->find('ul[class="dropdown-menu"] li');
+ foreach ($items as $item) {
+ $lang = trim($item->plaintext);
+ $uri = $item->find('a', 0)->href;
- $list = "\t'Corporate'\n\t=> 'https://www.desouttertools.com/industry-4-0/news',\n";
+ $list .= "\t'{$lang}'\n\t=> '{$uri}',\n";
+ }
- foreach($items as $item) {
- $lang = trim($item->plaintext);
- $uri = $item->find('a', 0)->href;
-
- $list .= "\t'{$lang}'\n\t=> '{$uri}',\n";
- }
-
- echo $list;
- }
+ echo $list;
+ }
}
diff --git a/bridges/DevToBridge.php b/bridges/DevToBridge.php
index f449d70a..3940fff2 100644
--- a/bridges/DevToBridge.php
+++ b/bridges/DevToBridge.php
@@ -1,107 +1,113 @@
<?php
-class DevToBridge extends BridgeAbstract {
-
- const CONTEXT_BY_TAG = 'By tag';
-
- const NAME = 'dev.to Bridge';
- const URI = 'https://dev.to';
- const DESCRIPTION = 'Returns feeds for tags';
- const MAINTAINER = 'logmanoriginal';
- const CACHE_TIMEOUT = 10800; // 15 min.
-
- const PARAMETERS = array(
- self::CONTEXT_BY_TAG => array(
- 'tag' => array(
- 'name' => 'Tag',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Insert your tag',
- 'exampleValue' => 'python'
- ),
- 'full' => array(
- 'name' => 'Full article',
- 'type' => 'checkbox',
- 'required' => false,
- 'title' => 'Enable to receive the full article for each item'
- )
- )
- );
-
- public function getURI() {
- switch($this->queriedContext) {
- case self::CONTEXT_BY_TAG:
- if($tag = $this->getInput('tag')) {
- return static::URI . '/t/' . urlencode($tag);
- }
- break;
- }
-
- return parent::getURI();
- }
-
- public function getIcon() {
- return 'https://practicaldev-herokuapp-com.freetls.fastly.net/assets/
+
+class DevToBridge extends BridgeAbstract
+{
+ const CONTEXT_BY_TAG = 'By tag';
+
+ const NAME = 'dev.to Bridge';
+ const URI = 'https://dev.to';
+ const DESCRIPTION = 'Returns feeds for tags';
+ const MAINTAINER = 'logmanoriginal';
+ const CACHE_TIMEOUT = 10800; // 15 min.
+
+ const PARAMETERS = [
+ self::CONTEXT_BY_TAG => [
+ 'tag' => [
+ 'name' => 'Tag',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert your tag',
+ 'exampleValue' => 'python'
+ ],
+ 'full' => [
+ 'name' => 'Full article',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'title' => 'Enable to receive the full article for each item'
+ ]
+ ]
+ ];
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case self::CONTEXT_BY_TAG:
+ if ($tag = $this->getInput('tag')) {
+ return static::URI . '/t/' . urlencode($tag);
+ }
+ break;
+ }
+
+ return parent::getURI();
+ }
+
+ public function getIcon()
+ {
+ return 'https://practicaldev-herokuapp-com.freetls.fastly.net/assets/
apple-icon-5c6fa9f2bce280428589c6195b7f1924206a53b782b371cfe2d02da932c8c173.png';
- }
+ }
- public function collectData() {
- $html = getSimpleHTMLDOMCached($this->getURI());
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOMCached($this->getURI());
- $html = defaultLinkTo($html, static::URI);
+ $html = defaultLinkTo($html, static::URI);
- $articles = $html->find('div.crayons-story')
- or returnServerError('Could not find articles!');
+ $articles = $html->find('div.crayons-story')
+ or returnServerError('Could not find articles!');
- foreach($articles as $article) {
- $item = array();
+ foreach ($articles as $article) {
+ $item = [];
- $item['uri'] = $article->find('a[id*=article-link]', 0)->href;
- $item['title'] = $article->find('h2 > a', 0)->plaintext;
+ $item['uri'] = $article->find('a[id*=article-link]', 0)->href;
+ $item['title'] = $article->find('h2 > a', 0)->plaintext;
- $item['timestamp'] = $article->find('time', 0)->datetime;
- $item['author'] = $article->find('a.crayons-story__secondary.fw-medium', 0)->plaintext;
+ $item['timestamp'] = $article->find('time', 0)->datetime;
+ $item['author'] = $article->find('a.crayons-story__secondary.fw-medium', 0)->plaintext;
- // Profile image
- $item['enclosures'] = array($article->find('img', 0)->src);
+ // Profile image
+ $item['enclosures'] = [$article->find('img', 0)->src];
- if($this->getInput('full')) {
- $fullArticle = $this->getFullArticle($item['uri']);
- $item['content'] = <<<EOD
+ if ($this->getInput('full')) {
+ $fullArticle = $this->getFullArticle($item['uri']);
+ $item['content'] = <<<EOD
<p>{$fullArticle}</p>
EOD;
- } else {
- $item['content'] = <<<EOD
+ } else {
+ $item['content'] = <<<EOD
<img src="{$item['enclosures'][0]}" alt="{$item['author']}">
<p>{$item['title']}</p>
EOD;
- }
+ }
- // categories
- foreach ($article->find('a.crayons-tag') as $tag) {
- $item['categories'][] = str_replace('#', '', $tag->plaintext);
- }
+ // categories
+ foreach ($article->find('a.crayons-tag') as $tag) {
+ $item['categories'][] = str_replace('#', '', $tag->plaintext);
+ }
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
- public function getName() {
- if (!is_null($this->getInput('tag'))) {
- return ucfirst($this->getInput('tag')) . ' - dev.to';
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('tag'))) {
+ return ucfirst($this->getInput('tag')) . ' - dev.to';
+ }
- return parent::getName();
- }
+ return parent::getName();
+ }
- private function getFullArticle($url) {
- $html = getSimpleHTMLDOMCached($url);
+ private function getFullArticle($url)
+ {
+ $html = getSimpleHTMLDOMCached($url);
- $html = defaultLinkTo($html, static::URI);
+ $html = defaultLinkTo($html, static::URI);
- if ($html->find('div.crayons-article__cover', 0)) {
- return $html->find('div.crayons-article__cover', 0) . $html->find('[id="article-body"]', 0);
- }
+ if ($html->find('div.crayons-article__cover', 0)) {
+ return $html->find('div.crayons-article__cover', 0) . $html->find('[id="article-body"]', 0);
+ }
- return $html->find('[id="article-body"]', 0);
- }
+ return $html->find('[id="article-body"]', 0);
+ }
}
diff --git a/bridges/DeveloppezDotComBridge.php b/bridges/DeveloppezDotComBridge.php
index 1d3244b0..d0d54d0a 100644
--- a/bridges/DeveloppezDotComBridge.php
+++ b/bridges/DeveloppezDotComBridge.php
@@ -2,406 +2,405 @@
class DeveloppezDotComBridge extends FeedExpander
{
+ const MAINTAINER = 'Binnette';
+ const NAME = 'Developpez.com Actus (FR)';
+ const URI = 'https://www.developpez.com/';
+ const DOMAIN = '.developpez.com/';
+ const RSS_URL = 'index/rss';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns complete posts from developpez.com';
+ // Encodings used by Developpez.com in their articles body
+ const ENCONDINGS = ['Windows-1252', 'UTF-8'];
+ const PARAMETERS = [
+ [
+ 'limit' => [
+ 'name' => 'Max items',
+ 'type' => 'number',
+ 'defaultValue' => 5,
+ ],
+ // list of the differents RSS availables
+ 'domain' => [
+ 'type' => 'list',
+ 'name' => 'Domaine',
+ 'title' => 'Chosissez un sous-domaine',
+ 'values' => [
+ '= Domaine principal =' => 'www',
+ '4d' => '4d',
+ 'abbyy' => 'abbyy',
+ 'access' => 'access',
+ 'agile' => 'agile',
+ 'ajax' => 'ajax',
+ 'algo' => 'algo',
+ 'alm' => 'alm',
+ 'android' => 'android',
+ 'apache' => 'apache',
+ 'applications' => 'applications',
+ 'arduino' => 'arduino',
+ 'asm' => 'asm',
+ 'asp' => 'asp',
+ 'aspose' => 'aspose',
+ 'bacasable' => 'bacasable',
+ 'big-data' => 'big-data',
+ 'bpm' => 'bpm',
+ 'bsd' => 'bsd',
+ 'business-intelligence' => 'business-intelligence',
+ 'c' => 'c',
+ 'cloud-computing' => 'cloud-computing',
+ 'club' => 'club',
+ 'cms' => 'cms',
+ 'cpp' => 'cpp',
+ 'crm' => 'crm',
+ 'css' => 'css',
+ 'd' => 'd',
+ 'dart' => 'dart',
+ 'data-science' => 'data-science',
+ 'db2' => 'db2',
+ 'delphi' => 'delphi',
+ 'dotnet' => 'dotnet',
+ 'droit' => 'droit',
+ 'eclipse' => 'eclipse',
+ 'edi' => 'edi',
+ 'embarque' => 'embarque',
+ 'emploi' => 'emploi',
+ 'etudes' => 'etudes',
+ 'excel' => 'excel',
+ 'firebird' => 'firebird',
+ 'flash' => 'flash',
+ 'go' => 'go',
+ 'green-it' => 'green-it',
+ 'gtk' => 'gtk',
+ 'hardware' => 'hardware',
+ 'hpc' => 'hpc',
+ 'humour' => 'humour',
+ 'ibmcloud' => 'ibmcloud',
+ 'intelligence-artificielle' => 'intelligence-artificielle',
+ 'interbase' => 'interbase',
+ 'ios' => 'ios',
+ 'java' => 'java',
+ 'javascript' => 'javascript',
+ 'javaweb' => 'javaweb',
+ 'jetbrains' => 'jetbrains',
+ 'jeux' => 'jeux',
+ 'kotlin' => 'kotlin',
+ 'labview' => 'labview',
+ 'laravel' => 'laravel',
+ 'latex' => 'latex',
+ 'lazarus' => 'lazarus',
+ 'linux' => 'linux',
+ 'mac' => 'mac',
+ 'matlab' => 'matlab',
+ 'megaoffice' => 'megaoffice',
+ 'merise' => 'merise',
+ 'microsoft' => 'microsoft',
+ 'mobiles' => 'mobiles',
+ 'mongodb' => 'mongodb',
+ 'mysql' => 'mysql',
+ 'netbeans' => 'netbeans',
+ 'nodejs' => 'nodejs',
+ 'nosql' => 'nosql',
+ 'objective-c' => 'objective-c',
+ 'office' => 'office',
+ 'open-source' => 'open-source',
+ 'openoffice-libreoffice' => 'openoffice-libreoffice',
+ 'oracle' => 'oracle',
+ 'outlook' => 'outlook',
+ 'pascal' => 'pascal',
+ 'perl' => 'perl',
+ 'php' => 'php',
+ 'portail-emploi' => 'portail-emploi',
+ 'portail-projets' => 'portail-projets',
+ 'postgresql' => 'postgresql',
+ 'powerpoint' => 'powerpoint',
+ 'preprod-emploi' => 'preprod-emploi',
+ 'programmation' => 'programmation',
+ 'project' => 'project',
+ 'purebasic' => 'purebasic',
+ 'pyqt' => 'pyqt',
+ 'python' => 'python',
+ 'qt-creator' => 'qt-creator',
+ 'qt' => 'qt',
+ 'r' => 'r',
+ 'raspberry-pi' => 'raspberry-pi',
+ 'reseau' => 'reseau',
+ 'ruby' => 'ruby',
+ 'rust' => 'rust',
+ 'sap' => 'sap',
+ 'sas' => 'sas',
+ 'scilab' => 'scilab',
+ 'securite' => 'securite',
+ 'sgbd' => 'sgbd',
+ 'sharepoint' => 'sharepoint',
+ 'solutions-entreprise' => 'solutions-entreprise',
+ 'spring' => 'spring',
+ 'sqlserver' => 'sqlserver',
+ 'stages' => 'stages',
+ 'supervision' => 'supervision',
+ 'swift' => 'swift',
+ 'sybase' => 'sybase',
+ 'symfony' => 'symfony',
+ 'systeme' => 'systeme',
+ 'talend' => 'talend',
+ 'typescript' => 'typescript',
+ 'uml' => 'uml',
+ 'unix' => 'unix',
+ 'vb' => 'vb',
+ 'vba' => 'vba',
+ 'virtualisation' => 'virtualisation',
+ 'visualstudio' => 'visualstudio',
+ 'web-semantique' => 'web-semantique',
+ 'web' => 'web',
+ 'webmarketing' => 'webmarketing',
+ 'wind' => 'wind',
+ 'windows-azure' => 'windows-azure',
+ 'windows' => 'windows',
+ 'windowsphone' => 'windowsphone',
+ 'word' => 'word',
+ 'xhtml' => 'xhtml',
+ 'xml' => 'xml',
+ 'zend-framework' => 'zend-framework'
+ ],
+ ]
+ ]
+ ];
- const MAINTAINER = 'Binnette';
- const NAME = 'Developpez.com Actus (FR)';
- const URI = 'https://www.developpez.com/';
- const DOMAIN = '.developpez.com/';
- const RSS_URL = 'index/rss';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Returns complete posts from developpez.com';
- // Encodings used by Developpez.com in their articles body
- const ENCONDINGS = array('Windows-1252', 'UTF-8');
- const PARAMETERS = array(
- array(
- 'limit' => array(
- 'name' => 'Max items',
- 'type' => 'number',
- 'defaultValue' => 5,
- ),
- // list of the differents RSS availables
- 'domain' => array(
- 'type' => 'list',
- 'name' => 'Domaine',
- 'title' => 'Chosissez un sous-domaine',
- 'values' => array(
- '= Domaine principal =' => 'www',
- '4d' => '4d',
- 'abbyy' => 'abbyy',
- 'access' => 'access',
- 'agile' => 'agile',
- 'ajax' => 'ajax',
- 'algo' => 'algo',
- 'alm' => 'alm',
- 'android' => 'android',
- 'apache' => 'apache',
- 'applications' => 'applications',
- 'arduino' => 'arduino',
- 'asm' => 'asm',
- 'asp' => 'asp',
- 'aspose' => 'aspose',
- 'bacasable' => 'bacasable',
- 'big-data' => 'big-data',
- 'bpm' => 'bpm',
- 'bsd' => 'bsd',
- 'business-intelligence' => 'business-intelligence',
- 'c' => 'c',
- 'cloud-computing' => 'cloud-computing',
- 'club' => 'club',
- 'cms' => 'cms',
- 'cpp' => 'cpp',
- 'crm' => 'crm',
- 'css' => 'css',
- 'd' => 'd',
- 'dart' => 'dart',
- 'data-science' => 'data-science',
- 'db2' => 'db2',
- 'delphi' => 'delphi',
- 'dotnet' => 'dotnet',
- 'droit' => 'droit',
- 'eclipse' => 'eclipse',
- 'edi' => 'edi',
- 'embarque' => 'embarque',
- 'emploi' => 'emploi',
- 'etudes' => 'etudes',
- 'excel' => 'excel',
- 'firebird' => 'firebird',
- 'flash' => 'flash',
- 'go' => 'go',
- 'green-it' => 'green-it',
- 'gtk' => 'gtk',
- 'hardware' => 'hardware',
- 'hpc' => 'hpc',
- 'humour' => 'humour',
- 'ibmcloud' => 'ibmcloud',
- 'intelligence-artificielle' => 'intelligence-artificielle',
- 'interbase' => 'interbase',
- 'ios' => 'ios',
- 'java' => 'java',
- 'javascript' => 'javascript',
- 'javaweb' => 'javaweb',
- 'jetbrains' => 'jetbrains',
- 'jeux' => 'jeux',
- 'kotlin' => 'kotlin',
- 'labview' => 'labview',
- 'laravel' => 'laravel',
- 'latex' => 'latex',
- 'lazarus' => 'lazarus',
- 'linux' => 'linux',
- 'mac' => 'mac',
- 'matlab' => 'matlab',
- 'megaoffice' => 'megaoffice',
- 'merise' => 'merise',
- 'microsoft' => 'microsoft',
- 'mobiles' => 'mobiles',
- 'mongodb' => 'mongodb',
- 'mysql' => 'mysql',
- 'netbeans' => 'netbeans',
- 'nodejs' => 'nodejs',
- 'nosql' => 'nosql',
- 'objective-c' => 'objective-c',
- 'office' => 'office',
- 'open-source' => 'open-source',
- 'openoffice-libreoffice' => 'openoffice-libreoffice',
- 'oracle' => 'oracle',
- 'outlook' => 'outlook',
- 'pascal' => 'pascal',
- 'perl' => 'perl',
- 'php' => 'php',
- 'portail-emploi' => 'portail-emploi',
- 'portail-projets' => 'portail-projets',
- 'postgresql' => 'postgresql',
- 'powerpoint' => 'powerpoint',
- 'preprod-emploi' => 'preprod-emploi',
- 'programmation' => 'programmation',
- 'project' => 'project',
- 'purebasic' => 'purebasic',
- 'pyqt' => 'pyqt',
- 'python' => 'python',
- 'qt-creator' => 'qt-creator',
- 'qt' => 'qt',
- 'r' => 'r',
- 'raspberry-pi' => 'raspberry-pi',
- 'reseau' => 'reseau',
- 'ruby' => 'ruby',
- 'rust' => 'rust',
- 'sap' => 'sap',
- 'sas' => 'sas',
- 'scilab' => 'scilab',
- 'securite' => 'securite',
- 'sgbd' => 'sgbd',
- 'sharepoint' => 'sharepoint',
- 'solutions-entreprise' => 'solutions-entreprise',
- 'spring' => 'spring',
- 'sqlserver' => 'sqlserver',
- 'stages' => 'stages',
- 'supervision' => 'supervision',
- 'swift' => 'swift',
- 'sybase' => 'sybase',
- 'symfony' => 'symfony',
- 'systeme' => 'systeme',
- 'talend' => 'talend',
- 'typescript' => 'typescript',
- 'uml' => 'uml',
- 'unix' => 'unix',
- 'vb' => 'vb',
- 'vba' => 'vba',
- 'virtualisation' => 'virtualisation',
- 'visualstudio' => 'visualstudio',
- 'web-semantique' => 'web-semantique',
- 'web' => 'web',
- 'webmarketing' => 'webmarketing',
- 'wind' => 'wind',
- 'windows-azure' => 'windows-azure',
- 'windows' => 'windows',
- 'windowsphone' => 'windowsphone',
- 'word' => 'word',
- 'xhtml' => 'xhtml',
- 'xml' => 'xml',
- 'zend-framework' => 'zend-framework'
- ),
- )
- )
- );
+ /**
+ * Return the RSS url for selected domain
+ */
+ private function getRssUrl()
+ {
+ $domain = $this->getInput('domain');
+ if (!empty($domain)) {
+ return 'https://' . $domain . self::DOMAIN . self::RSS_URL;
+ }
- /**
- * Return the RSS url for selected domain
- */
- private function getRssUrl()
- {
- $domain = $this->getInput('domain');
- if (!empty($domain)) {
- return 'https://' . $domain . self::DOMAIN . self::RSS_URL;
- }
+ return self::URI . self::RSS_URL;
+ }
- return self::URI . self::RSS_URL;
- }
+ /**
+ * Grabs the RSS item from Developpez.com
+ */
+ public function collectData()
+ {
+ $url = $this->getRssUrl();
+ $this->collectExpandableDatas($url, 20);
+ }
- /**
- * Grabs the RSS item from Developpez.com
- */
- public function collectData()
- {
- $url = $this->getRssUrl();
- $this->collectExpandableDatas($url, 20);
- }
+ /**
+ * Parse the content of every RSS item. And will try to get the full article
+ * pointed by the item URL intead of the default abstract.
+ */
+ protected function parseItem($newsItem)
+ {
+ if (count($this->items) >= $this->getInput('limit')) {
+ return null;
+ }
- /**
- * Parse the content of every RSS item. And will try to get the full article
- * pointed by the item URL intead of the default abstract.
- */
- protected function parseItem($newsItem)
- {
- if (count($this->items) >= $this->getInput('limit')) {
- return null;
- }
+ // This function parse each entry in the RSS with the default parse
+ $item = parent::parseItem($newsItem);
- // This function parse each entry in the RSS with the default parse
- $item = parent::parseItem($newsItem);
+ // There is a bug in Developpez RSS, coma are writtent as '~?' in the
+ // title, so I have to fix it manually
+ $item['title'] = $this->fixComaInTitle($item['title']);
- // There is a bug in Developpez RSS, coma are writtent as '~?' in the
- // title, so I have to fix it manually
- $item['title'] = $this->fixComaInTitle($item['title']);
+ // We get the content of the full article behind the RSS item URL
+ $articleHTMLContent = getSimpleHTMLDOMCached($item['uri']);
- // We get the content of the full article behind the RSS item URL
- $articleHTMLContent = getSimpleHTMLDOMCached($item['uri']);
+ // Here we call our custom parser
+ $fullText = $this->extractFullText($articleHTMLContent);
+ if (!is_null($fullText)) {
+ // if we manage to parse the page behind the url of the RSS item
+ // then we set it as the new content. Otherwise we keep the default
+ // content to avoid RSS Bridge to return an empty item
+ $item['content'] = $fullText;
+ }
- // Here we call our custom parser
- $fullText = $this->extractFullText($articleHTMLContent);
- if (!is_null($fullText)) {
- // if we manage to parse the page behind the url of the RSS item
- // then we set it as the new content. Otherwise we keep the default
- // content to avoid RSS Bridge to return an empty item
- $item['content'] = $fullText;
- }
+ // Now we will attach video url in item
+ $videosUrl = $this->getAllVideoUrl($articleHTMLContent);
+ if (!empty($videosUrl)) {
+ $item['enclosures'] = array_merge($item['enclosures'], $videosUrl);
+ }
- // Now we will attach video url in item
- $videosUrl = $this->getAllVideoUrl($articleHTMLContent);
- if (!empty($videosUrl)) {
- $item['enclosures'] = array_merge($item['enclosures'], $videosUrl);
- }
+ // Now we can look for the blog writer/creator
+ $author = $articleHTMLContent->find('[itemprop="creator"]', 0);
+ if (!empty($author)) {
+ $item['author'] = $author->outertext;
+ }
- // Now we can look for the blog writer/creator
- $author = $articleHTMLContent->find('[itemprop="creator"]', 0);
- if (!empty($author)) {
- $item['author'] = $author->outertext;
- }
+ return $item;
+ }
- return $item;
- }
+ /**
+ * Replace '~?' by a proper coma ','
+ */
+ private function fixComaInTitle($txt)
+ {
+ return str_replace('~?', ',', $txt);
+ }
- /**
- * Replace '~?' by a proper coma ','
- */
- private function fixComaInTitle($txt)
- {
- return str_replace('~?', ',', $txt);
- }
+ /**
+ * Return the full article pointed by the url in the RSS item
+ * Since Developpez.com only provides a short abstract of the article, we
+ * use the url to retrieve the complete article and return it as the content
+ */
+ private function extractFullText($articleHTMLContent)
+ {
+ // All blog entry contains a div with the class 'content'. This div
+ // contains the complete blog article. But the RSS can also return
+ // announcement and not a blog article. So the next if, should take
+ // care of the "non blog" entry
+ $divArticleEntry = $articleHTMLContent->find('div.content', 0);
+ if (is_null($divArticleEntry)) {
+ // Didn't find the div with class content. It is probably not a blog
+ // entry. It is probably just an announcement for an ebook, a PDF,
+ // etc. So we can use the default RSS item content.
+ return null;
+ }
- /**
- * Return the full article pointed by the url in the RSS item
- * Since Developpez.com only provides a short abstract of the article, we
- * use the url to retrieve the complete article and return it as the content
- */
- private function extractFullText($articleHTMLContent)
- {
- // All blog entry contains a div with the class 'content'. This div
- // contains the complete blog article. But the RSS can also return
- // announcement and not a blog article. So the next if, should take
- // care of the "non blog" entry
- $divArticleEntry = $articleHTMLContent->find('div.content', 0);
- if (is_null($divArticleEntry)) {
- // Didn't find the div with class content. It is probably not a blog
- // entry. It is probably just an announcement for an ebook, a PDF,
- // etc. So we can use the default RSS item content.
- return null;
- }
+ // The following code is a bit hacky, but I really manage to get the
+ // full content of articles without any encoding issues. What is very
+ // weird and ugly in Developpez.com is the fact the some paragraphs of
+ // the article will be encoded as UTF-8 and some other paragraphs will
+ // be encoded as Windows-1252. So we can NOT decode the full article
+ // with only one encoding. We have to check every paragraph and
+ // determine its encoding
- // The following code is a bit hacky, but I really manage to get the
- // full content of articles without any encoding issues. What is very
- // weird and ugly in Developpez.com is the fact the some paragraphs of
- // the article will be encoded as UTF-8 and some other paragraphs will
- // be encoded as Windows-1252. So we can NOT decode the full article
- // with only one encoding. We have to check every paragraph and
- // determine its encoding
+ // This contains all the 'paragraphs' of the article. It includes the
+ // pictures, the text and the links at the bottom of the article
+ $paragraphs = $divArticleEntry->nodes;
+ // This will store the complete decoded content
+ $fullText = '';
- // This contains all the 'paragraphs' of the article. It includes the
- // pictures, the text and the links at the bottom of the article
- $paragraphs = $divArticleEntry->nodes;
- // This will store the complete decoded content
- $fullText = '';
+ // For each paragraph, we will identify the encoding, then decode it
+ // and finally store the decoded content in $text
+ foreach ($paragraphs as $paragraph) {
+ // We have to recreate a new DOM document from the current node
+ // otherwise the find function will look in the complet article and
+ // not only in the current paragraph. This is an ugly behavior of
+ // the library Simple HTML DOM Parser...
+ $html = str_get_html($paragraph->outertext);
+ $fullText .= $this->decodeParagraph($html);
+ }
- // For each paragraph, we will identify the encoding, then decode it
- // and finally store the decoded content in $text
- foreach ($paragraphs as $paragraph) {
- // We have to recreate a new DOM document from the current node
- // otherwise the find function will look in the complet article and
- // not only in the current paragraph. This is an ugly behavior of
- // the library Simple HTML DOM Parser...
- $html = str_get_html($paragraph->outertext);
- $fullText .= $this->decodeParagraph($html);
- }
+ // Finally we return the full 'well' enconded content of the article
+ return $fullText;
+ }
- // Finally we return the full 'well' enconded content of the article
- return $fullText;
- }
-
- /**
- *
- */
- private function decodeParagraph($p)
- {
- // First we check if this paragraph is a video
- $videoUrl = $this->getVideoUrl($p);
- if (!empty($videoUrl)) {
- // If this is a video, we just return a link to the video
- // &#128250; => 🎞️
- return '<p>
+ /**
+ *
+ */
+ private function decodeParagraph($p)
+ {
+ // First we check if this paragraph is a video
+ $videoUrl = $this->getVideoUrl($p);
+ if (!empty($videoUrl)) {
+ // If this is a video, we just return a link to the video
+ // &#128250; => 🎞️
+ return '<p>
<b>&#128250; <a href="' . $videoUrl . '">Voir la vidéo</a></b>
</p>';
- }
+ }
- // We take outertext to get the complete paragraph not only the text
- // inside it. That way we still graph block <img> and so on.
- $pTxt = $p->outertext;
- // This will store the decoded text if we manage to decode it
- $decodedTxt = '';
+ // We take outertext to get the complete paragraph not only the text
+ // inside it. That way we still graph block <img> and so on.
+ $pTxt = $p->outertext;
+ // This will store the decoded text if we manage to decode it
+ $decodedTxt = '';
- // This is the only way to properly decode each paragraph. I tried
- // many stuffs but this is the only working way I found.
- foreach (self::ENCONDINGS as $enc) {
- // We check the encoding of the current paragraph
- if (mb_check_encoding($pTxt, $enc)) {
- // If the encoding is well recognized, we can convert from
- // this encoding to UTF-8
- $decodedTxt = iconv($enc, 'UTF-8', $pTxt);
- }
- }
+ // This is the only way to properly decode each paragraph. I tried
+ // many stuffs but this is the only working way I found.
+ foreach (self::ENCONDINGS as $enc) {
+ // We check the encoding of the current paragraph
+ if (mb_check_encoding($pTxt, $enc)) {
+ // If the encoding is well recognized, we can convert from
+ // this encoding to UTF-8
+ $decodedTxt = iconv($enc, 'UTF-8', $pTxt);
+ }
+ }
- // We should not trim the strings to avoid the <a> to be glued to the
- // text like: the software<a href="...">started</a>to...
- if (!empty($decodedTxt)) {
- // We manage to decode the text, so we take the decoded version
- return $this->formatParagraph($decodedTxt);
- } else {
- // Otherwise we take the non decoded version and hope it will
- // be displayed not too ugly in the fulltext content
- return $this->formatParagraph($pTxt);
- }
- }
+ // We should not trim the strings to avoid the <a> to be glued to the
+ // text like: the software<a href="...">started</a>to...
+ if (!empty($decodedTxt)) {
+ // We manage to decode the text, so we take the decoded version
+ return $this->formatParagraph($decodedTxt);
+ } else {
+ // Otherwise we take the non decoded version and hope it will
+ // be displayed not too ugly in the fulltext content
+ return $this->formatParagraph($pTxt);
+ }
+ }
- /**
- * Return true in $txt is a HTML tag and not plain text
- */
- private function isHtmlTagNotTxt($txt)
- {
- $html = str_get_html($txt);
- return $html && $html->root && count($html->root->children) > 0;
- }
+ /**
+ * Return true in $txt is a HTML tag and not plain text
+ */
+ private function isHtmlTagNotTxt($txt)
+ {
+ $html = str_get_html($txt);
+ return $html && $html->root && count($html->root->children) > 0;
+ }
- /**
- * Will add a space before paragraph when needed
- */
- private function formatParagraph($txt)
- {
- // If the paragraph is an html tag, we add a space before
- if ($this->isHtmlTagNotTxt($txt)) {
- // the first element is an html tag and not a text, so we can add a
- // space before it
- return ' ' . $txt;
- }
- // If the text start with word (not punctation), we had a space
- $pattern = '/^\w/';
- if (preg_match($pattern, $txt)) {
- return ' ' . $txt;
- }
- return $txt;
- }
+ /**
+ * Will add a space before paragraph when needed
+ */
+ private function formatParagraph($txt)
+ {
+ // If the paragraph is an html tag, we add a space before
+ if ($this->isHtmlTagNotTxt($txt)) {
+ // the first element is an html tag and not a text, so we can add a
+ // space before it
+ return ' ' . $txt;
+ }
+ // If the text start with word (not punctation), we had a space
+ $pattern = '/^\w/';
+ if (preg_match($pattern, $txt)) {
+ return ' ' . $txt;
+ }
+ return $txt;
+ }
- /**
- * Retrieve all video url in the article
- */
- private function getAllVideoUrl($item)
- {
- // Array of video url
- $url = array();
+ /**
+ * Retrieve all video url in the article
+ */
+ private function getAllVideoUrl($item)
+ {
+ // Array of video url
+ $url = [];
- // Developpez use a div with the class video-container
- $divsVideo = $item->find('div.video-container');
- if (empty($divsVideo)) {
- return $url;
- }
+ // Developpez use a div with the class video-container
+ $divsVideo = $item->find('div.video-container');
+ if (empty($divsVideo)) {
+ return $url;
+ }
- // get the url of the video
- foreach ($divsVideo as $div) {
- $html = str_get_html($div->outertext);
- $url[] = $this->getVideoUrl($html);
- }
+ // get the url of the video
+ foreach ($divsVideo as $div) {
+ $html = str_get_html($div->outertext);
+ $url[] = $this->getVideoUrl($html);
+ }
- return $url;
- }
+ return $url;
+ }
- /**
- * Retrieve URL video. We have to check for the src of an iframe
- * Work for Youtube. Will have to test for other video platform
- */
- private function getVideoUrl($p)
- {
- $divVideo = $p->find('div.video-container', 0);
- if (empty($divVideo)) {
- return null;
- }
- $iframe = $divVideo->find('iframe', 0);
- if (empty($iframe)) {
- return null;
- }
- $src = trim($iframe->getAttribute('src'));
- if (empty($src)) {
- return null;
- }
- if (str_starts_with($src, '//')) {
- $src = 'https:' . $src;
- }
- return $src;
- }
+ /**
+ * Retrieve URL video. We have to check for the src of an iframe
+ * Work for Youtube. Will have to test for other video platform
+ */
+ private function getVideoUrl($p)
+ {
+ $divVideo = $p->find('div.video-container', 0);
+ if (empty($divVideo)) {
+ return null;
+ }
+ $iframe = $divVideo->find('iframe', 0);
+ if (empty($iframe)) {
+ return null;
+ }
+ $src = trim($iframe->getAttribute('src'));
+ if (empty($src)) {
+ return null;
+ }
+ if (str_starts_with($src, '//')) {
+ $src = 'https:' . $src;
+ }
+ return $src;
+ }
}
diff --git a/bridges/DiarioDeNoticiasBridge.php b/bridges/DiarioDeNoticiasBridge.php
index b3ab85a1..ac51eb44 100644
--- a/bridges/DiarioDeNoticiasBridge.php
+++ b/bridges/DiarioDeNoticiasBridge.php
@@ -1,84 +1,89 @@
<?php
-class DiarioDeNoticiasBridge extends BridgeAbstract {
- const NAME = 'Diário de Notícias (PT)';
- const URI = 'https://dn.pt';
- const DESCRIPTION = 'Diário de Notícias (DN.PT)';
- const MAINTAINER = 'somini';
- const PARAMETERS = array(
- 'Tag' => array(
- 'n' => array(
- 'name' => 'Tag Name',
- 'required' => true,
- 'exampleValue' => 'rogerio-casanova',
- )
- )
- );
- const MONPT = array(
- 'jan',
- 'fev',
- 'mar',
- 'abr',
- 'mai',
- 'jun',
- 'jul',
- 'ago',
- 'set',
- 'out',
- 'nov',
- 'dez',
- );
+class DiarioDeNoticiasBridge extends BridgeAbstract
+{
+ const NAME = 'Diário de Notícias (PT)';
+ const URI = 'https://dn.pt';
+ const DESCRIPTION = 'Diário de Notícias (DN.PT)';
+ const MAINTAINER = 'somini';
+ const PARAMETERS = [
+ 'Tag' => [
+ 'n' => [
+ 'name' => 'Tag Name',
+ 'required' => true,
+ 'exampleValue' => 'rogerio-casanova',
+ ]
+ ]
+ ];
- public function getIcon() {
- return 'https://static.globalnoticias.pt/dn/common/images/favicons/favicon-128.png';
- }
+ const MONPT = [
+ 'jan',
+ 'fev',
+ 'mar',
+ 'abr',
+ 'mai',
+ 'jun',
+ 'jul',
+ 'ago',
+ 'set',
+ 'out',
+ 'nov',
+ 'dez',
+ ];
- public function getName() {
- switch($this->queriedContext) {
- case 'Tag':
- $name = self::NAME . ' | Tag | ' . $this->getInput('n');
- break;
- default:
- $name = self::NAME;
- }
- return $name;
- }
+ public function getIcon()
+ {
+ return 'https://static.globalnoticias.pt/dn/common/images/favicons/favicon-128.png';
+ }
- public function getURI() {
- switch($this->queriedContext) {
- case 'Tag':
- $url = self::URI . '/tag/' . $this->getInput('n') . '.html';
- break;
- default:
- $url = self::URI;
- }
- return $url;
- }
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Tag':
+ $name = self::NAME . ' | Tag | ' . $this->getInput('n');
+ break;
+ default:
+ $name = self::NAME;
+ }
+ return $name;
+ }
- public function collectData() {
- $archives = self::getURI();
- $html = getSimpleHTMLDOMCached($archives);
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Tag':
+ $url = self::URI . '/tag/' . $this->getInput('n') . '.html';
+ break;
+ default:
+ $url = self::URI;
+ }
+ return $url;
+ }
- foreach($html->find('article') as $element) {
- $item = array();
+ public function collectData()
+ {
+ $archives = self::getURI();
+ $html = getSimpleHTMLDOMCached($archives);
- $title = $element->find('.t-am-title', 0);
- $link = $element->find('a.t-am-text', 0);
+ foreach ($html->find('article') as $element) {
+ $item = [];
- $item['title'] = $title->plaintext;
- $item['uri'] = self::URI . $link->href;
+ $title = $element->find('.t-am-title', 0);
+ $link = $element->find('a.t-am-text', 0);
- $snippet = $element->find('.t-am-lead', 0);
- if ($snippet) {
- $item['content'] = $snippet->plaintext;
- }
- preg_match('|edicao-do-dia\\/(?P<day>\d\d)-(?P<monpt>\w\w\w)-(?P<year>\d\d\d\d)|', $link->href, $d);
- if ($d) {
- $item['timestamp'] = sprintf('%s-%s-%s', $d['year'], array_search($d['monpt'], self::MONPT) + 1, $d['day']);
- }
+ $item['title'] = $title->plaintext;
+ $item['uri'] = self::URI . $link->href;
- $this->items[] = $item;
- }
+ $snippet = $element->find('.t-am-lead', 0);
+ if ($snippet) {
+ $item['content'] = $snippet->plaintext;
+ }
+ preg_match('|edicao-do-dia\\/(?P<day>\d\d)-(?P<monpt>\w\w\w)-(?P<year>\d\d\d\d)|', $link->href, $d);
+ if ($d) {
+ $item['timestamp'] = sprintf('%s-%s-%s', $d['year'], array_search($d['monpt'], self::MONPT) + 1, $d['day']);
+ }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/DiarioDoAlentejoBridge.php b/bridges/DiarioDoAlentejoBridge.php
index 6e43b876..9b82b49f 100644
--- a/bridges/DiarioDoAlentejoBridge.php
+++ b/bridges/DiarioDoAlentejoBridge.php
@@ -1,59 +1,68 @@
<?php
-class DiarioDoAlentejoBridge extends BridgeAbstract {
- const MAINTAINER = 'somini';
- const NAME = 'Diário do Alentejo';
- const URI = 'https://www.diariodoalentejo.pt';
- const DESCRIPTION = 'Semanário Regionalista Independente';
- const CACHE_TIMEOUT = 28800; // 8h
-
- /* This is used to hack around obtaining a timestamp. It's just a list of Month names in Portuguese ... */
- const PT_MONTH_NAMES = array(
- 'janeiro',
- 'fevereiro',
- 'março',
- 'abril',
- 'maio',
- 'junho',
- 'julho',
- 'agosto',
- 'setembro',
- 'outubro',
- 'novembro',
- 'dezembro');
-
- public function getIcon() {
- return 'https://www.diariodoalentejo.pt/images/favicon/apple-touch-icon.png';
- }
-
- public function collectData(){
- /* This is slow as molasses (>30s!), keep the cache timeout high to avoid killing the host */
- $html = getSimpleHTMLDOMCached($this->getURI() . '/pt/noticias-listagem.aspx');
-
- foreach($html->find('.list_news .item') as $element) {
- $item = array();
-
- $item_link = $element->find('.body h2.title a', 0);
- /* Another broken URL, see also `bridges/ComboiosDePortugalBridge.php` */
- $item['uri'] = self::URI . implode('/', array_map('urlencode', explode('/', $item_link->href)));
- $item['title'] = $item_link->innertext;
-
- $item['timestamp'] = str_ireplace(
- array_map(function($name) { return ' ' . $name . ' '; }, self::PT_MONTH_NAMES),
- array_map(function($num) { return sprintf('-%02d-', $num); }, range(1, sizeof(self::PT_MONTH_NAMES))),
- $element->find('span.date', 0)->innertext);
-
- /* Fix the Image URL */
- $item_image = $element->find('img.thumb', 0);
- $item_image->src = preg_replace('/.*&img=([^&]+).*/', '\1', $item_image->getAttribute('data-src'));
-
- /* Content: */
- /* - Image */
- /* - Category */
- $content = $item_image .
- '<center>' . $element->find('a.category', 0) . '</center>';
- $item['content'] = defaultLinkTo($content, self::URI);
-
- $this->items[] = $item;
- }
- }
+
+class DiarioDoAlentejoBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'somini';
+ const NAME = 'Diário do Alentejo';
+ const URI = 'https://www.diariodoalentejo.pt';
+ const DESCRIPTION = 'Semanário Regionalista Independente';
+ const CACHE_TIMEOUT = 28800; // 8h
+
+ /* This is used to hack around obtaining a timestamp. It's just a list of Month names in Portuguese ... */
+ const PT_MONTH_NAMES = [
+ 'janeiro',
+ 'fevereiro',
+ 'março',
+ 'abril',
+ 'maio',
+ 'junho',
+ 'julho',
+ 'agosto',
+ 'setembro',
+ 'outubro',
+ 'novembro',
+ 'dezembro'];
+
+ public function getIcon()
+ {
+ return 'https://www.diariodoalentejo.pt/images/favicon/apple-touch-icon.png';
+ }
+
+ public function collectData()
+ {
+ /* This is slow as molasses (>30s!), keep the cache timeout high to avoid killing the host */
+ $html = getSimpleHTMLDOMCached($this->getURI() . '/pt/noticias-listagem.aspx');
+
+ foreach ($html->find('.list_news .item') as $element) {
+ $item = [];
+
+ $item_link = $element->find('.body h2.title a', 0);
+ /* Another broken URL, see also `bridges/ComboiosDePortugalBridge.php` */
+ $item['uri'] = self::URI . implode('/', array_map('urlencode', explode('/', $item_link->href)));
+ $item['title'] = $item_link->innertext;
+
+ $item['timestamp'] = str_ireplace(
+ array_map(function ($name) {
+ return ' ' . $name . ' ';
+ }, self::PT_MONTH_NAMES),
+ array_map(function ($num) {
+ return sprintf('-%02d-', $num);
+ }, range(1, sizeof(self::PT_MONTH_NAMES))),
+ $element->find('span.date', 0)->innertext
+ );
+
+ /* Fix the Image URL */
+ $item_image = $element->find('img.thumb', 0);
+ $item_image->src = preg_replace('/.*&img=([^&]+).*/', '\1', $item_image->getAttribute('data-src'));
+
+ /* Content: */
+ /* - Image */
+ /* - Category */
+ $content = $item_image .
+ '<center>' . $element->find('a.category', 0) . '</center>';
+ $item['content'] = defaultLinkTo($content, self::URI);
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/DiceBridge.php b/bridges/DiceBridge.php
index ced793fe..5b764aef 100644
--- a/bridges/DiceBridge.php
+++ b/bridges/DiceBridge.php
@@ -1,123 +1,127 @@
<?php
-class DiceBridge extends BridgeAbstract {
- const MAINTAINER = 'rogerdc';
- const NAME = 'Dice Unofficial RSS';
- const URI = 'https://www.dice.com/';
- const DESCRIPTION = 'The Unofficial Dice RSS';
- // const CACHE_TIMEOUT = 86400; // 1 day
+class DiceBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'rogerdc';
+ const NAME = 'Dice Unofficial RSS';
+ const URI = 'https://www.dice.com/';
+ const DESCRIPTION = 'The Unofficial Dice RSS';
+ // const CACHE_TIMEOUT = 86400; // 1 day
- const PARAMETERS = array(array(
- 'for_one' => array(
- 'name' => 'With at least one of the words',
- 'required' => false,
- ),
- 'for_all' => array(
- 'name' => 'With all of the words',
- 'required' => false,
- ),
- 'for_exact' => array(
- 'name' => 'With the exact phrase',
- 'required' => false,
- ),
- 'for_none' => array(
- 'name' => 'With none of these words',
- 'required' => false,
- ),
- 'for_jt' => array(
- 'name' => 'Within job title',
- 'required' => false,
- ),
- 'for_com' => array(
- 'name' => 'Within company name',
- 'required' => false,
- ),
- 'for_loc' => array(
- 'name' => 'City, State, or ZIP code',
- 'required' => false,
- ),
- 'radius' => array(
- 'name' => 'Radius in miles',
- 'type' => 'list',
- 'required' => false,
- 'values' => array(
- 'Exact Location' => 'El',
- 'Within 5 miles' => '5',
- 'Within 10 miles' => '10',
- 'Within 20 miles' => '20',
- 'Within 30 miles' => '0',
- 'Within 40 miles' => '40',
- 'Within 50 miles' => '50',
- 'Within 75 miles' => '75',
- 'Within 100 miles' => '100',
- ),
- 'defaultValue' => '0',
- ),
- 'jtype' => array(
- 'name' => 'Job type',
- 'type' => 'list',
- 'required' => false,
- 'values' => array(
- 'Full-Time' => 'Full Time',
- 'Part-Time' => 'Part Time',
- 'Contract - Independent' => 'Contract Independent',
- 'Contract - W2' => 'Contract W2',
- 'Contract to Hire - Independent' => 'C2H Independent',
- 'Contract to Hire - W2' => 'C2H W2',
- 'Third Party - Contract - Corp-to-Corp' => 'Contract Corp-To-Corp',
- 'Third Party - Contract to Hire - Corp-to-Corp' => 'C2H Corp-To-Corp',
- ),
- 'defaultValue' => 'Full Time',
- ),
- 'telecommute' => array(
- 'name' => 'Telecommute',
- 'type' => 'checkbox',
- ),
- ));
+ const PARAMETERS = [[
+ 'for_one' => [
+ 'name' => 'With at least one of the words',
+ 'required' => false,
+ ],
+ 'for_all' => [
+ 'name' => 'With all of the words',
+ 'required' => false,
+ ],
+ 'for_exact' => [
+ 'name' => 'With the exact phrase',
+ 'required' => false,
+ ],
+ 'for_none' => [
+ 'name' => 'With none of these words',
+ 'required' => false,
+ ],
+ 'for_jt' => [
+ 'name' => 'Within job title',
+ 'required' => false,
+ ],
+ 'for_com' => [
+ 'name' => 'Within company name',
+ 'required' => false,
+ ],
+ 'for_loc' => [
+ 'name' => 'City, State, or ZIP code',
+ 'required' => false,
+ ],
+ 'radius' => [
+ 'name' => 'Radius in miles',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => [
+ 'Exact Location' => 'El',
+ 'Within 5 miles' => '5',
+ 'Within 10 miles' => '10',
+ 'Within 20 miles' => '20',
+ 'Within 30 miles' => '0',
+ 'Within 40 miles' => '40',
+ 'Within 50 miles' => '50',
+ 'Within 75 miles' => '75',
+ 'Within 100 miles' => '100',
+ ],
+ 'defaultValue' => '0',
+ ],
+ 'jtype' => [
+ 'name' => 'Job type',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => [
+ 'Full-Time' => 'Full Time',
+ 'Part-Time' => 'Part Time',
+ 'Contract - Independent' => 'Contract Independent',
+ 'Contract - W2' => 'Contract W2',
+ 'Contract to Hire - Independent' => 'C2H Independent',
+ 'Contract to Hire - W2' => 'C2H W2',
+ 'Third Party - Contract - Corp-to-Corp' => 'Contract Corp-To-Corp',
+ 'Third Party - Contract to Hire - Corp-to-Corp' => 'C2H Corp-To-Corp',
+ ],
+ 'defaultValue' => 'Full Time',
+ ],
+ 'telecommute' => [
+ 'name' => 'Telecommute',
+ 'type' => 'checkbox',
+ ],
+ ]];
- public function getIcon() {
- return 'https://assets.dice.com/techpro/img/favicons/favicon.ico';
- }
+ public function getIcon()
+ {
+ return 'https://assets.dice.com/techpro/img/favicons/favicon.ico';
+ }
- public function collectData() {
- $uri = 'https://www.dice.com/jobs/advancedResult.html';
- $uri .= '?for_one=' . urlencode($this->getInput('for_one'));
- $uri .= '&for_all=' . urlencode($this->getInput('for_all'));
- $uri .= '&for_exact=' . urlencode($this->getInput('for_exact'));
- $uri .= '&for_none=' . urlencode($this->getInput('for_none'));
- $uri .= '&for_jt=' . urlencode($this->getInput('for_jt'));
- $uri .= '&for_com=' . urlencode($this->getInput('for_com'));
- $uri .= '&for_loc=' . urlencode($this->getInput('for_loc'));
- if ($this->getInput('jtype')) {
- $uri .= '&jtype=' . urlencode($this->getInput('jtype'));
- }
- $uri .= '&sort=date&limit=100';
- $uri .= '&radius=' . urlencode($this->getInput('radius'));
- if ($this->getInput('telecommute')) {
- $uri .= '&telecommute=true';
- }
+ public function collectData()
+ {
+ $uri = 'https://www.dice.com/jobs/advancedResult.html';
+ $uri .= '?for_one=' . urlencode($this->getInput('for_one'));
+ $uri .= '&for_all=' . urlencode($this->getInput('for_all'));
+ $uri .= '&for_exact=' . urlencode($this->getInput('for_exact'));
+ $uri .= '&for_none=' . urlencode($this->getInput('for_none'));
+ $uri .= '&for_jt=' . urlencode($this->getInput('for_jt'));
+ $uri .= '&for_com=' . urlencode($this->getInput('for_com'));
+ $uri .= '&for_loc=' . urlencode($this->getInput('for_loc'));
+ if ($this->getInput('jtype')) {
+ $uri .= '&jtype=' . urlencode($this->getInput('jtype'));
+ }
+ $uri .= '&sort=date&limit=100';
+ $uri .= '&radius=' . urlencode($this->getInput('radius'));
+ if ($this->getInput('telecommute')) {
+ $uri .= '&telecommute=true';
+ }
- $html = getSimpleHTMLDOM($uri);
- foreach($html->find('div.complete-serp-result-div') as $element) {
- $item = array();
- // Title
- $masterLink = $element->find('a[id^=position]', 0);
- $item['title'] = $masterLink->title;
- // URL
- $uri = $masterLink->href;
- // $uri = substr($uri, 0, strrpos($uri, '?'));
- $item['uri'] = substr($uri, 0, strrpos($uri, '?'));
- // ID
- $item['id'] = $masterLink->value;
- // Image
- $image = $element->find('img', 0);
- if ($image)
- $item['image'] = $image->getAttribute('src');
- // Content
- $shortdesc = $element->find('.shortdesc', '0');
- $shortdesc = ($shortdesc) ? $shortdesc->innertext : '';
- $item['content'] = $shortdesc;
- $this->items[] = $item;
- }
- }
+ $html = getSimpleHTMLDOM($uri);
+ foreach ($html->find('div.complete-serp-result-div') as $element) {
+ $item = [];
+ // Title
+ $masterLink = $element->find('a[id^=position]', 0);
+ $item['title'] = $masterLink->title;
+ // URL
+ $uri = $masterLink->href;
+ // $uri = substr($uri, 0, strrpos($uri, '?'));
+ $item['uri'] = substr($uri, 0, strrpos($uri, '?'));
+ // ID
+ $item['id'] = $masterLink->value;
+ // Image
+ $image = $element->find('img', 0);
+ if ($image) {
+ $item['image'] = $image->getAttribute('src');
+ }
+ // Content
+ $shortdesc = $element->find('.shortdesc', '0');
+ $shortdesc = ($shortdesc) ? $shortdesc->innertext : '';
+ $item['content'] = $shortdesc;
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/DilbertBridge.php b/bridges/DilbertBridge.php
index 827355d5..cd509ea4 100644
--- a/bridges/DilbertBridge.php
+++ b/bridges/DilbertBridge.php
@@ -1,35 +1,36 @@
<?php
-class DilbertBridge extends BridgeAbstract {
- const MAINTAINER = 'kranack';
- const NAME = 'Dilbert Daily Strip';
- const URI = 'https://dilbert.com';
- const CACHE_TIMEOUT = 21600; // 6h
- const DESCRIPTION = 'The Unofficial Dilbert Daily Comic Strip';
+class DilbertBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'kranack';
+ const NAME = 'Dilbert Daily Strip';
+ const URI = 'https://dilbert.com';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'The Unofficial Dilbert Daily Comic Strip';
- public function collectData(){
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
- $html = getSimpleHTMLDOM(self::URI);
+ foreach ($html->find('section.comic-item') as $element) {
+ $img = $element->find('img', 0);
+ $link = $element->find('a', 0);
+ $comic = $img->src;
+ $title = $img->alt;
+ $url = $link->href;
+ $date = substr(strrchr($url, '/'), 1);
+ if (empty($title)) {
+ $title = 'Dilbert Comic Strip on ' . $date;
+ }
+ $date = strtotime($date);
- foreach($html->find('section.comic-item') as $element) {
-
- $img = $element->find('img', 0);
- $link = $element->find('a', 0);
- $comic = $img->src;
- $title = $img->alt;
- $url = $link->href;
- $date = substr(strrchr($url, '/'), 1);
- if (empty($title))
- $title = 'Dilbert Comic Strip on ' . $date;
- $date = strtotime($date);
-
- $item = array();
- $item['uri'] = $url;
- $item['title'] = $title;
- $item['author'] = 'Scott Adams';
- $item['timestamp'] = $date;
- $item['content'] = '<img src="' . $comic . '" alt="' . $img->alt . '" />';
- $this->items[] = $item;
- }
- }
+ $item = [];
+ $item['uri'] = $url;
+ $item['title'] = $title;
+ $item['author'] = 'Scott Adams';
+ $item['timestamp'] = $date;
+ $item['content'] = '<img src="' . $comic . '" alt="' . $img->alt . '" />';
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/DiscogsBridge.php b/bridges/DiscogsBridge.php
index df94a030..ba011924 100644
--- a/bridges/DiscogsBridge.php
+++ b/bridges/DiscogsBridge.php
@@ -1,120 +1,114 @@
<?php
-class DiscogsBridge extends BridgeAbstract {
-
- const MAINTAINER = 'teromene';
- const NAME = 'DiscogsBridge';
- const URI = 'https://www.discogs.com/';
- const DESCRIPTION = 'Returns releases from discogs';
- const PARAMETERS = array(
- 'Artist Releases' => array(
- 'artistid' => array(
- 'name' => 'Artist ID',
- 'type' => 'number',
- 'required' => true,
- 'exampleValue' => '28104',
- 'title' => 'Only the ID from an artist page. EG /artist/28104-Aesop-Rock is 28104'
- )
- ),
- 'Label Releases' => array(
- 'labelid' => array(
- 'name' => 'Label ID',
- 'type' => 'number',
- 'required' => true,
- 'exampleValue' => '8201',
- 'title' => 'Only the ID from a label page. EG /label/8201-Rhymesayers-Entertainment is 8201'
- )
- ),
- 'User Wantlist' => array(
- 'username_wantlist' => array(
- 'name' => 'Username',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'TheBlindMaster',
- )
- ),
- 'User Folder' => array(
- 'username_folder' => array(
- 'name' => 'Username',
- 'type' => 'text',
- ),
- 'folderid' => array(
- 'name' => 'Folder ID',
- 'type' => 'number',
- )
- )
- );
-
- public function collectData() {
-
- if(!empty($this->getInput('artistid')) || !empty($this->getInput('labelid'))) {
-
- if(!empty($this->getInput('artistid'))) {
- $data = getContents('https://api.discogs.com/artists/'
- . $this->getInput('artistid')
- . '/releases?sort=year&sort_order=desc');
- } elseif(!empty($this->getInput('labelid'))) {
- $data = getContents('https://api.discogs.com/labels/'
- . $this->getInput('labelid')
- . '/releases?sort=year&sort_order=desc');
- }
-
- $jsonData = json_decode($data, true);
- foreach($jsonData['releases'] as $release) {
-
- $item = array();
- $item['author'] = $release['artist'];
- $item['title'] = $release['title'];
- $item['id'] = $release['id'];
- $resId = array_key_exists('main_release', $release) ? $release['main_release'] : $release['id'];
- $item['uri'] = self::URI . $this->getInput('artistid') . '/release/' . $resId;
-
- if(isset($release['year'])) {
- $item['timestamp'] = DateTime::createFromFormat('Y', $release['year'])->getTimestamp();
- }
-
- $item['content'] = $item['author'] . ' - ' . $item['title'];
- $this->items[] = $item;
- }
-
- } elseif(!empty($this->getInput('username_wantlist')) || !empty($this->getInput('username_folder'))) {
-
- if(!empty($this->getInput('username_wantlist'))) {
- $data = getContents('https://api.discogs.com/users/'
- . $this->getInput('username_wantlist')
- . '/wants?sort=added&sort_order=desc');
- $jsonData = json_decode($data, true)['wants'];
-
- } elseif(!empty($this->getInput('username_folder'))) {
- $data = getContents('https://api.discogs.com/users/'
- . $this->getInput('username_folder')
- . '/collection/folders/'
- . $this->getInput('folderid')
- . '/releases?sort=added&sort_order=desc');
- $jsonData = json_decode($data, true)['releases'];
- }
- foreach($jsonData as $element) {
-
- $infos = $element['basic_information'];
- $item = array();
- $item['title'] = $infos['title'];
- $item['author'] = $infos['artists'][0]['name'];
- $item['id'] = $infos['artists'][0]['id'];
- $item['uri'] = self::URI . $infos['artists'][0]['id'] . '/release/' . $infos['id'];
- $item['timestamp'] = strtotime($element['date_added']);
- $item['content'] = $item['author'] . ' - ' . $item['title'];
- $this->items[] = $item;
-
- }
- }
-
- }
-
- public function getURI() {
- return self::URI;
- }
-
- public function getName() {
- return static::NAME;
- }
+class DiscogsBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'teromene';
+ const NAME = 'DiscogsBridge';
+ const URI = 'https://www.discogs.com/';
+ const DESCRIPTION = 'Returns releases from discogs';
+ const PARAMETERS = [
+ 'Artist Releases' => [
+ 'artistid' => [
+ 'name' => 'Artist ID',
+ 'type' => 'number',
+ 'required' => true,
+ 'exampleValue' => '28104',
+ 'title' => 'Only the ID from an artist page. EG /artist/28104-Aesop-Rock is 28104'
+ ]
+ ],
+ 'Label Releases' => [
+ 'labelid' => [
+ 'name' => 'Label ID',
+ 'type' => 'number',
+ 'required' => true,
+ 'exampleValue' => '8201',
+ 'title' => 'Only the ID from a label page. EG /label/8201-Rhymesayers-Entertainment is 8201'
+ ]
+ ],
+ 'User Wantlist' => [
+ 'username_wantlist' => [
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'TheBlindMaster',
+ ]
+ ],
+ 'User Folder' => [
+ 'username_folder' => [
+ 'name' => 'Username',
+ 'type' => 'text',
+ ],
+ 'folderid' => [
+ 'name' => 'Folder ID',
+ 'type' => 'number',
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ if (!empty($this->getInput('artistid')) || !empty($this->getInput('labelid'))) {
+ if (!empty($this->getInput('artistid'))) {
+ $data = getContents('https://api.discogs.com/artists/'
+ . $this->getInput('artistid')
+ . '/releases?sort=year&sort_order=desc');
+ } elseif (!empty($this->getInput('labelid'))) {
+ $data = getContents('https://api.discogs.com/labels/'
+ . $this->getInput('labelid')
+ . '/releases?sort=year&sort_order=desc');
+ }
+
+ $jsonData = json_decode($data, true);
+ foreach ($jsonData['releases'] as $release) {
+ $item = [];
+ $item['author'] = $release['artist'];
+ $item['title'] = $release['title'];
+ $item['id'] = $release['id'];
+ $resId = array_key_exists('main_release', $release) ? $release['main_release'] : $release['id'];
+ $item['uri'] = self::URI . $this->getInput('artistid') . '/release/' . $resId;
+
+ if (isset($release['year'])) {
+ $item['timestamp'] = DateTime::createFromFormat('Y', $release['year'])->getTimestamp();
+ }
+
+ $item['content'] = $item['author'] . ' - ' . $item['title'];
+ $this->items[] = $item;
+ }
+ } elseif (!empty($this->getInput('username_wantlist')) || !empty($this->getInput('username_folder'))) {
+ if (!empty($this->getInput('username_wantlist'))) {
+ $data = getContents('https://api.discogs.com/users/'
+ . $this->getInput('username_wantlist')
+ . '/wants?sort=added&sort_order=desc');
+ $jsonData = json_decode($data, true)['wants'];
+ } elseif (!empty($this->getInput('username_folder'))) {
+ $data = getContents('https://api.discogs.com/users/'
+ . $this->getInput('username_folder')
+ . '/collection/folders/'
+ . $this->getInput('folderid')
+ . '/releases?sort=added&sort_order=desc');
+ $jsonData = json_decode($data, true)['releases'];
+ }
+ foreach ($jsonData as $element) {
+ $infos = $element['basic_information'];
+ $item = [];
+ $item['title'] = $infos['title'];
+ $item['author'] = $infos['artists'][0]['name'];
+ $item['id'] = $infos['artists'][0]['id'];
+ $item['uri'] = self::URI . $infos['artists'][0]['id'] . '/release/' . $infos['id'];
+ $item['timestamp'] = strtotime($element['date_added']);
+ $item['content'] = $item['author'] . ' - ' . $item['title'];
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ public function getURI()
+ {
+ return self::URI;
+ }
+
+ public function getName()
+ {
+ return static::NAME;
+ }
}
diff --git a/bridges/DockerHubBridge.php b/bridges/DockerHubBridge.php
index 343f832e..4ee72f5d 100644
--- a/bridges/DockerHubBridge.php
+++ b/bridges/DockerHubBridge.php
@@ -1,77 +1,81 @@
<?php
-class DockerHubBridge extends BridgeAbstract {
- const NAME = 'Docker Hub Bridge';
- const URI = 'https://hub.docker.com';
- const DESCRIPTION = 'Returns new images for a container';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(
- 'User Submitted Image' => array(
- 'user' => array(
- 'name' => 'User',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'rssbridge',
- ),
- 'repo' => array(
- 'name' => 'Repository',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'rss-bridge',
- )
- ),
- 'Official Image' => array(
- 'repo' => array(
- 'name' => 'Repository',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'postgres',
- )
- ),
- );
-
- const CACHE_TIMEOUT = 3600; // 1 hour
-
- private $apiURL = 'https://hub.docker.com/v2/repositories/';
- private $imageUrlRegex = '/hub\.docker\.com\/r\/([\w]+)\/([\w-]+)\/?/';
- private $officialImageUrlRegex = '/hub\.docker\.com\/_\/([\w-]+)\/?/';
-
- public function detectParameters($url) {
- $params = array();
-
- // user submitted image
- if(preg_match($this->imageUrlRegex, $url, $matches)) {
- $params['context'] = 'User Submitted Image';
- $params['user'] = $matches[1];
- $params['repo'] = $matches[2];
- return $params;
- }
-
- // official image
- if(preg_match($this->officialImageUrlRegex, $url, $matches)) {
- $params['context'] = 'Official Image';
- $params['repo'] = $matches[1];
- return $params;
- }
-
- return null;
- }
-
- public function collectData() {
- $json = getContents($this->getApiUrl());
-
- $data = json_decode($json, false);
-
- foreach ($data->results as $result) {
- $item = array();
-
- $lastPushed = date('Y-m-d H:i:s', strtotime($result->tag_last_pushed));
-
- $item['title'] = $result->name;
- $item['uid'] = $result->id;
- $item['uri'] = $this->getTagUrl($result->name);
- $item['author'] = $result->last_updater_username;
- $item['timestamp'] = $result->tag_last_pushed;
- $item['content'] = <<<EOD
+
+class DockerHubBridge extends BridgeAbstract
+{
+ const NAME = 'Docker Hub Bridge';
+ const URI = 'https://hub.docker.com';
+ const DESCRIPTION = 'Returns new images for a container';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [
+ 'User Submitted Image' => [
+ 'user' => [
+ 'name' => 'User',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'rssbridge',
+ ],
+ 'repo' => [
+ 'name' => 'Repository',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'rss-bridge',
+ ]
+ ],
+ 'Official Image' => [
+ 'repo' => [
+ 'name' => 'Repository',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'postgres',
+ ]
+ ],
+ ];
+
+ const CACHE_TIMEOUT = 3600; // 1 hour
+
+ private $apiURL = 'https://hub.docker.com/v2/repositories/';
+ private $imageUrlRegex = '/hub\.docker\.com\/r\/([\w]+)\/([\w-]+)\/?/';
+ private $officialImageUrlRegex = '/hub\.docker\.com\/_\/([\w-]+)\/?/';
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ // user submitted image
+ if (preg_match($this->imageUrlRegex, $url, $matches)) {
+ $params['context'] = 'User Submitted Image';
+ $params['user'] = $matches[1];
+ $params['repo'] = $matches[2];
+ return $params;
+ }
+
+ // official image
+ if (preg_match($this->officialImageUrlRegex, $url, $matches)) {
+ $params['context'] = 'Official Image';
+ $params['repo'] = $matches[1];
+ return $params;
+ }
+
+ return null;
+ }
+
+ public function collectData()
+ {
+ $json = getContents($this->getApiUrl());
+
+ $data = json_decode($json, false);
+
+ foreach ($data->results as $result) {
+ $item = [];
+
+ $lastPushed = date('Y-m-d H:i:s', strtotime($result->tag_last_pushed));
+
+ $item['title'] = $result->name;
+ $item['uid'] = $result->id;
+ $item['uri'] = $this->getTagUrl($result->name);
+ $item['author'] = $result->last_updater_username;
+ $item['timestamp'] = $result->tag_last_pushed;
+ $item['content'] = <<<EOD
<Strong>Tag</strong><br>
<p>{$result->name}</p>
<Strong>Last pushed</strong><br>
@@ -80,86 +84,93 @@ class DockerHubBridge extends BridgeAbstract {
{$this->getImages($result)}
EOD;
- $this->items[] = $item;
- }
-
- }
-
- public function getURI() {
- if ($this->queriedContext === 'Official Image') {
- return self::URI . '/_/' . $this->getRepo();
- }
-
- if ($this->getInput('repo')) {
- return self::URI . '/r/' . $this->getRepo();
- }
-
- return parent::getURI();
- }
-
- public function getName() {
- if ($this->getInput('repo')) {
- return $this->getRepo() . ' - Docker Hub';
- }
-
- return parent::getName();
- }
-
- private function getRepo() {
- if ($this->queriedContext === 'Official Image') {
- return $this->getInput('repo');
- }
-
- return $this->getInput('user') . '/' . $this->getInput('repo');
- }
-
- private function getApiUrl() {
- if ($this->queriedContext === 'Official Image') {
- return $this->apiURL . 'library/' . $this->getRepo() . '/tags/?page_size=25&page=1';
- }
-
- return $this->apiURL . $this->getRepo() . '/tags/?page_size=25&page=1';
- }
-
- private function getLayerUrl($name, $digest) {
- if ($this->queriedContext === 'Official Image') {
- return self::URI . '/layers/' . $this->getRepo() . '/library/' .
- $this->getRepo() . '/' . $name . '/images/' . $digest;
- }
-
- return self::URI . '/layers/' . $this->getRepo() . '/' . $name . '/images/' . $digest;
- }
-
- private function getTagUrl($name) {
- if ($this->queriedContext === 'Official Image') {
- return self::URI . '/_/' . $this->getRepo() . '?tab=tags&name=' . $name;
- }
-
- return self::URI . '/r/' . $this->getRepo() . '/tags?name=' . $name;
- }
-
- private function getImages($result) {
- $html = <<<EOD
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ if ($this->queriedContext === 'Official Image') {
+ return self::URI . '/_/' . $this->getRepo();
+ }
+
+ if ($this->getInput('repo')) {
+ return self::URI . '/r/' . $this->getRepo();
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if ($this->getInput('repo')) {
+ return $this->getRepo() . ' - Docker Hub';
+ }
+
+ return parent::getName();
+ }
+
+ private function getRepo()
+ {
+ if ($this->queriedContext === 'Official Image') {
+ return $this->getInput('repo');
+ }
+
+ return $this->getInput('user') . '/' . $this->getInput('repo');
+ }
+
+ private function getApiUrl()
+ {
+ if ($this->queriedContext === 'Official Image') {
+ return $this->apiURL . 'library/' . $this->getRepo() . '/tags/?page_size=25&page=1';
+ }
+
+ return $this->apiURL . $this->getRepo() . '/tags/?page_size=25&page=1';
+ }
+
+ private function getLayerUrl($name, $digest)
+ {
+ if ($this->queriedContext === 'Official Image') {
+ return self::URI . '/layers/' . $this->getRepo() . '/library/' .
+ $this->getRepo() . '/' . $name . '/images/' . $digest;
+ }
+
+ return self::URI . '/layers/' . $this->getRepo() . '/' . $name . '/images/' . $digest;
+ }
+
+ private function getTagUrl($name)
+ {
+ if ($this->queriedContext === 'Official Image') {
+ return self::URI . '/_/' . $this->getRepo() . '?tab=tags&name=' . $name;
+ }
+
+ return self::URI . '/r/' . $this->getRepo() . '/tags?name=' . $name;
+ }
+
+ private function getImages($result)
+ {
+ $html = <<<EOD
<table style="width:300px;"><thead><tr><th>Digest</th><th>OS/architecture</th></tr></thead></tbody>
EOD;
- foreach ($result->images as $image) {
- $layersUrl = $this->getLayerUrl($result->name, $image->digest);
- $id = $this->getShortDigestId($image->digest);
+ foreach ($result->images as $image) {
+ $layersUrl = $this->getLayerUrl($result->name, $image->digest);
+ $id = $this->getShortDigestId($image->digest);
- $html .= <<<EOD
+ $html .= <<<EOD
<tr>
<td><a href="{$layersUrl}">{$id}</a></td>
<td>{$image->os}/{$image->architecture}</td>
</tr>
EOD;
- }
+ }
- return $html . '</tbody></table>';
- }
+ return $html . '</tbody></table>';
+ }
- private function getShortDigestId($digest) {
- $parts = explode(':', $digest);
- return substr($parts[1], 0, 12);
- }
+ private function getShortDigestId($digest)
+ {
+ $parts = explode(':', $digest);
+ return substr($parts[1], 0, 12);
+ }
}
diff --git a/bridges/DonnonsBridge.php b/bridges/DonnonsBridge.php
index e499baae..a33a1013 100644
--- a/bridges/DonnonsBridge.php
+++ b/bridges/DonnonsBridge.php
@@ -1,79 +1,82 @@
<?php
+
/**
* Retourne les dons d'une recherche filtrée sur le site Donnons.org
* Example: https://donnons.org/Sport/Ile-de-France
*/
-class DonnonsBridge extends BridgeAbstract {
-
- const MAINTAINER = 'Binnette';
- const NAME = 'Donnons.org';
- const URI = 'https://donnons.org';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Retourne les dons depuis le site Donnons.org.';
-
- const PARAMETERS = array(
- array(
- 'q' => array(
- 'name' => 'Url de recherche',
- 'required' => true,
- 'exampleValue' => '/Sport/Ile-de-France',
- 'pattern' => '\/.*',
- 'title' => 'Faites une recherche sur le site. Puis copiez ici la fin de l’url. Doit commencer par /',
- ),
- 'p' => array(
- 'name' => 'Nombre de pages à scanner',
- 'type' => 'number',
- 'required' => true,
- 'defaultValue' => 5,
- 'title' => 'Indique le nombre de pages de donnons.org qui seront scannées'
- )
- )
- );
-
- public function collectData() {
- $pages = $this->getInput('p');
-
- for($i = 1; $i <= $pages; $i++) {
- $this->collectDataByPage($i);
- }
- }
-
- private function collectDataByPage($page) {
- $uri = $this->getPageURI($page);
-
- $html = getSimpleHTMLDOM($uri);
-
- $searchDiv = $html->find('div[id=search]', 0);
-
- if(!is_null($searchDiv)) {
- $elements = $searchDiv->find('a.lst-annonce');
- foreach($elements as $element) {
- $item = array();
-
- // Lien vers le don
- $item['uri'] = self::URI . $element->href;
- // Id de l'objet
- $item['uid'] = $element->getAttribute('data-id');
-
- // Grab info from json
- $jsonString = $element->find('script', 0)->innertext;
- $json = json_decode($jsonString, true);
-
- $name = $json['name'];
- $category = $json['category'];
- $date = $json['availabilityStarts'];
- $description = $json['description'];
- $city = $json['availableAtOrFrom']['address']['addressLocality'];
- $region = $json['availableAtOrFrom']['address']['addressRegion'];
-
- // Grab info from HTML
- $imageSrc = $element->find('img.ima-center', 0)->getAttribute('src');
- // Use large image instead of small one
- $imageSrc = str_replace('/xs/', '/lg/', $imageSrc);
- $image = self::URI . $imageSrc;
- $author = $element->find('div.avatar-holder', 0)->plaintext;
-
- $content = '
+class DonnonsBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Binnette';
+ const NAME = 'Donnons.org';
+ const URI = 'https://donnons.org';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Retourne les dons depuis le site Donnons.org.';
+
+ const PARAMETERS = [
+ [
+ 'q' => [
+ 'name' => 'Url de recherche',
+ 'required' => true,
+ 'exampleValue' => '/Sport/Ile-de-France',
+ 'pattern' => '\/.*',
+ 'title' => 'Faites une recherche sur le site. Puis copiez ici la fin de l’url. Doit commencer par /',
+ ],
+ 'p' => [
+ 'name' => 'Nombre de pages à scanner',
+ 'type' => 'number',
+ 'required' => true,
+ 'defaultValue' => 5,
+ 'title' => 'Indique le nombre de pages de donnons.org qui seront scannées'
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $pages = $this->getInput('p');
+
+ for ($i = 1; $i <= $pages; $i++) {
+ $this->collectDataByPage($i);
+ }
+ }
+
+ private function collectDataByPage($page)
+ {
+ $uri = $this->getPageURI($page);
+
+ $html = getSimpleHTMLDOM($uri);
+
+ $searchDiv = $html->find('div[id=search]', 0);
+
+ if (!is_null($searchDiv)) {
+ $elements = $searchDiv->find('a.lst-annonce');
+ foreach ($elements as $element) {
+ $item = [];
+
+ // Lien vers le don
+ $item['uri'] = self::URI . $element->href;
+ // Id de l'objet
+ $item['uid'] = $element->getAttribute('data-id');
+
+ // Grab info from json
+ $jsonString = $element->find('script', 0)->innertext;
+ $json = json_decode($jsonString, true);
+
+ $name = $json['name'];
+ $category = $json['category'];
+ $date = $json['availabilityStarts'];
+ $description = $json['description'];
+ $city = $json['availableAtOrFrom']['address']['addressLocality'];
+ $region = $json['availableAtOrFrom']['address']['addressRegion'];
+
+ // Grab info from HTML
+ $imageSrc = $element->find('img.ima-center', 0)->getAttribute('src');
+ // Use large image instead of small one
+ $imageSrc = str_replace('/xs/', '/lg/', $imageSrc);
+ $image = self::URI . $imageSrc;
+ $author = $element->find('div.avatar-holder', 0)->plaintext;
+
+ $content = '
<img style="margin-right:1em;" src="' . $image . '">
<div>
<h1>' . $name . '</h1>
@@ -84,42 +87,45 @@ class DonnonsBridge extends BridgeAbstract {
</div>
';
- // Titre du don
- $item['title'] = '[' . $category . '] ' . $name;
- $item['timestamp'] = $date;
- $item['author'] = $author;
- $item['content'] = $content;
- $item['enclosures'] = array($image);
-
- $this->items[] = $item;
- }
- }
- }
-
- private function getPageURI($page) {
- $uri = $this->getURI();
- $haveQueryParams = strpos($uri, '?') !== false;
-
- if($haveQueryParams) {
- return $uri . '&page=' . $page;
- } else {
- return $uri . '?page=' . $page;
- }
- }
-
- public function getURI() {
- if(!is_null($this->getInput('q'))) {
- return self::URI . $this->getInput('q');
- }
-
- return parent::getURI();
- }
-
- public function getName() {
- if(!is_null($this->getInput('q'))) {
- return 'Donnons.org - ' . $this->getInput('q');
- }
-
- return parent::getName();
- }
+ // Titre du don
+ $item['title'] = '[' . $category . '] ' . $name;
+ $item['timestamp'] = $date;
+ $item['author'] = $author;
+ $item['content'] = $content;
+ $item['enclosures'] = [$image];
+
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ private function getPageURI($page)
+ {
+ $uri = $this->getURI();
+ $haveQueryParams = strpos($uri, '?') !== false;
+
+ if ($haveQueryParams) {
+ return $uri . '&page=' . $page;
+ } else {
+ return $uri . '?page=' . $page;
+ }
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('q'))) {
+ return self::URI . $this->getInput('q');
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('q'))) {
+ return 'Donnons.org - ' . $this->getInput('q');
+ }
+
+ return parent::getName();
+ }
}
diff --git a/bridges/DribbbleBridge.php b/bridges/DribbbleBridge.php
index 0bb0eee6..3957c9de 100644
--- a/bridges/DribbbleBridge.php
+++ b/bridges/DribbbleBridge.php
@@ -1,103 +1,110 @@
<?php
-class DribbbleBridge extends BridgeAbstract {
- const MAINTAINER = 'quentinus95';
- const NAME = 'Dribbble popular shots';
- const URI = 'https://dribbble.com';
- const CACHE_TIMEOUT = 1800;
- const DESCRIPTION = 'Returns the newest popular shots from Dribbble.';
-
- public function getIcon() {
- return 'https://cdn.dribbble.com/assets/
+class DribbbleBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'quentinus95';
+ const NAME = 'Dribbble popular shots';
+ const URI = 'https://dribbble.com';
+ const CACHE_TIMEOUT = 1800;
+ const DESCRIPTION = 'Returns the newest popular shots from Dribbble.';
+
+ public function getIcon()
+ {
+ return 'https://cdn.dribbble.com/assets/
favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
- }
-
- public function collectData(){
- $html = getSimpleHTMLDOM(self::URI);
-
- $json = $this->loadEmbeddedJsonData($html);
-
- foreach($html->find('li[id^="screenshot-"]') as $shot) {
- $item = array();
-
- $additional_data = $this->findJsonForShot($shot, $json);
- if ($additional_data === null) {
- $item['uri'] = self::URI . $shot->find('a', 0)->href;
- $item['title'] = $shot->find('.shot-title', 0)->plaintext;
- } else {
- $item['timestamp'] = strtotime($additional_data['published_at']);
- $item['uri'] = self::URI . $additional_data['path'];
- $item['title'] = $additional_data['title'];
- }
-
- $item['author'] = trim($shot->find('.user-information .display-name', 0)->plaintext);
-
- $description = $shot->find('.comment', 0);
- $item['content'] = $description === null ? '' : $description->plaintext;
-
- $preview_path = $shot->find('figure img', 1)->attr['data-srcset'];
- $item['content'] .= $this->getImageTag($preview_path, $item['title']);
- $item['enclosures'] = array($this->getFullSizeImagePath($preview_path));
-
- $this->items[] = $item;
- }
- }
-
- private function loadEmbeddedJsonData($html){
- $json = array();
- $scripts = $html->find('script');
-
- foreach($scripts as $script) {
- if(strpos($script->innertext, 'newestShots') !== false) {
- // fix single quotes
- $script->innertext = preg_replace('/\'(.*)\'(,?)$/im', '"\1"\2', $script->innertext);
-
- // fix JavaScript JSON (why do they not adhere to the standard?)
- $script->innertext = preg_replace('/^(\s*)(\w+):/im', '\1"\2":', $script->innertext);
-
- // fix relative dates, so they are recognized by strtotime
- $script->innertext = preg_replace('/"about ([0-9]+ hours? ago)"(,?)$/im', '"\1"\2', $script->innertext);
-
- // find beginning of JSON array
- $start = strpos($script->innertext, '[');
-
- // find end of JSON array, compensate for missing character!
- $end = strpos($script->innertext, '];') + 1;
-
- // convert JSON to PHP array
- $json = json_decode(substr($script->innertext, $start, $end - $start), true);
- break;
- }
- }
-
- return $json;
- }
-
- private function findJsonForShot($shot, $json){
- foreach($json as $element) {
- if(strpos($shot->getAttribute('id'), (string)$element['id']) !== false) {
- return $element;
- }
- }
-
- return null;
- }
-
- private function getImageTag($preview_path, $title){
- return sprintf(
- '<br /> <a href="%s"><img srcset="%s" alt="%s" /></a>',
- $this->getFullSizeImagePath($preview_path),
- $preview_path,
- $title
- );
- }
-
- private function getFullSizeImagePath($preview_path){
- // Get last image from srcset
- $src_set_urls = explode(',', $preview_path);
- $url = end($src_set_urls);
- $url = explode(' ', $url)[1];
-
- return htmlspecialchars_decode($url);
- }
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
+
+ $json = $this->loadEmbeddedJsonData($html);
+
+ foreach ($html->find('li[id^="screenshot-"]') as $shot) {
+ $item = [];
+
+ $additional_data = $this->findJsonForShot($shot, $json);
+ if ($additional_data === null) {
+ $item['uri'] = self::URI . $shot->find('a', 0)->href;
+ $item['title'] = $shot->find('.shot-title', 0)->plaintext;
+ } else {
+ $item['timestamp'] = strtotime($additional_data['published_at']);
+ $item['uri'] = self::URI . $additional_data['path'];
+ $item['title'] = $additional_data['title'];
+ }
+
+ $item['author'] = trim($shot->find('.user-information .display-name', 0)->plaintext);
+
+ $description = $shot->find('.comment', 0);
+ $item['content'] = $description === null ? '' : $description->plaintext;
+
+ $preview_path = $shot->find('figure img', 1)->attr['data-srcset'];
+ $item['content'] .= $this->getImageTag($preview_path, $item['title']);
+ $item['enclosures'] = [$this->getFullSizeImagePath($preview_path)];
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function loadEmbeddedJsonData($html)
+ {
+ $json = [];
+ $scripts = $html->find('script');
+
+ foreach ($scripts as $script) {
+ if (strpos($script->innertext, 'newestShots') !== false) {
+ // fix single quotes
+ $script->innertext = preg_replace('/\'(.*)\'(,?)$/im', '"\1"\2', $script->innertext);
+
+ // fix JavaScript JSON (why do they not adhere to the standard?)
+ $script->innertext = preg_replace('/^(\s*)(\w+):/im', '\1"\2":', $script->innertext);
+
+ // fix relative dates, so they are recognized by strtotime
+ $script->innertext = preg_replace('/"about ([0-9]+ hours? ago)"(,?)$/im', '"\1"\2', $script->innertext);
+
+ // find beginning of JSON array
+ $start = strpos($script->innertext, '[');
+
+ // find end of JSON array, compensate for missing character!
+ $end = strpos($script->innertext, '];') + 1;
+
+ // convert JSON to PHP array
+ $json = json_decode(substr($script->innertext, $start, $end - $start), true);
+ break;
+ }
+ }
+
+ return $json;
+ }
+
+ private function findJsonForShot($shot, $json)
+ {
+ foreach ($json as $element) {
+ if (strpos($shot->getAttribute('id'), (string)$element['id']) !== false) {
+ return $element;
+ }
+ }
+
+ return null;
+ }
+
+ private function getImageTag($preview_path, $title)
+ {
+ return sprintf(
+ '<br /> <a href="%s"><img srcset="%s" alt="%s" /></a>',
+ $this->getFullSizeImagePath($preview_path),
+ $preview_path,
+ $title
+ );
+ }
+
+ private function getFullSizeImagePath($preview_path)
+ {
+ // Get last image from srcset
+ $src_set_urls = explode(',', $preview_path);
+ $url = end($src_set_urls);
+ $url = explode(' ', $url)[1];
+
+ return htmlspecialchars_decode($url);
+ }
}
diff --git a/bridges/Drive2ruBridge.php b/bridges/Drive2ruBridge.php
index 60df97d7..00e9e957 100644
--- a/bridges/Drive2ruBridge.php
+++ b/bridges/Drive2ruBridge.php
@@ -1,205 +1,232 @@
<?php
-class Drive2ruBridge extends BridgeAbstract {
- const MAINTAINER = 'dotter-ak';
- const NAME = 'Drive2.ru';
- const URI = 'https://drive2.ru/';
- const DESCRIPTION = 'Лента новостей и тестдрайвов, бортжурналов по выбранной марке или модели
+
+class Drive2ruBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'dotter-ak';
+ const NAME = 'Drive2.ru';
+ const URI = 'https://drive2.ru/';
+ const DESCRIPTION = 'Лента новостей и тестдрайвов, бортжурналов по выбранной марке или модели
(также работает с фильтром по категориям), блогов пользователей и публикаций по темам.';
- const PARAMETERS = array(
- 'Новости и тест-драйвы' => array(),
- 'Бортжурналы (По модели или марке)' => array(
- 'url' => array(
- 'name' => 'Ссылка на страницу с бортжурналом',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Например: https://www.drive2.ru/experience/suzuki/g4895/',
- 'exampleValue' => 'https://www.drive2.ru/experience/suzuki/g4895/'
- ),
- ),
- 'Личные блоги' => array(
- 'username' => array(
- 'name' => 'Никнейм пользователя на сайте',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Например: Mickey',
- 'exampleValue' => 'Mickey'
- )
- ),
- 'Публикации по темам (Стоит почитать)' => array(
- 'topic' => array(
- 'name' => 'Темы',
- 'type' => 'list',
- 'values' => array(
- 'Автозвук' => '16',
- 'Автомобильный дизайн' => '10',
- 'Автоспорт' => '11',
- 'Автошоу, музеи, выставки' => '12',
- 'Безопасность' => '18',
- 'Беспилотные автомобили' => '15',
- 'Видеосюжеты' => '20',
- 'Вне дорог' => '21',
- 'Встречи' => '22',
- 'Выбор и покупка машины' => '23',
- 'Гаджеты' => '30',
- 'Гибридные машины' => '32',
- 'Грузовики, автобусы, спецтехника' => '31',
- 'Доработка интерьера' => '35',
- 'Законодательство' => '40',
- 'История автомобилестроения' => '50',
- 'Мототехника' => '60',
- 'Новые модели и концепты' => '85',
- 'Обучение вождению' => '70',
- 'Путешествия' => '80',
- 'Ремонт и обслуживание' => '90',
- 'Реставрация ретро-авто' => '91',
- 'Сделай сам' => '104',
- 'Смешное' => '103',
- 'Спорткары' => '102',
- 'Стайлинг' => '101',
- 'Тест-драйвы' => '110',
- 'Тюнинг' => '111',
- 'Фотосессии' => '120',
- 'Шины и диски' => '140',
- 'Электрика' => '130',
- 'Электромобили' => '131'
- ),
- 'defaultValue' => '16',
- )
- ),
- 'global' => array(
- 'full_articles' => array(
- 'name' => 'Загружать в ленту полный текст',
- 'type' => 'checkbox'
- )
- )
- );
+ const PARAMETERS = [
+ 'Новости и тест-драйвы' => [],
+ 'Бортжурналы (По модели или марке)' => [
+ 'url' => [
+ 'name' => 'Ссылка на страницу с бортжурналом',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Например: https://www.drive2.ru/experience/suzuki/g4895/',
+ 'exampleValue' => 'https://www.drive2.ru/experience/suzuki/g4895/'
+ ],
+ ],
+ 'Личные блоги' => [
+ 'username' => [
+ 'name' => 'Никнейм пользователя на сайте',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Например: Mickey',
+ 'exampleValue' => 'Mickey'
+ ]
+ ],
+ 'Публикации по темам (Стоит почитать)' => [
+ 'topic' => [
+ 'name' => 'Темы',
+ 'type' => 'list',
+ 'values' => [
+ 'Автозвук' => '16',
+ 'Автомобильный дизайн' => '10',
+ 'Автоспорт' => '11',
+ 'Автошоу, музеи, выставки' => '12',
+ 'Безопасность' => '18',
+ 'Беспилотные автомобили' => '15',
+ 'Видеосюжеты' => '20',
+ 'Вне дорог' => '21',
+ 'Встречи' => '22',
+ 'Выбор и покупка машины' => '23',
+ 'Гаджеты' => '30',
+ 'Гибридные машины' => '32',
+ 'Грузовики, автобусы, спецтехника' => '31',
+ 'Доработка интерьера' => '35',
+ 'Законодательство' => '40',
+ 'История автомобилестроения' => '50',
+ 'Мототехника' => '60',
+ 'Новые модели и концепты' => '85',
+ 'Обучение вождению' => '70',
+ 'Путешествия' => '80',
+ 'Ремонт и обслуживание' => '90',
+ 'Реставрация ретро-авто' => '91',
+ 'Сделай сам' => '104',
+ 'Смешное' => '103',
+ 'Спорткары' => '102',
+ 'Стайлинг' => '101',
+ 'Тест-драйвы' => '110',
+ 'Тюнинг' => '111',
+ 'Фотосессии' => '120',
+ 'Шины и диски' => '140',
+ 'Электрика' => '130',
+ 'Электромобили' => '131'
+ ],
+ 'defaultValue' => '16',
+ ]
+ ],
+ 'global' => [
+ 'full_articles' => [
+ 'name' => 'Загружать в ленту полный текст',
+ 'type' => 'checkbox'
+ ]
+ ]
+ ];
- private $title;
+ private $title;
- private function getUserContent($url) {
- $html = getSimpleHTMLDOM($url);
- $this->title = $html->find('title', 0)->innertext;
- $articles = $html->find('div.js-entity');
- foreach ($articles as $article) {
- $item = array();
- $item['title'] = $article->find('a.c-link--text', 0)->plaintext;
- $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 0)->href);
- if($this->getInput('full_articles')) {
- $item['content'] = $this->addCommentsLink(
- $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.c-post__body', 0))->innertext,
- $item['uri']
- );
- } else {
- $item['content'] = $this->addReadMoreLink($article->find('div.c-post-preview__lead', 0), $item['uri']);
- }
- $item['author'] = $article->find('a.c-username--wrap', 0)->plaintext;
- if (!is_null($article->find('img', 1))) $item['enclosures'][] = $article->find('img', 1)->src;
- $this->items[] = $item;
- }
- }
+ private function getUserContent($url)
+ {
+ $html = getSimpleHTMLDOM($url);
+ $this->title = $html->find('title', 0)->innertext;
+ $articles = $html->find('div.js-entity');
+ foreach ($articles as $article) {
+ $item = [];
+ $item['title'] = $article->find('a.c-link--text', 0)->plaintext;
+ $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 0)->href);
+ if ($this->getInput('full_articles')) {
+ $item['content'] = $this->addCommentsLink(
+ $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.c-post__body', 0))->innertext,
+ $item['uri']
+ );
+ } else {
+ $item['content'] = $this->addReadMoreLink($article->find('div.c-post-preview__lead', 0), $item['uri']);
+ }
+ $item['author'] = $article->find('a.c-username--wrap', 0)->plaintext;
+ if (!is_null($article->find('img', 1))) {
+ $item['enclosures'][] = $article->find('img', 1)->src;
+ }
+ $this->items[] = $item;
+ }
+ }
- private function getLogbooksContent($url) {
- $html = getSimpleHTMLDOM($url);
- $this->title = $html->find('title', 0)->innertext;
- $articles = $html->find('div.js-entity');
- foreach ($articles as $article) {
- $item = array();
- $item['title'] = $article->find('a.c-link--text', 1)->plaintext;
- $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 1)->href);
- if($this->getInput('full_articles')) {
- $item['content'] = $this->addCommentsLink(
- $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.c-post__body', 0))->innertext,
- $item['uri']
- );
- } else {
- $item['content'] = $this->addReadMoreLink($article->find('div.c-post-preview__lead', 0), $item['uri']);
- }
- $item['author'] = $article->find('a.c-username--wrap', 0)->plaintext;
- if (!is_null($article->find('img', 1))) $item['enclosures'][] = $article->find('img', 1)->src;
- $this->items[] = $item;
- }
- }
+ private function getLogbooksContent($url)
+ {
+ $html = getSimpleHTMLDOM($url);
+ $this->title = $html->find('title', 0)->innertext;
+ $articles = $html->find('div.js-entity');
+ foreach ($articles as $article) {
+ $item = [];
+ $item['title'] = $article->find('a.c-link--text', 1)->plaintext;
+ $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 1)->href);
+ if ($this->getInput('full_articles')) {
+ $item['content'] = $this->addCommentsLink(
+ $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.c-post__body', 0))->innertext,
+ $item['uri']
+ );
+ } else {
+ $item['content'] = $this->addReadMoreLink($article->find('div.c-post-preview__lead', 0), $item['uri']);
+ }
+ $item['author'] = $article->find('a.c-username--wrap', 0)->plaintext;
+ if (!is_null($article->find('img', 1))) {
+ $item['enclosures'][] = $article->find('img', 1)->src;
+ }
+ $this->items[] = $item;
+ }
+ }
- private function getNews() {
- $html = getSimpleHTMLDOM('https://www.drive2.ru/editorial/');
- $this->title = $html->find('title', 0)->innertext;
- $articles = $html->find('div.c-article-card');
- foreach ($articles as $article) {
- $item = array();
- $item['title'] = $article->find('a.c-link--text', 0)->plaintext;
- $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 0)->href);
- if($this->getInput('full_articles')) {
- $item['content'] = $this->addCommentsLink(
- $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.article', 0))->innertext,
- $item['uri']
- );
- } else {
- $item['content'] = $this->addReadMoreLink($article->find('div.c-article-card__lead', 0), $item['uri']);
- }
- $item['author'] = 'Новости и тест-драйвы на Drive2.ru';
- if (!is_null($article->find('img', 0))) $item['enclosures'][] = $article->find('img', 0)->src;
- $this->items[] = $item;
- }
- }
+ private function getNews()
+ {
+ $html = getSimpleHTMLDOM('https://www.drive2.ru/editorial/');
+ $this->title = $html->find('title', 0)->innertext;
+ $articles = $html->find('div.c-article-card');
+ foreach ($articles as $article) {
+ $item = [];
+ $item['title'] = $article->find('a.c-link--text', 0)->plaintext;
+ $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 0)->href);
+ if ($this->getInput('full_articles')) {
+ $item['content'] = $this->addCommentsLink(
+ $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.article', 0))->innertext,
+ $item['uri']
+ );
+ } else {
+ $item['content'] = $this->addReadMoreLink($article->find('div.c-article-card__lead', 0), $item['uri']);
+ }
+ $item['author'] = 'Новости и тест-драйвы на Drive2.ru';
+ if (!is_null($article->find('img', 0))) {
+ $item['enclosures'][] = $article->find('img', 0)->src;
+ }
+ $this->items[] = $item;
+ }
+ }
- private function adjustContent($content) {
- foreach ($content->find('div.o-group') as $node)
- $node->outertext = '';
- foreach($content->find('div, span') as $attrs)
- foreach ($attrs->getAllAttributes() as $attr => $val)
- $attrs->removeAttribute($attr);
- foreach ($content->getElementsByTagName('figcaption') as $attrs)
- $attrs->setAttribute(
- 'style',
- 'font-style: italic; font-size: small; margin: 0 100px 75px;');
- foreach ($content->find('script') as $node)
- $node->outertext = '';
- foreach ($content->find('iframe') as $node) {
- preg_match('/embed\/(.*?)\?/', $node->src, $match);
- $node->outertext = '<a href="https://www.youtube.com/watch?v=' . $match[1] .
- '">https://www.youtube.com/watch?v=' . $match[1] . '</a>';
- }
- return $content;
- }
+ private function adjustContent($content)
+ {
+ foreach ($content->find('div.o-group') as $node) {
+ $node->outertext = '';
+ }
+ foreach ($content->find('div, span') as $attrs) {
+ foreach ($attrs->getAllAttributes() as $attr => $val) {
+ $attrs->removeAttribute($attr);
+ }
+ }
+ foreach ($content->getElementsByTagName('figcaption') as $attrs) {
+ $attrs->setAttribute(
+ 'style',
+ 'font-style: italic; font-size: small; margin: 0 100px 75px;'
+ );
+ }
+ foreach ($content->find('script') as $node) {
+ $node->outertext = '';
+ }
+ foreach ($content->find('iframe') as $node) {
+ preg_match('/embed\/(.*?)\?/', $node->src, $match);
+ $node->outertext = '<a href="https://www.youtube.com/watch?v=' . $match[1] .
+ '">https://www.youtube.com/watch?v=' . $match[1] . '</a>';
+ }
+ return $content;
+ }
- private function addCommentsLink ($content, $url) {
- return $content . '<br><a href="' . $url . '#comments">Перейти к комментариям</a>';
- }
+ private function addCommentsLink($content, $url)
+ {
+ return $content . '<br><a href="' . $url . '#comments">Перейти к комментариям</a>';
+ }
- private function addReadMoreLink ($content, $url) {
- if (!is_null($content))
- return preg_replace('!\s+!', ' ', str_replace('Читать дальше', '', $content->plaintext)) .
- '<br><a href="' . $url . '">Читать далее</a>';
- else return '';
- }
+ private function addReadMoreLink($content, $url)
+ {
+ if (!is_null($content)) {
+ return preg_replace('!\s+!', ' ', str_replace('Читать дальше', '', $content->plaintext)) .
+ '<br><a href="' . $url . '">Читать далее</a>';
+ } else {
+ return '';
+ }
+ }
- public function collectData() {
- switch($this->queriedContext) {
- default:
- case 'Новости и тест-драйвы':
- $this->getNews();
- break;
- case 'Бортжурналы (По модели или марке)':
- if (!preg_match('/^https:\/\/www.drive2.ru\/experience/', $this->getInput('url')))
- returnServerError('Invalid url');
- $this->getLogbooksContent($this->getInput('url'));
- break;
- case 'Личные блоги':
- if (!preg_match('/^[a-zA-Z0-9-]{3,16}$/', $this->getInput('username')))
- returnServerError('Invalid username');
- $this->getUserContent('https://www.drive2.ru/users/' . $this->getInput('username'));
- break;
- case 'Публикации по темам (Стоит почитать)':
- $this->getUserContent('https://www.drive2.ru/topics/' . $this->getInput('topic'));
- break;
- }
- }
+ public function collectData()
+ {
+ switch ($this->queriedContext) {
+ default:
+ case 'Новости и тест-драйвы':
+ $this->getNews();
+ break;
+ case 'Бортжурналы (По модели или марке)':
+ if (!preg_match('/^https:\/\/www.drive2.ru\/experience/', $this->getInput('url'))) {
+ returnServerError('Invalid url');
+ }
+ $this->getLogbooksContent($this->getInput('url'));
+ break;
+ case 'Личные блоги':
+ if (!preg_match('/^[a-zA-Z0-9-]{3,16}$/', $this->getInput('username'))) {
+ returnServerError('Invalid username');
+ }
+ $this->getUserContent('https://www.drive2.ru/users/' . $this->getInput('username'));
+ break;
+ case 'Публикации по темам (Стоит почитать)':
+ $this->getUserContent('https://www.drive2.ru/topics/' . $this->getInput('topic'));
+ break;
+ }
+ }
- public function getName() {
- return $this->title ?: parent::getName();
- }
+ public function getName()
+ {
+ return $this->title ?: parent::getName();
+ }
- public function getIcon() {
- return 'https://www.drive2.ru/favicon.ico';
- }
+ public function getIcon()
+ {
+ return 'https://www.drive2.ru/favicon.ico';
+ }
}
diff --git a/bridges/DuckDuckGoBridge.php b/bridges/DuckDuckGoBridge.php
index 378996da..5edf248b 100644
--- a/bridges/DuckDuckGoBridge.php
+++ b/bridges/DuckDuckGoBridge.php
@@ -1,42 +1,44 @@
<?php
-class DuckDuckGoBridge extends BridgeAbstract {
- const MAINTAINER = 'Astalaseven';
- const NAME = 'DuckDuckGo';
- const URI = 'https://duckduckgo.com/';
- const CACHE_TIMEOUT = 21600; // 6h
- const DESCRIPTION = 'Returns results from DuckDuckGo.';
+class DuckDuckGoBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Astalaseven';
+ const NAME = 'DuckDuckGo';
+ const URI = 'https://duckduckgo.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns results from DuckDuckGo.';
- const SORT_DATE = '+sort:date';
- const SORT_RELEVANCE = '';
+ const SORT_DATE = '+sort:date';
+ const SORT_RELEVANCE = '';
- const PARAMETERS = array( array(
- 'u' => array(
- 'name' => 'keyword',
- 'exampleValue' => 'duck',
- 'required' => true
- ),
- 'sort' => array(
- 'name' => 'sort by',
- 'type' => 'list',
- 'required' => false,
- 'values' => array(
- 'date' => self::SORT_DATE,
- 'relevance' => self::SORT_RELEVANCE
- ),
- 'defaultValue' => self::SORT_DATE
- )
- ));
+ const PARAMETERS = [ [
+ 'u' => [
+ 'name' => 'keyword',
+ 'exampleValue' => 'duck',
+ 'required' => true
+ ],
+ 'sort' => [
+ 'name' => 'sort by',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => [
+ 'date' => self::SORT_DATE,
+ 'relevance' => self::SORT_RELEVANCE
+ ],
+ 'defaultValue' => self::SORT_DATE
+ ]
+ ]];
- public function collectData(){
- $html = getSimpleHTMLDOM(self::URI . 'html/?kd=-1&q=' . $this->getInput('u') . $this->getInput('sort'));
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI . 'html/?kd=-1&q=' . $this->getInput('u') . $this->getInput('sort'));
- foreach($html->find('div.result') as $element) {
- $item = array();
- $item['uri'] = $element->find('a.result__a', 0)->href;
- $item['title'] = $element->find('h2.result__title', 0)->plaintext;
- $item['content'] = $element->find('a.result__snippet', 0)->plaintext;
- $this->items[] = $item;
- }
- }
+ foreach ($html->find('div.result') as $element) {
+ $item = [];
+ $item['uri'] = $element->find('a.result__a', 0)->href;
+ $item['title'] = $element->find('h2.result__title', 0)->plaintext;
+ $item['content'] = $element->find('a.result__snippet', 0)->plaintext;
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/EZTVBridge.php b/bridges/EZTVBridge.php
index 956776ed..cf969cb5 100644
--- a/bridges/EZTVBridge.php
+++ b/bridges/EZTVBridge.php
@@ -1,111 +1,118 @@
<?php
-class EZTVBridge extends BridgeAbstract {
- const MAINTAINER = 'alexAubin';
- const NAME = 'EZTV';
- const URI = 'https://eztv.re/';
- const DESCRIPTION = 'Returns list of torrents for specific show(s)
+class EZTVBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'alexAubin';
+ const NAME = 'EZTV';
+ const URI = 'https://eztv.re/';
+ const DESCRIPTION = 'Returns list of torrents for specific show(s)
on EZTV. Get IMDB IDs from IMDB.';
- const PARAMETERS = array(
- array(
- 'ids' => array(
- 'name' => 'Show IMDB IDs',
- 'exampleValue' => '8740790,1733785',
- 'required' => true,
- 'title' => 'One or more IMDB show IDs (can be found in the IMDB show URL)'
- ),
- 'no480' => array(
- 'name' => 'No 480p',
- 'type' => 'checkbox',
- 'title' => 'Activate to exclude 480p torrents'
- ),
- 'no720' => array(
- 'name' => 'No 720p',
- 'type' => 'checkbox',
- 'title' => 'Activate to exclude 720p torrents'
- ),
- 'no1080' => array(
- 'name' => 'No 1080p',
- 'type' => 'checkbox',
- 'title' => 'Activate to exclude 1080p torrents'
- ),
- 'no2160' => array(
- 'name' => 'No 2160p',
- 'type' => 'checkbox',
- 'title' => 'Activate to exclude 2160p torrents'
- ),
- 'noUnknownRes' => array(
- 'name' => 'No Unknown resolution',
- 'type' => 'checkbox',
- 'title' => 'Activate to exclude unknown resolution torrents'
- ),
- )
- );
+ const PARAMETERS = [
+ [
+ 'ids' => [
+ 'name' => 'Show IMDB IDs',
+ 'exampleValue' => '8740790,1733785',
+ 'required' => true,
+ 'title' => 'One or more IMDB show IDs (can be found in the IMDB show URL)'
+ ],
+ 'no480' => [
+ 'name' => 'No 480p',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to exclude 480p torrents'
+ ],
+ 'no720' => [
+ 'name' => 'No 720p',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to exclude 720p torrents'
+ ],
+ 'no1080' => [
+ 'name' => 'No 1080p',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to exclude 1080p torrents'
+ ],
+ 'no2160' => [
+ 'name' => 'No 2160p',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to exclude 2160p torrents'
+ ],
+ 'noUnknownRes' => [
+ 'name' => 'No Unknown resolution',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to exclude unknown resolution torrents'
+ ],
+ ]
+ ];
- // Shamelessly lifted from https://stackoverflow.com/a/2510459
- protected function formatBytes($bytes, $precision = 2) {
- $units = array('B', 'KB', 'MB', 'GB', 'TB');
+ // Shamelessly lifted from https://stackoverflow.com/a/2510459
+ protected function formatBytes($bytes, $precision = 2)
+ {
+ $units = ['B', 'KB', 'MB', 'GB', 'TB'];
- $bytes = max($bytes, 0);
- $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
- $pow = min($pow, count($units) - 1);
- $bytes /= pow(1024, $pow);
+ $bytes = max($bytes, 0);
+ $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
+ $pow = min($pow, count($units) - 1);
+ $bytes /= pow(1024, $pow);
- return round($bytes, $precision) . ' ' . $units[$pow];
- }
+ return round($bytes, $precision) . ' ' . $units[$pow];
+ }
- protected function getItemFromTorrent($torrent){
- $item = array();
- $item['uri'] = $torrent->episode_url;
- $item['author'] = $torrent->imdb_id;
- $item['timestamp'] = date('d F Y H:i:s', $torrent->date_released_unix);
- $item['title'] = $torrent->title;
- $item['enclosures'][] = $torrent->torrent_url;
+ protected function getItemFromTorrent($torrent)
+ {
+ $item = [];
+ $item['uri'] = $torrent->episode_url;
+ $item['author'] = $torrent->imdb_id;
+ $item['timestamp'] = date('d F Y H:i:s', $torrent->date_released_unix);
+ $item['title'] = $torrent->title;
+ $item['enclosures'][] = $torrent->torrent_url;
- $thumbnailUri = 'https:' . $torrent->small_screenshot;
- $torrentSize = $this->formatBytes($torrent->size_bytes);
+ $thumbnailUri = 'https:' . $torrent->small_screenshot;
+ $torrentSize = $this->formatBytes($torrent->size_bytes);
- $item['content'] = $torrent->filename . '<br>File size: '
- . $torrentSize . '<br><a href="' . $torrent->magnet_url
- . '">magnet link</a><br><a href="' . $torrent->torrent_url
- . '">torrent link</a><br><img src="' . $thumbnailUri . '" />';
+ $item['content'] = $torrent->filename . '<br>File size: '
+ . $torrentSize . '<br><a href="' . $torrent->magnet_url
+ . '">magnet link</a><br><a href="' . $torrent->torrent_url
+ . '">torrent link</a><br><img src="' . $thumbnailUri . '" />';
- return $item;
- }
+ return $item;
+ }
- private static function compareDate($torrent1, $torrent2) {
- return (strtotime($torrent1['timestamp']) < strtotime($torrent2['timestamp']) ? 1 : -1);
- }
+ private static function compareDate($torrent1, $torrent2)
+ {
+ return (strtotime($torrent1['timestamp']) < strtotime($torrent2['timestamp']) ? 1 : -1);
+ }
- public function collectData(){
- $showIds = explode(',', $this->getInput('ids'));
+ public function collectData()
+ {
+ $showIds = explode(',', $this->getInput('ids'));
- foreach($showIds as $showId) {
- $eztvUri = $this->getURI() . 'api/get-torrents?imdb_id=' . $showId;
- $content = getContents($eztvUri);
- $torrents = json_decode($content)->torrents;
- foreach($torrents as $torrent) {
- $title = $torrent->title;
- $regex480 = '/480p/';
- $regex720 = '/720p/';
- $regex1080 = '/1080p/';
- $regex2160 = '/2160p/';
- $regexUnknown = '/(480p|720p|1080p|2160p)/';
- // Skip unwanted resolution torrents
- if ((preg_match($regex480, $title) === 1 && $this->getInput('no480'))
- || (preg_match($regex720, $title) === 1 && $this->getInput('no720'))
- || (preg_match($regex1080, $title) === 1 && $this->getInput('no1080'))
- || (preg_match($regex2160, $title) === 1 && $this->getInput('no2160'))
- || (preg_match($regexUnknown, $title) !== 1 && $this->getInput('noUnknownRes'))) {
- continue;
- }
+ foreach ($showIds as $showId) {
+ $eztvUri = $this->getURI() . 'api/get-torrents?imdb_id=' . $showId;
+ $content = getContents($eztvUri);
+ $torrents = json_decode($content)->torrents;
+ foreach ($torrents as $torrent) {
+ $title = $torrent->title;
+ $regex480 = '/480p/';
+ $regex720 = '/720p/';
+ $regex1080 = '/1080p/';
+ $regex2160 = '/2160p/';
+ $regexUnknown = '/(480p|720p|1080p|2160p)/';
+ // Skip unwanted resolution torrents
+ if (
+ (preg_match($regex480, $title) === 1 && $this->getInput('no480'))
+ || (preg_match($regex720, $title) === 1 && $this->getInput('no720'))
+ || (preg_match($regex1080, $title) === 1 && $this->getInput('no1080'))
+ || (preg_match($regex2160, $title) === 1 && $this->getInput('no2160'))
+ || (preg_match($regexUnknown, $title) !== 1 && $this->getInput('noUnknownRes'))
+ ) {
+ continue;
+ }
- $this->items[] = $this->getItemFromTorrent($torrent);
- }
- }
+ $this->items[] = $this->getItemFromTorrent($torrent);
+ }
+ }
- // Sort all torrents in array by date
- usort($this->items, array('EZTVBridge', 'compareDate'));
- }
+ // Sort all torrents in array by date
+ usort($this->items, ['EZTVBridge', 'compareDate']);
+ }
}
diff --git a/bridges/EconomistBridge.php b/bridges/EconomistBridge.php
index 973711a5..79314a0d 100644
--- a/bridges/EconomistBridge.php
+++ b/bridges/EconomistBridge.php
@@ -1,143 +1,149 @@
<?php
-class EconomistBridge extends FeedExpander {
- const MAINTAINER = 'bockiii';
- const NAME = 'Economist Bridge';
- const URI = 'https://www.economist.com/';
- const CACHE_TIMEOUT = 3600; //1hour
- const DESCRIPTION = 'Returns the latest articles for the selected category';
+class EconomistBridge extends FeedExpander
+{
+ const MAINTAINER = 'bockiii';
+ const NAME = 'Economist Bridge';
+ const URI = 'https://www.economist.com/';
+ const CACHE_TIMEOUT = 3600; //1hour
+ const DESCRIPTION = 'Returns the latest articles for the selected category';
- const PARAMETERS = array(
- 'global' => array(
- 'limit' => array(
- 'name' => 'Feed Item Limit',
- 'required' => true,
- 'type' => 'number',
- 'defaultValue' => 10,
- 'title' => 'Maximum number of returned feed items. Maximum 30, default 10'
- )
- ),
- 'Topics' => array(
- 'topic' => array(
- 'name' => 'Topics',
- 'type' => 'list',
- 'title' => 'Select a Topic',
- 'defaultValue' => 'latest',
- 'values' => array(
- 'Latest' => 'latest',
- 'The world this week' => 'the-world-this-week',
- 'Letters' => 'letters',
- 'Leaders' => 'leaders',
- 'Briefings' => 'briefing',
- 'Special reports' => 'special-report',
- 'Britain' => 'britain',
- 'Europe' => 'europe',
- 'United States' => 'united-states',
- 'The Americas' => 'the-americas',
- 'Middle East and Africa' => 'middle-east-and-africa',
- 'Asia' => 'asia',
- 'China' => 'china',
- 'International' => 'international',
- 'Business' => 'business',
- 'Finance and economics' => 'finance-and-economics',
- 'Science and technology' => 'science-and-technology',
- 'Books and arts' => 'books-and-arts',
- 'Obituaries' => 'obituary',
- 'Graphic detail' => 'graphic-detail',
- 'Indicators' => 'economic-and-financial-indicators',
- )
- )
- ),
- 'Blogs' => array(
- 'blog' => array(
- 'name' => 'Blogs',
- 'type' => 'list',
- 'title' => 'Select a Blog',
- 'values' => array(
- 'Bagehots notebook' => 'bagehots-notebook',
- 'Bartleby' => 'bartleby',
- 'Buttonwoods notebook' => 'buttonwoods-notebook',
- 'Charlemagnes notebook' => 'charlemagnes-notebook',
- 'Democracy in America' => 'democracy-in-america',
- 'Erasmus' => 'erasmus',
- 'Free exchange' => 'free-exchange',
- 'Game theory' => 'game-theory',
- 'Gulliver' => 'gulliver',
- 'Kaffeeklatsch' => 'kaffeeklatsch',
- 'Prospero' => 'prospero',
- 'The Economist Explains' => 'the-economist-explains',
- )
- )
- )
- );
+ const PARAMETERS = [
+ 'global' => [
+ 'limit' => [
+ 'name' => 'Feed Item Limit',
+ 'required' => true,
+ 'type' => 'number',
+ 'defaultValue' => 10,
+ 'title' => 'Maximum number of returned feed items. Maximum 30, default 10'
+ ]
+ ],
+ 'Topics' => [
+ 'topic' => [
+ 'name' => 'Topics',
+ 'type' => 'list',
+ 'title' => 'Select a Topic',
+ 'defaultValue' => 'latest',
+ 'values' => [
+ 'Latest' => 'latest',
+ 'The world this week' => 'the-world-this-week',
+ 'Letters' => 'letters',
+ 'Leaders' => 'leaders',
+ 'Briefings' => 'briefing',
+ 'Special reports' => 'special-report',
+ 'Britain' => 'britain',
+ 'Europe' => 'europe',
+ 'United States' => 'united-states',
+ 'The Americas' => 'the-americas',
+ 'Middle East and Africa' => 'middle-east-and-africa',
+ 'Asia' => 'asia',
+ 'China' => 'china',
+ 'International' => 'international',
+ 'Business' => 'business',
+ 'Finance and economics' => 'finance-and-economics',
+ 'Science and technology' => 'science-and-technology',
+ 'Books and arts' => 'books-and-arts',
+ 'Obituaries' => 'obituary',
+ 'Graphic detail' => 'graphic-detail',
+ 'Indicators' => 'economic-and-financial-indicators',
+ ]
+ ]
+ ],
+ 'Blogs' => [
+ 'blog' => [
+ 'name' => 'Blogs',
+ 'type' => 'list',
+ 'title' => 'Select a Blog',
+ 'values' => [
+ 'Bagehots notebook' => 'bagehots-notebook',
+ 'Bartleby' => 'bartleby',
+ 'Buttonwoods notebook' => 'buttonwoods-notebook',
+ 'Charlemagnes notebook' => 'charlemagnes-notebook',
+ 'Democracy in America' => 'democracy-in-america',
+ 'Erasmus' => 'erasmus',
+ 'Free exchange' => 'free-exchange',
+ 'Game theory' => 'game-theory',
+ 'Gulliver' => 'gulliver',
+ 'Kaffeeklatsch' => 'kaffeeklatsch',
+ 'Prospero' => 'prospero',
+ 'The Economist Explains' => 'the-economist-explains',
+ ]
+ ]
+ ]
+ ];
- public function collectData(){
- // get if topics or blogs were selected and store the selected category
- switch ($this->queriedContext) {
- case 'Topics':
- $category = $this->getInput('topic');
- break;
- case 'Blogs':
- $category = $this->getInput('blog');
- break;
- default:
- $category = 'latest';
- }
- // limit the returned articles to 30 at max
- if ((int)$this->getInput('limit') <= 30) {
- $limit = (int)$this->getInput('limit');
- } else {
- $limit = 30;
- }
+ public function collectData()
+ {
+ // get if topics or blogs were selected and store the selected category
+ switch ($this->queriedContext) {
+ case 'Topics':
+ $category = $this->getInput('topic');
+ break;
+ case 'Blogs':
+ $category = $this->getInput('blog');
+ break;
+ default:
+ $category = 'latest';
+ }
+ // limit the returned articles to 30 at max
+ if ((int)$this->getInput('limit') <= 30) {
+ $limit = (int)$this->getInput('limit');
+ } else {
+ $limit = 30;
+ }
- $this->collectExpandableDatas('https://www.economist.com/' . $category . '/rss.xml', $limit);
- }
+ $this->collectExpandableDatas('https://www.economist.com/' . $category . '/rss.xml', $limit);
+ }
- protected function parseItem($feedItem){
- $item = parent::parseItem($feedItem);
- $article = getSimpleHTMLDOM($item['uri']);
- // before the article can be added, it needs to be cleaned up, thus, the extra function
- // We also need to distinguish between old style and new style articles
- if ($article->find('article', 0)->getAttribute('data-test-id') == 'Article') {
- $contentNode = 'div.layout-article-body';
- $imgNode = 'div.article__lead-image';
- $categoryNode = 'span.article__subheadline';
- } elseif ($article->find('article', 0)->getAttribute('data-test-id') === 'NewArticle') {
- $contentNode = 'section';
- $imgNode = 'figure.css-12eysrk.e3y6nua0';
- $categoryNode = 'span.ern1uyf0';
- } else {
- return;
- }
+ protected function parseItem($feedItem)
+ {
+ $item = parent::parseItem($feedItem);
+ $article = getSimpleHTMLDOM($item['uri']);
+ // before the article can be added, it needs to be cleaned up, thus, the extra function
+ // We also need to distinguish between old style and new style articles
+ if ($article->find('article', 0)->getAttribute('data-test-id') == 'Article') {
+ $contentNode = 'div.layout-article-body';
+ $imgNode = 'div.article__lead-image';
+ $categoryNode = 'span.article__subheadline';
+ } elseif ($article->find('article', 0)->getAttribute('data-test-id') === 'NewArticle') {
+ $contentNode = 'section';
+ $imgNode = 'figure.css-12eysrk.e3y6nua0';
+ $categoryNode = 'span.ern1uyf0';
+ } else {
+ return;
+ }
- $item['content'] = $this->cleanContent($article, $contentNode);
- // only the article lead image is retained if it's there
- if (!is_null($article->find($imgNode, 0))) {
- $item['enclosures'][] = $article->find($imgNode, 0)->find('img', 0)->getAttribute('src');
- } else {
- $item['enclosures'][] = '';
- }
- // add the subheadline as category. This will create a link in new articles
- // and a text in old articles
- $item['categories'][] = $article->find($categoryNode, 0)->innertext;
+ $item['content'] = $this->cleanContent($article, $contentNode);
+ // only the article lead image is retained if it's there
+ if (!is_null($article->find($imgNode, 0))) {
+ $item['enclosures'][] = $article->find($imgNode, 0)->find('img', 0)->getAttribute('src');
+ } else {
+ $item['enclosures'][] = '';
+ }
+ // add the subheadline as category. This will create a link in new articles
+ // and a text in old articles
+ $item['categories'][] = $article->find($categoryNode, 0)->innertext;
- return $item;
- }
+ return $item;
+ }
- private function cleanContent($article, $contentNode){
- // the actual article is in this div
- $content = $article->find($contentNode, 0)->innertext;
- // clean the article content. Remove all div's since the text is in paragraph elements
- foreach (array(
- '<div '
- ) as $tag_start) {
- $content = stripRecursiveHTMLSection($content, 'div', $tag_start);
- }
- // now remove embedded iframes. The podcast postings contain these for example
- $content = preg_replace('/<iframe.*?\/iframe>/i', '', $content);
- // fix the relative links
- $content = defaultLinkTo($content, $this->getURI());
+ private function cleanContent($article, $contentNode)
+ {
+ // the actual article is in this div
+ $content = $article->find($contentNode, 0)->innertext;
+ // clean the article content. Remove all div's since the text is in paragraph elements
+ foreach (
+ [
+ '<div '
+ ] as $tag_start
+ ) {
+ $content = stripRecursiveHTMLSection($content, 'div', $tag_start);
+ }
+ // now remove embedded iframes. The podcast postings contain these for example
+ $content = preg_replace('/<iframe.*?\/iframe>/i', '', $content);
+ // fix the relative links
+ $content = defaultLinkTo($content, $this->getURI());
- return $content;
- }
+ return $content;
+ }
}
diff --git a/bridges/EconomistWorldInBriefBridge.php b/bridges/EconomistWorldInBriefBridge.php
index 33240f6e..47782a51 100644
--- a/bridges/EconomistWorldInBriefBridge.php
+++ b/bridges/EconomistWorldInBriefBridge.php
@@ -1,141 +1,143 @@
<?php
+
class EconomistWorldInBriefBridge extends BridgeAbstract
{
- const MAINTAINER = 'sqrtminusone';
- const NAME = 'Economist the World in Brief Bridge';
- const URI = 'https://www.economist.com/the-world-in-brief';
+ const MAINTAINER = 'sqrtminusone';
+ const NAME = 'Economist the World in Brief Bridge';
+ const URI = 'https://www.economist.com/the-world-in-brief';
- const CACHE_TIMEOUT = 3600; // 1 hour
- const DESCRIPTION = 'Returns stories from the World in Brief section';
+ const CACHE_TIMEOUT = 3600; // 1 hour
+ const DESCRIPTION = 'Returns stories from the World in Brief section';
- const PARAMETERS = array(
- '' => array(
- 'splitGobbets' => array(
- 'name' => 'Split the short stories',
- 'type' => 'checkbox',
- 'defaultValue' => false,
- 'title' => 'Whether to split the short stories into separate entries'
- ),
- 'limit' => array(
- 'name' => 'Truncate headers for the short stories',
- 'type' => 'number',
- 'defaultValue' => 100
- ),
- 'agenda' => array(
- 'name' => 'Add agenda for the day',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'agendaPictures' => array(
- 'name' => 'Include pictures to the agenda',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'quote' => array(
- 'name' => 'Include the quote of the day',
- 'type' => 'checkbox'
- )
- )
- );
+ const PARAMETERS = [
+ '' => [
+ 'splitGobbets' => [
+ 'name' => 'Split the short stories',
+ 'type' => 'checkbox',
+ 'defaultValue' => false,
+ 'title' => 'Whether to split the short stories into separate entries'
+ ],
+ 'limit' => [
+ 'name' => 'Truncate headers for the short stories',
+ 'type' => 'number',
+ 'defaultValue' => 100
+ ],
+ 'agenda' => [
+ 'name' => 'Add agenda for the day',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'agendaPictures' => [
+ 'name' => 'Include pictures to the agenda',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'quote' => [
+ 'name' => 'Include the quote of the day',
+ 'type' => 'checkbox'
+ ]
+ ]
+ ];
- public function collectData()
- {
- $html = getSimpleHTMLDOM(self::URI);
- $gobbets = $html->find('._gobbets', 0);
- if ($this->getInput('splitGobbets') == 1) {
- $this->splitGobbets($gobbets);
- } else {
- $this->mergeGobbets($gobbets);
- };
- if ($this->getInput('agenda') == 1) {
- $articles = $html->find('._articles', 0);
- $this->collectArticles($articles);
- }
- if ($this->getInput('quote') == 1) {
- $quote = $html->find('._quote-container', 0);
- $this->addQuote($quote);
- }
- }
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
+ $gobbets = $html->find('._gobbets', 0);
+ if ($this->getInput('splitGobbets') == 1) {
+ $this->splitGobbets($gobbets);
+ } else {
+ $this->mergeGobbets($gobbets);
+ };
+ if ($this->getInput('agenda') == 1) {
+ $articles = $html->find('._articles', 0);
+ $this->collectArticles($articles);
+ }
+ if ($this->getInput('quote') == 1) {
+ $quote = $html->find('._quote-container', 0);
+ $this->addQuote($quote);
+ }
+ }
- private function splitGobbets($gobbets)
- {
- $today = new Datetime();
- $today->setTime(0, 0, 0, 0);
- $limit = $this->getInput('limit');
- foreach ($gobbets->find('._gobbet') as $gobbet) {
- $title = $gobbet->plaintext;
- $match = preg_match('/[\.,]/', $title, $matches, PREG_OFFSET_CAPTURE);
- if ($match > 0) {
- $point = $matches[0][1];
- $title = mb_substr($title, 0, $point);
- }
- if ($limit && mb_strlen($title) > $limit) {
- $title = mb_substr($title, 0, $limit) . '...';
- }
- $item = array(
- 'uri' => self::URI,
- 'title' => $title,
- 'content' => $gobbet->innertext,
- 'timestamp' => $today->format('U'),
- 'uid' => md5($gobbet->plaintext)
- );
- $this->items[] = $item;
- }
- }
+ private function splitGobbets($gobbets)
+ {
+ $today = new Datetime();
+ $today->setTime(0, 0, 0, 0);
+ $limit = $this->getInput('limit');
+ foreach ($gobbets->find('._gobbet') as $gobbet) {
+ $title = $gobbet->plaintext;
+ $match = preg_match('/[\.,]/', $title, $matches, PREG_OFFSET_CAPTURE);
+ if ($match > 0) {
+ $point = $matches[0][1];
+ $title = mb_substr($title, 0, $point);
+ }
+ if ($limit && mb_strlen($title) > $limit) {
+ $title = mb_substr($title, 0, $limit) . '...';
+ }
+ $item = [
+ 'uri' => self::URI,
+ 'title' => $title,
+ 'content' => $gobbet->innertext,
+ 'timestamp' => $today->format('U'),
+ 'uid' => md5($gobbet->plaintext)
+ ];
+ $this->items[] = $item;
+ }
+ }
- private function mergeGobbets($gobbets)
- {
- $today = new Datetime();
- $today->setTime(0, 0, 0, 0);
- $contents = '';
- foreach ($gobbets->find('._gobbet') as $gobbet) {
- $contents .= "<p>{$gobbet->innertext}";
- }
- $this->items[] = array(
- 'uri' => self::URI,
- 'title' => 'World in brief at ' . $today->format('Y.m.d'),
- 'content' => $contents,
- 'timestamp' => $today->format('U'),
- 'uid' => 'world-in-brief-' . $today->format('U')
- );
- }
+ private function mergeGobbets($gobbets)
+ {
+ $today = new Datetime();
+ $today->setTime(0, 0, 0, 0);
+ $contents = '';
+ foreach ($gobbets->find('._gobbet') as $gobbet) {
+ $contents .= "<p>{$gobbet->innertext}";
+ }
+ $this->items[] = [
+ 'uri' => self::URI,
+ 'title' => 'World in brief at ' . $today->format('Y.m.d'),
+ 'content' => $contents,
+ 'timestamp' => $today->format('U'),
+ 'uid' => 'world-in-brief-' . $today->format('U')
+ ];
+ }
- private function collectArticles($articles)
- {
- $i = 0;
- $today = new Datetime();
- $today->setTime(0, 0, 0, 0);
- foreach ($articles->find('._article') as $article) {
- $title = $article->find('._headline', 0)->plaintext;
- $image = $article->find('._main-image', 0);
- $content = $article->find('._content', 0);
+ private function collectArticles($articles)
+ {
+ $i = 0;
+ $today = new Datetime();
+ $today->setTime(0, 0, 0, 0);
+ foreach ($articles->find('._article') as $article) {
+ $title = $article->find('._headline', 0)->plaintext;
+ $image = $article->find('._main-image', 0);
+ $content = $article->find('._content', 0);
- $res_content = '';
- if ($image != null && $this->getInput('agendaPictures') == 1) {
- $img = $image->find('img', 0);
- $res_content .= '<img src="' . $img->src . '" />';
- }
- $res_content .= $content->innertext;
- $this->items[] = array(
- 'uri' => self::URI,
- 'title' => $title,
- 'content' => $res_content,
- 'timestamp' => $today->format('U'),
- 'uid' => 'story-' . $today->format('U') . "{$i}",
- );
- $i++;
- }
- }
+ $res_content = '';
+ if ($image != null && $this->getInput('agendaPictures') == 1) {
+ $img = $image->find('img', 0);
+ $res_content .= '<img src="' . $img->src . '" />';
+ }
+ $res_content .= $content->innertext;
+ $this->items[] = [
+ 'uri' => self::URI,
+ 'title' => $title,
+ 'content' => $res_content,
+ 'timestamp' => $today->format('U'),
+ 'uid' => 'story-' . $today->format('U') . "{$i}",
+ ];
+ $i++;
+ }
+ }
- private function addQuote($quote) {
- $today = new Datetime();
- $today->setTime(0, 0, 0, 0);
- $this->items[] = array(
- 'uri' => self::URI,
- 'title' => 'Quote of the day ' . $today->format('Y.m.d'),
- 'content' => $quote->innertext,
- 'timestamp' => $today->format('U'),
- 'uid' => 'quote-' . $today->format('U')
- );
- }
+ private function addQuote($quote)
+ {
+ $today = new Datetime();
+ $today->setTime(0, 0, 0, 0);
+ $this->items[] = [
+ 'uri' => self::URI,
+ 'title' => 'Quote of the day ' . $today->format('Y.m.d'),
+ 'content' => $quote->innertext,
+ 'timestamp' => $today->format('U'),
+ 'uid' => 'quote-' . $today->format('U')
+ ];
+ }
}
diff --git a/bridges/EliteDangerousGalnetBridge.php b/bridges/EliteDangerousGalnetBridge.php
index 510866c9..555be1cd 100644
--- a/bridges/EliteDangerousGalnetBridge.php
+++ b/bridges/EliteDangerousGalnetBridge.php
@@ -1,53 +1,55 @@
<?php
-class EliteDangerousGalnetBridge extends BridgeAbstract {
-
- const MAINTAINER = 'corenting';
- const NAME = 'Elite: Dangerous Galnet';
- const URI = 'https://community.elitedangerous.com/galnet/';
- const CACHE_TIMEOUT = 7200; // 2h
- const DESCRIPTION = 'Returns the latest page of news from Galnet';
- const PARAMETERS = array(
- array(
- 'language' => array(
- 'name' => 'Language',
- 'type' => 'list',
- 'values' => array(
- 'English' => 'en',
- 'French' => 'fr',
- 'German' => 'de'
- ),
- 'defaultValue' => 'en'
- )
- )
- );
-
- public function collectData(){
- $language = $this->getInput('language');
- $url = 'https://community.elitedangerous.com/';
- $url = $url . $language . '/galnet';
- $html = getSimpleHTMLDOM($url);
-
- foreach($html->find('div.article') as $element) {
- $item = array();
-
- $uri = $element->find('h3 a', 0)->href;
- $uri = 'https://community.elitedangerous.com/' . $language . $uri;
- $item['uri'] = $uri;
-
- $item['title'] = $element->find('h3 a', 0)->plaintext;
-
- $content = $element->find('p', -1)->innertext;
- $item['content'] = $content;
-
- $date = $element->find('p.small', 0)->innertext;
- $article_year = substr($date, -4) - 1286; //Convert E:D date to actual date
- $date = substr($date, 0, -4) . $article_year;
- $item['timestamp'] = strtotime($date);
-
- $this->items[] = $item;
- }
-
- //Remove duplicates that sometimes show up on the website
- $this->items = array_unique($this->items, SORT_REGULAR);
- }
+
+class EliteDangerousGalnetBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'corenting';
+ const NAME = 'Elite: Dangerous Galnet';
+ const URI = 'https://community.elitedangerous.com/galnet/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Returns the latest page of news from Galnet';
+ const PARAMETERS = [
+ [
+ 'language' => [
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'values' => [
+ 'English' => 'en',
+ 'French' => 'fr',
+ 'German' => 'de'
+ ],
+ 'defaultValue' => 'en'
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $language = $this->getInput('language');
+ $url = 'https://community.elitedangerous.com/';
+ $url = $url . $language . '/galnet';
+ $html = getSimpleHTMLDOM($url);
+
+ foreach ($html->find('div.article') as $element) {
+ $item = [];
+
+ $uri = $element->find('h3 a', 0)->href;
+ $uri = 'https://community.elitedangerous.com/' . $language . $uri;
+ $item['uri'] = $uri;
+
+ $item['title'] = $element->find('h3 a', 0)->plaintext;
+
+ $content = $element->find('p', -1)->innertext;
+ $item['content'] = $content;
+
+ $date = $element->find('p.small', 0)->innertext;
+ $article_year = substr($date, -4) - 1286; //Convert E:D date to actual date
+ $date = substr($date, 0, -4) . $article_year;
+ $item['timestamp'] = strtotime($date);
+
+ $this->items[] = $item;
+ }
+
+ //Remove duplicates that sometimes show up on the website
+ $this->items = array_unique($this->items, SORT_REGULAR);
+ }
}
diff --git a/bridges/ElloBridge.php b/bridges/ElloBridge.php
index e9b2980e..b0c7b09f 100644
--- a/bridges/ElloBridge.php
+++ b/bridges/ElloBridge.php
@@ -1,150 +1,141 @@
<?php
-class ElloBridge extends BridgeAbstract {
-
- const MAINTAINER = 'teromene';
- const NAME = 'Ello Bridge';
- const URI = 'https://ello.co/';
- const CACHE_TIMEOUT = 4800; //2hours
- const DESCRIPTION = 'Returns the newest posts for Ello';
-
- const PARAMETERS = array(
- 'By User' => array(
- 'u' => array(
- 'name' => 'Username',
- 'required' => true,
- 'exampleValue' => 'zteph',
- 'title' => 'Username'
- )
- ),
- 'Search' => array(
- 's' => array(
- 'name' => 'Search',
- 'required' => true,
- 'exampleValue' => 'bird',
- 'title' => 'Search'
- )
- )
- );
- public function collectData() {
-
- $header = array(
- 'Authorization: Bearer ' . $this->getAPIKey()
- );
-
- if(!empty($this->getInput('u'))) {
- $postData = getContents(self::URI . 'api/v2/users/~' . urlencode($this->getInput('u')) . '/posts', $header) or
- returnServerError('Unable to query Ello API.');
- } else {
- $postData = getContents(self::URI . 'api/v2/posts?terms=' . urlencode($this->getInput('s')), $header) or
- returnServerError('Unable to query Ello API.');
- }
-
- $postData = json_decode($postData);
- $count = 0;
- foreach($postData->posts as $post) {
-
- $item = array();
- $item['author'] = $this->getUsername($post, $postData);
- $item['timestamp'] = strtotime($post->created_at);
- $item['title'] = strip_tags($this->findText($post->summary));
- $item['content'] = $this->getPostContent($post->body);
- $item['enclosures'] = $this->getEnclosures($post, $postData);
- $item['uri'] = self::URI . $item['author'] . '/post/' . $post->token;
- $content = $post->body;
-
- $this->items[] = $item;
- $count += 1;
-
- }
-
- }
-
- private function findText($path) {
-
- foreach($path as $summaryElement) {
-
- if($summaryElement->kind == 'text') {
- return $summaryElement->data;
- }
-
- }
-
- return '';
-
- }
-
- private function getPostContent($path) {
-
- $content = '';
- foreach($path as $summaryElement) {
-
- if($summaryElement->kind == 'text') {
- $content .= $summaryElement->data;
- } elseif ($summaryElement->kind == 'image') {
- $alt = '';
- if(property_exists($summaryElement->data, 'alt')) {
- $alt = $summaryElement->data->alt;
- }
- $content .= '<img src="' . $summaryElement->data->url . '" alt="' . $alt . '" />';
- }
-
- }
-
- return $content;
-
- }
-
- private function getEnclosures($post, $postData) {
-
- $assets = array();
- foreach($post->links->assets as $asset) {
- foreach($postData->linked->assets as $assetLink) {
- if($asset == $assetLink->id) {
- $assets[] = $assetLink->attachment->original->url;
- break;
- }
- }
- }
-
- return $assets;
-
- }
-
- private function getUsername($post, $postData) {
-
- foreach($postData->linked->users as $user) {
- if($user->id == $post->links->author->id) {
- return $user->username;
- }
- }
-
- }
-
- private function getAPIKey() {
- $cacheFac = new CacheFactory();
-
- $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $cache->setScope(get_called_class());
- $cache->setKey(array('key'));
- $key = $cache->loadData();
-
- if($key == null) {
- $keyInfo = getContents(self::URI . 'api/webapp-token') or
- returnServerError('Unable to get token.');
- $key = json_decode($keyInfo)->token->access_token;
- $cache->saveData($key);
- }
-
- return $key;
-
- }
-
- public function getName(){
- if(!is_null($this->getInput('u'))) {
- return $this->getInput('u') . ' - Ello Bridge';
- }
-
- return parent::getName();
- }
+class ElloBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'teromene';
+ const NAME = 'Ello Bridge';
+ const URI = 'https://ello.co/';
+ const CACHE_TIMEOUT = 4800; //2hours
+ const DESCRIPTION = 'Returns the newest posts for Ello';
+
+ const PARAMETERS = [
+ 'By User' => [
+ 'u' => [
+ 'name' => 'Username',
+ 'required' => true,
+ 'exampleValue' => 'zteph',
+ 'title' => 'Username'
+ ]
+ ],
+ 'Search' => [
+ 's' => [
+ 'name' => 'Search',
+ 'required' => true,
+ 'exampleValue' => 'bird',
+ 'title' => 'Search'
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $header = [
+ 'Authorization: Bearer ' . $this->getAPIKey()
+ ];
+
+ if (!empty($this->getInput('u'))) {
+ $postData = getContents(self::URI . 'api/v2/users/~' . urlencode($this->getInput('u')) . '/posts', $header) or
+ returnServerError('Unable to query Ello API.');
+ } else {
+ $postData = getContents(self::URI . 'api/v2/posts?terms=' . urlencode($this->getInput('s')), $header) or
+ returnServerError('Unable to query Ello API.');
+ }
+
+ $postData = json_decode($postData);
+ $count = 0;
+ foreach ($postData->posts as $post) {
+ $item = [];
+ $item['author'] = $this->getUsername($post, $postData);
+ $item['timestamp'] = strtotime($post->created_at);
+ $item['title'] = strip_tags($this->findText($post->summary));
+ $item['content'] = $this->getPostContent($post->body);
+ $item['enclosures'] = $this->getEnclosures($post, $postData);
+ $item['uri'] = self::URI . $item['author'] . '/post/' . $post->token;
+ $content = $post->body;
+
+ $this->items[] = $item;
+ $count += 1;
+ }
+ }
+
+ private function findText($path)
+ {
+ foreach ($path as $summaryElement) {
+ if ($summaryElement->kind == 'text') {
+ return $summaryElement->data;
+ }
+ }
+
+ return '';
+ }
+
+ private function getPostContent($path)
+ {
+ $content = '';
+ foreach ($path as $summaryElement) {
+ if ($summaryElement->kind == 'text') {
+ $content .= $summaryElement->data;
+ } elseif ($summaryElement->kind == 'image') {
+ $alt = '';
+ if (property_exists($summaryElement->data, 'alt')) {
+ $alt = $summaryElement->data->alt;
+ }
+ $content .= '<img src="' . $summaryElement->data->url . '" alt="' . $alt . '" />';
+ }
+ }
+
+ return $content;
+ }
+
+ private function getEnclosures($post, $postData)
+ {
+ $assets = [];
+ foreach ($post->links->assets as $asset) {
+ foreach ($postData->linked->assets as $assetLink) {
+ if ($asset == $assetLink->id) {
+ $assets[] = $assetLink->attachment->original->url;
+ break;
+ }
+ }
+ }
+
+ return $assets;
+ }
+
+ private function getUsername($post, $postData)
+ {
+ foreach ($postData->linked->users as $user) {
+ if ($user->id == $post->links->author->id) {
+ return $user->username;
+ }
+ }
+ }
+
+ private function getAPIKey()
+ {
+ $cacheFac = new CacheFactory();
+
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope(get_called_class());
+ $cache->setKey(['key']);
+ $key = $cache->loadData();
+
+ if ($key == null) {
+ $keyInfo = getContents(self::URI . 'api/webapp-token') or
+ returnServerError('Unable to get token.');
+ $key = json_decode($keyInfo)->token->access_token;
+ $cache->saveData($key);
+ }
+
+ return $key;
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('u'))) {
+ return $this->getInput('u') . ' - Ello Bridge';
+ }
+
+ return parent::getName();
+ }
}
diff --git a/bridges/ElsevierBridge.php b/bridges/ElsevierBridge.php
index 5ab19a1a..8e246fc4 100644
--- a/bridges/ElsevierBridge.php
+++ b/bridges/ElsevierBridge.php
@@ -1,41 +1,44 @@
<?php
-class ElsevierBridge extends BridgeAbstract {
- const MAINTAINER = 'dvikan';
- const NAME = 'Elsevier journals recent articles';
- const URI = 'https://www.journals.elsevier.com/';
- const CACHE_TIMEOUT = 43200; //12h
- const DESCRIPTION = 'Returns the recent articles published in Elsevier journals';
+class ElsevierBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'dvikan';
+ const NAME = 'Elsevier journals recent articles';
+ const URI = 'https://www.journals.elsevier.com/';
+ const CACHE_TIMEOUT = 43200; //12h
+ const DESCRIPTION = 'Returns the recent articles published in Elsevier journals';
- const PARAMETERS = array( array(
- 'j' => array(
- 'name' => 'Journal name',
- 'required' => true,
- 'exampleValue' => 'academic-pediatrics',
- 'title' => 'Insert html-part of your journal'
- )
- ));
+ const PARAMETERS = [ [
+ 'j' => [
+ 'name' => 'Journal name',
+ 'required' => true,
+ 'exampleValue' => 'academic-pediatrics',
+ 'title' => 'Insert html-part of your journal'
+ ]
+ ]];
- public function collectData(){
- // Not all journals have the /recent-articles page
- $url = sprintf('https://www.journals.elsevier.com/%s/recent-articles/', $this->getInput('j'));
- $html = getSimpleHTMLDOM($url);
+ public function collectData()
+ {
+ // Not all journals have the /recent-articles page
+ $url = sprintf('https://www.journals.elsevier.com/%s/recent-articles/', $this->getInput('j'));
+ $html = getSimpleHTMLDOM($url);
- foreach($html->find('article') as $recentArticle) {
- $item = [];
- $item['uri'] = $recentArticle->find('a', 0)->getAttribute('href');
- $item['title'] = $recentArticle->find('h2', 0)->plaintext;
- $item['author'] = $recentArticle->find('p > span', 0)->plaintext;
- $publicationDateString = trim($recentArticle->find('p > span', 1)->plaintext);
- $publicationDate = DateTimeImmutable::createFromFormat('F d, Y', $publicationDateString);
- if ($publicationDate) {
- $item['timestamp'] = $publicationDate->getTimestamp();
- }
- $this->items[] = $item;
- }
- }
+ foreach ($html->find('article') as $recentArticle) {
+ $item = [];
+ $item['uri'] = $recentArticle->find('a', 0)->getAttribute('href');
+ $item['title'] = $recentArticle->find('h2', 0)->plaintext;
+ $item['author'] = $recentArticle->find('p > span', 0)->plaintext;
+ $publicationDateString = trim($recentArticle->find('p > span', 1)->plaintext);
+ $publicationDate = DateTimeImmutable::createFromFormat('F d, Y', $publicationDateString);
+ if ($publicationDate) {
+ $item['timestamp'] = $publicationDate->getTimestamp();
+ }
+ $this->items[] = $item;
+ }
+ }
- public function getIcon(): string {
- return 'https://cdn.elsevier.io/verona/includes/favicons/favicon-32x32.png';
- }
+ public function getIcon(): string
+ {
+ return 'https://cdn.elsevier.io/verona/includes/favicons/favicon-32x32.png';
+ }
}
diff --git a/bridges/EngadgetBridge.php b/bridges/EngadgetBridge.php
index cf200fa4..2bed4a4a 100644
--- a/bridges/EngadgetBridge.php
+++ b/bridges/EngadgetBridge.php
@@ -1,26 +1,30 @@
<?php
-class EngadgetBridge extends FeedExpander {
- const MAINTAINER = 'IceWreck';
- const NAME = 'Engadget Bridge';
- const URI = 'https://www.engadget.com/';
- const CACHE_TIMEOUT = 3600;
- const DESCRIPTION = 'Article content for Engadget.';
+class EngadgetBridge extends FeedExpander
+{
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'Engadget Bridge';
+ const URI = 'https://www.engadget.com/';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'Article content for Engadget.';
- public function collectData(){
- $this->collectExpandableDatas(static::URI . 'rss.xml', 15);
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas(static::URI . 'rss.xml', 15);
+ }
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- // $articlePage gets the entire page's contents
- $articlePage = getSimpleHTMLDOM($newsItem->link);
- // figure contain's the main article image
- $article = $articlePage->find('figure', 0);
- // .article-text has the actual article
- foreach($articlePage->find('.article-text') as $element)
- $article = $article . $element;
- $item['content'] = $article;
- return $item;
- }
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+ // figure contain's the main article image
+ $article = $articlePage->find('figure', 0);
+ // .article-text has the actual article
+ foreach ($articlePage->find('.article-text') as $element) {
+ $article = $article . $element;
+ }
+ $item['content'] = $article;
+ return $item;
+ }
}
diff --git a/bridges/EpicgamesBridge.php b/bridges/EpicgamesBridge.php
index d7dd6afe..dfb30b7f 100644
--- a/bridges/EpicgamesBridge.php
+++ b/bridges/EpicgamesBridge.php
@@ -1,92 +1,94 @@
<?php
-class EpicgamesBridge extends BridgeAbstract {
- const NAME = 'Epic Games Store News';
- const MAINTAINER = 'otakuf';
- const URI = 'https://www.epicgames.com';
- const DESCRIPTION = 'Returns the latest posts from epicgames.com';
- const CACHE_TIMEOUT = 3600; // 60min
+class EpicgamesBridge extends BridgeAbstract
+{
+ const NAME = 'Epic Games Store News';
+ const MAINTAINER = 'otakuf';
+ const URI = 'https://www.epicgames.com';
+ const DESCRIPTION = 'Returns the latest posts from epicgames.com';
+ const CACHE_TIMEOUT = 3600; // 60min
- const PARAMETERS = array( array(
- 'postcount' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => true,
- 'title' => 'Maximum number of items to return',
- 'defaultValue' => 10,
- ),
- 'language' => array(
- 'name' => 'Language',
- 'type' => 'list',
- 'values' => array(
- 'English' => 'en',
- 'العربية' => 'ar',
- 'Deutsch' => 'de',
- 'Español (Spain)' => 'es-ES',
- 'Español (LA)' => 'es-MX',
- 'Français' => 'fr',
- 'Italiano' => 'it',
- '日本語' => 'ja',
- '한국어' => 'ko',
- 'Polski' => 'pl',
- 'Português (Brasil)' => 'pt-BR',
- 'Русский' => 'ru',
- 'ไทย' => 'th',
- 'Türkçe' => 'tr',
- '简体中文' => 'zh-CN',
- '繁體中文' => 'zh-Hant',
- ),
- 'title' => 'Language of blog posts',
- 'defaultValue' => 'en',
- ),
- ));
+ const PARAMETERS = [ [
+ 'postcount' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => true,
+ 'title' => 'Maximum number of items to return',
+ 'defaultValue' => 10,
+ ],
+ 'language' => [
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'values' => [
+ 'English' => 'en',
+ 'العربية' => 'ar',
+ 'Deutsch' => 'de',
+ 'Español (Spain)' => 'es-ES',
+ 'Español (LA)' => 'es-MX',
+ 'Français' => 'fr',
+ 'Italiano' => 'it',
+ '日本語' => 'ja',
+ '한국어' => 'ko',
+ 'Polski' => 'pl',
+ 'Português (Brasil)' => 'pt-BR',
+ 'Русский' => 'ru',
+ 'ไทย' => 'th',
+ 'Türkçe' => 'tr',
+ '简体中文' => 'zh-CN',
+ '繁體中文' => 'zh-Hant',
+ ],
+ 'title' => 'Language of blog posts',
+ 'defaultValue' => 'en',
+ ],
+ ]];
- public function collectData() {
- $api = 'https://store-content.ak.epicgames.com/api/';
+ public function collectData()
+ {
+ $api = 'https://store-content.ak.epicgames.com/api/';
- // Get sticky posts first
- // Example: https://store-content.ak.epicgames.com/api/ru/content/blog/sticky?locale=ru
- $urlSticky = $api . $this->getInput('language') . '/content/blog/sticky';
- // Then get posts
- // Example: https://store-content.ak.epicgames.com/api/ru/content/blog?limit=25
- $urlBlog = $api . $this->getInput('language') . '/content/blog?limit=' . $this->getInput('postcount');
+ // Get sticky posts first
+ // Example: https://store-content.ak.epicgames.com/api/ru/content/blog/sticky?locale=ru
+ $urlSticky = $api . $this->getInput('language') . '/content/blog/sticky';
+ // Then get posts
+ // Example: https://store-content.ak.epicgames.com/api/ru/content/blog?limit=25
+ $urlBlog = $api . $this->getInput('language') . '/content/blog?limit=' . $this->getInput('postcount');
- $dataSticky = getContents($urlSticky);
- $dataBlog = getContents($urlBlog);
+ $dataSticky = getContents($urlSticky);
+ $dataBlog = getContents($urlBlog);
- // Merge data
- $decodedData = array_merge(json_decode($dataSticky), json_decode($dataBlog));
+ // Merge data
+ $decodedData = array_merge(json_decode($dataSticky), json_decode($dataBlog));
- foreach($decodedData as $key => $value) {
- $item = array();
- $item['uri'] = self::URI . $value->url;
- $item['title'] = $value->title;
- $item['timestamp'] = $value->date;
- $item['author'] = 'Epic Games Store';
- if(!empty($value->author)) {
- $item['author'] = $value->author;
- }
- if(!empty($value->content)) {
- $item['content'] = defaultLinkTo($value->content, self::URI);
- }
- if(!empty($value->image)) {
- $item['enclosures'][] = $value->image;
- }
- $item['uid'] = $value->_id;
- $item['id'] = $value->_id;
+ foreach ($decodedData as $key => $value) {
+ $item = [];
+ $item['uri'] = self::URI . $value->url;
+ $item['title'] = $value->title;
+ $item['timestamp'] = $value->date;
+ $item['author'] = 'Epic Games Store';
+ if (!empty($value->author)) {
+ $item['author'] = $value->author;
+ }
+ if (!empty($value->content)) {
+ $item['content'] = defaultLinkTo($value->content, self::URI);
+ }
+ if (!empty($value->image)) {
+ $item['enclosures'][] = $value->image;
+ }
+ $item['uid'] = $value->_id;
+ $item['id'] = $value->_id;
- $this->items[] = $item;
- }
+ $this->items[] = $item;
+ }
- // Sort data
- usort($this->items, function ($item1, $item2) {
- if ($item2['timestamp'] == $item1['timestamp']) {
- return 0;
- }
- return ($item2['timestamp'] < $item1['timestamp']) ? -1 : 1;
- });
+ // Sort data
+ usort($this->items, function ($item1, $item2) {
+ if ($item2['timestamp'] == $item1['timestamp']) {
+ return 0;
+ }
+ return ($item2['timestamp'] < $item1['timestamp']) ? -1 : 1;
+ });
- // Limit data
- $this->items = array_slice($this->items, 0, $this->getInput('postcount'));
- }
+ // Limit data
+ $this->items = array_slice($this->items, 0, $this->getInput('postcount'));
+ }
}
diff --git a/bridges/EsquerdaNetBridge.php b/bridges/EsquerdaNetBridge.php
index 5c56e95b..ffb4fd4e 100644
--- a/bridges/EsquerdaNetBridge.php
+++ b/bridges/EsquerdaNetBridge.php
@@ -1,69 +1,75 @@
<?php
-class EsquerdaNetBridge extends FeedExpander {
- const MAINTAINER = 'somini';
- const NAME = 'Esquerda.net';
- const URI = 'https://www.esquerda.net';
- const DESCRIPTION = 'Esquerda.net';
- const PARAMETERS = array(
- array(
- 'feed' => array(
- 'name' => 'Feed',
- 'type' => 'list',
- 'defaultValue' => 'Geral',
- 'values' => array(
- 'Geral' => 'geral',
- 'Dossier' => 'artigos-dossier',
- 'Vídeo' => 'video',
- 'Opinião' => 'opinioes',
- 'Rádio' => 'radio',
- )
- )
- )
- );
- public function getURI() {
- $type = $this->getInput('feed');
- return self::URI . '/rss/' . $type;
- }
+class EsquerdaNetBridge extends FeedExpander
+{
+ const MAINTAINER = 'somini';
+ const NAME = 'Esquerda.net';
+ const URI = 'https://www.esquerda.net';
+ const DESCRIPTION = 'Esquerda.net';
+ const PARAMETERS = [
+ [
+ 'feed' => [
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'defaultValue' => 'Geral',
+ 'values' => [
+ 'Geral' => 'geral',
+ 'Dossier' => 'artigos-dossier',
+ 'Vídeo' => 'video',
+ 'Opinião' => 'opinioes',
+ 'Rádio' => 'radio',
+ ]
+ ]
+ ]
+ ];
- public function getIcon() {
- return 'https://www.esquerda.net/sites/default/files/favicon_0.ico';
- }
+ public function getURI()
+ {
+ $type = $this->getInput('feed');
+ return self::URI . '/rss/' . $type;
+ }
- public function collectData(){
- parent::collectExpandableDatas($this->getURI());
- }
+ public function getIcon()
+ {
+ return 'https://www.esquerda.net/sites/default/files/favicon_0.ico';
+ }
- protected function parseItem($newsItem){
- # Fix Publish date
- $badDate = $newsItem->pubDate;
- preg_match('|(?P<day>\d\d)/(?P<month>\d\d)/(?P<year>\d\d\d\d) - (?P<hour>\d\d):(?P<minute>\d\d)|', $badDate, $d);
- $newsItem->pubDate = sprintf('%s-%s-%sT%s:%s', $d['year'], $d['month'], $d['day'], $d['hour'], $d['minute']);
- $item = parent::parseItem($newsItem);
- # Include all the content
- $uri = $item['uri'];
- $html = getSimpleHTMLDOMCached($uri);
- $content = $html->find('div#content div.content', 0);
- ## Fix author
- $authorHTML = $html->find('.field-name-field-op-author a', 0);
- if ($authorHTML) {
- $item['author'] = $authorHTML->innertext;
- $authorHTML->remove();
- }
- ## Remove crap
- $content->find('.field-name-addtoany', 0)->remove();
- ## Fix links
- $content = defaultLinkTo($content, self::URI);
- ## Fix Images
- foreach($content->find('img') as $img) {
- $altSrc = $img->getAttribute('data-src');
- if ($altSrc) {
- $img->setAttribute('src', $altSrc);
- }
- $img->width = null;
- $img->height = null;
- }
- $item['content'] = $content;
- return $item;
- }
+ public function collectData()
+ {
+ parent::collectExpandableDatas($this->getURI());
+ }
+
+ protected function parseItem($newsItem)
+ {
+ # Fix Publish date
+ $badDate = $newsItem->pubDate;
+ preg_match('|(?P<day>\d\d)/(?P<month>\d\d)/(?P<year>\d\d\d\d) - (?P<hour>\d\d):(?P<minute>\d\d)|', $badDate, $d);
+ $newsItem->pubDate = sprintf('%s-%s-%sT%s:%s', $d['year'], $d['month'], $d['day'], $d['hour'], $d['minute']);
+ $item = parent::parseItem($newsItem);
+ # Include all the content
+ $uri = $item['uri'];
+ $html = getSimpleHTMLDOMCached($uri);
+ $content = $html->find('div#content div.content', 0);
+ ## Fix author
+ $authorHTML = $html->find('.field-name-field-op-author a', 0);
+ if ($authorHTML) {
+ $item['author'] = $authorHTML->innertext;
+ $authorHTML->remove();
+ }
+ ## Remove crap
+ $content->find('.field-name-addtoany', 0)->remove();
+ ## Fix links
+ $content = defaultLinkTo($content, self::URI);
+ ## Fix Images
+ foreach ($content->find('img') as $img) {
+ $altSrc = $img->getAttribute('data-src');
+ if ($altSrc) {
+ $img->setAttribute('src', $altSrc);
+ }
+ $img->width = null;
+ $img->height = null;
+ }
+ $item['content'] = $content;
+ return $item;
+ }
}
diff --git a/bridges/EstCeQuonMetEnProdBridge.php b/bridges/EstCeQuonMetEnProdBridge.php
index 67e69f55..862567d2 100644
--- a/bridges/EstCeQuonMetEnProdBridge.php
+++ b/bridges/EstCeQuonMetEnProdBridge.php
@@ -1,26 +1,28 @@
<?php
-class EstCeQuonMetEnProdBridge extends BridgeAbstract {
- const MAINTAINER = 'ORelio';
- const NAME = 'Est-ce qu\'on met en prod aujourd\'hui ?';
- const URI = 'https://www.estcequonmetenprodaujourdhui.info/';
- const CACHE_TIMEOUT = 21600; // 6h
- const DESCRIPTION = 'Should we put a website in production today? (French)';
+class EstCeQuonMetEnProdBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = 'Est-ce qu\'on met en prod aujourd\'hui ?';
+ const URI = 'https://www.estcequonmetenprodaujourdhui.info/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Should we put a website in production today? (French)';
- public function collectData() {
- $html = getSimpleHTMLDOM(self::URI);
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
- $item = array();
- $item['uri'] = $this->getURI() . '#' . date('Y-m-d');
- $item['title'] = $this->getName();
- $item['author'] = 'Nicolas Hoffmann';
- $item['timestamp'] = strtotime('today midnight');
- $item['content'] = str_replace(
- 'src="/',
- 'src="' . self::URI,
- trim(extractFromDelimiters($html->outertext, '<body role="document">', '<div id="share'))
- );
+ $item = [];
+ $item['uri'] = $this->getURI() . '#' . date('Y-m-d');
+ $item['title'] = $this->getName();
+ $item['author'] = 'Nicolas Hoffmann';
+ $item['timestamp'] = strtotime('today midnight');
+ $item['content'] = str_replace(
+ 'src="/',
+ 'src="' . self::URI,
+ trim(extractFromDelimiters($html->outertext, '<body role="document">', '<div id="share'))
+ );
- $this->items[] = $item;
- }
+ $this->items[] = $item;
+ }
}
diff --git a/bridges/EtsyBridge.php b/bridges/EtsyBridge.php
index 7d79b82e..05bf7d26 100644
--- a/bridges/EtsyBridge.php
+++ b/bridges/EtsyBridge.php
@@ -1,81 +1,85 @@
<?php
-class EtsyBridge extends BridgeAbstract {
- const NAME = 'Etsy search';
- const URI = 'https://www.etsy.com';
- const DESCRIPTION = 'Returns feeds for search results';
- const MAINTAINER = 'logmanoriginal';
- const PARAMETERS = array(
- array(
- 'query' => array(
- 'name' => 'Search query',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Insert your search term here',
- 'exampleValue' => 'lamp'
- ),
- 'queryextension' => array(
- 'name' => 'Query extension',
- 'type' => 'text',
- 'required' => false,
- 'title' => 'Insert additional query parts here
+class EtsyBridge extends BridgeAbstract
+{
+ const NAME = 'Etsy search';
+ const URI = 'https://www.etsy.com';
+ const DESCRIPTION = 'Returns feeds for search results';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = [
+ [
+ 'query' => [
+ 'name' => 'Search query',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert your search term here',
+ 'exampleValue' => 'lamp'
+ ],
+ 'queryextension' => [
+ 'name' => 'Query extension',
+ 'type' => 'text',
+ 'required' => false,
+ 'title' => 'Insert additional query parts here
(anything after ?search=<your search query>)',
- 'exampleValue' => '&explicit=1&locationQuery=2921044'
- ),
- 'hideimage' => array(
- 'name' => 'Hide image in content',
- 'type' => 'checkbox',
- 'title' => 'Activate to hide the image in the content',
- )
- )
- );
+ 'exampleValue' => '&explicit=1&locationQuery=2921044'
+ ],
+ 'hideimage' => [
+ 'name' => 'Hide image in content',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to hide the image in the content',
+ ]
+ ]
+ ];
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI());
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
- $results = $html->find('li.wt-list-unstyled');
+ $results = $html->find('li.wt-list-unstyled');
- foreach($results as $result) {
- // Remove Lazy loading
- if($result->find('.wt-skeleton-ui', 0))
- continue;
+ foreach ($results as $result) {
+ // Remove Lazy loading
+ if ($result->find('.wt-skeleton-ui', 0)) {
+ continue;
+ }
- $item = array();
+ $item = [];
- $item['title'] = $result->find('a', 0)->title;
- $item['uri'] = $result->find('a', 0)->href;
- $item['author'] = $result->find('p.wt-text-gray > span', 2)->plaintext;
+ $item['title'] = $result->find('a', 0)->title;
+ $item['uri'] = $result->find('a', 0)->href;
+ $item['author'] = $result->find('p.wt-text-gray > span', 2)->plaintext;
- $item['content'] = '<p>'
- . $result->find('span.currency-symbol', 0)->plaintext
- . $result->find('span.currency-value', 0)->plaintext
- . '</p><p>'
- . $result->find('a', 0)->title
- . '</p>';
+ $item['content'] = '<p>'
+ . $result->find('span.currency-symbol', 0)->plaintext
+ . $result->find('span.currency-value', 0)->plaintext
+ . '</p><p>'
+ . $result->find('a', 0)->title
+ . '</p>';
- $image = $result->find('img.wt-display-block', 0)->src;
+ $image = $result->find('img.wt-display-block', 0)->src;
- if(!$this->getInput('hideimage')) {
- $item['content'] .= '<img src="' . $image . '">';
- }
+ if (!$this->getInput('hideimage')) {
+ $item['content'] .= '<img src="' . $image . '">';
+ }
- $item['enclosures'] = array($image);
+ $item['enclosures'] = [$image];
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
- public function getURI(){
- if(!is_null($this->getInput('query'))) {
- $uri = self::URI . '/search?q=' . urlencode($this->getInput('query'));
+ public function getURI()
+ {
+ if (!is_null($this->getInput('query'))) {
+ $uri = self::URI . '/search?q=' . urlencode($this->getInput('query'));
- if(!is_null($this->getInput('queryextension'))) {
- $uri .= $this->getInput('queryextension');
- }
+ if (!is_null($this->getInput('queryextension'))) {
+ $uri .= $this->getInput('queryextension');
+ }
- return $uri;
- }
+ return $uri;
+ }
- return parent::getURI();
- }
+ return parent::getURI();
+ }
}
diff --git a/bridges/EuronewsBridge.php b/bridges/EuronewsBridge.php
index df11014c..2508a274 100644
--- a/bridges/EuronewsBridge.php
+++ b/bridges/EuronewsBridge.php
@@ -1,209 +1,211 @@
<?php
+
class EuronewsBridge extends BridgeAbstract
{
- const MAINTAINER = 'sqrtminusone';
- const NAME = 'Euronews Bridge';
- const URI = 'https://www.euronews.com/';
- const CACHE_TIMEOUT = 600; // 10 minutes
- const DESCRIPTION = 'Return articles from the "Just In" feed of Euronews.';
+ const MAINTAINER = 'sqrtminusone';
+ const NAME = 'Euronews Bridge';
+ const URI = 'https://www.euronews.com/';
+ const CACHE_TIMEOUT = 600; // 10 minutes
+ const DESCRIPTION = 'Return articles from the "Just In" feed of Euronews.';
- const PARAMETERS = array(
- '' => array(
- 'lang' => array(
- 'name' => 'Language',
- 'type' => 'list',
- 'defaultValue' => 'euronews.com',
- 'values' => array(
- 'English' => 'euronews.com',
- 'French' => 'fr.euronews.com',
- 'German' => 'de.euronews.com',
- 'Italian' => 'it.euronews.com',
- 'Spanish' => 'es.euronews.com',
- 'Portuguese' => 'pt.euronews.com',
- 'Russian' => 'ru.euronews.com',
- 'Turkish' => 'tr.euronews.com',
- 'Greek' => 'gr.euronews.com',
- 'Hungarian' => 'hu.euronews.com',
- 'Persian' => 'per.euronews.com',
- 'Arabic' => 'arabic.euronews.com',
- /* These versions don't have timeline.json */
- // 'Albanian' => 'euronews.al',
- // 'Romanian' => 'euronews.ro',
- // 'Georigian' => 'euronewsgeorgia.com',
- // 'Bulgarian' => 'euronewsbulgaria.com'
- // 'Serbian' => 'euronews.rs'
- )
- ),
- 'limit' => array(
- 'name' => 'Limit of items per feed',
- 'required' => true,
- 'type' => 'number',
- 'defaultValue' => 10,
- 'title' => 'Maximum number of returned feed items. Maximum 50, default 10'
- ),
- )
- );
+ const PARAMETERS = [
+ '' => [
+ 'lang' => [
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'defaultValue' => 'euronews.com',
+ 'values' => [
+ 'English' => 'euronews.com',
+ 'French' => 'fr.euronews.com',
+ 'German' => 'de.euronews.com',
+ 'Italian' => 'it.euronews.com',
+ 'Spanish' => 'es.euronews.com',
+ 'Portuguese' => 'pt.euronews.com',
+ 'Russian' => 'ru.euronews.com',
+ 'Turkish' => 'tr.euronews.com',
+ 'Greek' => 'gr.euronews.com',
+ 'Hungarian' => 'hu.euronews.com',
+ 'Persian' => 'per.euronews.com',
+ 'Arabic' => 'arabic.euronews.com',
+ /* These versions don't have timeline.json */
+ // 'Albanian' => 'euronews.al',
+ // 'Romanian' => 'euronews.ro',
+ // 'Georigian' => 'euronewsgeorgia.com',
+ // 'Bulgarian' => 'euronewsbulgaria.com'
+ // 'Serbian' => 'euronews.rs'
+ ]
+ ],
+ 'limit' => [
+ 'name' => 'Limit of items per feed',
+ 'required' => true,
+ 'type' => 'number',
+ 'defaultValue' => 10,
+ 'title' => 'Maximum number of returned feed items. Maximum 50, default 10'
+ ],
+ ]
+ ];
- public function collectData()
- {
- $limit = $this->getInput('limit');
- $root_url = 'https://' . $this->getInput('lang');
- $url = $root_url . '/api/timeline.json?limit=' . $limit;
- $json = getContents($url);
- $data = json_decode($json, true);
+ public function collectData()
+ {
+ $limit = $this->getInput('limit');
+ $root_url = 'https://' . $this->getInput('lang');
+ $url = $root_url . '/api/timeline.json?limit=' . $limit;
+ $json = getContents($url);
+ $data = json_decode($json, true);
- foreach ($data as $datum) {
- $datum_uri = $root_url . $datum['fullUrl'];
- $url_datum = $this->getItemContent($datum_uri);
- $categories = array();
- if (array_key_exists('program', $datum)) {
- if (array_key_exists('title', $datum['program'])) {
- $categories[] = $datum['program']['title'];
- }
- }
- if (array_key_exists('themes', $datum)) {
- foreach ($datum['themes'] as $theme) {
- $categories[] = $theme['title'];
- }
- }
- $item = array(
- 'uri' => $datum_uri,
- 'title' => $datum['title'],
- 'uid' => strval($datum['id']),
- 'timestamp' => $datum['publishedAt'],
- 'content' => $url_datum['content'],
- 'author' => $url_datum['author'],
- 'enclosures' => $url_datum['enclosures'],
- 'categories' => array_unique($categories)
- );
- $this->items[] = $item;
- }
- }
+ foreach ($data as $datum) {
+ $datum_uri = $root_url . $datum['fullUrl'];
+ $url_datum = $this->getItemContent($datum_uri);
+ $categories = [];
+ if (array_key_exists('program', $datum)) {
+ if (array_key_exists('title', $datum['program'])) {
+ $categories[] = $datum['program']['title'];
+ }
+ }
+ if (array_key_exists('themes', $datum)) {
+ foreach ($datum['themes'] as $theme) {
+ $categories[] = $theme['title'];
+ }
+ }
+ $item = [
+ 'uri' => $datum_uri,
+ 'title' => $datum['title'],
+ 'uid' => strval($datum['id']),
+ 'timestamp' => $datum['publishedAt'],
+ 'content' => $url_datum['content'],
+ 'author' => $url_datum['author'],
+ 'enclosures' => $url_datum['enclosures'],
+ 'categories' => array_unique($categories)
+ ];
+ $this->items[] = $item;
+ }
+ }
- private function getItemContent($url)
- {
- try {
- $html = getSimpleHTMLDOMCached($url);
- } catch (Exception $e) {
- // Every once in a while it fails with too many redirects
- return array('author' => null, 'content' => null, 'enclosures' => null);
- }
- $data = $html->find('script[type="application/ld+json"]', 0)->innertext;
- $json = json_decode($data, true);
- $author = 'Euronews';
- $content = '';
- $enclosures = array();
- if (array_key_exists('@graph', $json)) {
- foreach ($json['@graph'] as $item) {
- if ($item['@type'] == 'NewsArticle') {
- if (array_key_exists('author', $item)) {
- $author = $item['author']['name'];
- }
- if (array_key_exists('image', $item)) {
- $content .= '<figure>';
- $content .= '<img src="' . $item['image']['url'] . '">';
- $content .= '<figcaption>' . $item['image']['caption'] . '</figcaption>';
- $content .= '</figure><br>';
- }
- if (array_key_exists('video', $item)) {
- $enclosures[] = $item['video']['contentUrl'];
- }
- }
- }
- }
+ private function getItemContent($url)
+ {
+ try {
+ $html = getSimpleHTMLDOMCached($url);
+ } catch (Exception $e) {
+ // Every once in a while it fails with too many redirects
+ return ['author' => null, 'content' => null, 'enclosures' => null];
+ }
+ $data = $html->find('script[type="application/ld+json"]', 0)->innertext;
+ $json = json_decode($data, true);
+ $author = 'Euronews';
+ $content = '';
+ $enclosures = [];
+ if (array_key_exists('@graph', $json)) {
+ foreach ($json['@graph'] as $item) {
+ if ($item['@type'] == 'NewsArticle') {
+ if (array_key_exists('author', $item)) {
+ $author = $item['author']['name'];
+ }
+ if (array_key_exists('image', $item)) {
+ $content .= '<figure>';
+ $content .= '<img src="' . $item['image']['url'] . '">';
+ $content .= '<figcaption>' . $item['image']['caption'] . '</figcaption>';
+ $content .= '</figure><br>';
+ }
+ if (array_key_exists('video', $item)) {
+ $enclosures[] = $item['video']['contentUrl'];
+ }
+ }
+ }
+ }
- // Normal article
- $article_content = $html->find('.c-article-content', 0);
- if ($article_content) {
- // Usually the .c-article-content is the root of the
- // content, but once in a blue moon the root is the second
- // div
- if ((count($article_content->children()) == 2)
- && ($article_content->children(1)->tag == 'div')
- ) {
- $article_content = $article_content->children(1);
- }
- // The content is interspersed with links and stuff, so we
- // iterate over the children
- foreach ($article_content->children() as $element) {
- if ($element->tag == 'p') {
- $scribble_live = $element->find('#scribblelive-items', 0);
- if (is_null($scribble_live)) {
- // A normal paragraph
- $content .= '<p>' . $element->innertext . '</p>';
- } else {
- // LIVE mode
- foreach ($scribble_live->children() as $child) {
- if ($child->tag == 'div') {
- $content .= '<div>' . $child->innertext . '</div>';
- }
- }
- }
- } elseif (preg_match('/h[1-6]/', $element->tag)) {
- // Header
- $content .= '<h' . $element->tag[1] . '>' . $element->innertext . '</h' . $element->tag[1] . '>';
- } elseif ($element->tag == 'div') {
- if (preg_match('/.*widget--type-image.*/', $element->class)) {
- // Image
- $content .= '<figure>';
- $content .= '<img src="' . $element->find('img', 0)->src . '">';
- $caption = $element->find('figcaption', 0);
- if ($caption) {
- $content .= '<figcaption>' . $element->plaintext . '</figcaption>';
- }
- $content .= '</figure><br>';
- } elseif (preg_match('/.*widget--type-quotation.*/', $element->class)) {
- // Quotation
- $quote = $element->find('.widget__quoteText', 0);
- $author = $element->find('.widget__author', 0);
- $content .= '<figure>';
- $content .= '<blockquote>' . $quote->plaintext . '</blockquote>';
- if ($author) {
- $content .= '<figcaption>' . $author->plaintext . '</figcaption>';
- }
- $content .= '</figure><br>';
- }
- }
- }
- }
+ // Normal article
+ $article_content = $html->find('.c-article-content', 0);
+ if ($article_content) {
+ // Usually the .c-article-content is the root of the
+ // content, but once in a blue moon the root is the second
+ // div
+ if (
+ (count($article_content->children()) == 2)
+ && ($article_content->children(1)->tag == 'div')
+ ) {
+ $article_content = $article_content->children(1);
+ }
+ // The content is interspersed with links and stuff, so we
+ // iterate over the children
+ foreach ($article_content->children() as $element) {
+ if ($element->tag == 'p') {
+ $scribble_live = $element->find('#scribblelive-items', 0);
+ if (is_null($scribble_live)) {
+ // A normal paragraph
+ $content .= '<p>' . $element->innertext . '</p>';
+ } else {
+ // LIVE mode
+ foreach ($scribble_live->children() as $child) {
+ if ($child->tag == 'div') {
+ $content .= '<div>' . $child->innertext . '</div>';
+ }
+ }
+ }
+ } elseif (preg_match('/h[1-6]/', $element->tag)) {
+ // Header
+ $content .= '<h' . $element->tag[1] . '>' . $element->innertext . '</h' . $element->tag[1] . '>';
+ } elseif ($element->tag == 'div') {
+ if (preg_match('/.*widget--type-image.*/', $element->class)) {
+ // Image
+ $content .= '<figure>';
+ $content .= '<img src="' . $element->find('img', 0)->src . '">';
+ $caption = $element->find('figcaption', 0);
+ if ($caption) {
+ $content .= '<figcaption>' . $element->plaintext . '</figcaption>';
+ }
+ $content .= '</figure><br>';
+ } elseif (preg_match('/.*widget--type-quotation.*/', $element->class)) {
+ // Quotation
+ $quote = $element->find('.widget__quoteText', 0);
+ $author = $element->find('.widget__author', 0);
+ $content .= '<figure>';
+ $content .= '<blockquote>' . $quote->plaintext . '</blockquote>';
+ if ($author) {
+ $content .= '<figcaption>' . $author->plaintext . '</figcaption>';
+ }
+ $content .= '</figure><br>';
+ }
+ }
+ }
+ }
- // Video article
- if (is_null($article_content)) {
- $image = $html->find('.c-article-media__img', 0);
- if ($image) {
- $content .= '<figure>';
- $content .= '<img src="' . $image->src . '">';
- $content .= '</figure><br>';
- }
+ // Video article
+ if (is_null($article_content)) {
+ $image = $html->find('.c-article-media__img', 0);
+ if ($image) {
+ $content .= '<figure>';
+ $content .= '<img src="' . $image->src . '">';
+ $content .= '</figure><br>';
+ }
- $description = $html->find('.m-object__description', 0);
- if ($description) {
- // In some editions the description is a link to the
- // current page
- $content .= '<div>' . $description->plaintext . '</div>';
- }
+ $description = $html->find('.m-object__description', 0);
+ if ($description) {
+ // In some editions the description is a link to the
+ // current page
+ $content .= '<div>' . $description->plaintext . '</div>';
+ }
- // Euronews usually hosts videos on dailymotion...
- $player_div = $html->find('.dmPlayer', 0);
- if ($player_div) {
- $video_id = $player_div->getAttribute('data-video-id');
- $video_url = 'https://www.dailymotion.com/video/' . $video_id;
- $content .= '<a href="' . $video_url . '">' . $video_url . '</a>';
- }
+ // Euronews usually hosts videos on dailymotion...
+ $player_div = $html->find('.dmPlayer', 0);
+ if ($player_div) {
+ $video_id = $player_div->getAttribute('data-video-id');
+ $video_url = 'https://www.dailymotion.com/video/' . $video_id;
+ $content .= '<a href="' . $video_url . '">' . $video_url . '</a>';
+ }
- // ...or on YouTube
- $player_div = $html->find('.js-player-pfp', 0);
- if ($player_div) {
- $video_id = $player_div->getAttribute('data-video-id');
- $video_url = 'https://www.youtube.com/watch?v=' . $video_id;
- $content .= '<a href="' . $video_url . '">' . $video_url . '</a>';
- }
- }
+ // ...or on YouTube
+ $player_div = $html->find('.js-player-pfp', 0);
+ if ($player_div) {
+ $video_id = $player_div->getAttribute('data-video-id');
+ $video_url = 'https://www.youtube.com/watch?v=' . $video_id;
+ $content .= '<a href="' . $video_url . '">' . $video_url . '</a>';
+ }
+ }
- return array(
- 'author' => $author,
- 'content' => $content,
- 'enclosures' => $enclosures
- );
- }
+ return [
+ 'author' => $author,
+ 'content' => $content,
+ 'enclosures' => $enclosures
+ ];
+ }
}
diff --git a/bridges/ExecuteProgramBridge.php b/bridges/ExecuteProgramBridge.php
index 24342d1f..a2da864e 100644
--- a/bridges/ExecuteProgramBridge.php
+++ b/bridges/ExecuteProgramBridge.php
@@ -2,37 +2,37 @@
class ExecuteProgramBridge extends BridgeAbstract
{
- const NAME = 'Execute Program Blog';
- const URI = 'https://www.executeprogram.com/blog';
- const DESCRIPTION = 'Unofficial feed for the www.executeprogram.com blog';
- const MAINTAINER = 'dvikan';
+ const NAME = 'Execute Program Blog';
+ const URI = 'https://www.executeprogram.com/blog';
+ const DESCRIPTION = 'Unofficial feed for the www.executeprogram.com blog';
+ const MAINTAINER = 'dvikan';
- public function collectData()
- {
- $data = json_decode(getContents('https://www.executeprogram.com/api/pages/blog'));
+ public function collectData()
+ {
+ $data = json_decode(getContents('https://www.executeprogram.com/api/pages/blog'));
- foreach ($data->posts as $post) {
- $year = $post->date->year;
- $month = $post->date->month;
- $day = $post->date->day;
+ foreach ($data->posts as $post) {
+ $year = $post->date->year;
+ $month = $post->date->month;
+ $day = $post->date->day;
- $item = array();
- $item['uri'] = sprintf('https://www.executeprogram.com/blog/%s', $post->slug);
- $item['title'] = $post->title;
- $dateTime = \DateTime::createFromFormat('Y-m-d', $year . '-' . $month . '-' . $day);
- $item['timestamp'] = $dateTime->format('U');
- $item['content'] = $post->body;
+ $item = [];
+ $item['uri'] = sprintf('https://www.executeprogram.com/blog/%s', $post->slug);
+ $item['title'] = $post->title;
+ $dateTime = \DateTime::createFromFormat('Y-m-d', $year . '-' . $month . '-' . $day);
+ $item['timestamp'] = $dateTime->format('U');
+ $item['content'] = $post->body;
- $this->items[] = $item;
- }
+ $this->items[] = $item;
+ }
- usort($this->items, function ($a, $b) {
- return $a['timestamp'] < $b['timestamp'];
- });
- }
+ usort($this->items, function ($a, $b) {
+ return $a['timestamp'] < $b['timestamp'];
+ });
+ }
- public function getIcon()
- {
- return 'https://www.executeprogram.com/favicon.ico';
- }
+ public function getIcon()
+ {
+ return 'https://www.executeprogram.com/favicon.ico';
+ }
}
diff --git a/bridges/ExplosmBridge.php b/bridges/ExplosmBridge.php
index cfe42195..8874c6cb 100644
--- a/bridges/ExplosmBridge.php
+++ b/bridges/ExplosmBridge.php
@@ -1,59 +1,61 @@
<?php
-class ExplosmBridge extends BridgeAbstract {
- const MAINTAINER = 'bockiii';
- const NAME = 'Explosm Bridge';
- const URI = 'https://www.explosm.net/';
- const CACHE_TIMEOUT = 4800; //2hours
- const DESCRIPTION = 'Returns the last 5 comics';
- const PARAMETERS = array(
- 'Get latest posts' => array(
- 'limit' => array(
- 'name' => 'Posts limit',
- 'type' => 'number',
- 'title' => 'Maximum number of items to return',
- 'defaultValue' => 5
- )
- )
- );
+class ExplosmBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'bockiii';
+ const NAME = 'Explosm Bridge';
+ const URI = 'https://www.explosm.net/';
+ const CACHE_TIMEOUT = 4800; //2hours
+ const DESCRIPTION = 'Returns the last 5 comics';
+ const PARAMETERS = [
+ 'Get latest posts' => [
+ 'limit' => [
+ 'name' => 'Posts limit',
+ 'type' => 'number',
+ 'title' => 'Maximum number of items to return',
+ 'defaultValue' => 5
+ ]
+ ]
+ ];
- public function collectData(){
- $limit = $this->getInput('limit');
- $latest = getSimpleHTMLDOM('https://explosm.net/comics/latest');
- $image = $latest->find('div[id=comic]', 0)->find('img', 0)->getAttribute('src');
- $date_string = $latest->find('p[class*=Author__P]', 0)->innertext;
- $next_data_string = $latest->find('script[id=__NEXT_DATA__]', 0)->innertext;
- $exp = '/{\\\"latest\\\":\[{\\\"slug\\\":\\\"(.*?)\\ /';
- $reg_array = array();
- preg_match($exp, $next_data_string, $reg_array);
- $comic_id = $reg_array[1];
- $comic_id = substr($comic_id, 0, strpos($comic_id, '\\'));
- $item = array();
- $item['uri'] = $this::URI . 'comics/' . $comic_id;
- $item['uid'] = $this::URI . 'comics/' . $comic_id;
- $item['title'] = 'Comic for ' . $date_string;
- $item['timestamp'] = strtotime($date_string);
- $item['author'] = $latest->find('p[class*=Author__P]', 2)->innertext;
- $item['content'] = '<img src="' . $image . '" />';
- $this->items[] = $item;
+ public function collectData()
+ {
+ $limit = $this->getInput('limit');
+ $latest = getSimpleHTMLDOM('https://explosm.net/comics/latest');
+ $image = $latest->find('div[id=comic]', 0)->find('img', 0)->getAttribute('src');
+ $date_string = $latest->find('p[class*=Author__P]', 0)->innertext;
+ $next_data_string = $latest->find('script[id=__NEXT_DATA__]', 0)->innertext;
+ $exp = '/{\\\"latest\\\":\[{\\\"slug\\\":\\\"(.*?)\\ /';
+ $reg_array = [];
+ preg_match($exp, $next_data_string, $reg_array);
+ $comic_id = $reg_array[1];
+ $comic_id = substr($comic_id, 0, strpos($comic_id, '\\'));
+ $item = [];
+ $item['uri'] = $this::URI . 'comics/' . $comic_id;
+ $item['uid'] = $this::URI . 'comics/' . $comic_id;
+ $item['title'] = 'Comic for ' . $date_string;
+ $item['timestamp'] = strtotime($date_string);
+ $item['author'] = $latest->find('p[class*=Author__P]', 2)->innertext;
+ $item['content'] = '<img src="' . $image . '" />';
+ $this->items[] = $item;
- $next_comic = substr($this::URI, 0, -1)
- . $latest->find('div[class*=MainComic__Selector]', 0)->find('a', 0)->getAttribute('href');
- // use index 1 as the latest comic was already found
- for ($i = 1; $i <= $limit; $i++) {
- $this_comic = getSimpleHTMLDOM($next_comic);
- $image = $this_comic->find('div[id=comic]', 0)->find('img', 0)->getAttribute('src');
- $date_string = $this_comic->find('p[class*=Author__P]', 0)->innertext;
- $item = array();
- $item['uri'] = $next_comic;
- $item['uid'] = $next_comic;
- $item['title'] = 'Comic for ' . $date_string;
- $item['timestamp'] = strtotime($date_string);
- $item['author'] = $this_comic->find('p[class*=Author__P]', 2)->innertext;
- $item['content'] = '<img src="' . $image . '" />';
- $this->items[] = $item;
- $next_comic = substr($this::URI, 0, -1)
- . $this_comic->find('div[class*=MainComic__Selector]', 0)->find('a', 0)->getAttribute('href'); // get next comic link
- }
- }
+ $next_comic = substr($this::URI, 0, -1)
+ . $latest->find('div[class*=MainComic__Selector]', 0)->find('a', 0)->getAttribute('href');
+ // use index 1 as the latest comic was already found
+ for ($i = 1; $i <= $limit; $i++) {
+ $this_comic = getSimpleHTMLDOM($next_comic);
+ $image = $this_comic->find('div[id=comic]', 0)->find('img', 0)->getAttribute('src');
+ $date_string = $this_comic->find('p[class*=Author__P]', 0)->innertext;
+ $item = [];
+ $item['uri'] = $next_comic;
+ $item['uid'] = $next_comic;
+ $item['title'] = 'Comic for ' . $date_string;
+ $item['timestamp'] = strtotime($date_string);
+ $item['author'] = $this_comic->find('p[class*=Author__P]', 2)->innertext;
+ $item['content'] = '<img src="' . $image . '" />';
+ $this->items[] = $item;
+ $next_comic = substr($this::URI, 0, -1)
+ . $this_comic->find('div[class*=MainComic__Selector]', 0)->find('a', 0)->getAttribute('href'); // get next comic link
+ }
+ }
}
diff --git a/bridges/ExtremeDownloadBridge.php b/bridges/ExtremeDownloadBridge.php
index 60301fe7..074045df 100644
--- a/bridges/ExtremeDownloadBridge.php
+++ b/bridges/ExtremeDownloadBridge.php
@@ -1,111 +1,116 @@
<?php
-class ExtremeDownloadBridge extends BridgeAbstract {
- const NAME = 'Extreme Download';
- const URI = 'https://www.extreme-down.plus/';
- const DESCRIPTION = 'Suivi de série sur Extreme Download';
- const MAINTAINER = 'sysadminstory';
- const PARAMETERS = array(
- 'Suivre la publication des épisodes d\'une série en cours de diffusion' => array(
- 'url' => array(
- 'name' => 'URL de la série',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'URL d\'une série sans le https://www.extreme-down.plus/',
- 'exampleValue' => 'series-hd/hd-series-vostfr/46631-halt-and-catch-fire-saison-04-vostfr-hdtv-720p.html'),
- 'filter' => array(
- 'name' => 'Type de contenu',
- 'type' => 'list',
- 'title' => 'Type de contenu à suivre : Téléchargement, Streaming ou les deux',
- 'values' => array(
- 'Streaming et Téléchargement' => 'both',
- 'Téléchargement' => 'download',
- 'Streaming' => 'streaming'
- )
- )
- )
- );
- public function collectData(){
- $html = getSimpleHTMLDOM(self::URI . $this->getInput('url'));
+class ExtremeDownloadBridge extends BridgeAbstract
+{
+ const NAME = 'Extreme Download';
+ const URI = 'https://www.extreme-down.plus/';
+ const DESCRIPTION = 'Suivi de série sur Extreme Download';
+ const MAINTAINER = 'sysadminstory';
+ const PARAMETERS = [
+ 'Suivre la publication des épisodes d\'une série en cours de diffusion' => [
+ 'url' => [
+ 'name' => 'URL de la série',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'URL d\'une série sans le https://www.extreme-down.plus/',
+ 'exampleValue' => 'series-hd/hd-series-vostfr/46631-halt-and-catch-fire-saison-04-vostfr-hdtv-720p.html'],
+ 'filter' => [
+ 'name' => 'Type de contenu',
+ 'type' => 'list',
+ 'title' => 'Type de contenu à suivre : Téléchargement, Streaming ou les deux',
+ 'values' => [
+ 'Streaming et Téléchargement' => 'both',
+ 'Téléchargement' => 'download',
+ 'Streaming' => 'streaming'
+ ]
+ ]
+ ]
+ ];
- $filter = $this->getInput('filter');
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI . $this->getInput('url'));
- $typesText = array(
- 'download' => 'Téléchargement',
- 'streaming' => 'Streaming'
- );
+ $filter = $this->getInput('filter');
- // Get the TV show title
- $this->showTitle = trim($html->find('span[id=news-title]', 0)->plaintext);
+ $typesText = [
+ 'download' => 'Téléchargement',
+ 'streaming' => 'Streaming'
+ ];
- $list = $html->find('div[class=prez_7]');
- foreach($list as $element) {
- $add = false;
- // Link type is needed is needed to generate an unique link
- $type = $this->findLinkType($element);
- if($filter == 'both') {
- $add = true;
- } else {
- if($type == $filter) {
- $add = true;
- }
- }
- if($add == true) {
- $item = array();
+ // Get the TV show title
+ $this->showTitle = trim($html->find('span[id=news-title]', 0)->plaintext);
- // Get the element name
- $title = $element->plaintext;
+ $list = $html->find('div[class=prez_7]');
+ foreach ($list as $element) {
+ $add = false;
+ // Link type is needed is needed to generate an unique link
+ $type = $this->findLinkType($element);
+ if ($filter == 'both') {
+ $add = true;
+ } else {
+ if ($type == $filter) {
+ $add = true;
+ }
+ }
+ if ($add == true) {
+ $item = [];
- // Get thee element links
- $links = $element->next_sibling()->innertext;
+ // Get the element name
+ $title = $element->plaintext;
- $item['content'] = $links;
- $item['title'] = $this->showTitle . ' ' . $title . ' - ' . $typesText[$type];
- // As RSS Bridge use the URI as GUID they need to be unique : adding a md5 hash of the title element
- // should geneerate unique URI to prevent confusion for RSS readers
- $item['uri'] = self::URI . $this->getInput('url') . '#' . hash('md5', $item['title']);
+ // Get thee element links
+ $links = $element->next_sibling()->innertext;
- $this->items[] = $item;
- }
- }
- }
+ $item['content'] = $links;
+ $item['title'] = $this->showTitle . ' ' . $title . ' - ' . $typesText[$type];
+ // As RSS Bridge use the URI as GUID they need to be unique : adding a md5 hash of the title element
+ // should geneerate unique URI to prevent confusion for RSS readers
+ $item['uri'] = self::URI . $this->getInput('url') . '#' . hash('md5', $item['title']);
- public function getName(){
- switch($this->queriedContext) {
- case 'Suivre la publication des épisodes d\'une série en cours de diffusion':
- return $this->showTitle . ' - ' . self::NAME;
- break;
- default:
- return self::NAME;
- }
- }
+ $this->items[] = $item;
+ }
+ }
+ }
- public function getURI() {
- switch($this->queriedContext) {
- case 'Suivre la publication des épisodes d\'une série en cours de diffusion':
- return self::URI . $this->getInput('url');
- break;
- default:
- return self::URI;
- }
- }
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Suivre la publication des épisodes d\'une série en cours de diffusion':
+ return $this->showTitle . ' - ' . self::NAME;
+ break;
+ default:
+ return self::NAME;
+ }
+ }
- private function findLinkType($element)
- {
- $return = '';
- // Walk through all elements in the reverse order until finding one with class 'presz_2'
- while($element->class != 'prez_2') {
- $element = $element->prev_sibling();
- }
- $text = html_entity_decode($element->plaintext);
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Suivre la publication des épisodes d\'une série en cours de diffusion':
+ return self::URI . $this->getInput('url');
+ break;
+ default:
+ return self::URI;
+ }
+ }
- // Regarding the text of the element, return the according link type
- if(stristr($text, 'téléchargement') != false) {
- $return = 'download';
- } else if(stristr($text, 'streaming') != false) {
- $return = 'streaming';
- }
+ private function findLinkType($element)
+ {
+ $return = '';
+ // Walk through all elements in the reverse order until finding one with class 'presz_2'
+ while ($element->class != 'prez_2') {
+ $element = $element->prev_sibling();
+ }
+ $text = html_entity_decode($element->plaintext);
- return $return;
- }
+ // Regarding the text of the element, return the according link type
+ if (stristr($text, 'téléchargement') != false) {
+ $return = 'download';
+ } elseif (stristr($text, 'streaming') != false) {
+ $return = 'streaming';
+ }
+
+ return $return;
+ }
}
diff --git a/bridges/FB2Bridge.php b/bridges/FB2Bridge.php
index 46a92c56..efebd48b 100644
--- a/bridges/FB2Bridge.php
+++ b/bridges/FB2Bridge.php
@@ -1,311 +1,327 @@
<?php
-class FB2Bridge extends BridgeAbstract {
- const MAINTAINER = 'teromene';
- const NAME = 'Facebook Bridge | Touch Site';
- const URI = 'https://www.facebook.com/';
- const CACHE_TIMEOUT = 1000;
- const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
+class FB2Bridge extends BridgeAbstract
+{
+ const MAINTAINER = 'teromene';
+ const NAME = 'Facebook Bridge | Touch Site';
+ const URI = 'https://www.facebook.com/';
+ const CACHE_TIMEOUT = 1000;
+ const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
please insert the parameter as follow : myExamplePage/132621766841117';
- const PARAMETERS = array( array(
- 'u' => array(
- 'name' => 'Username',
- 'required' => true
- ),
- 'abbrev_name' => array(
- 'name' => 'Abbreviate author name in title',
- 'type' => 'checkbox',
- 'defaultValue' => true,
- ),
- ));
-
- public function getIcon() {
- return 'https://static.xx.fbcdn.net/rsrc.php/yo/r/iRmz9lCMBD2.ico';
- }
-
- public function collectData(){
-
- //Utility function for cleaning a Facebook link
- $unescape_fb_link = function($matches){
- if(is_array($matches) && count($matches) > 1) {
- $link = $matches[1];
- if(strpos($link, '/') === 0)
- $link = self::URI . substr($link, 1);
- if(strpos($link, 'facebook.com/l.php?u=') !== false)
- $link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&'));
- return ' href="' . $link . '"';
- }
- };
-
- //Utility function for converting facebook emoticons
- $unescape_fb_emote = function($matches){
- static $facebook_emoticons = array(
- 'smile' => ':)',
- 'frown' => ':(',
- 'tongue' => ':P',
- 'grin' => ':D',
- 'gasp' => ':O',
- 'wink' => ';)',
- 'pacman' => ':<',
- 'grumpy' => '>_<',
- 'unsure' => ':/',
- 'cry' => ':\'(',
- 'kiki' => '^_^',
- 'glasses' => '8-)',
- 'sunglasses' => 'B-)',
- 'heart' => '<3',
- 'devil' => ']:D',
- 'angel' => '0:)',
- 'squint' => '-_-',
- 'confused' => 'o_O',
- 'upset' => 'xD',
- 'colonthree' => ':3',
- 'like' => '&#x1F44D;');
- $len = count($matches);
- if ($len > 1)
- for ($i = 1; $i < $len; $i++)
- foreach ($facebook_emoticons as $name => $emote)
- if ($matches[$i] === $name)
- return $emote;
- return $matches[0];
- };
-
- if($this->getInput('u') !== null) {
- $page = 'https://touch.facebook.com/' . $this->getInput('u');
- $cookies = $this->getCookies($page);
- $pageInfo = $this->getPageInfos($page, $cookies);
-
- if($pageInfo['userId'] === null) {
- returnClientError(<<<EOD
+ const PARAMETERS = [ [
+ 'u' => [
+ 'name' => 'Username',
+ 'required' => true
+ ],
+ 'abbrev_name' => [
+ 'name' => 'Abbreviate author name in title',
+ 'type' => 'checkbox',
+ 'defaultValue' => true,
+ ],
+ ]];
+
+ public function getIcon()
+ {
+ return 'https://static.xx.fbcdn.net/rsrc.php/yo/r/iRmz9lCMBD2.ico';
+ }
+
+ public function collectData()
+ {
+ //Utility function for cleaning a Facebook link
+ $unescape_fb_link = function ($matches) {
+ if (is_array($matches) && count($matches) > 1) {
+ $link = $matches[1];
+ if (strpos($link, '/') === 0) {
+ $link = self::URI . substr($link, 1);
+ }
+ if (strpos($link, 'facebook.com/l.php?u=') !== false) {
+ $link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&'));
+ }
+ return ' href="' . $link . '"';
+ }
+ };
+
+ //Utility function for converting facebook emoticons
+ $unescape_fb_emote = function ($matches) {
+ static $facebook_emoticons = [
+ 'smile' => ':)',
+ 'frown' => ':(',
+ 'tongue' => ':P',
+ 'grin' => ':D',
+ 'gasp' => ':O',
+ 'wink' => ';)',
+ 'pacman' => ':<',
+ 'grumpy' => '>_<',
+ 'unsure' => ':/',
+ 'cry' => ':\'(',
+ 'kiki' => '^_^',
+ 'glasses' => '8-)',
+ 'sunglasses' => 'B-)',
+ 'heart' => '<3',
+ 'devil' => ']:D',
+ 'angel' => '0:)',
+ 'squint' => '-_-',
+ 'confused' => 'o_O',
+ 'upset' => 'xD',
+ 'colonthree' => ':3',
+ 'like' => '&#x1F44D;'];
+ $len = count($matches);
+ if ($len > 1) {
+ for ($i = 1; $i < $len; $i++) {
+ foreach ($facebook_emoticons as $name => $emote) {
+ if ($matches[$i] === $name) {
+ return $emote;
+ }
+ }
+ }
+ }
+ return $matches[0];
+ };
+
+ if ($this->getInput('u') !== null) {
+ $page = 'https://touch.facebook.com/' . $this->getInput('u');
+ $cookies = $this->getCookies($page);
+ $pageInfo = $this->getPageInfos($page, $cookies);
+
+ if ($pageInfo['userId'] === null) {
+ returnClientError(<<<EOD
Unable to get the page id. You should consider getting the ID by hand, then importing it into FB2Bridge
EOD
- );
- } elseif($pageInfo['userId'] == -1) {
- returnClientError(<<<EOD
+ );
+ } elseif ($pageInfo['userId'] == -1) {
+ returnClientError(<<<EOD
This page is not accessible without being logged in.
EOD
- );
- }
- }
-
- //Build the string for the first request
- $requestString = 'https://touch.facebook.com/page_content_list_view/more/?page_id='
- . $pageInfo['userId']
- . '&start_cursor=1&num_to_fetch=105&surface_type=timeline';
- $fileContent = getContents($requestString);
- $html = $this->buildContent($fileContent);
- $author = $pageInfo['username'];
-
- foreach($html->find('article') as $content) {
-
- $item = array();
-
- preg_match('/publish_time\\\":([0-9]+),/', $content->getAttribute('data-store', 0), $match);
- if(isset($match[1]))
- $timestamp = $match[1];
- else
- $timestamp = 0;
-
- $item['uri'] = html_entity_decode('https://touch.facebook.com'
- . $content->find("div[class='_52jc _5qc4 _78cz _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href'), ENT_QUOTES);
-
- //Decode images
- $imagecleaned = preg_replace_callback('/<i [^>]* style="[^"]*url\(\'(.*?)\'\).*?><\/i>/m', function ($matches) {
- return "<img src='" . str_replace(array('\\3a ', '\\3d ', '\\26 '), array(':', '=', '&'), $matches[1]) . "' />";
- }, $content);
- $content = str_get_html($imagecleaned);
-
- if($content->find('header', 0) !== null) {
- $content->find('header', 0)->innertext = '';
- }
-
- if($content->find('footer', 0) !== null) {
- $content->find('footer', 0)->innertext = '';
- }
-
- // Replace emoticon images by their textual representation (part of the span)
- foreach($content->find('span[title*="emoticon"]') as $emoticon) {
- $emoticon->innertext = $emoticon->find('span[aria-hidden="true"]', 0)->innertext;
- }
-
- //Remove html nodes, keep only img, links, basic formatting
- $content = strip_tags($content, '<a><img><i><u><br><p><h3><h4><section>');
-
- //Adapt link hrefs: convert relative links into absolute links and bypass external link redirection
- $content = preg_replace_callback('/ href=\"([^"]+)\"/i', $unescape_fb_link, $content);
-
- //Clean useless html tag properties and fix link closing tags
- foreach (array(
- 'onmouseover',
- 'onclick',
- 'target',
- 'ajaxify',
- 'tabindex',
- 'class',
- 'data-[^=]*',
- 'aria-[^=]*',
- 'role',
- 'rel',
- 'id') as $property_name)
- $content = preg_replace('/ ' . $property_name . '=\"[^"]*\"/i', '', $content);
- $content = preg_replace('/<\/a [^>]+>/i', '</a>', $content);
-
- //Convert textual representation of emoticons eg
- // "<i><u>smile emoticon</u></i>" back to ASCII emoticons eg ":)"
- $content = preg_replace_callback('/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i', $unescape_fb_emote, $content);
-
- //Remove the "...Plus" tag
- $content = preg_replace(
- '/… (<span>|)<a href="https:\/\/www\.facebook\.com\/story\.php\?story_fbid=.*?<\/a>/m',
- '', $content, 1);
-
- //Remove tracking images
- $content = preg_replace('/<img src=\'.*?safe_image\.php.*?\' \/>/m', '', $content);
-
- //Remove the double section tags
- $content = str_replace(
- array('<section><section>', '</section></section>'),
- array('<section>', '</section>'),
- $content
- );
-
- //Move the section tag link upper, if it is down
- $content = str_get_html($content);
- $sectionContent = $content->find('section', 0);
- if($sectionContent != null) {
- $sectionLink = $sectionContent->nextSibling();
- if($sectionLink != null) {
- $fullLink = '<a href="' . $sectionLink->getAttribute('href') . '">' . $sectionContent->innertext . '</a>';
- $sectionContent->innertext = $fullLink;
- }
- }
-
- //Move the href tag upper if it is inside the section
- foreach($content->find('section > a') as $sectionToFix) {
- $sectionLink = $sectionToFix->getAttribute('href');
- $section = $sectionToFix->parent();
- $section->outertext = '<a href="' . $sectionLink . '">' . $section . '</a>';
- }
-
- $item['content'] = html_entity_decode($content, ENT_QUOTES);
-
- $title = $author;
- if ($this->getInput('abbrev_name') === true) {
- if (strlen($title) > 24)
- $title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...';
- }
- $title = $title . ' | ' . strip_tags($content);
- if (strlen($title) > 64)
- $title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
-
- $item['title'] = html_entity_decode($title, ENT_QUOTES);
- $item['author'] = html_entity_decode($author, ENT_QUOTES);
- $item['timestamp'] = html_entity_decode($timestamp, ENT_QUOTES);
-
- if($item['timestamp'] != 0)
- array_push($this->items, $item);
- }
-
- }
-
- //Builds the HTML from the encoded JS that Facebook provides.
- private function buildContent($pageContent){
- // The html ends with:
- // /div>","replaceifexists
- $regex = '/\\"html\\":(\".+\/div>"),"replace/';
- preg_match($regex, $pageContent, $result);
-
- $htmlContent = json_decode($result[1]);
- $htmlContent = preg_replace('/(?<!style)="(.*?)"/', '=\'$1\'', $htmlContent);
- $htmlContent = html_entity_decode($htmlContent, ENT_QUOTES, 'UTF-8');
-
- return str_get_html($htmlContent);
- }
-
- //Builds the cookie from the page, as Facebook sometimes refuses to give
- //the page if no cookie is provided.
- private function getCookies($pageURL){
-
- $ctx = stream_context_create(array(
- 'http' => array(
- 'user_agent' => Configuration::getConfig('http', 'useragent'),
- 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
- )
- )
- );
- $a = file_get_contents($pageURL, 0, $ctx);
-
- //First request to get the cookie
- $cookies = '';
- foreach($http_response_header as $hdr) {
- if(strpos($hdr, 'Set-Cookie') !== false) {
- $cLine = explode(':', $hdr)[1];
- $cLine = explode(';', $cLine)[0];
- $cookies .= ';' . $cLine;
- }
- }
-
- return substr($cookies, 1);
- }
-
- //Get the page ID and username from the Facebook page.
- private function getPageInfos($page, $cookies){
-
- $context = stream_context_create(array(
- 'http' => array(
- 'user_agent' => Configuration::getConfig('http', 'useragent'),
- 'header' => 'Cookie: ' . $cookies
- )
- )
- );
-
- $pageContent = file_get_contents($page, 0, $context);
-
- if(strpos($pageContent, 'signup-button') != false) {
- return -1;
- }
-
- //Get the username
- $usernameRegex = '/data-nt=\"FB:TEXT4\">(.*?)<\/div>/m';
- preg_match($usernameRegex, $pageContent, $usernameMatches);
- if(count($usernameMatches) > 0) {
- $username = strip_tags($usernameMatches[1]);
- } else {
- $username = $this->getInput('u');
- }
-
- //Get the page ID if we don't have a captcha
- $regex = '/page_id=([0-9]*)&/';
- preg_match($regex, $pageContent, $matches);
-
- if(count($matches) > 0) {
- return array('userId' => $matches[1], 'username' => $username);
- }
-
- //Get the page ID if we do have a captcha
- $regex = '/"pageID":"([0-9]*)"/';
- preg_match($regex, $pageContent, $matches);
-
- return array('userId' => $matches[1], 'username' => $username);
-
- }
-
- public function getName(){
- $username = $this->getInput('u');
- if (isset($username)) {
- return $this->getInput('u') . ' | Facebook';
- } else {
- return self::NAME;
- }
- }
-
- public function getURI(){
- $username = $this->getInput('u');
- if (isset($username)) {
- return 'https://facebook.com/' . $this->getInput('u') . '/posts';
- } else {
- return self::URI;
- }
- }
+ );
+ }
+ }
+
+ //Build the string for the first request
+ $requestString = 'https://touch.facebook.com/page_content_list_view/more/?page_id='
+ . $pageInfo['userId']
+ . '&start_cursor=1&num_to_fetch=105&surface_type=timeline';
+ $fileContent = getContents($requestString);
+ $html = $this->buildContent($fileContent);
+ $author = $pageInfo['username'];
+
+ foreach ($html->find('article') as $content) {
+ $item = [];
+
+ preg_match('/publish_time\\\":([0-9]+),/', $content->getAttribute('data-store', 0), $match);
+ if (isset($match[1])) {
+ $timestamp = $match[1];
+ } else {
+ $timestamp = 0;
+ }
+
+ $item['uri'] = html_entity_decode('https://touch.facebook.com'
+ . $content->find("div[class='_52jc _5qc4 _78cz _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href'), ENT_QUOTES);
+
+ //Decode images
+ $imagecleaned = preg_replace_callback('/<i [^>]* style="[^"]*url\(\'(.*?)\'\).*?><\/i>/m', function ($matches) {
+ return "<img src='" . str_replace(['\\3a ', '\\3d ', '\\26 '], [':', '=', '&'], $matches[1]) . "' />";
+ }, $content);
+ $content = str_get_html($imagecleaned);
+
+ if ($content->find('header', 0) !== null) {
+ $content->find('header', 0)->innertext = '';
+ }
+
+ if ($content->find('footer', 0) !== null) {
+ $content->find('footer', 0)->innertext = '';
+ }
+
+ // Replace emoticon images by their textual representation (part of the span)
+ foreach ($content->find('span[title*="emoticon"]') as $emoticon) {
+ $emoticon->innertext = $emoticon->find('span[aria-hidden="true"]', 0)->innertext;
+ }
+
+ //Remove html nodes, keep only img, links, basic formatting
+ $content = strip_tags($content, '<a><img><i><u><br><p><h3><h4><section>');
+
+ //Adapt link hrefs: convert relative links into absolute links and bypass external link redirection
+ $content = preg_replace_callback('/ href=\"([^"]+)\"/i', $unescape_fb_link, $content);
+
+ //Clean useless html tag properties and fix link closing tags
+ foreach (
+ [
+ 'onmouseover',
+ 'onclick',
+ 'target',
+ 'ajaxify',
+ 'tabindex',
+ 'class',
+ 'data-[^=]*',
+ 'aria-[^=]*',
+ 'role',
+ 'rel',
+ 'id'] as $property_name
+ ) {
+ $content = preg_replace('/ ' . $property_name . '=\"[^"]*\"/i', '', $content);
+ }
+ $content = preg_replace('/<\/a [^>]+>/i', '</a>', $content);
+
+ //Convert textual representation of emoticons eg
+ // "<i><u>smile emoticon</u></i>" back to ASCII emoticons eg ":)"
+ $content = preg_replace_callback('/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i', $unescape_fb_emote, $content);
+
+ //Remove the "...Plus" tag
+ $content = preg_replace(
+ '/… (<span>|)<a href="https:\/\/www\.facebook\.com\/story\.php\?story_fbid=.*?<\/a>/m',
+ '',
+ $content,
+ 1
+ );
+
+ //Remove tracking images
+ $content = preg_replace('/<img src=\'.*?safe_image\.php.*?\' \/>/m', '', $content);
+
+ //Remove the double section tags
+ $content = str_replace(
+ ['<section><section>', '</section></section>'],
+ ['<section>', '</section>'],
+ $content
+ );
+
+ //Move the section tag link upper, if it is down
+ $content = str_get_html($content);
+ $sectionContent = $content->find('section', 0);
+ if ($sectionContent != null) {
+ $sectionLink = $sectionContent->nextSibling();
+ if ($sectionLink != null) {
+ $fullLink = '<a href="' . $sectionLink->getAttribute('href') . '">' . $sectionContent->innertext . '</a>';
+ $sectionContent->innertext = $fullLink;
+ }
+ }
+
+ //Move the href tag upper if it is inside the section
+ foreach ($content->find('section > a') as $sectionToFix) {
+ $sectionLink = $sectionToFix->getAttribute('href');
+ $section = $sectionToFix->parent();
+ $section->outertext = '<a href="' . $sectionLink . '">' . $section . '</a>';
+ }
+
+ $item['content'] = html_entity_decode($content, ENT_QUOTES);
+
+ $title = $author;
+ if ($this->getInput('abbrev_name') === true) {
+ if (strlen($title) > 24) {
+ $title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...';
+ }
+ }
+ $title = $title . ' | ' . strip_tags($content);
+ if (strlen($title) > 64) {
+ $title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
+ }
+
+ $item['title'] = html_entity_decode($title, ENT_QUOTES);
+ $item['author'] = html_entity_decode($author, ENT_QUOTES);
+ $item['timestamp'] = html_entity_decode($timestamp, ENT_QUOTES);
+
+ if ($item['timestamp'] != 0) {
+ array_push($this->items, $item);
+ }
+ }
+ }
+
+ //Builds the HTML from the encoded JS that Facebook provides.
+ private function buildContent($pageContent)
+ {
+ // The html ends with:
+ // /div>","replaceifexists
+ $regex = '/\\"html\\":(\".+\/div>"),"replace/';
+ preg_match($regex, $pageContent, $result);
+
+ $htmlContent = json_decode($result[1]);
+ $htmlContent = preg_replace('/(?<!style)="(.*?)"/', '=\'$1\'', $htmlContent);
+ $htmlContent = html_entity_decode($htmlContent, ENT_QUOTES, 'UTF-8');
+
+ return str_get_html($htmlContent);
+ }
+
+ //Builds the cookie from the page, as Facebook sometimes refuses to give
+ //the page if no cookie is provided.
+ private function getCookies($pageURL)
+ {
+ $ctx = stream_context_create([
+ 'http' => [
+ 'user_agent' => Configuration::getConfig('http', 'useragent'),
+ 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
+ ]
+ ]);
+ $a = file_get_contents($pageURL, 0, $ctx);
+
+ //First request to get the cookie
+ $cookies = '';
+ foreach ($http_response_header as $hdr) {
+ if (strpos($hdr, 'Set-Cookie') !== false) {
+ $cLine = explode(':', $hdr)[1];
+ $cLine = explode(';', $cLine)[0];
+ $cookies .= ';' . $cLine;
+ }
+ }
+
+ return substr($cookies, 1);
+ }
+
+ //Get the page ID and username from the Facebook page.
+ private function getPageInfos($page, $cookies)
+ {
+ $context = stream_context_create([
+ 'http' => [
+ 'user_agent' => Configuration::getConfig('http', 'useragent'),
+ 'header' => 'Cookie: ' . $cookies
+ ]
+ ]);
+
+ $pageContent = file_get_contents($page, 0, $context);
+
+ if (strpos($pageContent, 'signup-button') != false) {
+ return -1;
+ }
+
+ //Get the username
+ $usernameRegex = '/data-nt=\"FB:TEXT4\">(.*?)<\/div>/m';
+ preg_match($usernameRegex, $pageContent, $usernameMatches);
+ if (count($usernameMatches) > 0) {
+ $username = strip_tags($usernameMatches[1]);
+ } else {
+ $username = $this->getInput('u');
+ }
+
+ //Get the page ID if we don't have a captcha
+ $regex = '/page_id=([0-9]*)&/';
+ preg_match($regex, $pageContent, $matches);
+
+ if (count($matches) > 0) {
+ return ['userId' => $matches[1], 'username' => $username];
+ }
+
+ //Get the page ID if we do have a captcha
+ $regex = '/"pageID":"([0-9]*)"/';
+ preg_match($regex, $pageContent, $matches);
+
+ return ['userId' => $matches[1], 'username' => $username];
+ }
+
+ public function getName()
+ {
+ $username = $this->getInput('u');
+ if (isset($username)) {
+ return $this->getInput('u') . ' | Facebook';
+ } else {
+ return self::NAME;
+ }
+ }
+
+ public function getURI()
+ {
+ $username = $this->getInput('u');
+ if (isset($username)) {
+ return 'https://facebook.com/' . $this->getInput('u') . '/posts';
+ } else {
+ return self::URI;
+ }
+ }
}
diff --git a/bridges/FDroidBridge.php b/bridges/FDroidBridge.php
index ca494b8c..d5663903 100644
--- a/bridges/FDroidBridge.php
+++ b/bridges/FDroidBridge.php
@@ -1,83 +1,88 @@
<?php
-class FDroidBridge extends BridgeAbstract {
- const MAINTAINER = 'Mitsukarenai';
- const NAME = 'F-Droid Bridge';
- const URI = 'https://f-droid.org/';
- const CACHE_TIMEOUT = 60 * 60 * 4; // 4 hours
- const DESCRIPTION = 'Returns latest added/updated apps on the open-source Android apps repository F-Droid';
+class FDroidBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Mitsukarenai';
+ const NAME = 'F-Droid Bridge';
+ const URI = 'https://f-droid.org/';
+ const CACHE_TIMEOUT = 60 * 60 * 4; // 4 hours
+ const DESCRIPTION = 'Returns latest added/updated apps on the open-source Android apps repository F-Droid';
- const PARAMETERS = array( array(
- 'u' => array(
- 'name' => 'Widget selection',
- 'type' => 'list',
- 'values' => array(
- 'Latest added apps' => 'added',
- 'Latest updated apps' => 'updated'
- )
- )
- ));
+ const PARAMETERS = [ [
+ 'u' => [
+ 'name' => 'Widget selection',
+ 'type' => 'list',
+ 'values' => [
+ 'Latest added apps' => 'added',
+ 'Latest updated apps' => 'updated'
+ ]
+ ]
+ ]];
- public function getIcon() {
- return self::URI . 'assets/favicon.ico?v=8j6PKzW9Mk';
- }
+ public function getIcon()
+ {
+ return self::URI . 'assets/favicon.ico?v=8j6PKzW9Mk';
+ }
- private function getTimestamp($url) {
- $curlOptions = array(
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_HEADER => true,
- CURLOPT_NOBODY => true,
- CURLOPT_CONNECTTIMEOUT => 19,
- CURLOPT_TIMEOUT => 19,
- );
- $ch = curl_init($url);
- curl_setopt_array($ch, $curlOptions);
- $curlHeaders = curl_exec($ch);
- $curlError = curl_error($ch);
- curl_close($ch);
- if(!empty($curlError))
- return false;
- $curlHeaders = explode("\n", $curlHeaders);
- $timestamp = false;
- foreach($curlHeaders as $header) {
- if(strpos($header, 'Last-Modified') !== false) {
- $timestamp = str_replace('Last-Modified: ', '', $header);
- $timestamp = strtotime($timestamp);
- }
- }
- return $timestamp;
- }
+ private function getTimestamp($url)
+ {
+ $curlOptions = [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_HEADER => true,
+ CURLOPT_NOBODY => true,
+ CURLOPT_CONNECTTIMEOUT => 19,
+ CURLOPT_TIMEOUT => 19,
+ ];
+ $ch = curl_init($url);
+ curl_setopt_array($ch, $curlOptions);
+ $curlHeaders = curl_exec($ch);
+ $curlError = curl_error($ch);
+ curl_close($ch);
+ if (!empty($curlError)) {
+ return false;
+ }
+ $curlHeaders = explode("\n", $curlHeaders);
+ $timestamp = false;
+ foreach ($curlHeaders as $header) {
+ if (strpos($header, 'Last-Modified') !== false) {
+ $timestamp = str_replace('Last-Modified: ', '', $header);
+ $timestamp = strtotime($timestamp);
+ }
+ }
+ return $timestamp;
+ }
- public function collectData(){
- $url = self::URI;
- $html = getSimpleHTMLDOM($url);
+ public function collectData()
+ {
+ $url = self::URI;
+ $html = getSimpleHTMLDOM($url);
- // targetting the corresponding widget based on user selection
- // "updated" is the 5th widget on the page, "added" is the 6th
+ // targetting the corresponding widget based on user selection
+ // "updated" is the 5th widget on the page, "added" is the 6th
- switch($this->getInput('u')) {
- case 'updated':
- $html_widget = $html->find('div.sidebar-widget', 5);
- break;
- default:
- $html_widget = $html->find('div.sidebar-widget', 6);
- break;
- }
+ switch ($this->getInput('u')) {
+ case 'updated':
+ $html_widget = $html->find('div.sidebar-widget', 5);
+ break;
+ default:
+ $html_widget = $html->find('div.sidebar-widget', 6);
+ break;
+ }
- // and now extracting app info from the selected widget (and yeah turns out icons are of heterogeneous sizes)
+ // and now extracting app info from the selected widget (and yeah turns out icons are of heterogeneous sizes)
- foreach($html_widget->find('a') as $element) {
- $item = array();
- $item['uri'] = self::URI . $element->href;
- $item['title'] = $element->find('h4', 0)->plaintext;
- $item['icon'] = $element->find('img', 0)->src;
- $item['timestamp'] = $this->getTimestamp($item['icon']);
- $item['summary'] = $element->find('span.package-summary', 0)->plaintext;
- $item['content'] = '
+ foreach ($html_widget->find('a') as $element) {
+ $item = [];
+ $item['uri'] = self::URI . $element->href;
+ $item['title'] = $element->find('h4', 0)->plaintext;
+ $item['icon'] = $element->find('img', 0)->src;
+ $item['timestamp'] = $this->getTimestamp($item['icon']);
+ $item['summary'] = $element->find('span.package-summary', 0)->plaintext;
+ $item['content'] = '
<a href="' . $item['uri'] . '">
<img alt="" style="max-height:128px" src="' . $item['icon'] . '">
</a><br>' . $item['summary'];
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/FDroidRepoBridge.php b/bridges/FDroidRepoBridge.php
index c26bbacf..74147310 100644
--- a/bridges/FDroidRepoBridge.php
+++ b/bridges/FDroidRepoBridge.php
@@ -1,150 +1,162 @@
<?php
-class FDroidRepoBridge extends BridgeAbstract {
- const NAME = 'F-Droid Repository Bridge';
- const URI = 'https://f-droid.org/';
- const DESCRIPTION = 'Query any F-Droid Repository for its latest updates.';
-
- const ITEM_LIMIT = 50;
-
- const PARAMETERS = array(
- 'global' => array(
- 'url' => array(
- 'name' => 'Repository URL',
- 'title' => 'Usually ends with /repo/',
- 'required' => true,
- 'exampleValue' => 'https://srv.tt-rss.org/fdroid/repo'
- )
- ),
- 'Latest Updates' => array(
- 'sorting' => array(
- 'name' => 'Sort By',
- 'type' => 'list',
- 'values' => array(
- 'Latest added apps' => 'added',
- 'Latest updated apps' => 'lastUpdated'
- )
- ),
- 'locale' => array(
- 'name' => 'Locale',
- 'defaultValue' => 'en-US'
- )
- ),
- 'Follow Package' => array(
- 'package' => array(
- 'name' => 'Package Identifier',
- 'required' => true,
- 'exampleValue' => 'org.fox.ttrss'
- )
- )
- );
-
- // Stores repo information
- private $repo;
-
- public function getURI() {
- if (empty($this->queriedContext))
- return parent::getURI();
-
- $url = rtrim($this->GetInput('url'), '/');
- return strstr($url, '?', true) ?: $url;
- }
-
- public function getName() {
- if (empty($this->queriedContext))
- return parent::getName();
-
- $name = $this->repo['repo']['name'];
- switch($this->queriedContext) {
- case 'Latest Updates':
- return $name;
- case 'Follow Package':
- return $this->getInput('package') . ' - ' . $name;
- default:
- returnServerError('Unimplemented Context (getName)');
- }
- }
-
- public function collectData() {
- $this->repo = $this->getRepo();
- switch($this->queriedContext) {
- case 'Latest Updates':
- $this->getAllUpdates();
- break;
- case 'Follow Package':
- $this->getPackage($this->getInput('package'));
- break;
- default:
- returnServerError('Unimplemented Context (collectData)');
- }
- }
-
- private function getRepo() {
- $url = $this->getURI();
-
- // Get repo information (only available as JAR)
- $jar = getContents($url . '/index-v1.jar');
- $jar_loc = tempnam(sys_get_temp_dir(), '');
- file_put_contents($jar_loc, $jar);
-
- // JAR files are specially formatted ZIP files
- $jar = new ZipArchive;
- if ($jar->open($jar_loc) !== true) {
- returnServerError('Failed to extract archive');
- }
-
- // Get file pointer to the relevant JSON inside
- $fp = $jar->getStream('index-v1.json');
- if (!$fp) {
- returnServerError('Failed to get file pointer');
- }
-
- $data = json_decode(stream_get_contents($fp), true);
- fclose($fp);
- $jar->close();
- return $data;
- }
-
- private function getAllUpdates() {
- $apps = $this->repo['apps'];
- usort($apps, function($a, $b) {
- return $b[$this->getInput('sorting')] <=> $a[$this->getInput('sorting')];
- });
- $apps = array_slice($apps, 0, self::ITEM_LIMIT);
- foreach($apps as $app) {
- $latest = reset($this->repo['packages'][$app['packageName']]);
-
- if (isset($app['localized'])) {
- // Try provided locale, then en-US, then any
- $lang = $app['localized'];
- $lang = $lang[$this->getInput('locale')] ?? $lang['en-US'] ?? reset($lang);
- } else
- $lang = array();
-
- $item = array();
- $item['uri'] = $this->getURI() . '/' . $latest['apkName'];
- $item['title'] = $lang['name'] ?? $app['packageName'];
- $item['title'] .= ' ' . $latest['versionName'];
- $item['timestamp'] = date(DateTime::ISO8601, (int) ($app['lastUpdated'] / 1000));
- if (isset($app['authorName']))
- $item['author'] = $app['authorName'];
- if (isset($app['categories']))
- $item['categories'] = $app['categories'];
-
- // Adding Content
- $icon = $app['icon'] ?? '';
- if (!empty($icon)) {
- $icon = $this->getURI() . '/icons-320/' . $icon;
- $item['enclosures'] = array($icon);
- $icon = '<img src="' . $icon . '">';
- }
- $summary = $lang['summary'] ?? $app['summary'] ?? '';
- $description = markdownToHtml(trim($lang['description'] ?? $app['description'] ?? 'None'));
- $whatsNew = markdownToHtml(trim($lang['whatsNew'] ?? 'None'));
- $website = $this->link($lang['webSite'] ?? $app['webSite'] ?? $app['authorWebSite'] ?? null);
- $source = $this->link($app['sourceCode'] ?? null);
- $issueTracker = $this->link($app['issueTracker'] ?? null);
- $license = $app['license'] ?? 'None';
- $item['content'] = <<<EOD
+
+class FDroidRepoBridge extends BridgeAbstract
+{
+ const NAME = 'F-Droid Repository Bridge';
+ const URI = 'https://f-droid.org/';
+ const DESCRIPTION = 'Query any F-Droid Repository for its latest updates.';
+
+ const ITEM_LIMIT = 50;
+
+ const PARAMETERS = [
+ 'global' => [
+ 'url' => [
+ 'name' => 'Repository URL',
+ 'title' => 'Usually ends with /repo/',
+ 'required' => true,
+ 'exampleValue' => 'https://srv.tt-rss.org/fdroid/repo'
+ ]
+ ],
+ 'Latest Updates' => [
+ 'sorting' => [
+ 'name' => 'Sort By',
+ 'type' => 'list',
+ 'values' => [
+ 'Latest added apps' => 'added',
+ 'Latest updated apps' => 'lastUpdated'
+ ]
+ ],
+ 'locale' => [
+ 'name' => 'Locale',
+ 'defaultValue' => 'en-US'
+ ]
+ ],
+ 'Follow Package' => [
+ 'package' => [
+ 'name' => 'Package Identifier',
+ 'required' => true,
+ 'exampleValue' => 'org.fox.ttrss'
+ ]
+ ]
+ ];
+
+ // Stores repo information
+ private $repo;
+
+ public function getURI()
+ {
+ if (empty($this->queriedContext)) {
+ return parent::getURI();
+ }
+
+ $url = rtrim($this->GetInput('url'), '/');
+ return strstr($url, '?', true) ?: $url;
+ }
+
+ public function getName()
+ {
+ if (empty($this->queriedContext)) {
+ return parent::getName();
+ }
+
+ $name = $this->repo['repo']['name'];
+ switch ($this->queriedContext) {
+ case 'Latest Updates':
+ return $name;
+ case 'Follow Package':
+ return $this->getInput('package') . ' - ' . $name;
+ default:
+ returnServerError('Unimplemented Context (getName)');
+ }
+ }
+
+ public function collectData()
+ {
+ $this->repo = $this->getRepo();
+ switch ($this->queriedContext) {
+ case 'Latest Updates':
+ $this->getAllUpdates();
+ break;
+ case 'Follow Package':
+ $this->getPackage($this->getInput('package'));
+ break;
+ default:
+ returnServerError('Unimplemented Context (collectData)');
+ }
+ }
+
+ private function getRepo()
+ {
+ $url = $this->getURI();
+
+ // Get repo information (only available as JAR)
+ $jar = getContents($url . '/index-v1.jar');
+ $jar_loc = tempnam(sys_get_temp_dir(), '');
+ file_put_contents($jar_loc, $jar);
+
+ // JAR files are specially formatted ZIP files
+ $jar = new ZipArchive();
+ if ($jar->open($jar_loc) !== true) {
+ returnServerError('Failed to extract archive');
+ }
+
+ // Get file pointer to the relevant JSON inside
+ $fp = $jar->getStream('index-v1.json');
+ if (!$fp) {
+ returnServerError('Failed to get file pointer');
+ }
+
+ $data = json_decode(stream_get_contents($fp), true);
+ fclose($fp);
+ $jar->close();
+ return $data;
+ }
+
+ private function getAllUpdates()
+ {
+ $apps = $this->repo['apps'];
+ usort($apps, function ($a, $b) {
+ return $b[$this->getInput('sorting')] <=> $a[$this->getInput('sorting')];
+ });
+ $apps = array_slice($apps, 0, self::ITEM_LIMIT);
+ foreach ($apps as $app) {
+ $latest = reset($this->repo['packages'][$app['packageName']]);
+
+ if (isset($app['localized'])) {
+ // Try provided locale, then en-US, then any
+ $lang = $app['localized'];
+ $lang = $lang[$this->getInput('locale')] ?? $lang['en-US'] ?? reset($lang);
+ } else {
+ $lang = [];
+ }
+
+ $item = [];
+ $item['uri'] = $this->getURI() . '/' . $latest['apkName'];
+ $item['title'] = $lang['name'] ?? $app['packageName'];
+ $item['title'] .= ' ' . $latest['versionName'];
+ $item['timestamp'] = date(DateTime::ISO8601, (int) ($app['lastUpdated'] / 1000));
+ if (isset($app['authorName'])) {
+ $item['author'] = $app['authorName'];
+ }
+ if (isset($app['categories'])) {
+ $item['categories'] = $app['categories'];
+ }
+
+ // Adding Content
+ $icon = $app['icon'] ?? '';
+ if (!empty($icon)) {
+ $icon = $this->getURI() . '/icons-320/' . $icon;
+ $item['enclosures'] = [$icon];
+ $icon = '<img src="' . $icon . '">';
+ }
+ $summary = $lang['summary'] ?? $app['summary'] ?? '';
+ $description = markdownToHtml(trim($lang['description'] ?? $app['description'] ?? 'None'));
+ $whatsNew = markdownToHtml(trim($lang['whatsNew'] ?? 'None'));
+ $website = $this->link($lang['webSite'] ?? $app['webSite'] ?? $app['authorWebSite'] ?? null);
+ $source = $this->link($app['sourceCode'] ?? null);
+ $issueTracker = $this->link($app['issueTracker'] ?? null);
+ $license = $app['license'] ?? 'None';
+ $item['content'] = <<<EOD
{$icon}
<p>{$summary}</p>
<h1>Description</h1>
@@ -157,40 +169,44 @@ class FDroidRepoBridge extends BridgeAbstract {
<p>Issue Tracker: {$issueTracker}</p>
<p>license: {$app['license']}</p>
EOD;
- $this->items[] = $item;
- }
- }
-
- private function getPackage($package) {
- if (!isset($this->repo['packages'][$package])) {
- returnClientError('Invalid Package Name');
- }
- $package = $this->repo['packages'][$package];
-
- $count = self::ITEM_LIMIT;
- foreach($package as $version) {
- $item = array();
- $item['uri'] = $this->getURI() . '/' . $version['apkName'];
- $item['title'] = $version['versionName'];
- $item['timestamp'] = date(DateTime::ISO8601, (int) ($version['added'] / 1000));
- $item['uid'] = $version['versionCode'];
- $size = round($version['size'] / 1048576, 1); // Bytes -> MB
- $sdk_link = 'https://developer.android.com/studio/releases/platforms';
- $item['content'] = <<<EOD
+ $this->items[] = $item;
+ }
+ }
+
+ private function getPackage($package)
+ {
+ if (!isset($this->repo['packages'][$package])) {
+ returnClientError('Invalid Package Name');
+ }
+ $package = $this->repo['packages'][$package];
+
+ $count = self::ITEM_LIMIT;
+ foreach ($package as $version) {
+ $item = [];
+ $item['uri'] = $this->getURI() . '/' . $version['apkName'];
+ $item['title'] = $version['versionName'];
+ $item['timestamp'] = date(DateTime::ISO8601, (int) ($version['added'] / 1000));
+ $item['uid'] = $version['versionCode'];
+ $size = round($version['size'] / 1048576, 1); // Bytes -> MB
+ $sdk_link = 'https://developer.android.com/studio/releases/platforms';
+ $item['content'] = <<<EOD
<p>size: {$size}MB</p>
<p>Minimum SDK: {$version['minSdkVersion']}
(<a href="{$sdk_link}">SDK to Android Version List</a>)</p>
<p>hash ({$version['hashType']}): {$version['hash']}</p>
EOD;
- $this->items[] = $item;
- if (--$count <= 0)
- break;
- }
- }
-
- private function link($url) {
- if (empty($url))
- return null;
- return '<a href="' . $url . '">' . $url . '</a>';
- }
+ $this->items[] = $item;
+ if (--$count <= 0) {
+ break;
+ }
+ }
+ }
+
+ private function link($url)
+ {
+ if (empty($url)) {
+ return null;
+ }
+ return '<a href="' . $url . '">' . $url . '</a>';
+ }
}
diff --git a/bridges/FM4Bridge.php b/bridges/FM4Bridge.php
index b477f4cc..45bccd52 100644
--- a/bridges/FM4Bridge.php
+++ b/bridges/FM4Bridge.php
@@ -2,64 +2,67 @@
class FM4Bridge extends BridgeAbstract
{
- const MAINTAINER = 'joni1993';
- const NAME = 'FM4 Bridge';
- const URI = 'https://fm4.orf.at';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Feed for FM4 articles by tags (authors)';
- const PARAMETERS = array(
- array(
- 'tag' => array(
- 'name' => 'Tag (author, category, ...)',
- 'title' => 'Tag to retrieve',
- 'exampleValue' => 'musik'
- ),
- 'loadcontent' => array(
- 'name' => 'Load Full Article Content',
- 'title' => 'Retrieve full content of articles (may take longer)',
- 'type' => 'checkbox'
- ),
- 'pages' => array(
- 'name' => 'Pages',
- 'title' => 'Amount of pages to load',
- 'type' => 'number',
- 'defaultValue' => 1
- )
- )
- );
+ const MAINTAINER = 'joni1993';
+ const NAME = 'FM4 Bridge';
+ const URI = 'https://fm4.orf.at';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Feed for FM4 articles by tags (authors)';
+ const PARAMETERS = [
+ [
+ 'tag' => [
+ 'name' => 'Tag (author, category, ...)',
+ 'title' => 'Tag to retrieve',
+ 'exampleValue' => 'musik'
+ ],
+ 'loadcontent' => [
+ 'name' => 'Load Full Article Content',
+ 'title' => 'Retrieve full content of articles (may take longer)',
+ 'type' => 'checkbox'
+ ],
+ 'pages' => [
+ 'name' => 'Pages',
+ 'title' => 'Amount of pages to load',
+ 'type' => 'number',
+ 'defaultValue' => 1
+ ]
+ ]
+ ];
- private function getPageData($tag, $page) {
- if($tag)
- $uri = self::URI . '/tags/' . $tag;
- else
- $uri = self::URI;
+ private function getPageData($tag, $page)
+ {
+ if ($tag) {
+ $uri = self::URI . '/tags/' . $tag;
+ } else {
+ $uri = self::URI;
+ }
- $uri = $uri . '?page=' . $page;
+ $uri = $uri . '?page=' . $page;
- $html = getSimpleHTMLDOM($uri);
+ $html = getSimpleHTMLDOM($uri);
- $page_items = array();
+ $page_items = [];
- foreach ($html->find('div[class*=listItem]') as $article) {
- $item = array();
+ foreach ($html->find('div[class*=listItem]') as $article) {
+ $item = [];
- $item['uri'] = $article->find('a', 0)->href;
- $item['title'] = $article->find('h2', 0)->plaintext;
- $item['author'] = $article->find('p[class*=keyword]', 0)->plaintext;
- $item['timestamp'] = strtotime($article->find('p[class*=time]', 0)->plaintext);
+ $item['uri'] = $article->find('a', 0)->href;
+ $item['title'] = $article->find('h2', 0)->plaintext;
+ $item['author'] = $article->find('p[class*=keyword]', 0)->plaintext;
+ $item['timestamp'] = strtotime($article->find('p[class*=time]', 0)->plaintext);
- if ($this->getInput('loadcontent')) {
- $item['content'] = getSimpleHTMLDOM($item['uri'])->find('div[class=storyText]', 0);
- }
+ if ($this->getInput('loadcontent')) {
+ $item['content'] = getSimpleHTMLDOM($item['uri'])->find('div[class=storyText]', 0);
+ }
- $page_items[] = $item;
- }
- return $page_items;
- }
+ $page_items[] = $item;
+ }
+ return $page_items;
+ }
- public function collectData() {
- for ($cur_page = 1; $cur_page <= $this->getInput('pages'); $cur_page++) {
- $this->items = array_merge($this->items, $this->getPageData($this->getInput('tag'), $cur_page));
- }
- }
+ public function collectData()
+ {
+ for ($cur_page = 1; $cur_page <= $this->getInput('pages'); $cur_page++) {
+ $this->items = array_merge($this->items, $this->getPageData($this->getInput('tag'), $cur_page));
+ }
+ }
}
diff --git a/bridges/FSecureBlogBridge.php b/bridges/FSecureBlogBridge.php
index 46ad8ac0..cc1c0683 100644
--- a/bridges/FSecureBlogBridge.php
+++ b/bridges/FSecureBlogBridge.php
@@ -1,120 +1,126 @@
<?php
-class FSecureBlogBridge extends BridgeAbstract {
- const NAME = 'F-Secure Blog';
- const URI = 'https://blog.f-secure.com';
- const DESCRIPTION = 'F-Secure Blog';
- const MAINTAINER = 'simon816';
- const PARAMETERS = array(
- '' => array(
- 'categories' => array(
- 'name' => 'Blog categories',
- 'exampleValue' => 'home-security',
- ),
- 'language' => array(
- 'name' => 'Language',
- 'required' => true,
- 'defaultValue' => 'en',
- ),
- 'oldest_date' => array(
- 'name' => 'Oldest article date',
- 'exampleValue' => '-6 months',
- ),
- )
- );
+class FSecureBlogBridge extends BridgeAbstract
+{
+ const NAME = 'F-Secure Blog';
+ const URI = 'https://blog.f-secure.com';
+ const DESCRIPTION = 'F-Secure Blog';
+ const MAINTAINER = 'simon816';
+ const PARAMETERS = [
+ '' => [
+ 'categories' => [
+ 'name' => 'Blog categories',
+ 'exampleValue' => 'home-security',
+ ],
+ 'language' => [
+ 'name' => 'Language',
+ 'required' => true,
+ 'defaultValue' => 'en',
+ ],
+ 'oldest_date' => [
+ 'name' => 'Oldest article date',
+ 'exampleValue' => '-6 months',
+ ],
+ ]
+ ];
- public function getURI() {
- $lang = $this->getInput('language') or 'en';
- if ($lang === 'en') {
- return self::URI;
- }
- return self::URI . "/$lang";
- }
+ public function getURI()
+ {
+ $lang = $this->getInput('language') or 'en';
+ if ($lang === 'en') {
+ return self::URI;
+ }
+ return self::URI . "/$lang";
+ }
- public function collectData() {
- $this->items = array();
- $this->seen = array();
+ public function collectData()
+ {
+ $this->items = [];
+ $this->seen = [];
- $this->oldest = strtotime($this->getInput('oldest_date')) ?: 0;
+ $this->oldest = strtotime($this->getInput('oldest_date')) ?: 0;
- $categories = $this->getInput('categories');
- if (!empty($categories)) {
- foreach (explode(',', $categories) as $cat) {
- if (!empty($cat)) {
- $this->collectCategory($cat);
- }
- }
- return;
- }
+ $categories = $this->getInput('categories');
+ if (!empty($categories)) {
+ foreach (explode(',', $categories) as $cat) {
+ if (!empty($cat)) {
+ $this->collectCategory($cat);
+ }
+ }
+ return;
+ }
- $html = getSimpleHTMLDOMCached($this->getURI() . '/');
+ $html = getSimpleHTMLDOMCached($this->getURI() . '/');
- foreach ($html->find('ul.c-header-menu-desktop__list li a') as $link) {
- $url = parse_url($link->href);
- if (($pos = strpos($url['path'], '/category/')) !== false) {
- $cat = substr($url['path'], $pos + strlen('/category/'), -1);
- $this->collectCategory($cat);
- }
- }
- }
+ foreach ($html->find('ul.c-header-menu-desktop__list li a') as $link) {
+ $url = parse_url($link->href);
+ if (($pos = strpos($url['path'], '/category/')) !== false) {
+ $cat = substr($url['path'], $pos + strlen('/category/'), -1);
+ $this->collectCategory($cat);
+ }
+ }
+ }
- private function collectCategory($category) {
- $url = $this->getURI() . "/category/$category/";
- while ($url) {
- //Limit total amount of requests
- if(count($this->items) >= 20) {
- break;
- }
- $url = $this->collectListing($url);
- }
- }
+ private function collectCategory($category)
+ {
+ $url = $this->getURI() . "/category/$category/";
+ while ($url) {
+ //Limit total amount of requests
+ if (count($this->items) >= 20) {
+ break;
+ }
+ $url = $this->collectListing($url);
+ }
+ }
- // n.b. this relies on articles to be ordered by date so the cutoff works
- private function collectListing($url) {
- $html = getSimpleHTMLDOMCached($url, 60 * 60);
- $items = $html->find('section.b-blog .l-blog__content__listing div.c-listing-item');
+ // n.b. this relies on articles to be ordered by date so the cutoff works
+ private function collectListing($url)
+ {
+ $html = getSimpleHTMLDOMCached($url, 60 * 60);
+ $items = $html->find('section.b-blog .l-blog__content__listing div.c-listing-item');
- $catName = trim($html->find('section.b-blog .c-blog-header__title', 0)->plaintext);
+ $catName = trim($html->find('section.b-blog .c-blog-header__title', 0)->plaintext);
- foreach ($items as $item) {
- $url = $item->getAttribute('data-url');
- if (!$this->collectArticle($url)) {
- return null; // Too old, stop collecting
- }
- }
+ foreach ($items as $item) {
+ $url = $item->getAttribute('data-url');
+ if (!$this->collectArticle($url)) {
+ return null; // Too old, stop collecting
+ }
+ }
- // Point's to 404 for non-english blog
- // $next = $html->find('link[rel=next]', 0);
- $next = $html->find('ul.page-numbers a.next', 0);
- return $next ? $next->href : null;
- }
+ // Point's to 404 for non-english blog
+ // $next = $html->find('link[rel=next]', 0);
+ $next = $html->find('ul.page-numbers a.next', 0);
+ return $next ? $next->href : null;
+ }
- // Returns a boolean whether to continue collecting articles
- // i.e. date is after oldest cutoff
- private function collectArticle($url) {
- if (array_key_exists($url, $this->seen)) {
- return true;
- }
- $html = getSimpleHTMLDOMCached($url);
+ // Returns a boolean whether to continue collecting articles
+ // i.e. date is after oldest cutoff
+ private function collectArticle($url)
+ {
+ if (array_key_exists($url, $this->seen)) {
+ return true;
+ }
+ $html = getSimpleHTMLDOMCached($url);
- $rssItem = array( 'uri' => $url, 'uid' => $url );
- $rssItem['title'] = $html->find('meta[property=og:title]', 0)->content;
- $dt = $html->find('meta[property=article:published_time]', 0)->content;
- // Exit if too old
- if (strtotime($dt) < $this->oldest) {
- return false;
- }
- $rssItem['timestamp'] = $dt;
- $img = $html->find('meta[property=og:image]', 0);
- $rssItem['enclosures'] = $img ? array($img->content) : array();
- $rssItem['author'] = trim($html->find('.c-blog-author__text a', 0)->plaintext);
- $rssItem['categories'] = array_map(function ($link) {
- return trim($link->plaintext);
- }, $html->find('.b-single-header__categories .c-category-list a'));
- $rssItem['content'] = trim($html->find('article', 0)->innertext);
+ $rssItem = [ 'uri' => $url, 'uid' => $url ];
+ $rssItem['title'] = $html->find('meta[property=og:title]', 0)->content;
+ $dt = $html->find('meta[property=article:published_time]', 0)->content;
+ // Exit if too old
+ if (strtotime($dt) < $this->oldest) {
+ return false;
+ }
+ $rssItem['timestamp'] = $dt;
+ $img = $html->find('meta[property=og:image]', 0);
+ $rssItem['enclosures'] = $img ? [$img->content] : [];
+ $rssItem['author'] = trim($html->find('.c-blog-author__text a', 0)->plaintext);
+ $rssItem['categories'] = array_map(function ($link) {
+ return trim($link->plaintext);
+ }, $html->find('.b-single-header__categories .c-category-list a'));
+ $rssItem['content'] = trim($html->find('article', 0)->innertext);
- $this->items[] = $rssItem;
- $this->seen[$url] = 1;
- return true;
- }
+ $this->items[] = $rssItem;
+ $this->seen[$url] = 1;
+ return true;
+ }
}
diff --git a/bridges/FabriceBellardBridge.php b/bridges/FabriceBellardBridge.php
index 0085e924..5e012665 100644
--- a/bridges/FabriceBellardBridge.php
+++ b/bridges/FabriceBellardBridge.php
@@ -1,35 +1,38 @@
<?php
-class FabriceBellardBridge extends BridgeAbstract {
- const NAME = 'Fabrice Bellard';
- const URI = 'https://bellard.org/';
- const DESCRIPTION = "Fabrice Bellard's Home Page";
- const MAINTAINER = 'somini';
-
- public function collectData() {
- $html = getSimpleHTMLDOM(self::URI);
-
- foreach ($html->find('p') as $obj) {
- $item = array();
-
- $html = defaultLinkTo($html, $this->getURI());
-
- $links = $obj->find('a');
- if (count($links) > 0) {
- $link_uri = $links[0]->href;
- } else {
- $link_uri = $this->getURI();
- }
-
- /* try to make sure the link is valid */
- if ($link_uri[-1] !== '/' && strpos($link_uri, '/') === false) {
- $link_uri = $link_uri . '/';
- }
-
- $item['title'] = strip_tags($obj->innertext);
- $item['uri'] = $link_uri;
- $item['content'] = $obj->innertext;
-
- $this->items[] = $item;
- }
- }
+
+class FabriceBellardBridge extends BridgeAbstract
+{
+ const NAME = 'Fabrice Bellard';
+ const URI = 'https://bellard.org/';
+ const DESCRIPTION = "Fabrice Bellard's Home Page";
+ const MAINTAINER = 'somini';
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
+
+ foreach ($html->find('p') as $obj) {
+ $item = [];
+
+ $html = defaultLinkTo($html, $this->getURI());
+
+ $links = $obj->find('a');
+ if (count($links) > 0) {
+ $link_uri = $links[0]->href;
+ } else {
+ $link_uri = $this->getURI();
+ }
+
+ /* try to make sure the link is valid */
+ if ($link_uri[-1] !== '/' && strpos($link_uri, '/') === false) {
+ $link_uri = $link_uri . '/';
+ }
+
+ $item['title'] = strip_tags($obj->innertext);
+ $item['uri'] = $link_uri;
+ $item['content'] = $obj->innertext;
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/FacebookBridge.php b/bridges/FacebookBridge.php
index e5cc6c34..99fa346f 100644
--- a/bridges/FacebookBridge.php
+++ b/bridges/FacebookBridge.php
@@ -1,500 +1,494 @@
<?php
-class FacebookBridge extends BridgeAbstract {
- // const MAINTAINER = 'teromene, logmanoriginal';
- const NAME = 'Facebook Bridge | Main Site';
- const URI = 'https://www.facebook.com/';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
+class FacebookBridge extends BridgeAbstract
+{
+ // const MAINTAINER = 'teromene, logmanoriginal';
+ const NAME = 'Facebook Bridge | Main Site';
+ const URI = 'https://www.facebook.com/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
please insert the parameter as follow : myExamplePage/132621766841117';
- const PARAMETERS = array(
- 'User' => array(
- 'u' => array(
- 'name' => 'Username',
- 'required' => true
- ),
- 'media_type' => array(
- 'name' => 'Media type',
- 'type' => 'list',
- 'required' => false,
- 'values' => array(
- 'All' => 'all',
- 'Video' => 'video',
- 'No Video' => 'novideo'
- ),
- 'defaultValue' => 'all'
- ),
- 'skip_reviews' => array(
- 'name' => 'Skip reviews',
- 'type' => 'checkbox',
- 'required' => false,
- 'defaultValue' => false,
- 'title' => 'Feed includes reviews when unchecked'
- )
- ),
- 'Group' => array(
- 'g' => array(
- 'name' => 'Group',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'https://www.facebook.com/groups/743149642484225',
- 'title' => 'Insert group name or facebook group URL'
- )
- ),
- 'global' => array(
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Specify the number of items to return (default: -1)',
- 'defaultValue' => -1
- )
- )
- );
-
- private $authorName = '';
- private $groupName = '';
-
- public function getIcon() {
- return 'https://static.xx.fbcdn.net/rsrc.php/yo/r/iRmz9lCMBD2.ico';
- }
-
- public function getName(){
-
- switch($this->queriedContext) {
-
- case 'User':
- if(!empty($this->authorName)) {
- return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName;
- }
- break;
-
- case 'Group':
- if(!empty($this->groupName)) {
- return $this->groupName;
- }
- break;
-
- }
-
- return parent::getName();
- }
-
- public function detectParameters($url){
- $params = array();
-
- // By profile
- $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/profile\.php\?id\=([^\/?&\n]+)?(.*)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['u'] = urldecode($matches[3]);
- return $params;
- }
-
- // By group
- $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/groups\/([^\/?\n]+)?(.*)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['g'] = urldecode($matches[3]);
- return $params;
- }
-
- // By username
- $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/([^\/?\n]+)/';
-
- if(preg_match($regex, $url, $matches) > 0) {
- $params['u'] = urldecode($matches[3]);
- return $params;
- }
-
- return null;
- }
-
- public function getURI() {
- $uri = self::URI;
-
- switch($this->queriedContext) {
-
- case 'Group':
- // Discover groups via https://www.facebook.com/groups/
- // Example group: https://www.facebook.com/groups/sailors.worldwide
- $uri .= 'groups/' . $this->sanitizeGroup(filter_var($this->getInput('g'), FILTER_SANITIZE_URL));
- break;
-
- case 'User':
- // Example user 1: https://www.facebook.com/artetv/
- // Example user 2: artetv
- $user = $this->sanitizeUser($this->getInput('u'));
-
- if(!strpos($user, '/')) {
- $uri .= urlencode($user) . '/posts';
- } else {
- $uri .= 'pages/' . $user;
- }
-
- break;
-
- }
-
- // Request the mobile version to reduce page size (no javascript)
- // More information: https://stackoverflow.com/a/11103592
- return $uri .= '?_fb_noscript=1';
- }
-
- public function collectData() {
-
- switch($this->queriedContext) {
-
- case 'Group':
- $this->collectGroupData();
- break;
-
- case 'User':
- $this->collectUserData();
- break;
-
- default:
- returnClientError('Unknown context: "' . $this->queriedContext . '"!');
-
- }
-
- $limit = $this->getInput('limit') ?: -1;
-
- if($limit > 0 && count($this->items) > $limit) {
- $this->items = array_slice($this->items, 0, $limit);
- }
-
- }
-
- #region Group
-
- private function collectGroupData() {
-
- if(getEnv('HTTP_ACCEPT_LANGUAGE')) {
- $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE'));
- } else {
- $header = array();
- }
-
- $touchURI = str_replace(
- 'https://www.facebook',
- 'https://touch.facebook',
- $this->getURI()
- );
-
- $html = getSimpleHTMLDOM($touchURI, $header);
-
- if(!$this->isPublicGroup($html)) {
- returnClientError('This group is not public! RSS-Bridge only supports public groups!');
- }
-
- defaultLinkTo($html, substr(self::URI, 0, strlen(self::URI) - 1));
-
- $this->groupName = $this->extractGroupName($html);
-
- $posts = $html->find('div.story_body_container')
- or returnServerError('Failed finding posts!');
-
- foreach($posts as $post) {
-
- $item = array();
-
- $item['uri'] = $this->extractGroupPostURI($post);
- $item['title'] = $this->extractGroupPostTitle($post);
- $item['author'] = $this->extractGroupPostAuthor($post);
- $item['content'] = $this->extractGroupPostContent($post);
- $item['enclosures'] = $this->extractGroupPostEnclosures($post);
-
- $this->items[] = $item;
-
- }
-
- }
-
- private function sanitizeGroup($group) {
-
- if(filter_var(
- $group,
- FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) {
- // User provided a URL
-
- $urlparts = parse_url($group);
-
- $this->validateHost($urlparts['host']);
-
- return explode('/', $urlparts['path'])[2];
-
- } elseif(strpos($group, '/') !== false) {
- returnClientError('The group you provided is invalid: ' . $group);
- } else {
- return $group;
- }
-
- }
-
- private function validateHost($provided_host) {
- // Handle mobile links
- if (strpos($provided_host, 'm.') === 0) {
- $provided_host = substr($provided_host, strlen('m.'));
- }
- if (strpos($provided_host, 'touch.') === 0) {
- $provided_host = substr($provided_host, strlen('touch.'));
- }
-
- $facebook_host = parse_url(self::URI)['host'];
-
- if ($provided_host !== $facebook_host
- && 'www.' . $provided_host !== $facebook_host) {
- returnClientError('The host you provided is invalid! Received "'
- . $provided_host
- . '", expected "'
- . $facebook_host
- . '"!');
- }
- }
-
- /**
- * @param $html simple_html_dom
- * @return bool
- */
- private function isPublicGroup($html) {
-
- // Facebook touch just presents a login page for non-public groups
- $title = $html->find('title', 0);
- return $title->plaintext !== 'Log in to Facebook | Facebook';
- }
-
- private function extractGroupName($html) {
-
- $ogtitle = $html->find('._de1', 0)
- or returnServerError('Unable to find group title!');
-
- return html_entity_decode($ogtitle->plaintext, ENT_QUOTES);
- }
-
- private function extractGroupPostURI($post) {
-
- $elements = $post->find('a')
- or returnServerError('Unable to find URI!');
-
- foreach($elements as $anchor) {
-
- // Find the one that is a permalink
- if(strpos($anchor->href, 'permalink') !== false) {
- $arr = explode('?', $anchor->href, 2);
- return $arr[0];
- }
-
- }
-
- return null;
-
- }
-
- private function extractGroupPostContent($post) {
-
- $content = $post->find('div._5rgt', 0)
- or returnServerError('Unable to find user content!');
-
- $context_text = $content->innertext;
- if ($content->next_sibling() !== null) {
- $context_text .= $content->next_sibling()->innertext;
- }
- return $context_text;
-
- }
-
- private function extractGroupPostAuthor($post) {
-
- $element = $post->find('h3 a', 0)
- or returnServerError('Unable to find author information!');
-
- return $element->plaintext;
-
- }
-
- private function extractGroupPostEnclosures($post) {
-
- $elements = $post->find('span._6qdm');
- if ($post->find('div._5rgt', 0)->next_sibling() !== null) {
- array_push($elements, ...$post->find('div._5rgt', 0)->next_sibling()->find('i.img'));
- }
-
- $enclosures = array();
-
- $background_img_regex = '/background-image: ?url\\((.+?)\\);/';
-
- foreach($elements as $enclosure) {
- if(preg_match($background_img_regex, $enclosure, $matches) > 0) {
- $bg_img_value = trim(html_entity_decode($matches[1], ENT_QUOTES), "'\"");
- $bg_img_url = urldecode(preg_replace('/\\\([0-9a-z]{2}) /', '%$1', $bg_img_value));
- $enclosures[] = urldecode($bg_img_url);
- }
- }
-
- return empty($enclosures) ? null : $enclosures;
-
- }
-
- private function extractGroupPostTitle($post) {
-
- $element = $post->find('h3', 0)
- or returnServerError('Unable to find title!');
-
- if(strpos($element->plaintext, 'shared') === false) {
-
- $content = strip_tags($this->extractGroupPostContent($post));
-
- return $this->extractGroupPostAuthor($post)
- . ' posted: '
- . substr(
- $content,
- 0,
- strpos(wordwrap($content, 64), "\n")
- )
- . '...';
-
- }
-
- return $element->plaintext;
-
- }
-
- #endregion (Group)
-
- #region User
-
- /**
- * Checks if $user is a valid username or URI and returns the username
- */
- private function sanitizeUser($user) {
- if (filter_var($user, FILTER_VALIDATE_URL)) {
-
- $urlparts = parse_url($user);
-
- $this->validateHost($urlparts['host']);
-
- if(!array_key_exists('path', $urlparts)
- || $urlparts['path'] === '/') {
- returnClientError('The URL you provided doesn\'t contain the user name!');
- }
-
- return explode('/', $urlparts['path'])[1];
-
- } else {
-
- // First character cannot be a forward slash
- if(strpos($user, '/') === 0) {
- returnClientError('Remove leading slash "/" from the username!');
- }
-
- return $user;
-
- }
- }
-
- /**
- * Bypass external link redirection
- */
- private function unescapeFacebookLink($content){
- return preg_replace_callback('/ href=\"([^"]+)\"/i', function($matches){
- if(is_array($matches) && count($matches) > 1) {
-
- $link = $matches[1];
-
- if(strpos($link, 'facebook.com/l.php?u=') !== false)
- $link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&'));
-
- return ' href="' . $link . '"';
-
- }
- }, $content);
- }
-
- /**
- * Remove Facebook's tracking code
- */
- private function removeTrackingCodes($content){
- return preg_replace_callback('/ href=\"([^"]+)\"/i', function($matches){
- if(is_array($matches) && count($matches) > 1) {
-
- $link = $matches[1];
-
- if(strpos($link, 'facebook.com') !== false) {
- if(strpos($link, '?') !== false) {
- $link = substr($link, 0, strpos($link, '?'));
- }
- }
- return ' href="' . $link . '"';
-
- }
- }, $content);
- }
-
- /**
- * Convert textual representation of emoticons back to ASCII emoticons.
- * i.e. "<i><u>smile emoticon</u></i>" => ":)"
- */
- private function unescapeFacebookEmote($content){
- return preg_replace_callback('/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i', function($matches){
- static $facebook_emoticons = array(
- 'smile' => ':)',
- 'frown' => ':(',
- 'tongue' => ':P',
- 'grin' => ':D',
- 'gasp' => ':O',
- 'wink' => ';)',
- 'pacman' => ':<',
- 'grumpy' => '>_<',
- 'unsure' => ':/',
- 'cry' => ':\'(',
- 'kiki' => '^_^',
- 'glasses' => '8-)',
- 'sunglasses' => 'B-)',
- 'heart' => '<3',
- 'devil' => ']:D',
- 'angel' => '0:)',
- 'squint' => '-_-',
- 'confused' => 'o_O',
- 'upset' => 'xD',
- 'colonthree' => ':3',
- 'like' => '&#x1F44D;');
-
- $len = count($matches);
-
- if ($len > 1)
- for ($i = 1; $i < $len; $i++)
- foreach ($facebook_emoticons as $name => $emote)
- if ($matches[$i] === $name)
- return $emote;
-
- return $matches[0];
- }, $content);
- }
-
- /**
- * Returns the captcha message for the given captcha
- */
- private function returnCaptchaMessage($captcha) {
- // Save form for submitting after getting captcha response
- if (session_status() == PHP_SESSION_NONE) {
- session_start();
- }
-
- $captcha_fields = array();
-
- foreach ($captcha->find('input, button') as $input) {
- $captcha_fields[$input->name] = $input->value;
- }
-
- $_SESSION['captcha_fields'] = $captcha_fields;
- $_SESSION['captcha_action'] = $captcha->find('form', 0)->action;
-
- // Show captcha filling form to the viewer, proxying the captcha image
- $img = base64_encode(getContents($captcha->find('img', 0)->src));
-
- header('Content-Type: text/html', true, 500);
-
- $message = <<<EOD
+ const PARAMETERS = [
+ 'User' => [
+ 'u' => [
+ 'name' => 'Username',
+ 'required' => true
+ ],
+ 'media_type' => [
+ 'name' => 'Media type',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => [
+ 'All' => 'all',
+ 'Video' => 'video',
+ 'No Video' => 'novideo'
+ ],
+ 'defaultValue' => 'all'
+ ],
+ 'skip_reviews' => [
+ 'name' => 'Skip reviews',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'defaultValue' => false,
+ 'title' => 'Feed includes reviews when unchecked'
+ ]
+ ],
+ 'Group' => [
+ 'g' => [
+ 'name' => 'Group',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'https://www.facebook.com/groups/743149642484225',
+ 'title' => 'Insert group name or facebook group URL'
+ ]
+ ],
+ 'global' => [
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Specify the number of items to return (default: -1)',
+ 'defaultValue' => -1
+ ]
+ ]
+ ];
+
+ private $authorName = '';
+ private $groupName = '';
+
+ public function getIcon()
+ {
+ return 'https://static.xx.fbcdn.net/rsrc.php/yo/r/iRmz9lCMBD2.ico';
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'User':
+ if (!empty($this->authorName)) {
+ return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName;
+ }
+ break;
+
+ case 'Group':
+ if (!empty($this->groupName)) {
+ return $this->groupName;
+ }
+ break;
+ }
+
+ return parent::getName();
+ }
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ // By profile
+ $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/profile\.php\?id\=([^\/?&\n]+)?(.*)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['u'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ // By group
+ $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/groups\/([^\/?\n]+)?(.*)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['g'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ // By username
+ $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/([^\/?\n]+)/';
+
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['u'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ return null;
+ }
+
+ public function getURI()
+ {
+ $uri = self::URI;
+
+ switch ($this->queriedContext) {
+ case 'Group':
+ // Discover groups via https://www.facebook.com/groups/
+ // Example group: https://www.facebook.com/groups/sailors.worldwide
+ $uri .= 'groups/' . $this->sanitizeGroup(filter_var($this->getInput('g'), FILTER_SANITIZE_URL));
+ break;
+
+ case 'User':
+ // Example user 1: https://www.facebook.com/artetv/
+ // Example user 2: artetv
+ $user = $this->sanitizeUser($this->getInput('u'));
+
+ if (!strpos($user, '/')) {
+ $uri .= urlencode($user) . '/posts';
+ } else {
+ $uri .= 'pages/' . $user;
+ }
+
+ break;
+ }
+
+ // Request the mobile version to reduce page size (no javascript)
+ // More information: https://stackoverflow.com/a/11103592
+ return $uri .= '?_fb_noscript=1';
+ }
+
+ public function collectData()
+ {
+ switch ($this->queriedContext) {
+ case 'Group':
+ $this->collectGroupData();
+ break;
+
+ case 'User':
+ $this->collectUserData();
+ break;
+
+ default:
+ returnClientError('Unknown context: "' . $this->queriedContext . '"!');
+ }
+
+ $limit = $this->getInput('limit') ?: -1;
+
+ if ($limit > 0 && count($this->items) > $limit) {
+ $this->items = array_slice($this->items, 0, $limit);
+ }
+ }
+
+ #region Group
+
+ private function collectGroupData()
+ {
+ if (getEnv('HTTP_ACCEPT_LANGUAGE')) {
+ $header = ['Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE')];
+ } else {
+ $header = [];
+ }
+
+ $touchURI = str_replace(
+ 'https://www.facebook',
+ 'https://touch.facebook',
+ $this->getURI()
+ );
+
+ $html = getSimpleHTMLDOM($touchURI, $header);
+
+ if (!$this->isPublicGroup($html)) {
+ returnClientError('This group is not public! RSS-Bridge only supports public groups!');
+ }
+
+ defaultLinkTo($html, substr(self::URI, 0, strlen(self::URI) - 1));
+
+ $this->groupName = $this->extractGroupName($html);
+
+ $posts = $html->find('div.story_body_container')
+ or returnServerError('Failed finding posts!');
+
+ foreach ($posts as $post) {
+ $item = [];
+
+ $item['uri'] = $this->extractGroupPostURI($post);
+ $item['title'] = $this->extractGroupPostTitle($post);
+ $item['author'] = $this->extractGroupPostAuthor($post);
+ $item['content'] = $this->extractGroupPostContent($post);
+ $item['enclosures'] = $this->extractGroupPostEnclosures($post);
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function sanitizeGroup($group)
+ {
+ if (
+ filter_var(
+ $group,
+ FILTER_VALIDATE_URL,
+ FILTER_FLAG_PATH_REQUIRED
+ )
+ ) {
+ // User provided a URL
+
+ $urlparts = parse_url($group);
+
+ $this->validateHost($urlparts['host']);
+
+ return explode('/', $urlparts['path'])[2];
+ } elseif (strpos($group, '/') !== false) {
+ returnClientError('The group you provided is invalid: ' . $group);
+ } else {
+ return $group;
+ }
+ }
+
+ private function validateHost($provided_host)
+ {
+ // Handle mobile links
+ if (strpos($provided_host, 'm.') === 0) {
+ $provided_host = substr($provided_host, strlen('m.'));
+ }
+ if (strpos($provided_host, 'touch.') === 0) {
+ $provided_host = substr($provided_host, strlen('touch.'));
+ }
+
+ $facebook_host = parse_url(self::URI)['host'];
+
+ if (
+ $provided_host !== $facebook_host
+ && 'www.' . $provided_host !== $facebook_host
+ ) {
+ returnClientError('The host you provided is invalid! Received "'
+ . $provided_host
+ . '", expected "'
+ . $facebook_host
+ . '"!');
+ }
+ }
+
+ /**
+ * @param $html simple_html_dom
+ * @return bool
+ */
+ private function isPublicGroup($html)
+ {
+ // Facebook touch just presents a login page for non-public groups
+ $title = $html->find('title', 0);
+ return $title->plaintext !== 'Log in to Facebook | Facebook';
+ }
+
+ private function extractGroupName($html)
+ {
+ $ogtitle = $html->find('._de1', 0)
+ or returnServerError('Unable to find group title!');
+
+ return html_entity_decode($ogtitle->plaintext, ENT_QUOTES);
+ }
+
+ private function extractGroupPostURI($post)
+ {
+ $elements = $post->find('a')
+ or returnServerError('Unable to find URI!');
+
+ foreach ($elements as $anchor) {
+ // Find the one that is a permalink
+ if (strpos($anchor->href, 'permalink') !== false) {
+ $arr = explode('?', $anchor->href, 2);
+ return $arr[0];
+ }
+ }
+
+ return null;
+ }
+
+ private function extractGroupPostContent($post)
+ {
+ $content = $post->find('div._5rgt', 0)
+ or returnServerError('Unable to find user content!');
+
+ $context_text = $content->innertext;
+ if ($content->next_sibling() !== null) {
+ $context_text .= $content->next_sibling()->innertext;
+ }
+ return $context_text;
+ }
+
+ private function extractGroupPostAuthor($post)
+ {
+ $element = $post->find('h3 a', 0)
+ or returnServerError('Unable to find author information!');
+
+ return $element->plaintext;
+ }
+
+ private function extractGroupPostEnclosures($post)
+ {
+ $elements = $post->find('span._6qdm');
+ if ($post->find('div._5rgt', 0)->next_sibling() !== null) {
+ array_push($elements, ...$post->find('div._5rgt', 0)->next_sibling()->find('i.img'));
+ }
+
+ $enclosures = [];
+
+ $background_img_regex = '/background-image: ?url\\((.+?)\\);/';
+
+ foreach ($elements as $enclosure) {
+ if (preg_match($background_img_regex, $enclosure, $matches) > 0) {
+ $bg_img_value = trim(html_entity_decode($matches[1], ENT_QUOTES), "'\"");
+ $bg_img_url = urldecode(preg_replace('/\\\([0-9a-z]{2}) /', '%$1', $bg_img_value));
+ $enclosures[] = urldecode($bg_img_url);
+ }
+ }
+
+ return empty($enclosures) ? null : $enclosures;
+ }
+
+ private function extractGroupPostTitle($post)
+ {
+ $element = $post->find('h3', 0)
+ or returnServerError('Unable to find title!');
+
+ if (strpos($element->plaintext, 'shared') === false) {
+ $content = strip_tags($this->extractGroupPostContent($post));
+
+ return $this->extractGroupPostAuthor($post)
+ . ' posted: '
+ . substr(
+ $content,
+ 0,
+ strpos(wordwrap($content, 64), "\n")
+ )
+ . '...';
+ }
+
+ return $element->plaintext;
+ }
+
+ #endregion (Group)
+
+ #region User
+
+ /**
+ * Checks if $user is a valid username or URI and returns the username
+ */
+ private function sanitizeUser($user)
+ {
+ if (filter_var($user, FILTER_VALIDATE_URL)) {
+ $urlparts = parse_url($user);
+
+ $this->validateHost($urlparts['host']);
+
+ if (
+ !array_key_exists('path', $urlparts)
+ || $urlparts['path'] === '/'
+ ) {
+ returnClientError('The URL you provided doesn\'t contain the user name!');
+ }
+
+ return explode('/', $urlparts['path'])[1];
+ } else {
+ // First character cannot be a forward slash
+ if (strpos($user, '/') === 0) {
+ returnClientError('Remove leading slash "/" from the username!');
+ }
+
+ return $user;
+ }
+ }
+
+ /**
+ * Bypass external link redirection
+ */
+ private function unescapeFacebookLink($content)
+ {
+ return preg_replace_callback('/ href=\"([^"]+)\"/i', function ($matches) {
+ if (is_array($matches) && count($matches) > 1) {
+ $link = $matches[1];
+
+ if (strpos($link, 'facebook.com/l.php?u=') !== false) {
+ $link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&'));
+ }
+
+ return ' href="' . $link . '"';
+ }
+ }, $content);
+ }
+
+ /**
+ * Remove Facebook's tracking code
+ */
+ private function removeTrackingCodes($content)
+ {
+ return preg_replace_callback('/ href=\"([^"]+)\"/i', function ($matches) {
+ if (is_array($matches) && count($matches) > 1) {
+ $link = $matches[1];
+
+ if (strpos($link, 'facebook.com') !== false) {
+ if (strpos($link, '?') !== false) {
+ $link = substr($link, 0, strpos($link, '?'));
+ }
+ }
+ return ' href="' . $link . '"';
+ }
+ }, $content);
+ }
+
+ /**
+ * Convert textual representation of emoticons back to ASCII emoticons.
+ * i.e. "<i><u>smile emoticon</u></i>" => ":)"
+ */
+ private function unescapeFacebookEmote($content)
+ {
+ return preg_replace_callback('/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i', function ($matches) {
+ static $facebook_emoticons = [
+ 'smile' => ':)',
+ 'frown' => ':(',
+ 'tongue' => ':P',
+ 'grin' => ':D',
+ 'gasp' => ':O',
+ 'wink' => ';)',
+ 'pacman' => ':<',
+ 'grumpy' => '>_<',
+ 'unsure' => ':/',
+ 'cry' => ':\'(',
+ 'kiki' => '^_^',
+ 'glasses' => '8-)',
+ 'sunglasses' => 'B-)',
+ 'heart' => '<3',
+ 'devil' => ']:D',
+ 'angel' => '0:)',
+ 'squint' => '-_-',
+ 'confused' => 'o_O',
+ 'upset' => 'xD',
+ 'colonthree' => ':3',
+ 'like' => '&#x1F44D;'];
+
+ $len = count($matches);
+
+ if ($len > 1) {
+ for ($i = 1; $i < $len; $i++) {
+ foreach ($facebook_emoticons as $name => $emote) {
+ if ($matches[$i] === $name) {
+ return $emote;
+ }
+ }
+ }
+ }
+
+ return $matches[0];
+ }, $content);
+ }
+
+ /**
+ * Returns the captcha message for the given captcha
+ */
+ private function returnCaptchaMessage($captcha)
+ {
+ // Save form for submitting after getting captcha response
+ if (session_status() == PHP_SESSION_NONE) {
+ session_start();
+ }
+
+ $captcha_fields = [];
+
+ foreach ($captcha->find('input, button') as $input) {
+ $captcha_fields[$input->name] = $input->value;
+ }
+
+ $_SESSION['captcha_fields'] = $captcha_fields;
+ $_SESSION['captcha_action'] = $captcha->find('form', 0)->action;
+
+ // Show captcha filling form to the viewer, proxying the captcha image
+ $img = base64_encode(getContents($captcha->find('img', 0)->src));
+
+ header('Content-Type: text/html', true, 500);
+
+ $message = <<<EOD
<form method="post" action="?{$_SERVER['QUERY_STRING']}">
<h2>Facebook captcha challenge</h2>
<p>Unfortunately, rss-bridge cannot fetch the requested page.<br />
@@ -505,246 +499,257 @@ Facebook wants rss-bridge to resolve the following captcha:</p>
</form>
EOD;
- die($message);
- }
-
- /**
- * Checks if a capture response was received and tries to load the contents
- * @return mixed null if no capture response was received, simplhtmldom document otherwise
- */
- private function handleCaptchaResponse() {
- if (isset($_POST['captcha_response'])) {
- if (session_status() == PHP_SESSION_NONE)
- session_start();
-
- if (isset($_SESSION['captcha_fields'], $_SESSION['captcha_action'])) {
- $captcha_action = $_SESSION['captcha_action'];
- $captcha_fields = $_SESSION['captcha_fields'];
- $captcha_fields['captcha_response'] = preg_replace('/[^a-zA-Z0-9]+/', '', $_POST['captcha_response']);
-
- $header = array(
- 'Content-type: application/x-www-form-urlencoded',
- 'Referer: ' . $captcha_action,
- 'Cookie: noscript=1'
- );
-
- $opts = array(
- CURLOPT_POST => 1,
- CURLOPT_POSTFIELDS => http_build_query($captcha_fields)
- );
-
- $html = getSimpleHTMLDOM($captcha_action, $header, $opts);
-
- return $html;
- }
-
- unset($_SESSION['captcha_fields']);
- unset($_SESSION['captcha_action']);
- }
-
- return null;
- }
-
- private function collectUserData(){
-
- $html = $this->handleCaptchaResponse();
-
- // Retrieve page contents
- if(is_null($html)) {
-
- if(getEnv('HTTP_ACCEPT_LANGUAGE')) {
- $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE'));
- } else {
- $header = array();
- }
-
- $html = getSimpleHTMLDOM($this->getURI(), $header);
-
- }
-
- // Handle captcha form?
- $captcha = $html->find('div.captcha_interstitial', 0);
-
- if (!is_null($captcha)) {
- $this->returnCaptchaMessage($captcha);
- }
-
- // No captcha? We can carry on retrieving page contents :)
- // First, we check whether the page is public or not
- $loginForm = $html->find('._585r', 0);
-
- if($loginForm != null) {
- returnServerError('You must be logged in to view this page. This is not supported by RSS-Bridge.');
- }
-
- $element = $html
- ->find('#pagelet_timeline_main_column')[0]
- ->children(0)
- ->children(0)
- ->next_sibling()
- ->children(0);
-
- if(isset($element)) {
-
- $author = str_replace(' - Posts | Facebook', '', $html->find('title#pageTitle', 0)->innertext);
-
- $profilePic = $html->find('meta[property="og:image"]', 0)->content;
-
- $this->authorName = $author;
-
- foreach($element->children() as $cell) {
- // Manage summary posts
- if(strpos($cell->class, '_3xaf') !== false) {
- $posts = $cell->children();
- } else {
- $posts = array($cell);
- }
-
- // Optionally skip reviews
- if($this->getInput('skip_reviews')
- && !is_null($cell->find('#review_composer_container', 0))) {
- continue;
- }
-
- foreach($posts as $post) {
- // Check media type
- switch($this->getInput('media_type')) {
- case 'all': break;
- case 'video':
- if(empty($post->find('[aria-label=Video]'))) continue 2;
- break;
- case 'novideo':
- if(!empty($post->find('[aria-label=Video]'))) continue 2;
- break;
- default: break;
- }
-
- $item = array();
-
- if(count($post->find('abbr')) > 0) {
-
- $content = $post->find('.userContentWrapper', 0);
-
- // This array specifies filters applied to all posts in order of appearance
- $content_filters = array(
- '._5mly', // Remove embedded videos (the preview image remains)
- '._2ezg', // Remove "Views ..."
- '.hidden_elem', // Remove hidden elements (they are hidden anyway)
- '.timestampContent', // Remove relative timestamp
- '._6spk', // Remove redundant separator
- );
-
- foreach($content_filters as $filter) {
- foreach($content->find($filter) as $subject) {
- $subject->outertext = '';
- }
- }
-
- // Change origin tag for embedded media from div to paragraph
- foreach($content->find('._59tj') as $subject) {
- $subject->outertext = '<p>' . $subject->innertext . '</p>';
- }
-
- // Change title tag for embedded media from anchor to paragraph
- foreach($content->find('._3n1k a') as $anchor) {
- $anchor->outertext = '<p>' . $anchor->innertext . '</p>';
- }
-
- $content = preg_replace(
- '/(?i)><div class=\"_3dp([^>]+)>(.+?)div\ class=\"[^u]+userContent\"/i',
- '',
- $content);
-
- $content = preg_replace(
- '/(?i)><div class=\"_4l5([^>]+)>(.+?)<\/div>/i',
- '',
- $content);
-
- // Remove "SpSonsSoriSsés"
- $content = preg_replace(
- '/(?iU)<a [^>]+ href="#" role="link" [^>}]+>.+<\/a>/iU',
- '',
- $content);
-
- // Remove html nodes, keep only img, links, basic formatting
- $content = strip_tags($content, '<a><img><i><u><br><p>');
-
- $content = $this->unescapeFacebookLink($content);
-
- // Clean useless html tag properties and fix link closing tags
- foreach (array(
- 'onmouseover',
- 'onclick',
- 'target',
- 'ajaxify',
- 'tabindex',
- 'class',
- 'style',
- 'data-[^=]*',
- 'aria-[^=]*',
- 'role',
- 'rel',
- 'id') as $property_name) {
- $content = preg_replace('/ ' . $property_name . '=\"[^"]*\"/i', '', $content);
- }
-
- $content = preg_replace('/<\/a [^>]+>/i', '</a>', $content);
-
- $this->unescapeFacebookEmote($content);
-
- // Restore links in the post before further parsing
- $post = defaultLinkTo($post, self::URI);
-
- // Restore links in the content before adding to the item
- $content = defaultLinkTo($content, self::URI);
-
- $content = $this->removeTrackingCodes($content);
-
- // Retrieve date of the post
- $date = $post->find('abbr')[0];
-
- if(isset($date) && $date->hasAttribute('data-utime')) {
- $date = $date->getAttribute('data-utime');
- } else {
- $date = 0;
- }
-
- // Build title from content
- $title = strip_tags($post->find('.userContent', 0)->innertext);
- if(strlen($title) > 64)
- $title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
-
- $uri = $post->find('abbr')[0]->parent()->getAttribute('href');
-
- // Extract fbid and patch link
- if (strpos($uri, '?') !== false) {
- $query = substr($uri, strpos($uri, '?') + 1);
- parse_str($query, $query_params);
- if (isset($query_params['story_fbid'])) {
- $uri = self::URI . $query_params['story_fbid'];
- } else {
- $uri = substr($uri, 0, strpos($uri, '?'));
- }
- }
-
- //Build and add final item
- $item['uri'] = htmlspecialchars_decode($uri, ENT_QUOTES);
- $item['content'] = htmlspecialchars_decode($content, ENT_QUOTES);
- $item['title'] = htmlspecialchars_decode($title, ENT_QUOTES);
- $item['author'] = htmlspecialchars_decode($author, ENT_QUOTES);
- $item['timestamp'] = $date;
-
- if(strpos($item['content'], '<img') === false) {
- $item['enclosures'] = array($profilePic);
- }
-
- $this->items[] = $item;
- }
- }
- }
- }
- }
-
- #endregion (User)
-
+ die($message);
+ }
+
+ /**
+ * Checks if a capture response was received and tries to load the contents
+ * @return mixed null if no capture response was received, simplhtmldom document otherwise
+ */
+ private function handleCaptchaResponse()
+ {
+ if (isset($_POST['captcha_response'])) {
+ if (session_status() == PHP_SESSION_NONE) {
+ session_start();
+ }
+
+ if (isset($_SESSION['captcha_fields'], $_SESSION['captcha_action'])) {
+ $captcha_action = $_SESSION['captcha_action'];
+ $captcha_fields = $_SESSION['captcha_fields'];
+ $captcha_fields['captcha_response'] = preg_replace('/[^a-zA-Z0-9]+/', '', $_POST['captcha_response']);
+
+ $header = [
+ 'Content-type: application/x-www-form-urlencoded',
+ 'Referer: ' . $captcha_action,
+ 'Cookie: noscript=1'
+ ];
+
+ $opts = [
+ CURLOPT_POST => 1,
+ CURLOPT_POSTFIELDS => http_build_query($captcha_fields)
+ ];
+
+ $html = getSimpleHTMLDOM($captcha_action, $header, $opts);
+
+ return $html;
+ }
+
+ unset($_SESSION['captcha_fields']);
+ unset($_SESSION['captcha_action']);
+ }
+
+ return null;
+ }
+
+ private function collectUserData()
+ {
+ $html = $this->handleCaptchaResponse();
+
+ // Retrieve page contents
+ if (is_null($html)) {
+ if (getEnv('HTTP_ACCEPT_LANGUAGE')) {
+ $header = ['Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE')];
+ } else {
+ $header = [];
+ }
+
+ $html = getSimpleHTMLDOM($this->getURI(), $header);
+ }
+
+ // Handle captcha form?
+ $captcha = $html->find('div.captcha_interstitial', 0);
+
+ if (!is_null($captcha)) {
+ $this->returnCaptchaMessage($captcha);
+ }
+
+ // No captcha? We can carry on retrieving page contents :)
+ // First, we check whether the page is public or not
+ $loginForm = $html->find('._585r', 0);
+
+ if ($loginForm != null) {
+ returnServerError('You must be logged in to view this page. This is not supported by RSS-Bridge.');
+ }
+
+ $element = $html
+ ->find('#pagelet_timeline_main_column')[0]
+ ->children(0)
+ ->children(0)
+ ->next_sibling()
+ ->children(0);
+
+ if (isset($element)) {
+ $author = str_replace(' - Posts | Facebook', '', $html->find('title#pageTitle', 0)->innertext);
+
+ $profilePic = $html->find('meta[property="og:image"]', 0)->content;
+
+ $this->authorName = $author;
+
+ foreach ($element->children() as $cell) {
+ // Manage summary posts
+ if (strpos($cell->class, '_3xaf') !== false) {
+ $posts = $cell->children();
+ } else {
+ $posts = [$cell];
+ }
+
+ // Optionally skip reviews
+ if (
+ $this->getInput('skip_reviews')
+ && !is_null($cell->find('#review_composer_container', 0))
+ ) {
+ continue;
+ }
+
+ foreach ($posts as $post) {
+ // Check media type
+ switch ($this->getInput('media_type')) {
+ case 'all':
+ break;
+ case 'video':
+ if (empty($post->find('[aria-label=Video]'))) {
+ continue 2;
+ }
+ break;
+ case 'novideo':
+ if (!empty($post->find('[aria-label=Video]'))) {
+ continue 2;
+ }
+ break;
+ default:
+ break;
+ }
+
+ $item = [];
+
+ if (count($post->find('abbr')) > 0) {
+ $content = $post->find('.userContentWrapper', 0);
+
+ // This array specifies filters applied to all posts in order of appearance
+ $content_filters = [
+ '._5mly', // Remove embedded videos (the preview image remains)
+ '._2ezg', // Remove "Views ..."
+ '.hidden_elem', // Remove hidden elements (they are hidden anyway)
+ '.timestampContent', // Remove relative timestamp
+ '._6spk', // Remove redundant separator
+ ];
+
+ foreach ($content_filters as $filter) {
+ foreach ($content->find($filter) as $subject) {
+ $subject->outertext = '';
+ }
+ }
+
+ // Change origin tag for embedded media from div to paragraph
+ foreach ($content->find('._59tj') as $subject) {
+ $subject->outertext = '<p>' . $subject->innertext . '</p>';
+ }
+
+ // Change title tag for embedded media from anchor to paragraph
+ foreach ($content->find('._3n1k a') as $anchor) {
+ $anchor->outertext = '<p>' . $anchor->innertext . '</p>';
+ }
+
+ $content = preg_replace(
+ '/(?i)><div class=\"_3dp([^>]+)>(.+?)div\ class=\"[^u]+userContent\"/i',
+ '',
+ $content
+ );
+
+ $content = preg_replace(
+ '/(?i)><div class=\"_4l5([^>]+)>(.+?)<\/div>/i',
+ '',
+ $content
+ );
+
+ // Remove "SpSonsSoriSsés"
+ $content = preg_replace(
+ '/(?iU)<a [^>]+ href="#" role="link" [^>}]+>.+<\/a>/iU',
+ '',
+ $content
+ );
+
+ // Remove html nodes, keep only img, links, basic formatting
+ $content = strip_tags($content, '<a><img><i><u><br><p>');
+
+ $content = $this->unescapeFacebookLink($content);
+
+ // Clean useless html tag properties and fix link closing tags
+ foreach (
+ [
+ 'onmouseover',
+ 'onclick',
+ 'target',
+ 'ajaxify',
+ 'tabindex',
+ 'class',
+ 'style',
+ 'data-[^=]*',
+ 'aria-[^=]*',
+ 'role',
+ 'rel',
+ 'id'] as $property_name
+ ) {
+ $content = preg_replace('/ ' . $property_name . '=\"[^"]*\"/i', '', $content);
+ }
+
+ $content = preg_replace('/<\/a [^>]+>/i', '</a>', $content);
+
+ $this->unescapeFacebookEmote($content);
+
+ // Restore links in the post before further parsing
+ $post = defaultLinkTo($post, self::URI);
+
+ // Restore links in the content before adding to the item
+ $content = defaultLinkTo($content, self::URI);
+
+ $content = $this->removeTrackingCodes($content);
+
+ // Retrieve date of the post
+ $date = $post->find('abbr')[0];
+
+ if (isset($date) && $date->hasAttribute('data-utime')) {
+ $date = $date->getAttribute('data-utime');
+ } else {
+ $date = 0;
+ }
+
+ // Build title from content
+ $title = strip_tags($post->find('.userContent', 0)->innertext);
+ if (strlen($title) > 64) {
+ $title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
+ }
+
+ $uri = $post->find('abbr')[0]->parent()->getAttribute('href');
+
+ // Extract fbid and patch link
+ if (strpos($uri, '?') !== false) {
+ $query = substr($uri, strpos($uri, '?') + 1);
+ parse_str($query, $query_params);
+ if (isset($query_params['story_fbid'])) {
+ $uri = self::URI . $query_params['story_fbid'];
+ } else {
+ $uri = substr($uri, 0, strpos($uri, '?'));
+ }
+ }
+
+ //Build and add final item
+ $item['uri'] = htmlspecialchars_decode($uri, ENT_QUOTES);
+ $item['content'] = htmlspecialchars_decode($content, ENT_QUOTES);
+ $item['title'] = htmlspecialchars_decode($title, ENT_QUOTES);
+ $item['author'] = htmlspecialchars_decode($author, ENT_QUOTES);
+ $item['timestamp'] = $date;
+
+ if (strpos($item['content'], '<img') === false) {
+ $item['enclosures'] = [$profilePic];
+ }
+
+ $this->items[] = $item;
+ }
+ }
+ }
+ }
+ }
+
+ #endregion (User)
}
diff --git a/bridges/FeedExpanderExampleBridge.php b/bridges/FeedExpanderExampleBridge.php
index 708d4c13..a6b37f65 100644
--- a/bridges/FeedExpanderExampleBridge.php
+++ b/bridges/FeedExpanderExampleBridge.php
@@ -1,61 +1,66 @@
<?php
-class FeedExpanderExampleBridge extends FeedExpander {
- const MAINTAINER = 'logmanoriginal';
- const NAME = 'FeedExpander Example';
- const URI = 'http://github.com/RSS-Bridge/rss-bridge/';
- const DESCRIPTION = 'Example bridge to test FeedExpander';
+class FeedExpanderExampleBridge extends FeedExpander
+{
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'FeedExpander Example';
+ const URI = 'http://github.com/RSS-Bridge/rss-bridge/';
+ const DESCRIPTION = 'Example bridge to test FeedExpander';
- const PARAMETERS = array(
- 'Feed' => array(
- 'version' => array(
- 'name' => 'Version',
- 'type' => 'list',
- 'title' => 'Select your feed format/version',
- 'defaultValue' => 'RSS 2.0',
- 'values' => array(
- 'RSS 0.91' => 'rss_0_9_1',
- 'RSS 1.0' => 'rss_1_0',
- 'RSS 2.0' => 'rss_2_0',
- 'ATOM 1.0' => 'atom_1_0'
- )
- )
- )
- );
+ const PARAMETERS = [
+ 'Feed' => [
+ 'version' => [
+ 'name' => 'Version',
+ 'type' => 'list',
+ 'title' => 'Select your feed format/version',
+ 'defaultValue' => 'RSS 2.0',
+ 'values' => [
+ 'RSS 0.91' => 'rss_0_9_1',
+ 'RSS 1.0' => 'rss_1_0',
+ 'RSS 2.0' => 'rss_2_0',
+ 'ATOM 1.0' => 'atom_1_0'
+ ]
+ ]
+ ]
+ ];
- public function collectData(){
- switch($this->getInput('version')) {
- case 'rss_0_9_1':
- parent::collectExpandableDatas('http://static.userland.com/gems/backend/sampleRss.xml');
- break;
- case 'rss_1_0':
- parent::collectExpandableDatas('http://feeds.nature.com/nature/rss/current?format=xml');
- break;
- case 'rss_2_0':
- parent::collectExpandableDatas('http://feeds.rssboard.org/rssboard?format=xml');
- break;
- case 'atom_1_0':
- parent::collectExpandableDatas('http://segfault.linuxmint.com/feed/atom/');
- break;
- default: returnClientError('Unknown version ' . $this->getInput('version') . '!');
- }
- }
+ public function collectData()
+ {
+ switch ($this->getInput('version')) {
+ case 'rss_0_9_1':
+ parent::collectExpandableDatas('http://static.userland.com/gems/backend/sampleRss.xml');
+ break;
+ case 'rss_1_0':
+ parent::collectExpandableDatas('http://feeds.nature.com/nature/rss/current?format=xml');
+ break;
+ case 'rss_2_0':
+ parent::collectExpandableDatas('http://feeds.rssboard.org/rssboard?format=xml');
+ break;
+ case 'atom_1_0':
+ parent::collectExpandableDatas('http://segfault.linuxmint.com/feed/atom/');
+ break;
+ default:
+ returnClientError('Unknown version ' . $this->getInput('version') . '!');
+ }
+ }
- protected function parseItem($newsItem) {
- switch($this->getInput('version')) {
- case 'rss_0_9_1':
- return $this->parseRss091Item($newsItem);
- break;
- case 'rss_1_0':
- return $this->parseRss1Item($newsItem);
- break;
- case 'rss_2_0':
- return $this->parseRss2Item($newsItem);
- break;
- case 'atom_1_0':
- return $this->parseATOMItem($newsItem);
- break;
- default: returnClientError('Unknown version ' . $this->getInput('version') . '!');
- }
- }
+ protected function parseItem($newsItem)
+ {
+ switch ($this->getInput('version')) {
+ case 'rss_0_9_1':
+ return $this->parseRss091Item($newsItem);
+ break;
+ case 'rss_1_0':
+ return $this->parseRss1Item($newsItem);
+ break;
+ case 'rss_2_0':
+ return $this->parseRss2Item($newsItem);
+ break;
+ case 'atom_1_0':
+ return $this->parseATOMItem($newsItem);
+ break;
+ default:
+ returnClientError('Unknown version ' . $this->getInput('version') . '!');
+ }
+ }
}
diff --git a/bridges/FeedMergeBridge.php b/bridges/FeedMergeBridge.php
index b90055e5..df3d39c4 100644
--- a/bridges/FeedMergeBridge.php
+++ b/bridges/FeedMergeBridge.php
@@ -1,61 +1,65 @@
<?php
-class FeedMergeBridge extends FeedExpander {
- const MAINTAINER = 'dvikan';
- const NAME = 'FeedMerge';
- const URI = 'https://github.com/RSS-Bridge/rss-bridge';
- const DESCRIPTION = <<<'TEXT'
+class FeedMergeBridge extends FeedExpander
+{
+ const MAINTAINER = 'dvikan';
+ const NAME = 'FeedMerge';
+ const URI = 'https://github.com/RSS-Bridge/rss-bridge';
+ const DESCRIPTION = <<<'TEXT'
This bridge merges two or more feeds into a single feed. Max 10 items are fetched from each feed.
TEXT;
- const PARAMETERS = [
- [
- 'feed_name' => [
- 'name' => 'Feed name',
- 'type' => 'text',
- 'exampleValue' => 'rss-bridge/FeedMerger',
- ],
- 'feed_1' => [
- 'name' => 'Feed url',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'https://lorem-rss.herokuapp.com/feed?unit=day'
- ],
- 'feed_2' => ['name' => 'Feed url', 'type' => 'text'],
- 'feed_3' => ['name' => 'Feed url', 'type' => 'text'],
- 'feed_4' => ['name' => 'Feed url', 'type' => 'text'],
- 'feed_5' => ['name' => 'Feed url', 'type' => 'text'],
+ const PARAMETERS = [
+ [
+ 'feed_name' => [
+ 'name' => 'Feed name',
+ 'type' => 'text',
+ 'exampleValue' => 'rss-bridge/FeedMerger',
+ ],
+ 'feed_1' => [
+ 'name' => 'Feed url',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'https://lorem-rss.herokuapp.com/feed?unit=day'
+ ],
+ 'feed_2' => ['name' => 'Feed url', 'type' => 'text'],
+ 'feed_3' => ['name' => 'Feed url', 'type' => 'text'],
+ 'feed_4' => ['name' => 'Feed url', 'type' => 'text'],
+ 'feed_5' => ['name' => 'Feed url', 'type' => 'text'],
- 'limit' => self::LIMIT,
- ]
- ];
+ 'limit' => self::LIMIT,
+ ]
+ ];
- public function collectData() {
- $limit = (int)($this->getInput('limit') ?: 10);
- $feeds = [
- $this->getInput('feed_1'),
- $this->getInput('feed_2'),
- $this->getInput('feed_3'),
- $this->getInput('feed_4'),
- $this->getInput('feed_5'),
- ];
- // Remove empty values
- $feeds = array_filter($feeds);
- foreach ($feeds as $feed) {
- // Fetch all items from the feed
- $this->collectExpandableDatas($feed);
- }
- // Sort by timestamp descending
- usort($this->items, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']);
- // Grab the first $limit items
- $this->items = array_slice($this->items, 0, $limit);
- }
+ public function collectData()
+ {
+ $limit = (int)($this->getInput('limit') ?: 10);
+ $feeds = [
+ $this->getInput('feed_1'),
+ $this->getInput('feed_2'),
+ $this->getInput('feed_3'),
+ $this->getInput('feed_4'),
+ $this->getInput('feed_5'),
+ ];
+ // Remove empty values
+ $feeds = array_filter($feeds);
+ foreach ($feeds as $feed) {
+ // Fetch all items from the feed
+ $this->collectExpandableDatas($feed);
+ }
+ // Sort by timestamp descending
+ usort($this->items, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']);
+ // Grab the first $limit items
+ $this->items = array_slice($this->items, 0, $limit);
+ }
- public function getIcon() {
- return 'https://cdn.jsdelivr.net/npm/famfamfam-silk@1.0.0/dist/png/folder_feed.png';
- }
+ public function getIcon()
+ {
+ return 'https://cdn.jsdelivr.net/npm/famfamfam-silk@1.0.0/dist/png/folder_feed.png';
+ }
- public function getName() {
- return $this->getInput('feed_name') ?: 'rss-bridge/FeedMerger';
- }
+ public function getName()
+ {
+ return $this->getInput('feed_name') ?: 'rss-bridge/FeedMerger';
+ }
}
diff --git a/bridges/FeedReducerBridge.php b/bridges/FeedReducerBridge.php
index 613a5539..a37824c9 100644
--- a/bridges/FeedReducerBridge.php
+++ b/bridges/FeedReducerBridge.php
@@ -1,60 +1,64 @@
<?php
-class FeedReducerBridge extends FeedExpander {
-
- const MAINTAINER = 'mdemoss';
- const NAME = 'Feed Reducer';
- const URI = 'http://github.com/RSS-Bridge/rss-bridge/';
- const DESCRIPTION = 'Choose a percentage of a feed you want to see.';
- const PARAMETERS = array( array(
- 'url' => array(
- 'name' => 'Feed URI',
- 'exampleValue' => 'https://lorem-rss.herokuapp.com/feed?length=42',
- 'required' => true
- ),
- 'percentage' => array(
- 'name' => 'percentage',
- 'type' => 'number',
- 'exampleValue' => 50,
- 'required' => true
- )
- ));
- const CACHE_TIMEOUT = 3600;
-
- public function collectData(){
- if(preg_match('#^http(s?)://#i', $this->getInput('url'))) {
- $this->collectExpandableDatas($this->getInput('url'));
- } else {
- throw new Exception('URI must begin with http(s)://');
- }
- }
-
- public function getItems(){
- $filteredItems = array();
- $intPercentage = (int)preg_replace('/[^0-9]/', '', $this->getInput('percentage'));
-
- foreach ($this->items as $thisItem) {
- // The URL is included in the hash:
- // - so you can change the output by adding a local-part to the URL
- // - so items with the same URI in different feeds won't be correlated
-
- // $pseudoRandomInteger will be a 16 bit unsigned int mod 100.
- // This won't be uniformly distributed 1-100, but should be close enough.
-
- $pseudoRandomInteger = unpack(
- 'S', // unsigned 16-bit int
- hash( 'sha256', $thisItem['uri'] . '::' . $this->getInput('url'), true )
- )[1] % 100;
-
- if ($pseudoRandomInteger < $intPercentage) {
- $filteredItems[] = $thisItem;
- }
- }
-
- return $filteredItems;
- }
-
- public function getName(){
- $trimmedPercentage = preg_replace('/[^0-9]/', '', $this->getInput('percentage') ?? '');
- return parent::getName() . ' [' . $trimmedPercentage . '%]';
- }
+
+class FeedReducerBridge extends FeedExpander
+{
+ const MAINTAINER = 'mdemoss';
+ const NAME = 'Feed Reducer';
+ const URI = 'http://github.com/RSS-Bridge/rss-bridge/';
+ const DESCRIPTION = 'Choose a percentage of a feed you want to see.';
+ const PARAMETERS = [ [
+ 'url' => [
+ 'name' => 'Feed URI',
+ 'exampleValue' => 'https://lorem-rss.herokuapp.com/feed?length=42',
+ 'required' => true
+ ],
+ 'percentage' => [
+ 'name' => 'percentage',
+ 'type' => 'number',
+ 'exampleValue' => 50,
+ 'required' => true
+ ]
+ ]];
+ const CACHE_TIMEOUT = 3600;
+
+ public function collectData()
+ {
+ if (preg_match('#^http(s?)://#i', $this->getInput('url'))) {
+ $this->collectExpandableDatas($this->getInput('url'));
+ } else {
+ throw new Exception('URI must begin with http(s)://');
+ }
+ }
+
+ public function getItems()
+ {
+ $filteredItems = [];
+ $intPercentage = (int)preg_replace('/[^0-9]/', '', $this->getInput('percentage'));
+
+ foreach ($this->items as $thisItem) {
+ // The URL is included in the hash:
+ // - so you can change the output by adding a local-part to the URL
+ // - so items with the same URI in different feeds won't be correlated
+
+ // $pseudoRandomInteger will be a 16 bit unsigned int mod 100.
+ // This won't be uniformly distributed 1-100, but should be close enough.
+
+ $pseudoRandomInteger = unpack(
+ 'S', // unsigned 16-bit int
+ hash('sha256', $thisItem['uri'] . '::' . $this->getInput('url'), true)
+ )[1] % 100;
+
+ if ($pseudoRandomInteger < $intPercentage) {
+ $filteredItems[] = $thisItem;
+ }
+ }
+
+ return $filteredItems;
+ }
+
+ public function getName()
+ {
+ $trimmedPercentage = preg_replace('/[^0-9]/', '', $this->getInput('percentage') ?? '');
+ return parent::getName() . ' [' . $trimmedPercentage . '%]';
+ }
}
diff --git a/bridges/FicbookBridge.php b/bridges/FicbookBridge.php
index 64cdb32d..2a245d4e 100644
--- a/bridges/FicbookBridge.php
+++ b/bridges/FicbookBridge.php
@@ -1,184 +1,195 @@
<?php
-class FicbookBridge extends BridgeAbstract {
-
- const NAME = 'Ficbook Bridge';
- const URI = 'https://ficbook.net/';
- const DESCRIPTION = 'No description provided';
- const MAINTAINER = 'logmanoriginal';
-
- const PARAMETERS = array(
- 'Site News' => array(),
- 'Fiction Updates' => array(
- 'fiction_id' => array(
- 'name' => 'Fanfiction ID',
- 'type' => 'text',
- 'pattern' => '[0-9]+',
- 'required' => true,
- 'title' => 'Insert fanfiction ID',
- 'exampleValue' => '5783919',
- ),
- 'include_contents' => array(
- 'name' => 'Include contents',
- 'type' => 'checkbox',
- 'title' => 'Activate to include contents in the feed',
- ),
- ),
- 'Fiction Comments' => array(
- 'fiction_id' => array(
- 'name' => 'Fanfiction ID',
- 'type' => 'text',
- 'pattern' => '[0-9]+',
- 'required' => true,
- 'title' => 'Insert fanfiction ID',
- 'exampleValue' => '5783919',
- ),
- ),
- );
-
- protected $titleName;
-
- public function getURI() {
- switch($this->queriedContext) {
- case 'Site News':
- // For some reason this is not HTTPS
- return 'http://ficbook.net/sitenews';
-
- case 'Fiction Updates':
- return self::URI
- . 'readfic/'
- . urlencode($this->getInput('fiction_id'));
-
- case 'Fiction Comments':
- return self::URI
- . 'readfic/'
- . urlencode($this->getInput('fiction_id'))
- . '/comments#content';
-
- default: return parent::getURI();
- }
- }
-
- public function getName() {
- switch($this->queriedContext) {
- case 'Site News':
- return $this->queriedContext . ' | ' . self::NAME;
-
- case 'Fiction Updates':
- return $this->titleName . ' | ' . self::NAME;
-
- case 'Fiction Comments':
- return $this->titleName . ' | Comments | ' . self::NAME;
-
- default: return self::NAME;
- }
- }
-
- public function collectData() {
-
- $header = array('Accept-Language: en-US');
-
- $html = getSimpleHTMLDOM($this->getURI(), $header);
-
- $html = defaultLinkTo($html, self::URI);
-
- if ($this->queriedContext == 'Fiction Updates' or $this->queriedContext == 'Fiction Comments') {
- $this->titleName = $html->find('.fanfic-main-info > h1', 0)->innertext;
- }
-
- switch($this->queriedContext) {
- case 'Site News': return $this->collectSiteNews($html);
- case 'Fiction Updates': return $this->collectUpdatesData($html);
- case 'Fiction Comments': return $this->collectCommentsData($html);
- }
-
- }
-
- private function collectSiteNews($html) {
- foreach($html->find('.news_view') as $news) {
- $this->items[] = array(
- 'title' => $news->find('h1.title', 0)->plaintext,
- 'timestamp' => strtotime($this->fixDate($news->find('span[title]', 0)->title)),
- 'content' => $news->find('.news_text', 0),
- );
- }
- }
-
- private function collectCommentsData($html) {
- foreach($html->find('article.comment-container') as $article) {
- $this->items[] = array(
- 'uri' => $article->find('.comment_link_to_fic > a', 0)->href,
- 'title' => $article->find('.comment_author', 0)->plaintext,
- 'author' => $article->find('.comment_author', 0)->plaintext,
- 'timestamp' => strtotime($this->fixDate($article->find('time[datetime]', 0)->datetime)),
- 'content' => $article->find('.comment_message', 0),
- 'enclosures' => array($article->find('img', 0)->src),
- );
- }
- }
-
- private function collectUpdatesData($html) {
- foreach($html->find('ul.list-of-fanfic-parts > li') as $chapter) {
- $item = array(
- 'uri' => $chapter->find('a', 0)->href,
- 'title' => $chapter->find('a', 0)->plaintext,
- 'timestamp' => strtotime($this->fixDate($chapter->find('span[title]', 0)->title)),
- );
-
- if($this->getInput('include_contents')) {
- $content = getSimpleHTMLDOMCached($item['uri']);
- $item['content'] = $content->find('#content', 0);
- }
-
- $this->items[] = $item;
-
- // Sort by time, descending
- usort($this->items, function($a, $b){ return $b['timestamp'] - $a['timestamp']; });
- }
- }
-
- private function fixDate($date) {
-
- // FIXME: This list was generated using Google tranlator. Someone who
- // actually knows russian should check this list! Please keep in mind
- // that month names must match exactly the names returned by Ficbook.
- $ru_month = array(
- 'января',
- 'февраля',
- 'марта',
- 'апреля',
- 'мая',
- 'июня',
- 'июля',
- 'августа',
- 'сентября',
- 'октября',
- 'ноября',
- 'декабря',
- );
-
- $en_month = array(
- 'January',
- 'February',
- 'March',
- 'April',
- 'May',
- 'June',
- 'July',
- 'August',
- 'September',
- 'October',
- 'November',
- 'December',
- );
-
- $fixed_date = str_replace($ru_month, $en_month, $date);
-
- if($fixed_date === $date) {
- Debug::log('Unable to fix date: ' . $date);
- return null;
- }
-
- return $fixed_date;
-
- }
+
+class FicbookBridge extends BridgeAbstract
+{
+ const NAME = 'Ficbook Bridge';
+ const URI = 'https://ficbook.net/';
+ const DESCRIPTION = 'No description provided';
+ const MAINTAINER = 'logmanoriginal';
+
+ const PARAMETERS = [
+ 'Site News' => [],
+ 'Fiction Updates' => [
+ 'fiction_id' => [
+ 'name' => 'Fanfiction ID',
+ 'type' => 'text',
+ 'pattern' => '[0-9]+',
+ 'required' => true,
+ 'title' => 'Insert fanfiction ID',
+ 'exampleValue' => '5783919',
+ ],
+ 'include_contents' => [
+ 'name' => 'Include contents',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to include contents in the feed',
+ ],
+ ],
+ 'Fiction Comments' => [
+ 'fiction_id' => [
+ 'name' => 'Fanfiction ID',
+ 'type' => 'text',
+ 'pattern' => '[0-9]+',
+ 'required' => true,
+ 'title' => 'Insert fanfiction ID',
+ 'exampleValue' => '5783919',
+ ],
+ ],
+ ];
+
+ protected $titleName;
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Site News':
+ // For some reason this is not HTTPS
+ return 'http://ficbook.net/sitenews';
+
+ case 'Fiction Updates':
+ return self::URI
+ . 'readfic/'
+ . urlencode($this->getInput('fiction_id'));
+
+ case 'Fiction Comments':
+ return self::URI
+ . 'readfic/'
+ . urlencode($this->getInput('fiction_id'))
+ . '/comments#content';
+
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Site News':
+ return $this->queriedContext . ' | ' . self::NAME;
+
+ case 'Fiction Updates':
+ return $this->titleName . ' | ' . self::NAME;
+
+ case 'Fiction Comments':
+ return $this->titleName . ' | Comments | ' . self::NAME;
+
+ default:
+ return self::NAME;
+ }
+ }
+
+ public function collectData()
+ {
+ $header = ['Accept-Language: en-US'];
+
+ $html = getSimpleHTMLDOM($this->getURI(), $header);
+
+ $html = defaultLinkTo($html, self::URI);
+
+ if ($this->queriedContext == 'Fiction Updates' or $this->queriedContext == 'Fiction Comments') {
+ $this->titleName = $html->find('.fanfic-main-info > h1', 0)->innertext;
+ }
+
+ switch ($this->queriedContext) {
+ case 'Site News':
+ return $this->collectSiteNews($html);
+ case 'Fiction Updates':
+ return $this->collectUpdatesData($html);
+ case 'Fiction Comments':
+ return $this->collectCommentsData($html);
+ }
+ }
+
+ private function collectSiteNews($html)
+ {
+ foreach ($html->find('.news_view') as $news) {
+ $this->items[] = [
+ 'title' => $news->find('h1.title', 0)->plaintext,
+ 'timestamp' => strtotime($this->fixDate($news->find('span[title]', 0)->title)),
+ 'content' => $news->find('.news_text', 0),
+ ];
+ }
+ }
+
+ private function collectCommentsData($html)
+ {
+ foreach ($html->find('article.comment-container') as $article) {
+ $this->items[] = [
+ 'uri' => $article->find('.comment_link_to_fic > a', 0)->href,
+ 'title' => $article->find('.comment_author', 0)->plaintext,
+ 'author' => $article->find('.comment_author', 0)->plaintext,
+ 'timestamp' => strtotime($this->fixDate($article->find('time[datetime]', 0)->datetime)),
+ 'content' => $article->find('.comment_message', 0),
+ 'enclosures' => [$article->find('img', 0)->src],
+ ];
+ }
+ }
+
+ private function collectUpdatesData($html)
+ {
+ foreach ($html->find('ul.list-of-fanfic-parts > li') as $chapter) {
+ $item = [
+ 'uri' => $chapter->find('a', 0)->href,
+ 'title' => $chapter->find('a', 0)->plaintext,
+ 'timestamp' => strtotime($this->fixDate($chapter->find('span[title]', 0)->title)),
+ ];
+
+ if ($this->getInput('include_contents')) {
+ $content = getSimpleHTMLDOMCached($item['uri']);
+ $item['content'] = $content->find('#content', 0);
+ }
+
+ $this->items[] = $item;
+
+ // Sort by time, descending
+ usort($this->items, function ($a, $b) {
+ return $b['timestamp'] - $a['timestamp'];
+ });
+ }
+ }
+
+ private function fixDate($date)
+ {
+ // FIXME: This list was generated using Google tranlator. Someone who
+ // actually knows russian should check this list! Please keep in mind
+ // that month names must match exactly the names returned by Ficbook.
+ $ru_month = [
+ 'января',
+ 'февраля',
+ 'марта',
+ 'апреля',
+ 'мая',
+ 'июня',
+ 'июля',
+ 'августа',
+ 'сентября',
+ 'октября',
+ 'ноября',
+ 'декабря',
+ ];
+
+ $en_month = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+ ];
+
+ $fixed_date = str_replace($ru_month, $en_month, $date);
+
+ if ($fixed_date === $date) {
+ Debug::log('Unable to fix date: ' . $date);
+ return null;
+ }
+
+ return $fixed_date;
+ }
}
diff --git a/bridges/FilterBridge.php b/bridges/FilterBridge.php
index fdf1fec8..6878804b 100644
--- a/bridges/FilterBridge.php
+++ b/bridges/FilterBridge.php
@@ -1,141 +1,144 @@
<?php
-class FilterBridge extends FeedExpander {
+class FilterBridge extends FeedExpander
+{
+ const MAINTAINER = 'Frenzie, ORelio';
+ const NAME = 'Filter';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Filters a feed of your choice';
+ const URI = 'https://github.com/RSS-Bridge/rss-bridge';
- const MAINTAINER = 'Frenzie, ORelio';
- const NAME = 'Filter';
- const CACHE_TIMEOUT = 3600; // 1h
- const DESCRIPTION = 'Filters a feed of your choice';
- const URI = 'https://github.com/RSS-Bridge/rss-bridge';
+ const PARAMETERS = [[
+ 'url' => [
+ 'name' => 'Feed URL',
+ 'type' => 'text',
+ 'defaultValue' => 'https://lorem-rss.herokuapp.com/feed?unit=day',
+ 'required' => true,
+ ],
+ 'filter' => [
+ 'name' => 'Filter (regular expression)',
+ 'required' => false,
+ ],
+ 'filter_type' => [
+ 'name' => 'Filter type',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => [
+ 'Keep matching items' => 'permit',
+ 'Hide matching items' => 'block',
+ ],
+ 'defaultValue' => 'permit',
+ ],
+ 'case_insensitive' => [
+ 'name' => 'Case-insensitive filter',
+ 'type' => 'checkbox',
+ 'required' => false,
+ ],
+ 'fix_encoding' => [
+ 'name' => 'Attempt Latin1/UTF-8 fixes when evaluating filter',
+ 'type' => 'checkbox',
+ 'required' => false,
+ ],
+ 'target_title' => [
+ 'name' => 'Apply filter on title',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'defaultValue' => 'checked'
+ ],
+ 'target_content' => [
+ 'name' => 'Apply filter on content',
+ 'type' => 'checkbox',
+ 'required' => false,
+ ],
+ 'target_author' => [
+ 'name' => 'Apply filter on author',
+ 'type' => 'checkbox',
+ 'required' => false,
+ ],
+ 'title_from_content' => [
+ 'name' => 'Generate title from content (overwrite existing title)',
+ 'type' => 'checkbox',
+ 'required' => false,
+ ],
+ 'length_limit' => [
+ 'name' => 'Max length analyzed by filter (-1: no limit)',
+ 'type' => 'number',
+ 'required' => false,
+ 'defaultValue' => -1,
+ ],
+ ]];
- const PARAMETERS = array(array(
- 'url' => array(
- 'name' => 'Feed URL',
- 'type' => 'text',
- 'defaultValue' => 'https://lorem-rss.herokuapp.com/feed?unit=day',
- 'required' => true,
- ),
- 'filter' => array(
- 'name' => 'Filter (regular expression)',
- 'required' => false,
- ),
- 'filter_type' => array(
- 'name' => 'Filter type',
- 'type' => 'list',
- 'required' => false,
- 'values' => array(
- 'Keep matching items' => 'permit',
- 'Hide matching items' => 'block',
- ),
- 'defaultValue' => 'permit',
- ),
- 'case_insensitive' => array(
- 'name' => 'Case-insensitive filter',
- 'type' => 'checkbox',
- 'required' => false,
- ),
- 'fix_encoding' => array(
- 'name' => 'Attempt Latin1/UTF-8 fixes when evaluating filter',
- 'type' => 'checkbox',
- 'required' => false,
- ),
- 'target_title' => array(
- 'name' => 'Apply filter on title',
- 'type' => 'checkbox',
- 'required' => false,
- 'defaultValue' => 'checked'
- ),
- 'target_content' => array(
- 'name' => 'Apply filter on content',
- 'type' => 'checkbox',
- 'required' => false,
- ),
- 'target_author' => array(
- 'name' => 'Apply filter on author',
- 'type' => 'checkbox',
- 'required' => false,
- ),
- 'title_from_content' => array(
- 'name' => 'Generate title from content (overwrite existing title)',
- 'type' => 'checkbox',
- 'required' => false,
- ),
- 'length_limit' => array(
- 'name' => 'Max length analyzed by filter (-1: no limit)',
- 'type' => 'number',
- 'required' => false,
- 'defaultValue' => -1,
- ),
- ));
+ protected function parseItem($newItem)
+ {
+ $item = parent::parseItem($newItem);
- protected function parseItem($newItem){
- $item = parent::parseItem($newItem);
+ // Generate title from first 50 characters of content?
+ if ($this->getInput('title_from_content') && array_key_exists('content', $item)) {
+ $content = str_get_html($item['content']);
+ $pos = strpos($item['content'], ' ', 50);
+ $item['title'] = substr($content->plaintext, 0, $pos);
+ if (strlen($content->plaintext) >= $pos) {
+ $item['title'] .= '...';
+ }
+ }
- // Generate title from first 50 characters of content?
- if($this->getInput('title_from_content') && array_key_exists('content', $item)) {
- $content = str_get_html($item['content']);
- $pos = strpos($item['content'], ' ', 50);
- $item['title'] = substr($content->plaintext, 0, $pos);
- if(strlen($content->plaintext) >= $pos) {
- $item['title'] .= '...';
- }
- }
+ // Build regular expression
+ $regex = '/' . $this->getInput('filter') . '/';
+ if ($this->getInput('case_insensitive')) {
+ $regex .= 'i';
+ }
- // Build regular expression
- $regex = '/' . $this->getInput('filter') . '/';
- if($this->getInput('case_insensitive')) {
- $regex .= 'i';
- }
+ // Retrieve fields to check
+ $filter_fields = [];
+ if ($this->getInput('target_title')) {
+ $filter_fields[] = $item['title'];
+ }
+ if ($this->getInput('target_content')) {
+ $filter_fields[] = $item['content'];
+ }
+ if ($this->getInput('target_author')) {
+ $filter_fields[] = $item['author'];
+ }
- // Retrieve fields to check
- $filter_fields = array();
- if($this->getInput('target_title')) {
- $filter_fields[] = $item['title'];
- }
- if($this->getInput('target_content')) {
- $filter_fields[] = $item['content'];
- }
- if($this->getInput('target_author')) {
- $filter_fields[] = $item['author'];
- }
+ // Apply filter on item
+ $keep_item = false;
+ $length_limit = intval($this->getInput('length_limit'));
+ foreach ($filter_fields as $field) {
+ if ($length_limit > 0) {
+ $field = substr($field, 0, $length_limit);
+ }
+ $keep_item |= boolval(preg_match($regex, $field));
+ if ($this->getInput('fix_encoding')) {
+ $keep_item |= boolval(preg_match($regex, utf8_decode($field)));
+ $keep_item |= boolval(preg_match($regex, utf8_encode($field)));
+ }
+ }
- // Apply filter on item
- $keep_item = false;
- $length_limit = intval($this->getInput('length_limit'));
- foreach($filter_fields as $field) {
- if($length_limit > 0) {
- $field = substr($field, 0, $length_limit);
- }
- $keep_item |= boolval(preg_match($regex, $field));
- if($this->getInput('fix_encoding')) {
- $keep_item |= boolval(preg_match($regex, utf8_decode($field)));
- $keep_item |= boolval(preg_match($regex, utf8_encode($field)));
- }
- }
+ // Reverse result? (keep everything but matching items)
+ if ($this->getInput('filter_type') === 'block') {
+ $keep_item = !$keep_item;
+ }
- // Reverse result? (keep everything but matching items)
- if($this->getInput('filter_type') === 'block') {
- $keep_item = !$keep_item;
- }
+ return $keep_item ? $item : null;
+ }
- return $keep_item ? $item : null;
- }
+ public function getURI()
+ {
+ $url = $this->getInput('url');
- public function getURI(){
- $url = $this->getInput('url');
+ if (empty($url)) {
+ $url = parent::getURI();
+ }
- if(empty($url)) {
- $url = parent::getURI();
- }
+ return $url;
+ }
- return $url;
- }
-
- public function collectData(){
- if($this->getInput('url') && substr($this->getInput('url'), 0, 4) !== 'http') {
- // just in case someone finds a way to access local files by playing with the url
- returnClientError('The url parameter must either refer to http or https protocol.');
- }
- $this->collectExpandableDatas($this->getURI());
- }
+ public function collectData()
+ {
+ if ($this->getInput('url') && substr($this->getInput('url'), 0, 4) !== 'http') {
+ // just in case someone finds a way to access local files by playing with the url
+ returnClientError('The url parameter must either refer to http or https protocol.');
+ }
+ $this->collectExpandableDatas($this->getURI());
+ }
}
diff --git a/bridges/FindACrewBridge.php b/bridges/FindACrewBridge.php
index 8282ead1..9119535b 100644
--- a/bridges/FindACrewBridge.php
+++ b/bridges/FindACrewBridge.php
@@ -1,89 +1,93 @@
<?php
-class FindACrewBridge extends BridgeAbstract {
- const MAINTAINER = 'couraudt';
- const NAME = 'Find A Crew Bridge';
- const URI = 'https://www.findacrew.net';
- const DESCRIPTION = 'Returns the newest sailing offers.';
- const PARAMETERS = array(
- array(
- 'type' => array(
- 'name' => 'Type of search',
- 'title' => 'Choose between finding a boat or a crew',
- 'type' => 'list',
- 'values' => array(
- 'Find a boat' => 'boat',
- 'Find a crew' => 'crew'
- )
- ),
- 'long' => array(
- 'name' => 'Longitude of the searched location',
- 'title' => 'Center the search at that longitude (e.g: -42.02)'
- ),
- 'lat' => array(
- 'name' => 'Latitude of the searched location',
- 'title' => 'Center the search at that latitude (e.g: 12.42)'
- ),
- 'distance' => array(
- 'name' => 'Limit boundary of search in KM',
- 'title' => 'Boundary of the search in kilometers when using longitude and latitude'
- ),
- 'limit' => self::LIMIT,
- )
- );
- public function collectData() {
- $url = $this->getURI();
+class FindACrewBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'couraudt';
+ const NAME = 'Find A Crew Bridge';
+ const URI = 'https://www.findacrew.net';
+ const DESCRIPTION = 'Returns the newest sailing offers.';
+ const PARAMETERS = [
+ [
+ 'type' => [
+ 'name' => 'Type of search',
+ 'title' => 'Choose between finding a boat or a crew',
+ 'type' => 'list',
+ 'values' => [
+ 'Find a boat' => 'boat',
+ 'Find a crew' => 'crew'
+ ]
+ ],
+ 'long' => [
+ 'name' => 'Longitude of the searched location',
+ 'title' => 'Center the search at that longitude (e.g: -42.02)'
+ ],
+ 'lat' => [
+ 'name' => 'Latitude of the searched location',
+ 'title' => 'Center the search at that latitude (e.g: 12.42)'
+ ],
+ 'distance' => [
+ 'name' => 'Limit boundary of search in KM',
+ 'title' => 'Boundary of the search in kilometers when using longitude and latitude'
+ ],
+ 'limit' => self::LIMIT,
+ ]
+ ];
- if ($this->getInput('type') == 'boat') {
- $data = array('SrhLstBtAction' => 'Create');
- } else {
- $data = array('SrhLstCwAction' => 'Create');
- }
+ public function collectData()
+ {
+ $url = $this->getURI();
- if ($this->getInput('long') && $this->getInput('lat')) {
- $data['real_LocSrh_Lng'] = $this->getInput('long');
- $data['real_LocSrh_Lat'] = $this->getInput('lat');
- if ($this->getInput('distance')) {
- $data['LocDis'] = (int)$this->getInput('distance') * 1000;
- }
- }
+ if ($this->getInput('type') == 'boat') {
+ $data = ['SrhLstBtAction' => 'Create'];
+ } else {
+ $data = ['SrhLstCwAction' => 'Create'];
+ }
- $header = array(
- 'Content-Type: application/x-www-form-urlencoded'
- );
+ if ($this->getInput('long') && $this->getInput('lat')) {
+ $data['real_LocSrh_Lng'] = $this->getInput('long');
+ $data['real_LocSrh_Lat'] = $this->getInput('lat');
+ if ($this->getInput('distance')) {
+ $data['LocDis'] = (int)$this->getInput('distance') * 1000;
+ }
+ }
- $opts = array(
- CURLOPT_CUSTOMREQUEST => 'POST',
- CURLOPT_POSTFIELDS => http_build_query($data) . "\n"
- );
+ $header = [
+ 'Content-Type: application/x-www-form-urlencoded'
+ ];
- $html = getSimpleHTMLDOM($url, $header, $opts) or returnClientError('No results for this query.');
+ $opts = [
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_POSTFIELDS => http_build_query($data) . "\n"
+ ];
- $annonces = $html->find('.css_SrhRst');
- $limit = $this->getInput('limit') ?? 10;
- foreach (array_slice($annonces, 0, $limit) as $annonce) {
- $item = array();
+ $html = getSimpleHTMLDOM($url, $header, $opts) or returnClientError('No results for this query.');
- $link = parent::getURI() . $annonce->find('.lstsum-btn-con a', 0)->href;
- $htmlDetail = getSimpleHTMLDOMCached($link . '?mdl=2'); // add ?mdl=2 for xhr content not full html page
+ $annonces = $html->find('.css_SrhRst');
+ $limit = $this->getInput('limit') ?? 10;
+ foreach (array_slice($annonces, 0, $limit) as $annonce) {
+ $item = [];
- $img = parent::getURI() . $htmlDetail->find('img.img-responsive', 0)->getAttribute('src');
- $item['title'] = $htmlDetail->find('div.label-account', 0)->plaintext;
- $item['uri'] = $link;
- $content = $htmlDetail->find('.panel-body div.clearfix.row > div', 1)->innertext;
- $content .= $htmlDetail->find('.panel-body > div', 1)->innertext;
- $content = defaultLinkTo($content, parent::getURI());
- $item['content'] = $content;
- $item['enclosures'] = array($img);
- $item['categories'] = array($annonce->find('.css_AccLocCur', 0)->plaintext);
- $this->items[] = $item;
- }
- }
+ $link = parent::getURI() . $annonce->find('.lstsum-btn-con a', 0)->href;
+ $htmlDetail = getSimpleHTMLDOMCached($link . '?mdl=2'); // add ?mdl=2 for xhr content not full html page
- public function getURI() {
- $uri = parent::getURI();
- // Those params must be in the URL
- $uri .= '/en/' . $this->getInput('type') . '/search?srhtyp=srhrst&mdl=2';
- return $uri;
- }
+ $img = parent::getURI() . $htmlDetail->find('img.img-responsive', 0)->getAttribute('src');
+ $item['title'] = $htmlDetail->find('div.label-account', 0)->plaintext;
+ $item['uri'] = $link;
+ $content = $htmlDetail->find('.panel-body div.clearfix.row > div', 1)->innertext;
+ $content .= $htmlDetail->find('.panel-body > div', 1)->innertext;
+ $content = defaultLinkTo($content, parent::getURI());
+ $item['content'] = $content;
+ $item['enclosures'] = [$img];
+ $item['categories'] = [$annonce->find('.css_AccLocCur', 0)->plaintext];
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ $uri = parent::getURI();
+ // Those params must be in the URL
+ $uri .= '/en/' . $this->getInput('type') . '/search?srhtyp=srhrst&mdl=2';
+ return $uri;
+ }
}
diff --git a/bridges/FirefoxAddonsBridge.php b/bridges/FirefoxAddonsBridge.php
index ca237f77..f85c3ea4 100644
--- a/bridges/FirefoxAddonsBridge.php
+++ b/bridges/FirefoxAddonsBridge.php
@@ -1,75 +1,78 @@
<?php
-class FirefoxAddonsBridge extends BridgeAbstract {
- const NAME = 'Firefox Add-ons Bridge';
- const URI = 'https://addons.mozilla.org/';
- const DESCRIPTION = 'Returns version history for a Firefox Add-on.';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(array(
- 'id' => array(
- 'name' => 'Add-on ID',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'save-to-the-wayback-machine',
- )
- )
- );
- const CACHE_TIMEOUT = 3600;
-
- private $feedName = '';
- private $releaseDateRegex = '/Released ([\w, ]+) - ([\w. ]+)/';
- private $xpiFileRegex = '/([A-Za-z0-9_.-]+)\.xpi$/';
- private $outgoingRegex = '/https:\/\/outgoing.prod.mozaws.net\/v1\/(?:[A-z0-9]+)\//';
-
- private $urlRegex = '/addons\.mozilla\.org\/(?:[\w-]+\/)?firefox\/addon\/([\w-]+)/';
-
- public function detectParameters($url) {
- $params = array();
-
- if(preg_match($this->urlRegex, $url, $matches)) {
- $params['id'] = $matches[1];
- return $params;
- }
-
- return null;
- }
-
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI());
-
- $this->feedName = $html->find('h1[class="AddonTitle"] > a', 0)->innertext;
- $author = $html->find('span.AddonTitle-author > a', 0)->plaintext;
-
- foreach ($html->find('li.AddonVersionCard') as $li) {
- $item = array();
-
- $item['title'] = $li->find('h2.AddonVersionCard-version', 0)->plaintext;
- $item['uid'] = $item['title'];
- $item['uri'] = $this->getURI();
- $item['author'] = $author;
-
- if (preg_match($this->releaseDateRegex, $li->find('div.AddonVersionCard-fileInfo', 0)->plaintext, $match)) {
- $item['timestamp'] = $match[1];
- $size = $match[2];
- }
-
- $compatibility = $li->find('div.AddonVersionCard-compatibility', 0)->plaintext;
- $license = $li->find('p.AddonVersionCard-license', 0)->innertext;
-
- if ($li->find('a.InstallButtonWrapper-download-link', 0)) {
- $downloadlink = $li->find('a.InstallButtonWrapper-download-link', 0)->href;
-
- } elseif ($li->find('a.Button.Button--action.AMInstallButton-button.Button--puffy', 0)) {
- $downloadlink = $li->find('a.Button.Button--action.AMInstallButton-button.Button--puffy', 0)->href;
- }
-
- $releaseNotes = $this->removeOutgoinglink($li->find('div.AddonVersionCard-releaseNotes', 0));
-
- if (preg_match($this->xpiFileRegex, $downloadlink, $match)) {
- $xpiFilename = $match[0];
- }
-
- $item['content'] = <<<EOD
+class FirefoxAddonsBridge extends BridgeAbstract
+{
+ const NAME = 'Firefox Add-ons Bridge';
+ const URI = 'https://addons.mozilla.org/';
+ const DESCRIPTION = 'Returns version history for a Firefox Add-on.';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [[
+ 'id' => [
+ 'name' => 'Add-on ID',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'save-to-the-wayback-machine',
+ ]
+ ]
+ ];
+
+ const CACHE_TIMEOUT = 3600;
+
+ private $feedName = '';
+ private $releaseDateRegex = '/Released ([\w, ]+) - ([\w. ]+)/';
+ private $xpiFileRegex = '/([A-Za-z0-9_.-]+)\.xpi$/';
+ private $outgoingRegex = '/https:\/\/outgoing.prod.mozaws.net\/v1\/(?:[A-z0-9]+)\//';
+
+ private $urlRegex = '/addons\.mozilla\.org\/(?:[\w-]+\/)?firefox\/addon\/([\w-]+)/';
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ if (preg_match($this->urlRegex, $url, $matches)) {
+ $params['id'] = $matches[1];
+ return $params;
+ }
+
+ return null;
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $this->feedName = $html->find('h1[class="AddonTitle"] > a', 0)->innertext;
+ $author = $html->find('span.AddonTitle-author > a', 0)->plaintext;
+
+ foreach ($html->find('li.AddonVersionCard') as $li) {
+ $item = [];
+
+ $item['title'] = $li->find('h2.AddonVersionCard-version', 0)->plaintext;
+ $item['uid'] = $item['title'];
+ $item['uri'] = $this->getURI();
+ $item['author'] = $author;
+
+ if (preg_match($this->releaseDateRegex, $li->find('div.AddonVersionCard-fileInfo', 0)->plaintext, $match)) {
+ $item['timestamp'] = $match[1];
+ $size = $match[2];
+ }
+
+ $compatibility = $li->find('div.AddonVersionCard-compatibility', 0)->plaintext;
+ $license = $li->find('p.AddonVersionCard-license', 0)->innertext;
+
+ if ($li->find('a.InstallButtonWrapper-download-link', 0)) {
+ $downloadlink = $li->find('a.InstallButtonWrapper-download-link', 0)->href;
+ } elseif ($li->find('a.Button.Button--action.AMInstallButton-button.Button--puffy', 0)) {
+ $downloadlink = $li->find('a.Button.Button--action.AMInstallButton-button.Button--puffy', 0)->href;
+ }
+
+ $releaseNotes = $this->removeOutgoinglink($li->find('div.AddonVersionCard-releaseNotes', 0));
+
+ if (preg_match($this->xpiFileRegex, $downloadlink, $match)) {
+ $xpiFilename = $match[0];
+ }
+
+ $item['content'] = <<<EOD
<strong>Release Notes</strong>
<p>{$releaseNotes}</p>
<strong>Compatibility</strong>
@@ -80,31 +83,34 @@ class FirefoxAddonsBridge extends BridgeAbstract {
<p><a href="{$downloadlink}">{$xpiFilename}</a> ($size)</p>
EOD;
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
- public function getURI() {
- if (!is_null($this->getInput('id'))) {
- return self::URI . 'en-US/firefox/addon/' . $this->getInput('id') . '/versions/';
- }
+ public function getURI()
+ {
+ if (!is_null($this->getInput('id'))) {
+ return self::URI . 'en-US/firefox/addon/' . $this->getInput('id') . '/versions/';
+ }
- return parent::getURI();
- }
+ return parent::getURI();
+ }
- public function getName() {
- if (!empty($this->feedName)) {
- return $this->feedName . ' - Firefox Add-on';
- }
+ public function getName()
+ {
+ if (!empty($this->feedName)) {
+ return $this->feedName . ' - Firefox Add-on';
+ }
- return parent::getName();
- }
+ return parent::getName();
+ }
- private function removeOutgoinglink($html) {
- foreach ($html->find('a') as $a) {
- $a->href = urldecode(preg_replace($this->outgoingRegex, '', $a->href));
- }
+ private function removeOutgoinglink($html)
+ {
+ foreach ($html->find('a') as $a) {
+ $a->href = urldecode(preg_replace($this->outgoingRegex, '', $a->href));
+ }
- return $html->innertext;
- }
+ return $html->innertext;
+ }
}
diff --git a/bridges/FirstLookMediaTechBridge.php b/bridges/FirstLookMediaTechBridge.php
index 67d0ead1..f9963c6f 100644
--- a/bridges/FirstLookMediaTechBridge.php
+++ b/bridges/FirstLookMediaTechBridge.php
@@ -1,49 +1,52 @@
<?php
-class FirstLookMediaTechBridge extends BridgeAbstract {
- const NAME = 'First Look Media - Technology';
- const URI = 'https://tech.firstlook.media';
- const DESCRIPTION = 'First Look Media Technology page';
- const MAINTAINER = 'somini';
- const PARAMETERS = array(
- array(
- 'projects' => array(
- 'type' => 'checkbox',
- 'name' => 'Include Projects?',
- )
- )
- );
-
- public function collectData() {
- $html = getSimpleHTMLDOM(self::URI);
-
- if ($this->getInput('projects')) {
- $top_projects = $html->find('.PromoList-ul', 0);
- foreach($top_projects->find('li.PromoList-item') as $element) {
- $item = array();
-
- $item_uri = $element->find('a', 0);
- $item['uri'] = $item_uri->href;
- $item['title'] = strip_tags($item_uri->innertext);
- $item['content'] = $element->find('div > div', 0);
-
- $this->items[] = $item;
- }
- }
-
- $top_articles = $html->find('.PromoList-ul', 1);
- foreach($top_articles->find('li.PromoList-item') as $element) {
- $item = array();
-
- $item_left = $element->find('div > div', 0);
- $item_date = $element->find('.PromoList-date', 0);
- $item['timestamp'] = strtotime($item_date->innertext);
- $item_date->outertext = ''; /* Remove */
- $item['author'] = $item_left->innertext;
- $item_uri = $element->find('a', 0);
- $item['uri'] = self::URI . $item_uri->href;
- $item['title'] = strip_tags($item_uri);
-
- $this->items[] = $item;
- }
- }
+
+class FirstLookMediaTechBridge extends BridgeAbstract
+{
+ const NAME = 'First Look Media - Technology';
+ const URI = 'https://tech.firstlook.media';
+ const DESCRIPTION = 'First Look Media Technology page';
+ const MAINTAINER = 'somini';
+ const PARAMETERS = [
+ [
+ 'projects' => [
+ 'type' => 'checkbox',
+ 'name' => 'Include Projects?',
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
+
+ if ($this->getInput('projects')) {
+ $top_projects = $html->find('.PromoList-ul', 0);
+ foreach ($top_projects->find('li.PromoList-item') as $element) {
+ $item = [];
+
+ $item_uri = $element->find('a', 0);
+ $item['uri'] = $item_uri->href;
+ $item['title'] = strip_tags($item_uri->innertext);
+ $item['content'] = $element->find('div > div', 0);
+
+ $this->items[] = $item;
+ }
+ }
+
+ $top_articles = $html->find('.PromoList-ul', 1);
+ foreach ($top_articles->find('li.PromoList-item') as $element) {
+ $item = [];
+
+ $item_left = $element->find('div > div', 0);
+ $item_date = $element->find('.PromoList-date', 0);
+ $item['timestamp'] = strtotime($item_date->innertext);
+ $item_date->outertext = ''; /* Remove */
+ $item['author'] = $item_left->innertext;
+ $item_uri = $element->find('a', 0);
+ $item['uri'] = self::URI . $item_uri->href;
+ $item['title'] = strip_tags($item_uri);
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/FlashbackBridge.php b/bridges/FlashbackBridge.php
index 3d64f852..f3bff5ff 100644
--- a/bridges/FlashbackBridge.php
+++ b/bridges/FlashbackBridge.php
@@ -2,184 +2,190 @@
class FlashbackBridge extends BridgeAbstract
{
- const MAINTAINER = 'fatuus';
- const NAME = 'Flashback forum';
- const URI = 'https://www.flashback.org';
- const DESCRIPTION = 'Returns post from forum';
- const CACHE_TIMEOUT = 10800; // 3h
+ const MAINTAINER = 'fatuus';
+ const NAME = 'Flashback forum';
+ const URI = 'https://www.flashback.org';
+ const DESCRIPTION = 'Returns post from forum';
+ const CACHE_TIMEOUT = 10800; // 3h
- const PARAMETERS = array(
- 'Category' => array(
- 'c' => array(
- 'name' => 'Category number',
- 'type' => 'number',
- 'exampleValue' => '249',
- 'required' => true
- )
- ),
- 'Tag' => array(
- 'a' => array(
- 'name' => 'Tag',
- 'type' => 'text',
- 'exampleValue' => 'stockholm',
- 'required' => true
- )
- ),
- 'Thread' => array(
- 't' => array(
- 'name' => 'Thread number',
- 'type' => 'number',
- 'exampleValue' => '1420554',
- 'required' => true
- )
- ),
- /*'User' => array(
- 'u' => array(
- 'name' => 'User number',
- 'type' => 'text',
- 'exampleValue' => 'not working, need login',
- 'required' => true
- )
- ),*/
- 'Search string' => array(
- 's' => array(
- 'name' => 'Words',
- 'type' => 'text',
- 'exampleValue' => 'sök',
- 'required' => true
- ),
- 'type' => array(
- 'name' => 'Type of search',
- 'type' => 'list',
- 'defaultValue' => 'Posts',
- 'values' => array(
- 'Posts' => 'posts',
- 'Subjects' => 'subjects'
- )
- )
- )
- );
+ const PARAMETERS = [
+ 'Category' => [
+ 'c' => [
+ 'name' => 'Category number',
+ 'type' => 'number',
+ 'exampleValue' => '249',
+ 'required' => true
+ ]
+ ],
+ 'Tag' => [
+ 'a' => [
+ 'name' => 'Tag',
+ 'type' => 'text',
+ 'exampleValue' => 'stockholm',
+ 'required' => true
+ ]
+ ],
+ 'Thread' => [
+ 't' => [
+ 'name' => 'Thread number',
+ 'type' => 'number',
+ 'exampleValue' => '1420554',
+ 'required' => true
+ ]
+ ],
+ /*'User' => array(
+ 'u' => array(
+ 'name' => 'User number',
+ 'type' => 'text',
+ 'exampleValue' => 'not working, need login',
+ 'required' => true
+ )
+ ),*/
+ 'Search string' => [
+ 's' => [
+ 'name' => 'Words',
+ 'type' => 'text',
+ 'exampleValue' => 'sök',
+ 'required' => true
+ ],
+ 'type' => [
+ 'name' => 'Type of search',
+ 'type' => 'list',
+ 'defaultValue' => 'Posts',
+ 'values' => [
+ 'Posts' => 'posts',
+ 'Subjects' => 'subjects'
+ ]
+ ]
+ ]
+ ];
- public function getName()
- {
- if ($this->getInput('c')) {
- $category = $this->getInput('c');
- return 'Category ' . $category . ' - Flashback';
- } elseif ($this->getInput('a')) {
- $tag = $this->getInput('a');
- return 'Tag: ' . $tag . ' - Flashback';
- } elseif ($this->getInput('t')) {
- $thread = $this->getInput('t');
- return 'Thread ' . $thread . ' - Flashback';
- } elseif ($this->getInput('u')) {
- $user = $this->getInput('u');
- return 'User ' . $user . ' - Flashback';
- } elseif ($this->getInput('s')) {
- $search = $this->getInput('s');
- return 'Search: ' . $search . ' - Flashback';
- }
+ public function getName()
+ {
+ if ($this->getInput('c')) {
+ $category = $this->getInput('c');
+ return 'Category ' . $category . ' - Flashback';
+ } elseif ($this->getInput('a')) {
+ $tag = $this->getInput('a');
+ return 'Tag: ' . $tag . ' - Flashback';
+ } elseif ($this->getInput('t')) {
+ $thread = $this->getInput('t');
+ return 'Thread ' . $thread . ' - Flashback';
+ } elseif ($this->getInput('u')) {
+ $user = $this->getInput('u');
+ return 'User ' . $user . ' - Flashback';
+ } elseif ($this->getInput('s')) {
+ $search = $this->getInput('s');
+ return 'Search: ' . $search . ' - Flashback';
+ }
- return self::NAME;
- }
+ return self::NAME;
+ }
- public function collectData()
- {
- if ($this->getInput('c')) {
- $page = self::URI . '/f' . $this->getInput('c');
- } elseif ($this->getInput('a')) {
- $page = self::URI . '/find_threads_by_tag.php?tag=' . $this->getInput('a');
- } elseif ($this->getInput('t')) {
- $page = self::URI . '/t' . $this->getInput('t');
- $page = $page . 's'; // last-page
- } elseif ($this->getInput('u')) {
- $page = self::URI . '/find_posts_by_user.php?userid=' . $this->getInput('u');
- } elseif ($this->getInput('s')) {
- if ($this->getInput('type') == 'posts') {
- $page = self::URI . '/sok/?query=' . $this->getInput('s') . '&search_post=1&sp=1&so=pd';
- } else {
- $page = self::URI . '/sok/?query=' . $this->getInput('s') . '&search_post=0&sp=1&so=pd';
- }
- }
+ public function collectData()
+ {
+ if ($this->getInput('c')) {
+ $page = self::URI . '/f' . $this->getInput('c');
+ } elseif ($this->getInput('a')) {
+ $page = self::URI . '/find_threads_by_tag.php?tag=' . $this->getInput('a');
+ } elseif ($this->getInput('t')) {
+ $page = self::URI . '/t' . $this->getInput('t');
+ $page = $page . 's'; // last-page
+ } elseif ($this->getInput('u')) {
+ $page = self::URI . '/find_posts_by_user.php?userid=' . $this->getInput('u');
+ } elseif ($this->getInput('s')) {
+ if ($this->getInput('type') == 'posts') {
+ $page = self::URI . '/sok/?query=' . $this->getInput('s') . '&search_post=1&sp=1&so=pd';
+ } else {
+ $page = self::URI . '/sok/?query=' . $this->getInput('s') . '&search_post=0&sp=1&so=pd';
+ }
+ }
- $html = getSimpleHTMLDOM($page);
+ $html = getSimpleHTMLDOM($page);
- if ($this->getInput('c') || $this->getInput('a')) {
- $category = $this->getInput('c');
- $array = $html->find('table#threadslist tbody tr');
- foreach ($array as $key => $element) {
- $item = array();
- $item['uri'] = self::URI . $element->find('td.td_title a', 0)->href;
- $item['title'] = trim(utf8_encode($element->find('td.td_title a', 0)->innertext));
- $item['author'] = trim(utf8_encode(
- $element->find('td.td_title span.thread-poster span', 0)->innertext)
- );
- $timestamp = $element->find('td.td_last_post div', 0);
- if (isset($timestamp->plaintext)) {
- $item['timestamp'] = strtotime(str_replace(array('Ig&aring;r', 'Idag'),
- array('yesterday', 'today'), trim($timestamp->plaintext)));
- }
- $item['content'] = $item['title'] . '<br />' . trim(preg_replace('/\t+/', '',
- $element->find('td.td_replies', 0)->innertext));
- $item['uid'] = preg_split('/(\/)/', $element->find('td.td_title a', 0)->href)[1];
- $this->items[] = $item;
- }
- } elseif ($this->getInput('t')) {
- $tags = $html->find('div.hidden-xs a.tag');
- $array = $html->find('div.post');
+ if ($this->getInput('c') || $this->getInput('a')) {
+ $category = $this->getInput('c');
+ $array = $html->find('table#threadslist tbody tr');
+ foreach ($array as $key => $element) {
+ $item = [];
+ $item['uri'] = self::URI . $element->find('td.td_title a', 0)->href;
+ $item['title'] = trim(utf8_encode($element->find('td.td_title a', 0)->innertext));
+ $item['author'] = trim(utf8_encode(
+ $element->find('td.td_title span.thread-poster span', 0)->innertext
+ ));
+ $timestamp = $element->find('td.td_last_post div', 0);
+ if (isset($timestamp->plaintext)) {
+ $item['timestamp'] = strtotime(str_replace(
+ ['Ig&aring;r', 'Idag'],
+ ['yesterday', 'today'],
+ trim($timestamp->plaintext)
+ ));
+ }
+ $item['content'] = $item['title'] . '<br />' . trim(preg_replace(
+ '/\t+/',
+ '',
+ $element->find('td.td_replies', 0)->innertext
+ ));
+ $item['uid'] = preg_split('/(\/)/', $element->find('td.td_title a', 0)->href)[1];
+ $this->items[] = $item;
+ }
+ } elseif ($this->getInput('t')) {
+ $tags = $html->find('div.hidden-xs a.tag');
+ $array = $html->find('div.post');
- foreach ($array as $key => $element) {
- $item = array();
- $item['uri_post'] = self::URI . $element->find('div.post-heading a', 2)->href;
- $item['uri'] = self::URI . '/' . preg_split('/(\/s)/', $item['uri_post'])[1] . '#' .
- preg_split('/(\/s)/', $item['uri_post'])[1];
- $item['uri_thread'] = $page;
- $item['author'] = utf8_encode($element->find('div.post-user ul li', 0)->innertext);
- $item['author_link'] = self::URI . $element->find('div.post-user ul li a', 0)->href;
- $item['post_nr'] = $element->find('div.post-heading a strong', 0)->innertext;
- $item['timestamp'] = strtotime(
- str_replace(
- array('Ig&aring;r', 'Idag'), array('yesterday', 'today'),
- current(explode("\t", str_replace("\t\t", "\t", trim(
- $element->find('div.post-heading', 0)->plaintext)
- )))
- )
- );
- if ($element->find('div.smallfont strong', 0)) {
- $item['title'] = trim(utf8_encode($element->find('div.smallfont strong', 0)->innertext));
- }
- if (empty($item['title'])) {
- $item['title'] = date('D j M y H:i', $item['timestamp']);
- }
- $item['content'] = trim(preg_replace('/\t+/', '', $element->find('div.post_message', 0)));
- $item['uid'] = preg_split('/(\#|\/)/', $element->find('div.post-heading a', 2)->href)[1];
- foreach ($tags as $tag_key => $tag) {
- $item['categories'][] = trim(utf8_encode($tag->innertext));
- }
- $this->items[] = $item;
- }
- // } elseif ( $this->getInput('u') ) {
+ foreach ($array as $key => $element) {
+ $item = [];
+ $item['uri_post'] = self::URI . $element->find('div.post-heading a', 2)->href;
+ $item['uri'] = self::URI . '/' . preg_split('/(\/s)/', $item['uri_post'])[1] . '#' .
+ preg_split('/(\/s)/', $item['uri_post'])[1];
+ $item['uri_thread'] = $page;
+ $item['author'] = utf8_encode($element->find('div.post-user ul li', 0)->innertext);
+ $item['author_link'] = self::URI . $element->find('div.post-user ul li a', 0)->href;
+ $item['post_nr'] = $element->find('div.post-heading a strong', 0)->innertext;
+ $item['timestamp'] = strtotime(
+ str_replace(
+ ['Ig&aring;r', 'Idag'],
+ ['yesterday', 'today'],
+ current(explode("\t", str_replace("\t\t", "\t", trim(
+ $element->find('div.post-heading', 0)->plaintext
+ ))))
+ )
+ );
+ if ($element->find('div.smallfont strong', 0)) {
+ $item['title'] = trim(utf8_encode($element->find('div.smallfont strong', 0)->innertext));
+ }
+ if (empty($item['title'])) {
+ $item['title'] = date('D j M y H:i', $item['timestamp']);
+ }
+ $item['content'] = trim(preg_replace('/\t+/', '', $element->find('div.post_message', 0)));
+ $item['uid'] = preg_split('/(\#|\/)/', $element->find('div.post-heading a', 2)->href)[1];
+ foreach ($tags as $tag_key => $tag) {
+ $item['categories'][] = trim(utf8_encode($tag->innertext));
+ }
+ $this->items[] = $item;
+ }
+ // } elseif ( $this->getInput('u') ) {
+ } elseif ($this->getInput('s')) {
+ $array = $html->find('div.post');
+ foreach ($array as $key => $element) {
+ $item = [];
+ $item['uri'] = self::URI . $element->find('div.post-body a', 0)->href;
+ $item['uri_thread'] = $page . $element->find('div.post-heading a', 0)->href . 's';
+ $item['author'] = $element->find('div.post-body a', 1)->innertext;
+ $item['author_link'] = self::URI . $element->find('div.post-body a', 1)->href;
+ $time = preg_split('/(\>)/', $element->find('div.post-heading', 0)->innertext);
+ $item['timestamp'] = strtotime(trim(end($time)));
+ $item['title'] = trim(utf8_encode($element->find('div.post-body strong', 0)->innertext));
+ if (empty($item['title'])) {
+ $item['title'] = date('D j M y H:i', $item['timestamp']);
+ }
- } elseif ($this->getInput('s')) {
- $array = $html->find('div.post');
- foreach ($array as $key => $element) {
- $item = array();
- $item['uri'] = self::URI . $element->find('div.post-body a', 0)->href;
- $item['uri_thread'] = $page . $element->find('div.post-heading a', 0)->href . 's';
- $item['author'] = $element->find('div.post-body a', 1)->innertext;
- $item['author_link'] = self::URI . $element->find('div.post-body a', 1)->href;
- $time = preg_split('/(\>)/', $element->find('div.post-heading', 0)->innertext);
- $item['timestamp'] = strtotime(trim(end($time)));
- $item['title'] = trim(utf8_encode($element->find('div.post-body strong', 0)->innertext));
- if (empty($item['title'])) {
- $item['title'] = date('D j M y H:i', $item['timestamp']);
- }
-
- $item['datetime'] = (trim(end($time)));
- $item['categories'][] = trim(utf8_encode($element->find('div.post-heading a', 0)->innertext));
- $item['content'] = trim(preg_replace('/\t+/', '', $element->find('div.post_message', 0)));
- $item['uid'] = preg_split('/(\#|\/)/', $element->find('div.post-body a', 0)->href)[1];
- $this->items[] = $item;
- }
- }
- }
+ $item['datetime'] = (trim(end($time)));
+ $item['categories'][] = trim(utf8_encode($element->find('div.post-heading a', 0)->innertext));
+ $item['content'] = trim(preg_replace('/\t+/', '', $element->find('div.post_message', 0)));
+ $item['uid'] = preg_split('/(\#|\/)/', $element->find('div.post-body a', 0)->href)[1];
+ $this->items[] = $item;
+ }
+ }
+ }
}
diff --git a/bridges/FlickrBridge.php b/bridges/FlickrBridge.php
index 39351ea4..99cd811d 100644
--- a/bridges/FlickrBridge.php
+++ b/bridges/FlickrBridge.php
@@ -3,290 +3,276 @@
/* This is a mashup of FlickrExploreBridge by sebsauvage and FlickrTagBridge
* by erwang, providing the functionality of both in one.
*/
-class FlickrBridge extends BridgeAbstract {
-
- const MAINTAINER = 'logmanoriginal';
- const NAME = 'Flickr Bridge';
- const URI = 'https://www.flickr.com/';
- const CACHE_TIMEOUT = 21600; // 6 hours
- const DESCRIPTION = 'Returns images from Flickr';
-
- const PARAMETERS = array(
- 'Explore' => array(),
- 'By keyword' => array(
- 'q' => array(
- 'name' => 'Keyword',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Insert keyword',
- 'exampleValue' => 'bird'
- ),
- 'media' => array(
- 'name' => 'Media',
- 'type' => 'list',
- 'values' => array(
- 'All (Photos & videos)' => 'all',
- 'Photos' => 'photos',
- 'Videos' => 'videos',
- ),
- 'defaultValue' => 'all',
- ),
- 'sort' => array(
- 'name' => 'Sort By',
- 'type' => 'list',
- 'values' => array(
- 'Relevance' => 'relevance',
- 'Date uploaded' => 'date-posted-desc',
- 'Date taken' => 'date-taken-desc',
- 'Interesting' => 'interestingness-desc',
- ),
- 'defaultValue' => 'relevance',
- )
- ),
- 'By username' => array(
- 'u' => array(
- 'name' => 'Username',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Insert username (as shown in the address bar)',
- 'exampleValue' => 'flickr'
- ),
- 'content' => array(
- 'name' => 'Content',
- 'type' => 'list',
- 'values' => array(
- 'Uploads' => 'uploads',
- 'Favorites' => 'faves',
- ),
- 'defaultValue' => 'uploads',
- ),
- 'media' => array(
- 'name' => 'Media',
- 'type' => 'list',
- 'values' => array(
- 'All (Photos & videos)' => 'all',
- 'Photos' => 'photos',
- 'Videos' => 'videos',
- ),
- 'defaultValue' => 'all',
- ),
- 'sort' => array(
- 'name' => 'Sort By',
- 'type' => 'list',
- 'values' => array(
- 'Relevance' => 'relevance',
- 'Date uploaded' => 'date-posted-desc',
- 'Date taken' => 'date-taken-desc',
- 'Interesting' => 'interestingness-desc',
- ),
- 'defaultValue' => 'date-posted-desc',
- )
- )
- );
-
- private $username = '';
-
- public function collectData() {
-
- switch($this->queriedContext) {
-
- case 'Explore':
- $filter = 'photo-lite-models';
- $html = getSimpleHTMLDOM($this->getURI());
- break;
-
- case 'By keyword':
- $filter = 'photo-lite-models';
- $html = getSimpleHTMLDOM($this->getURI());
- break;
-
- case 'By username':
- //$filter = 'photo-models';
- $filter = 'photo-lite-models';
- $html = getSimpleHTMLDOM($this->getURI());
-
- $this->username = $this->getInput('u');
-
- if ($html->find('span.search-pill-name', 0)) {
- $this->username = $html->find('span.search-pill-name', 0)->plaintext;
- }
- break;
-
- default:
- returnClientError('Invalid context: ' . $this->queriedContext);
-
- }
-
- $model_json = $this->extractJsonModel($html);
- $photo_models = $this->getPhotoModels($model_json, $filter);
-
- foreach($photo_models as $model) {
- $item = array();
-
- /* Author name depends on scope. On a keyword search the
- * author is part of the picture data. On a username search
- * the author is part of the owner data.
- */
- if(array_key_exists('username', $model)) {
- $item['author'] = urldecode($model['username']);
- } elseif (array_key_exists('owner', reset($model_json)[0])) {
- $item['author'] = urldecode(reset($model_json)[0]['owner']['username']);
- }
-
- $item['title'] = urldecode((array_key_exists('title', $model) ? $model['title'] : 'Untitled'));
- $item['uri'] = self::URI . 'photo.gne?id=' . $model['id'];
-
- $description = (array_key_exists('description', $model) ? $model['description'] : '');
-
- $item['content'] = '<a href="'
- . $item['uri']
- . '"><img src="'
- . $this->extractContentImage($model)
- . '" style="max-width: 640px; max-height: 480px;"/></a><br><p>'
- . urldecode($description)
- . '</p>';
-
- $item['enclosures'] = $this->extractEnclosures($model);
-
- $this->items[] = $item;
-
- }
-
- }
-
- public function getURI() {
-
- switch($this->queriedContext) {
- case 'Explore':
- return self::URI . 'explore';
- break;
- case 'By keyword':
- return self::URI . 'search/?q=' . urlencode($this->getInput('q'))
- . '&sort=' . $this->getInput('sort') . '&media=' . $this->getInput('media');
- break;
- case 'By username':
- $uri = self::URI . 'search/?user_id=' . urlencode($this->getInput('u'))
- . '&sort=date-posted-desc&media=' . $this->getInput('media');
-
- if ($this->getInput('content') === 'faves') {
- return $uri . '&faves=1';
- }
-
- return $uri;
- break;
-
- default:
- return parent::getURI();
- }
- }
-
- public function getName() {
-
- switch($this->queriedContext) {
- case 'Explore':
- return 'Explore - ' . self::NAME;
- break;
- case 'By keyword':
- return $this->getInput('q') . ' - keyword - ' . self::NAME;
- break;
- case 'By username':
-
- if ($this->getInput('content') === 'faves') {
- return $this->username . ' - favorites - ' . self::NAME;
- }
-
- return $this->username . ' - ' . self::NAME;
- break;
-
- default:
- return parent::getName();
- }
-
- return parent::getName();
- }
-
- private function extractJsonModel($html) {
-
- // Find SCRIPT containing JSON data
- $model = $html->find('.modelExport', 0);
- $model_text = $model->innertext;
-
- // Find start and end of JSON data
- $start = strpos($model_text, 'modelExport:') + strlen('modelExport:');
- $end = strpos($model_text, 'auth:') - strlen('auth:');
-
- // Extract JSON data, remove trailing comma
- $model_text = trim(substr($model_text, $start, $end - $start));
- $model_text = substr($model_text, 0, strlen($model_text) - 1);
-
- return json_decode($model_text, true);
-
- }
-
- private function getPhotoModels($json, $filter) {
-
- // The JSON model contains a "legend" array, where each element contains
- // the path to an element in the "main" object
- $photo_models = array();
-
- foreach($json['legend'] as $legend) {
-
- $photo_model = $json['main'];
-
- foreach($legend as $element) { // Traverse tree
- $photo_model = $photo_model[$element];
- }
-
- // We are only interested in content
- if($photo_model['_flickrModelRegistry'] === $filter) {
- $photo_models[] = $photo_model;
- }
-
- }
-
- return $photo_models;
-
- }
-
- private function extractEnclosures($model) {
-
- $areas = array();
-
- foreach($model['sizes'] as $size) {
- $areas[$size['width'] * $size['height']] = $size['url'];
- }
-
- return array($this->fixURL(max($areas)));
-
- }
-
- private function extractContentImage($model) {
-
- $areas = array();
- $limit = 320 * 240;
-
- foreach($model['sizes'] as $size) {
-
- $image_area = $size['width'] * $size['height'];
-
- if($image_area >= $limit) {
- $areas[$image_area] = $size['url'];
- }
-
- }
-
- return $this->fixURL(min($areas));
-
- }
-
- private function fixURL($url) {
-
- // For some reason the image URLs don't include the protocol (https)
- if(strpos($url, '//') === 0) {
- $url = 'https:' . $url;
- }
-
- return $url;
-
- }
+class FlickrBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Flickr Bridge';
+ const URI = 'https://www.flickr.com/';
+ const CACHE_TIMEOUT = 21600; // 6 hours
+ const DESCRIPTION = 'Returns images from Flickr';
+
+ const PARAMETERS = [
+ 'Explore' => [],
+ 'By keyword' => [
+ 'q' => [
+ 'name' => 'Keyword',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert keyword',
+ 'exampleValue' => 'bird'
+ ],
+ 'media' => [
+ 'name' => 'Media',
+ 'type' => 'list',
+ 'values' => [
+ 'All (Photos & videos)' => 'all',
+ 'Photos' => 'photos',
+ 'Videos' => 'videos',
+ ],
+ 'defaultValue' => 'all',
+ ],
+ 'sort' => [
+ 'name' => 'Sort By',
+ 'type' => 'list',
+ 'values' => [
+ 'Relevance' => 'relevance',
+ 'Date uploaded' => 'date-posted-desc',
+ 'Date taken' => 'date-taken-desc',
+ 'Interesting' => 'interestingness-desc',
+ ],
+ 'defaultValue' => 'relevance',
+ ]
+ ],
+ 'By username' => [
+ 'u' => [
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert username (as shown in the address bar)',
+ 'exampleValue' => 'flickr'
+ ],
+ 'content' => [
+ 'name' => 'Content',
+ 'type' => 'list',
+ 'values' => [
+ 'Uploads' => 'uploads',
+ 'Favorites' => 'faves',
+ ],
+ 'defaultValue' => 'uploads',
+ ],
+ 'media' => [
+ 'name' => 'Media',
+ 'type' => 'list',
+ 'values' => [
+ 'All (Photos & videos)' => 'all',
+ 'Photos' => 'photos',
+ 'Videos' => 'videos',
+ ],
+ 'defaultValue' => 'all',
+ ],
+ 'sort' => [
+ 'name' => 'Sort By',
+ 'type' => 'list',
+ 'values' => [
+ 'Relevance' => 'relevance',
+ 'Date uploaded' => 'date-posted-desc',
+ 'Date taken' => 'date-taken-desc',
+ 'Interesting' => 'interestingness-desc',
+ ],
+ 'defaultValue' => 'date-posted-desc',
+ ]
+ ]
+ ];
+
+ private $username = '';
+
+ public function collectData()
+ {
+ switch ($this->queriedContext) {
+ case 'Explore':
+ $filter = 'photo-lite-models';
+ $html = getSimpleHTMLDOM($this->getURI());
+ break;
+
+ case 'By keyword':
+ $filter = 'photo-lite-models';
+ $html = getSimpleHTMLDOM($this->getURI());
+ break;
+
+ case 'By username':
+ //$filter = 'photo-models';
+ $filter = 'photo-lite-models';
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $this->username = $this->getInput('u');
+
+ if ($html->find('span.search-pill-name', 0)) {
+ $this->username = $html->find('span.search-pill-name', 0)->plaintext;
+ }
+ break;
+
+ default:
+ returnClientError('Invalid context: ' . $this->queriedContext);
+ }
+
+ $model_json = $this->extractJsonModel($html);
+ $photo_models = $this->getPhotoModels($model_json, $filter);
+
+ foreach ($photo_models as $model) {
+ $item = [];
+
+ /* Author name depends on scope. On a keyword search the
+ * author is part of the picture data. On a username search
+ * the author is part of the owner data.
+ */
+ if (array_key_exists('username', $model)) {
+ $item['author'] = urldecode($model['username']);
+ } elseif (array_key_exists('owner', reset($model_json)[0])) {
+ $item['author'] = urldecode(reset($model_json)[0]['owner']['username']);
+ }
+
+ $item['title'] = urldecode((array_key_exists('title', $model) ? $model['title'] : 'Untitled'));
+ $item['uri'] = self::URI . 'photo.gne?id=' . $model['id'];
+
+ $description = (array_key_exists('description', $model) ? $model['description'] : '');
+
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $this->extractContentImage($model)
+ . '" style="max-width: 640px; max-height: 480px;"/></a><br><p>'
+ . urldecode($description)
+ . '</p>';
+
+ $item['enclosures'] = $this->extractEnclosures($model);
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Explore':
+ return self::URI . 'explore';
+ break;
+ case 'By keyword':
+ return self::URI . 'search/?q=' . urlencode($this->getInput('q'))
+ . '&sort=' . $this->getInput('sort') . '&media=' . $this->getInput('media');
+ break;
+ case 'By username':
+ $uri = self::URI . 'search/?user_id=' . urlencode($this->getInput('u'))
+ . '&sort=date-posted-desc&media=' . $this->getInput('media');
+
+ if ($this->getInput('content') === 'faves') {
+ return $uri . '&faves=1';
+ }
+
+ return $uri;
+ break;
+
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Explore':
+ return 'Explore - ' . self::NAME;
+ break;
+ case 'By keyword':
+ return $this->getInput('q') . ' - keyword - ' . self::NAME;
+ break;
+ case 'By username':
+ if ($this->getInput('content') === 'faves') {
+ return $this->username . ' - favorites - ' . self::NAME;
+ }
+
+ return $this->username . ' - ' . self::NAME;
+ break;
+
+ default:
+ return parent::getName();
+ }
+
+ return parent::getName();
+ }
+
+ private function extractJsonModel($html)
+ {
+ // Find SCRIPT containing JSON data
+ $model = $html->find('.modelExport', 0);
+ $model_text = $model->innertext;
+
+ // Find start and end of JSON data
+ $start = strpos($model_text, 'modelExport:') + strlen('modelExport:');
+ $end = strpos($model_text, 'auth:') - strlen('auth:');
+
+ // Extract JSON data, remove trailing comma
+ $model_text = trim(substr($model_text, $start, $end - $start));
+ $model_text = substr($model_text, 0, strlen($model_text) - 1);
+
+ return json_decode($model_text, true);
+ }
+
+ private function getPhotoModels($json, $filter)
+ {
+ // The JSON model contains a "legend" array, where each element contains
+ // the path to an element in the "main" object
+ $photo_models = [];
+
+ foreach ($json['legend'] as $legend) {
+ $photo_model = $json['main'];
+
+ foreach ($legend as $element) { // Traverse tree
+ $photo_model = $photo_model[$element];
+ }
+
+ // We are only interested in content
+ if ($photo_model['_flickrModelRegistry'] === $filter) {
+ $photo_models[] = $photo_model;
+ }
+ }
+
+ return $photo_models;
+ }
+
+ private function extractEnclosures($model)
+ {
+ $areas = [];
+
+ foreach ($model['sizes'] as $size) {
+ $areas[$size['width'] * $size['height']] = $size['url'];
+ }
+
+ return [$this->fixURL(max($areas))];
+ }
+
+ private function extractContentImage($model)
+ {
+ $areas = [];
+ $limit = 320 * 240;
+
+ foreach ($model['sizes'] as $size) {
+ $image_area = $size['width'] * $size['height'];
+
+ if ($image_area >= $limit) {
+ $areas[$image_area] = $size['url'];
+ }
+ }
+
+ return $this->fixURL(min($areas));
+ }
+
+ private function fixURL($url)
+ {
+ // For some reason the image URLs don't include the protocol (https)
+ if (strpos($url, '//') === 0) {
+ $url = 'https:' . $url;
+ }
+
+ return $url;
+ }
}
diff --git a/bridges/FolhaDeSaoPauloBridge.php b/bridges/FolhaDeSaoPauloBridge.php
index 6506fdba..d8d93c4f 100644
--- a/bridges/FolhaDeSaoPauloBridge.php
+++ b/bridges/FolhaDeSaoPauloBridge.php
@@ -1,69 +1,73 @@
<?php
-class FolhaDeSaoPauloBridge extends FeedExpander {
- const MAINTAINER = 'somini';
- const NAME = 'Folha de São Paulo';
- const URI = 'https://www1.folha.uol.com.br';
- const DESCRIPTION = 'Returns the newest posts from Folha de São Paulo (full text)';
- const PARAMETERS = array(
- array(
- 'feed' => array(
- 'name' => 'Feed sub-URL',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Select the sub-feed (see https://www1.folha.uol.com.br/feed/)',
- 'exampleValue' => 'emcimadahora/rss091.xml',
- ),
- 'amount' => array(
- 'name' => 'Amount of items to fetch',
- 'type' => 'number',
- 'defaultValue' => 15,
- ),
- 'deep_crawl' => array(
- 'name' => 'Deep Crawl',
- 'description' => 'Crawl each item "deeply", that is, return the article contents',
- 'type' => 'checkbox',
- 'defaultValue' => true,
- ),
- )
- );
- protected function parseItem($item){
- $item = parent::parseItem($item);
+class FolhaDeSaoPauloBridge extends FeedExpander
+{
+ const MAINTAINER = 'somini';
+ const NAME = 'Folha de São Paulo';
+ const URI = 'https://www1.folha.uol.com.br';
+ const DESCRIPTION = 'Returns the newest posts from Folha de São Paulo (full text)';
+ const PARAMETERS = [
+ [
+ 'feed' => [
+ 'name' => 'Feed sub-URL',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Select the sub-feed (see https://www1.folha.uol.com.br/feed/)',
+ 'exampleValue' => 'emcimadahora/rss091.xml',
+ ],
+ 'amount' => [
+ 'name' => 'Amount of items to fetch',
+ 'type' => 'number',
+ 'defaultValue' => 15,
+ ],
+ 'deep_crawl' => [
+ 'name' => 'Deep Crawl',
+ 'description' => 'Crawl each item "deeply", that is, return the article contents',
+ 'type' => 'checkbox',
+ 'defaultValue' => true,
+ ],
+ ]
+ ];
- if ($this->getInput('deep_crawl')) {
- $articleHTMLContent = getSimpleHTMLDOMCached($item['uri']);
- if($articleHTMLContent) {
- foreach ($articleHTMLContent->find('div.c-news__body .is-hidden') as $toRemove) {
- $toRemove->innertext = '';
- }
- $item_content = $articleHTMLContent->find('div.c-news__body', 0);
- if ($item_content) {
- $text = $item_content->innertext;
- $text = strip_tags($text, '<p><b><a><blockquote><figure><figcaption><img><strong><em><ul><li>');
- $item['content'] = $text;
- $item['uri'] = explode('*', $item['uri'])[1];
- }
- } else {
- Debug::log('???: ' . $item['uri']);
- }
- } else {
- $item['uri'] = explode('*', $item['uri'])[1];
- }
+ protected function parseItem($item)
+ {
+ $item = parent::parseItem($item);
- return $item;
- }
+ if ($this->getInput('deep_crawl')) {
+ $articleHTMLContent = getSimpleHTMLDOMCached($item['uri']);
+ if ($articleHTMLContent) {
+ foreach ($articleHTMLContent->find('div.c-news__body .is-hidden') as $toRemove) {
+ $toRemove->innertext = '';
+ }
+ $item_content = $articleHTMLContent->find('div.c-news__body', 0);
+ if ($item_content) {
+ $text = $item_content->innertext;
+ $text = strip_tags($text, '<p><b><a><blockquote><figure><figcaption><img><strong><em><ul><li>');
+ $item['content'] = $text;
+ $item['uri'] = explode('*', $item['uri'])[1];
+ }
+ } else {
+ Debug::log('???: ' . $item['uri']);
+ }
+ } else {
+ $item['uri'] = explode('*', $item['uri'])[1];
+ }
- public function collectData(){
- $feed_input = $this->getInput('feed');
- if (substr($feed_input, 0, strlen(self::URI)) === self::URI) {
- Debug::log('Input:: ' . $feed_input);
- $feed_url = $feed_input;
- } else {
- /* TODO: prepend `/` if missing */
- $feed_url = self::URI . '/' . $this->getInput('feed');
- }
- Debug::log('URL: ' . $feed_url);
- $limit = $this->getInput('amount');
- $this->collectExpandableDatas($feed_url, $limit);
- }
+ return $item;
+ }
+
+ public function collectData()
+ {
+ $feed_input = $this->getInput('feed');
+ if (substr($feed_input, 0, strlen(self::URI)) === self::URI) {
+ Debug::log('Input:: ' . $feed_input);
+ $feed_url = $feed_input;
+ } else {
+ /* TODO: prepend `/` if missing */
+ $feed_url = self::URI . '/' . $this->getInput('feed');
+ }
+ Debug::log('URL: ' . $feed_url);
+ $limit = $this->getInput('amount');
+ $this->collectExpandableDatas($feed_url, $limit);
+ }
}
diff --git a/bridges/ForGifsBridge.php b/bridges/ForGifsBridge.php
index ea599b9b..03848d04 100644
--- a/bridges/ForGifsBridge.php
+++ b/bridges/ForGifsBridge.php
@@ -1,40 +1,41 @@
<?php
-class ForGifsBridge extends FeedExpander {
- const MAINTAINER = 'logmanoriginal';
- const NAME = 'forgifs Bridge';
- const URI = 'https://forgifs.com';
- const DESCRIPTION = 'Returns the forgifs feed with actual gifs instead of images';
-
- public function collectData() {
- $this->collectExpandableDatas('https://forgifs.com/gallery/srss/7');
- }
-
- protected function parseItem($feedItem) {
-
- $item = parent::parseItem($feedItem);
-
- $content = str_get_html($item['content']);
- $img = $content->find('img', 0);
- $poster = $img->src;
-
- // The actual gif is the same path but its id must be decremented by one.
- // Example:
- // http://forgifs.com/gallery/d/279419-2/Reporter-videobombed-shoulder-checks.gif
- // http://forgifs.com/gallery/d/279418-2/Reporter-videobombed-shoulder-checks.gif
- // Notice how this changes ----------^
- // Now let's extract that number and do some math
- // Notice: Technically we could also load the content page but that would
- // require unnecessary traffic. As long as it works...
- $num = substr($img->src, 29, 6);
- $num -= 1;
- $img->src = substr_replace($img->src, $num, 29, strlen($num));
- $img->width = 'auto';
- $img->height = 'auto';
-
- $item['content'] = $content;
-
- return $item;
-
- }
+class ForGifsBridge extends FeedExpander
+{
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'forgifs Bridge';
+ const URI = 'https://forgifs.com';
+ const DESCRIPTION = 'Returns the forgifs feed with actual gifs instead of images';
+
+ public function collectData()
+ {
+ $this->collectExpandableDatas('https://forgifs.com/gallery/srss/7');
+ }
+
+ protected function parseItem($feedItem)
+ {
+ $item = parent::parseItem($feedItem);
+
+ $content = str_get_html($item['content']);
+ $img = $content->find('img', 0);
+ $poster = $img->src;
+
+ // The actual gif is the same path but its id must be decremented by one.
+ // Example:
+ // http://forgifs.com/gallery/d/279419-2/Reporter-videobombed-shoulder-checks.gif
+ // http://forgifs.com/gallery/d/279418-2/Reporter-videobombed-shoulder-checks.gif
+ // Notice how this changes ----------^
+ // Now let's extract that number and do some math
+ // Notice: Technically we could also load the content page but that would
+ // require unnecessary traffic. As long as it works...
+ $num = substr($img->src, 29, 6);
+ $num -= 1;
+ $img->src = substr_replace($img->src, $num, 29, strlen($num));
+ $img->width = 'auto';
+ $img->height = 'auto';
+
+ $item['content'] = $content;
+
+ return $item;
+ }
}
diff --git a/bridges/Formula1Bridge.php b/bridges/Formula1Bridge.php
index e34c3411..2adce583 100644
--- a/bridges/Formula1Bridge.php
+++ b/bridges/Formula1Bridge.php
@@ -1,68 +1,71 @@
<?php
-class Formula1Bridge extends BridgeAbstract {
- const NAME = 'Formula1 Bridge';
- const URI = 'https://formula1.com/';
- const DESCRIPTION = 'Returns latest official Formula 1 news';
- const MAINTAINER = 'AxorPL';
- const API_KEY = 'qPgPPRJyGCIPxFT3el4MF7thXHyJCzAP';
- const API_URL = 'https://api.formula1.com/v1/editorial/articles?limit=%u';
+class Formula1Bridge extends BridgeAbstract
+{
+ const NAME = 'Formula1 Bridge';
+ const URI = 'https://formula1.com/';
+ const DESCRIPTION = 'Returns latest official Formula 1 news';
+ const MAINTAINER = 'AxorPL';
- const ARTICLE_AUTHOR = 'Formula 1';
- const ARTICLE_HTML = '<p>%s</p><a href="%s" target="_blank"><img src="%s" alt="%s" title="%s"></a>';
- const ARTICLE_URL = 'https://formula1.com/en/latest/article.%s.%s.html';
+ const API_KEY = 'qPgPPRJyGCIPxFT3el4MF7thXHyJCzAP';
+ const API_URL = 'https://api.formula1.com/v1/editorial/articles?limit=%u';
- const LIMIT_MIN = 1;
- const LIMIT_DEFAULT = 10;
- const LIMIT_MAX = 100;
+ const ARTICLE_AUTHOR = 'Formula 1';
+ const ARTICLE_HTML = '<p>%s</p><a href="%s" target="_blank"><img src="%s" alt="%s" title="%s"></a>';
+ const ARTICLE_URL = 'https://formula1.com/en/latest/article.%s.%s.html';
- const PARAMETERS = array(
- array(
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Number of articles to return',
- 'exampleValue' => self::LIMIT_DEFAULT,
- 'defaultValue' => self::LIMIT_DEFAULT
- )
- )
- );
+ const LIMIT_MIN = 1;
+ const LIMIT_DEFAULT = 10;
+ const LIMIT_MAX = 100;
- public function collectData() {
- $limit = $this->getInput('limit') ?: self::LIMIT_DEFAULT;
- $limit = min(self::LIMIT_MAX, max(self::LIMIT_MIN, $limit));
- $url = sprintf(self::API_URL, $limit);
+ const PARAMETERS = [
+ [
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Number of articles to return',
+ 'exampleValue' => self::LIMIT_DEFAULT,
+ 'defaultValue' => self::LIMIT_DEFAULT
+ ]
+ ]
+ ];
- $json = json_decode(getContents($url, array('apikey: ' . self::API_KEY)));
- if(property_exists($json, 'error')) {
- returnServerError($json->message);
- }
- $list = $json->items;
+ public function collectData()
+ {
+ $limit = $this->getInput('limit') ?: self::LIMIT_DEFAULT;
+ $limit = min(self::LIMIT_MAX, max(self::LIMIT_MIN, $limit));
+ $url = sprintf(self::API_URL, $limit);
- foreach($list as $article) {
- if(property_exists($article->thumbnail, 'caption')) {
- $caption = $article->thumbnail->caption;
- } else {
- $caption = $article->thumbnail->image->title;
- }
+ $json = json_decode(getContents($url, ['apikey: ' . self::API_KEY]));
+ if (property_exists($json, 'error')) {
+ returnServerError($json->message);
+ }
+ $list = $json->items;
- $item = array();
- $item['uri'] = sprintf(self::ARTICLE_URL, $article->slug, $article->id);
- $item['title'] = $article->title;
- $item['timestamp'] = $article->updatedAt;
- $item['author'] = self::ARTICLE_AUTHOR;
- $item['enclosures'] = array($article->thumbnail->image->url);
- $item['uid'] = $article->id;
- $item['content'] = sprintf(
- self::ARTICLE_HTML,
- $article->metaDescription,
- $item['uri'],
- $item['enclosures'][0],
- $caption,
- $caption
- );
- $this->items[] = $item;
- }
- }
+ foreach ($list as $article) {
+ if (property_exists($article->thumbnail, 'caption')) {
+ $caption = $article->thumbnail->caption;
+ } else {
+ $caption = $article->thumbnail->image->title;
+ }
+
+ $item = [];
+ $item['uri'] = sprintf(self::ARTICLE_URL, $article->slug, $article->id);
+ $item['title'] = $article->title;
+ $item['timestamp'] = $article->updatedAt;
+ $item['author'] = self::ARTICLE_AUTHOR;
+ $item['enclosures'] = [$article->thumbnail->image->url];
+ $item['uid'] = $article->id;
+ $item['content'] = sprintf(
+ self::ARTICLE_HTML,
+ $article->metaDescription,
+ $item['uri'],
+ $item['enclosures'][0],
+ $caption,
+ $caption
+ );
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/FourchanBridge.php b/bridges/FourchanBridge.php
index 4680475e..179ae91f 100644
--- a/bridges/FourchanBridge.php
+++ b/bridges/FourchanBridge.php
@@ -1,79 +1,82 @@
<?php
-class FourchanBridge extends BridgeAbstract {
- const MAINTAINER = 'mitsukarenai';
- const NAME = '4chan';
- const URI = 'https://boards.4chan.org/';
- const CACHE_TIMEOUT = 300; // 5min
- const DESCRIPTION = 'Returns posts from the specified thread';
+class FourchanBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = '4chan';
+ const URI = 'https://boards.4chan.org/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns posts from the specified thread';
- const PARAMETERS = array( array(
- 'c' => array(
- 'name' => 'Thread category',
- 'required' => true,
- 'exampleValue' => 'po',
- ),
- 't' => array(
- 'name' => 'Thread number',
- 'type' => 'number',
- 'exampleValue' => '597271',
- 'required' => true
- )
- ));
+ const PARAMETERS = [ [
+ 'c' => [
+ 'name' => 'Thread category',
+ 'required' => true,
+ 'exampleValue' => 'po',
+ ],
+ 't' => [
+ 'name' => 'Thread number',
+ 'type' => 'number',
+ 'exampleValue' => '597271',
+ 'required' => true
+ ]
+ ]];
- public function getURI(){
- if(!is_null($this->getInput('c')) && !is_null($this->getInput('t'))) {
- return static::URI . $this->getInput('c') . '/thread/' . $this->getInput('t');
- }
+ public function getURI()
+ {
+ if (!is_null($this->getInput('c')) && !is_null($this->getInput('t'))) {
+ return static::URI . $this->getInput('c') . '/thread/' . $this->getInput('t');
+ }
- return parent::getURI();
- }
+ return parent::getURI();
+ }
- public function collectData(){
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
- $html = getSimpleHTMLDOM($this->getURI());
+ foreach ($html->find('div.postContainer') as $element) {
+ $item = [];
+ $item['id'] = $element->find('.post', 0)->getAttribute('id');
+ $item['uri'] = $this->getURI() . '#' . $item['id'];
+ $item['timestamp'] = $element->find('span.dateTime', 0)->getAttribute('data-utc');
+ $item['author'] = $element->find('span.name', 0)->plaintext;
- foreach($html->find('div.postContainer') as $element) {
- $item = array();
- $item['id'] = $element->find('.post', 0)->getAttribute('id');
- $item['uri'] = $this->getURI() . '#' . $item['id'];
- $item['timestamp'] = $element->find('span.dateTime', 0)->getAttribute('data-utc');
- $item['author'] = $element->find('span.name', 0)->plaintext;
+ $file = $element->find('.file', 0);
- $file = $element->find('.file', 0);
+ if (!empty($file)) {
+ $item['image'] = $element->find('.file a', 0)->href;
+ $item['imageThumb'] = $element->find('.file img', 0)->src;
+ if (!isset($item['imageThumb']) and strpos($item['image'], '.swf') !== false) {
+ $item['imageThumb'] = 'http://i.imgur.com/eO0cxf9.jpg';
+ }
+ }
- if(!empty($file)) {
- $item['image'] = $element->find('.file a', 0)->href;
- $item['imageThumb'] = $element->find('.file img', 0)->src;
- if(!isset($item['imageThumb']) and strpos($item['image'], '.swf') !== false)
- $item['imageThumb'] = 'http://i.imgur.com/eO0cxf9.jpg';
- }
+ if (!empty($element->find('span.subject', 0)->innertext)) {
+ $item['subject'] = $element->find('span.subject', 0)->innertext;
+ }
- if(!empty($element->find('span.subject', 0)->innertext)) {
- $item['subject'] = $element->find('span.subject', 0)->innertext;
- }
+ $item['title'] = 'reply ' . $item['id'] . ' | ' . $item['author'];
+ if (isset($item['subject'])) {
+ $item['title'] = $item['subject'] . ' - ' . $item['title'];
+ }
- $item['title'] = 'reply ' . $item['id'] . ' | ' . $item['author'];
- if(isset($item['subject'])) {
- $item['title'] = $item['subject'] . ' - ' . $item['title'];
- }
+ $content = $element->find('.postMessage', 0)->innertext;
+ $content = str_replace('href="#p', 'href="' . $this->getURI() . '#p', $content);
+ $item['content'] = '<span id="' . $item['id'] . '">' . $content . '</span>';
- $content = $element->find('.postMessage', 0)->innertext;
- $content = str_replace('href="#p', 'href="' . $this->getURI() . '#p', $content);
- $item['content'] = '<span id="' . $item['id'] . '">' . $content . '</span>';
-
- if(isset($item['image'])) {
- $item['content'] = '<a href="'
- . $item['image']
- . '"><img alt="'
- . $item['id']
- . '" src="'
- . $item['imageThumb']
- . '" /></a><br>'
- . $item['content'];
- }
- $this->items[] = $item;
- }
- $this->items = array_reverse($this->items);
- }
+ if (isset($item['image'])) {
+ $item['content'] = '<a href="'
+ . $item['image']
+ . '"><img alt="'
+ . $item['id']
+ . '" src="'
+ . $item['imageThumb']
+ . '" /></a><br>'
+ . $item['content'];
+ }
+ $this->items[] = $item;
+ }
+ $this->items = array_reverse($this->items);
+ }
}
diff --git a/bridges/FreeCodeCampBridge.php b/bridges/FreeCodeCampBridge.php
index da0b5c7d..89d8c53a 100644
--- a/bridges/FreeCodeCampBridge.php
+++ b/bridges/FreeCodeCampBridge.php
@@ -1,27 +1,31 @@
<?php
-class FreeCodeCampBridge extends FeedExpander {
- const MAINTAINER = 'IceWreck';
- const NAME = 'FreeCodecamp Bridge';
- const URI = 'https://www.freecodecamp.org';
- const CACHE_TIMEOUT = 3600;
- const DESCRIPTION = 'RSS feed for FreeCodeCamp';
- // Freecodecamp removed their old full content rss feed and replaced it with one liner content.
+class FreeCodeCampBridge extends FeedExpander
+{
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'FreeCodecamp Bridge';
+ const URI = 'https://www.freecodecamp.org';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'RSS feed for FreeCodeCamp';
+ // Freecodecamp removed their old full content rss feed and replaced it with one liner content.
- public function collectData(){
- $this->collectExpandableDatas('https://www.freecodecamp.org/news/rss/', 15);
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas('https://www.freecodecamp.org/news/rss/', 15);
+ }
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- // $articlePage gets the entire page's contents
- $articlePage = getSimpleHTMLDOM($newsItem->link);
- // figure contain's the main article image
- $article = $articlePage->find('figure', 0);
- // the actual article
- foreach($articlePage->find('.post-full-content') as $element)
- $article = $article . $element;
- $item['content'] = $article;
- return $item;
- }
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+ // figure contain's the main article image
+ $article = $articlePage->find('figure', 0);
+ // the actual article
+ foreach ($articlePage->find('.post-full-content') as $element) {
+ $article = $article . $element;
+ }
+ $item['content'] = $article;
+ return $item;
+ }
}
diff --git a/bridges/FunkBridge.php b/bridges/FunkBridge.php
index 65a61e74..69be4928 100644
--- a/bridges/FunkBridge.php
+++ b/bridges/FunkBridge.php
@@ -1,84 +1,90 @@
<?php
-class FunkBridge extends BridgeAbstract {
- const MAINTAINER = 'µKöff';
- const NAME = 'Funk';
- const URI = 'https://www.funk.net/';
- const DESCRIPTION = 'Videos per channel of German public video-on-demand service Funk';
+class FunkBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'µKöff';
+ const NAME = 'Funk';
+ const URI = 'https://www.funk.net/';
+ const DESCRIPTION = 'Videos per channel of German public video-on-demand service Funk';
- const PARAMETERS = array(
- 'Channel' => array(
- 'channel' => array(
- 'name' => 'Slug',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'game-two-856'
- ),
- 'max' => array(
- 'name' => 'Maximum',
- 'type' => 'number',
- 'defaultValue' => '-1'
- )
- )
- );
+ const PARAMETERS = [
+ 'Channel' => [
+ 'channel' => [
+ 'name' => 'Slug',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'game-two-856'
+ ],
+ 'max' => [
+ 'name' => 'Maximum',
+ 'type' => 'number',
+ 'defaultValue' => '-1'
+ ]
+ ]
+ ];
- public function collectData(){
- switch($this->queriedContext) {
- case 'Channel':
- $url = static::URI . 'data/videos/byChannelAlias/' . $this->getInput('channel') . '/';
- if(!empty($this->getInput('max')) && $this->getInput('max') >= 0) {
- $url .= '?size=' . $this->getInput('max');
- }
+ public function collectData()
+ {
+ switch ($this->queriedContext) {
+ case 'Channel':
+ $url = static::URI . 'data/videos/byChannelAlias/' . $this->getInput('channel') . '/';
+ if (!empty($this->getInput('max')) && $this->getInput('max') >= 0) {
+ $url .= '?size=' . $this->getInput('max');
+ }
- $jsonString = getContents($url) or returnServerError('No contents received!');
- $json = json_decode($jsonString, true);
+ $jsonString = getContents($url) or returnServerError('No contents received!');
+ $json = json_decode($jsonString, true);
- foreach($json['list'] as $element) {
- $this->items[] = $this->collectArticle($element);
- }
- break;
- default:
- returnServerError('Unknown context!');
- }
- }
+ foreach ($json['list'] as $element) {
+ $this->items[] = $this->collectArticle($element);
+ }
+ break;
+ default:
+ returnServerError('Unknown context!');
+ }
+ }
- private function collectArticle($element) {
- $item = array();
- $item['uri'] = static::URI . 'channel/' . $element['channelAlias'] . '/' . $element['alias'];
- $item['title'] = $element['title'];
- $item['timestamp'] = $element['publicationDate'];
- $item['author'] = str_replace('-' . $element['channelId'], '', $element['channelAlias']);
- $item['content'] = $element['shortDescription'];
- $item['enclosures'] = array(
- 'https://www.funk.net/api/v4.0/thumbnails/' . $element['imageLandscape']
- );
- $item['uid'] = $element['entityId'];
- return $item;
- }
+ private function collectArticle($element)
+ {
+ $item = [];
+ $item['uri'] = static::URI . 'channel/' . $element['channelAlias'] . '/' . $element['alias'];
+ $item['title'] = $element['title'];
+ $item['timestamp'] = $element['publicationDate'];
+ $item['author'] = str_replace('-' . $element['channelId'], '', $element['channelAlias']);
+ $item['content'] = $element['shortDescription'];
+ $item['enclosures'] = [
+ 'https://www.funk.net/api/v4.0/thumbnails/' . $element['imageLandscape']
+ ];
+ $item['uid'] = $element['entityId'];
+ return $item;
+ }
- public function detectParameters($url) {
- $regex = '/^https?:\/\/(?:www\.)?funk\.net\/channel\/([^\/]+).*$/';
- if(preg_match($regex, $url, $urlMatches) > 0) {
- return array(
- 'channel' => $urlMatches[1]
- );
- } else {
- return null;
- }
- }
+ public function detectParameters($url)
+ {
+ $regex = '/^https?:\/\/(?:www\.)?funk\.net\/channel\/([^\/]+).*$/';
+ if (preg_match($regex, $url, $urlMatches) > 0) {
+ return [
+ 'channel' => $urlMatches[1]
+ ];
+ } else {
+ return null;
+ }
+ }
- public function getIcon() {
- return 'https://www.funk.net/img/favicons/favicon-192x192.png';
- }
+ public function getIcon()
+ {
+ return 'https://www.funk.net/img/favicons/favicon-192x192.png';
+ }
- public function getName(){
- switch($this->queriedContext) {
- case 'Channel':
- if(!empty($this->getInput('channel'))) {
- return $this->getInput('channel');
- }
- break;
- }
- return parent::getName();
- }
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Channel':
+ if (!empty($this->getInput('channel'))) {
+ return $this->getInput('channel');
+ }
+ break;
+ }
+ return parent::getName();
+ }
}
diff --git a/bridges/FurAffinityBridge.php b/bridges/FurAffinityBridge.php
index b5bd3ead..7e1dfd82 100644
--- a/bridges/FurAffinityBridge.php
+++ b/bridges/FurAffinityBridge.php
@@ -1,903 +1,925 @@
<?php
-class FurAffinityBridge extends BridgeAbstract {
- const NAME = 'FurAffinity Bridge';
- const URI = 'https://www.furaffinity.net';
- const CACHE_TIMEOUT = 300; // 5min
- const DESCRIPTION = 'Returns posts from various sections of FurAffinity';
- const MAINTAINER = 'Roliga';
- const PARAMETERS = array(
- 'Search' => array(
- 'q' => array(
- 'name' => 'Query',
- 'required' => true,
- 'exampleValue' => 'dog',
- ),
- 'rating-general' => array(
- 'name' => 'General',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'rating-mature' => array(
- 'name' => 'Mature',
- 'type' => 'checkbox',
- ),
- 'rating-adult' => array(
- 'name' => 'Adult',
- 'type' => 'checkbox',
- ),
- 'range' => array(
- 'name' => 'Time range',
- 'type' => 'list',
- 'values' => array(
- 'A Day' => 'day',
- '3 Days' => '3days',
- 'A Week' => 'week',
- 'A Month' => 'month',
- 'All time' => 'all'
- ),
- 'defaultValue' => 'all'
- ),
- 'type-art' => array(
- 'name' => 'Art',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'type-flash' => array(
- 'name' => 'Flash',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'type-photo' => array(
- 'name' => 'Photography',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'type-music' => array(
- 'name' => 'Music',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'type-story' => array(
- 'name' => 'Story',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'type-poetry' => array(
- 'name' => 'Poetry',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'mode' => array(
- 'name' => 'Match mode',
- 'type' => 'list',
- 'values' => array(
- 'All of the words' => 'all',
- 'Any of the words' => 'any',
- 'Extended' => 'extended'
- ),
- 'defaultValue' => 'extended'
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => true,
- 'defaultValue' => 10,
- 'title' => 'Limit number of submissions to return. -1 for unlimited.'
- ),
- 'full' => array(
- 'name' => 'Full view',
- 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'cache' => array(
- 'name' => 'Cache submission pages',
- 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- )
- ),
- 'Browse' => array(
- 'cat' => array(
- 'name' => 'Category',
- 'type' => 'list',
- 'values' => array(
- 'Visual Art' => array(
- 'All' => 1,
- 'Artwork (Digital)' => 2,
- 'Artwork (Traditional)' => 3,
- 'Cellshading' => 4,
- 'Crafting' => 5,
- 'Designs' => 6,
- 'Flash' => 7,
- 'Fursuiting' => 8,
- 'Icons' => 9,
- 'Mosaics' => 10,
- 'Photography' => 11,
- 'Sculpting' => 12
- ),
- 'Readable Art' => array(
- 'Story' => 13,
- 'Poetry' => 14,
- 'Prose' => 15
- ),
- 'Audio Art' => array(
- 'Music' => 16,
- 'Podcasts' => 17
- ),
- 'Downloadable' => array(
- 'Skins' => 18,
- 'Handhelds' => 19,
- 'Resources' => 20
- ),
- 'Other Stuff' => array(
- 'Adoptables' => 21,
- 'Auctions' => 22,
- 'Contests' => 23,
- 'Current Events' => 24,
- 'Desktops' => 25,
- 'Stockart' => 26,
- 'Screenshots' => 27,
- 'Scraps' => 28,
- 'Wallpaper' => 29,
- 'YCH / Sale' => 30,
- 'Other' => 31
- )
- ),
- 'defaultValue' => 1
- ),
- 'atype' => array(
- 'name' => 'Type',
- 'type' => 'list',
- 'values' => array(
- 'General Things' => array(
- 'All' => 1,
- 'Abstract' => 2,
- 'Animal related (non-anthro)' => 3,
- 'Anime' => 4,
- 'Comics' => 5,
- 'Doodle' => 6,
- 'Fanart' => 7,
- 'Fantasy' => 8,
- 'Human' => 9,
- 'Portraits' => 10,
- 'Scenery' => 11,
- 'Still Life' => 12,
- 'Tutorials' => 13,
- 'Miscellaneous' => 14
- ),
- 'Fetish / Furry specialty' => array(
- 'Baby fur' => 101,
- 'Bondage' => 102,
- 'Digimon' => 103,
- 'Fat Furs' => 104,
- 'Fetish Other' => 105,
- 'Fursuit' => 106,
- 'Gore / Macabre Art' => 119,
- 'Hyper' => 107,
- 'Inflation' => 108,
- 'Macro / Micro' => 109,
- 'Muscle' => 110,
- 'My Little Pony / Brony' => 111,
- 'Paw' => 112,
- 'Pokemon' => 113,
- 'Pregnancy' => 114,
- 'Sonic' => 115,
- 'Transformation' => 116,
- 'Vore' => 117,
- 'Water Sports' => 118,
- 'General Furry Art' => 100
- ),
- 'Music' => array(
- 'Techno' => 201,
- 'Trance' => 202,
- 'House' => 203,
- '90s' => 204,
- '80s' => 205,
- '70s' => 206,
- '60s' => 207,
- 'Pre-60s' => 208,
- 'Classical' => 209,
- 'Game Music' => 210,
- 'Rock' => 211,
- 'Pop' => 212,
- 'Rap' => 213,
- 'Industrial' => 214,
- 'Other Music' => 200
- )
- ),
- 'defaultValue' => 1
- ),
- 'species' => array(
- 'name' => 'Species',
- 'type' => 'list',
- 'values' => array(
- 'Unspecified / Any' => 1,
- 'Amphibian' => array(
- 'Frog' => 1001,
- 'Newt' => 1002,
- 'Salamander' => 1003,
- 'Amphibian (Other)' => 1000
- ),
- 'Aquatic' => array(
- 'Cephalopod' => 2001,
- 'Dolphin' => 2002,
- 'Fish' => 2005,
- 'Porpoise' => 2004,
- 'Seal' => 6068,
- 'Shark' => 2006,
- 'Whale' => 2003,
- 'Aquatic (Other)' => 2000
- ),
- 'Avian' => array(
- 'Corvid' => 3001,
- 'Crow' => 3002,
- 'Duck' => 3003,
- 'Eagle' => 3004,
- 'Falcon' => 3005,
- 'Goose' => 3006,
- 'Gryphon' => 3007,
- 'Hawk' => 3008,
- 'Owl' => 3009,
- 'Phoenix' => 3010,
- 'Swan' => 3011,
- 'Avian (Other)' => 3000
- ),
- 'Bears &amp; Ursines' => array(
- 'Bear' => 6002
- ),
- 'Camelids' => array(
- 'Camel' => 6074,
- 'Llama' => 6036
- ),
- 'Canines &amp; Lupines' => array(
- 'Coyote' => 6008,
- 'Doberman' => 6009,
- 'Dog' => 6010,
- 'Dingo' => 6011,
- 'German Shepherd' => 6012,
- 'Jackal' => 6013,
- 'Husky' => 6014,
- 'Wolf' => 6016,
- 'Canine (Other)' => 6017
- ),
- 'Cervines' => array(
- 'Cervine (Other)' => 6018
- ),
- 'Cows &amp; Bovines' => array(
- 'Antelope' => 6004,
- 'Cows' => 6003,
- 'Gazelle' => 6005,
- 'Goat' => 6006,
- 'Bovines (General)' => 6007
- ),
- 'Dragons' => array(
- 'Eastern Dragon' => 4001,
- 'Hydra' => 4002,
- 'Serpent' => 4003,
- 'Western Dragon' => 4004,
- 'Wyvern' => 4005,
- 'Dragon (Other)' => 4000
- ),
- 'Equestrians' => array(
- 'Donkey' => 6019,
- 'Horse' => 6034,
- 'Pony' => 6073,
- 'Zebra' => 6071
- ),
- 'Exotic &amp; Mythicals' => array(
- 'Argonian' => 5002,
- 'Chakat' => 5003,
- 'Chocobo' => 5004,
- 'Citra' => 5005,
- 'Crux' => 5006,
- 'Daemon' => 5007,
- 'Digimon' => 5008,
- 'Dracat' => 5009,
- 'Draenei' => 5010,
- 'Elf' => 5011,
- 'Gargoyle' => 5012,
- 'Iksar' => 5013,
- 'Kaiju/Monster' => 5015,
- 'Langurhali' => 5014,
- 'Moogle' => 5017,
- 'Naga' => 5016,
- 'Orc' => 5018,
- 'Pokemon' => 5019,
- 'Satyr' => 5020,
- 'Sergal' => 5021,
- 'Tanuki' => 5022,
- 'Unicorn' => 5023,
- 'Xenomorph' => 5024,
- 'Alien (Other)' => 5001,
- 'Exotic (Other)' => 5000
- ),
- 'Felines' => array(
- 'Domestic Cat' => 6020,
- 'Cheetah' => 6021,
- 'Cougar' => 6022,
- 'Jaguar' => 6023,
- 'Leopard' => 6024,
- 'Lion' => 6025,
- 'Lynx' => 6026,
- 'Ocelot' => 6027,
- 'Panther' => 6028,
- 'Tiger' => 6029,
- 'Feline (Other)' => 6030
- ),
- 'Insects' => array(
- 'Arachnid' => 8000,
- 'Mantid' => 8004,
- 'Scorpion' => 8005,
- 'Insect (Other)' => 8003
- ),
- 'Mammals (Other)' => array(
- 'Bat' => 6001,
- 'Giraffe' => 6031,
- 'Hedgehog' => 6032,
- 'Hippopotamus' => 6033,
- 'Hyena' => 6035,
- 'Panda' => 6052,
- 'Pig/Swine' => 6053,
- 'Rabbit/Hare' => 6059,
- 'Raccoon' => 6060,
- 'Red Panda' => 6062,
- 'Meerkat' => 6043,
- 'Mongoose' => 6044,
- 'Rhinoceros' => 6063,
- 'Mammals (Other)' => 6000
- ),
- 'Marsupials' => array(
- 'Opossum' => 6037,
- 'Kangaroo' => 6038,
- 'Koala' => 6039,
- 'Quoll' => 6040,
- 'Wallaby' => 6041,
- 'Marsupial (Other)' => 6042
- ),
- 'Mustelids' => array(
- 'Badger' => 6045,
- 'Ferret' => 6046,
- 'Mink' => 6048,
- 'Otter' => 6047,
- 'Skunk' => 6069,
- 'Weasel' => 6049,
- 'Mustelid (Other)' => 6051
- ),
- 'Primates' => array(
- 'Gorilla' => 6054,
- 'Human' => 6055,
- 'Lemur' => 6056,
- 'Monkey' => 6057,
- 'Primate (Other)' => 6058
- ),
- 'Reptillian' => array(
- 'Alligator &amp; Crocodile' => 7001,
- 'Gecko' => 7003,
- 'Iguana' => 7004,
- 'Lizard' => 7005,
- 'Snakes &amp; Serpents' => 7006,
- 'Turtle' => 7007,
- 'Reptilian (Other)' => 7000
- ),
- 'Rodents' => array(
- 'Beaver' => 6064,
- 'Mouse' => 6065,
- 'Rat' => 6061,
- 'Squirrel' => 6070,
- 'Rodent (Other)' => 6067
- ),
- 'Vulpines' => array(
- 'Fennec' => 6072,
- 'Fox' => 6075,
- 'Vulpine (Other)' => 6015
- ),
- 'Other' => array(
- 'Dinosaur' => 8001,
- 'Wolverine' => 6050
- )
- ),
- 'defaultValue' => 1
- ),
- 'gender' => array(
- 'name' => 'Gender',
- 'type' => 'list',
- 'values' => array(
- 'Any' => 0,
- 'Male' => 2,
- 'Female' => 3,
- 'Herm' => 4,
- 'Transgender' => 5,
- 'Multiple characters' => 6,
- 'Other / Not Specified' => 7
- ),
- 'defaultValue' => 0
- ),
- 'rating_general' => array(
- 'name' => 'General',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'rating_mature' => array(
- 'name' => 'Mature',
- 'type' => 'checkbox',
- ),
- 'rating_adult' => array(
- 'name' => 'Adult',
- 'type' => 'checkbox',
- ),
- 'limit-browse' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => true,
- 'defaultValue' => 10,
- 'title' => 'Limit number of submissions to return. -1 for unlimited.'
- ),
- 'full' => array(
- 'name' => 'Full view',
- 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'cache' => array(
- 'name' => 'Cache submission pages',
- 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- )
-
- ),
- 'Journals' => array(
- 'username-journals' => array(
- 'name' => 'Username',
- 'required' => true,
- 'exampleValue' => 'dhw',
- 'title' => 'Lowercase username as seen in URLs'
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'defaultValue' => -1,
- 'title' => 'Limit number of journals to return. -1 for unlimited.'
- )
-
- ),
- 'Single Journal' => array(
- 'journal-id' => array(
- 'name' => 'Journal ID',
- 'required' => true,
- 'exampleValue' => '10008853',
- 'type' => 'number',
- 'title' => 'Number seen in journal URL'
- )
- ),
- 'Gallery' => array(
- 'username-gallery' => array(
- 'name' => 'Username',
- 'required' => true,
- 'exampleValue' => 'dhw',
- 'title' => 'Lowercase username as seen in URLs'
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => true,
- 'defaultValue' => 10,
- 'title' => 'Limit number of submissions to return. -1 for unlimited.'
- ),
- 'full' => array(
- 'name' => 'Full view',
- 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'cache' => array(
- 'name' => 'Cache submission pages',
- 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- )
- ),
- 'Scraps' => array(
- 'username-scraps' => array(
- 'name' => 'Username',
- 'required' => true,
- 'exampleValue' => 'dhw',
- 'title' => 'Lowercase username as seen in URLs'
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => true,
- 'defaultValue' => 10,
- 'title' => 'Limit number of submissions to return. -1 for unlimited.'
- ),
- 'full' => array(
- 'name' => 'Full view',
- 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'cache' => array(
- 'name' => 'Cache submission pages',
- 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- )
- ),
- 'Favorites' => array(
- 'username-favorites' => array(
- 'name' => 'Username',
- 'required' => true,
- 'exampleValue' => 'dhw',
- 'title' => 'Lowercase username as seen in URLs'
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => true,
- 'defaultValue' => 10,
- 'title' => 'Limit number of submissions to return. -1 for unlimited.'
- ),
- 'full' => array(
- 'name' => 'Full view',
- 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'cache' => array(
- 'name' => 'Cache submission pages',
- 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- )
- ),
- 'Gallery Folder' => array(
- 'username-folder' => array(
- 'name' => 'Username',
- 'required' => true,
- 'exampleValue' => 'kopk',
- 'title' => 'Lowercase username as seen in URLs'
- ),
- 'folder-id' => array(
- 'name' => 'Folder ID',
- 'required' => true,
- 'exampleValue' => '1031990',
- 'type' => 'number',
- 'title' => 'Number seen in folder URL'
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => true,
- 'defaultValue' => 10,
- 'title' => 'Limit number of submissions to return. -1 for unlimited.'
- ),
- 'full' => array(
- 'name' => 'Full view',
- 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'cache' => array(
- 'name' => 'Cache submission pages',
- 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- )
- )
- );
-
- /*
- * This was aquired by creating a new user on FA then
- * extracting the cookie from the browsers dev console.
- */
- const FA_AUTH_COOKIE = 'b=4ce65691-b50f-4742-a990-bf28d6de16ee; a=ca6e4566-9d81-4263-9444-653b142e35f8';
-
- public function detectParameters($url) {
- $params = array();
-
- // Single journal
- $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journal\/(\d+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['journal-id'] = urldecode($matches[3]);
- return $params;
- }
-
- // Journals
- $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journals\/([^\/&?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['username-journals'] = urldecode($matches[3]);
- return $params;
- }
-
- // Gallery folder
- $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/gallery\/([^\/&?\n]+)\/folder\/(\d+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['username-folder'] = urldecode($matches[3]);
- $params['folder-id'] = urldecode($matches[4]);
- $params['full'] = 'on';
- return $params;
- }
-
- // Gallery (must be after gallery folder)
- $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/(gallery|scraps|favorites)\/([^\/&?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['username-' . $matches[3]] = urldecode($matches[4]);
- $params['full'] = 'on';
- return $params;
- }
-
- return null;
- }
-
- public function getName() {
- switch($this->queriedContext) {
- case 'Search':
- return 'Search For '
- . $this->getInput('q');
- case 'Browse':
- return 'Browse';
- case 'Journals':
- return $this->getInput('username-journals');
- case 'Single Journal':
- return 'Journal '
- . $this->getInput('journal-id');
- case 'Gallery':
- return $this->getInput('username-gallery');
- case 'Scraps':
- return $this->getInput('username-scraps');
- case 'Favorites':
- return $this->getInput('username-favorites');
- case 'Gallery Folder':
- return $this->getInput('username-folder')
- . '\'s Folder '
- . $this->getInput('folder-id');
- default: return parent::getName();
- }
- }
-
- public function getDescription() {
- switch($this->queriedContext) {
- case 'Search':
- return 'FurAffinity Search For '
- . $this->getInput('q');
- case 'Browse':
- return 'FurAffinity Browse';
- case 'Journals':
- return 'FurAffinity Journals By '
- . $this->getInput('username-journals');
- case 'Single Journal':
- return 'FurAffinity Journal '
- . $this->getInput('journal-id');
- case 'Gallery':
- return 'FurAffinity Gallery By '
- . $this->getInput('username-gallery');
- case 'Scraps':
- return 'FurAffinity Scraps By '
- . $this->getInput('username-scraps');
- case 'Favorites':
- return 'FurAffinity Favorites By '
- . $this->getInput('username-favorites');
- case 'Gallery Folder':
- return 'FurAffinity Gallery Folder '
- . $this->getInput('folder-id')
- . ' By '
- . $this->getInput('username-folder');
- default: return parent::getDescription();
- }
- }
-
- public function getURI() {
- switch($this->queriedContext) {
- case 'Search':
- return SELF::URI
- . '/search';
- case 'Browse':
- return SELF::URI
- . '/browse';
- case 'Journals':
- return SELF::URI
- . '/journals/'
- . $this->getInput('username-journals');
- case 'Single Journal':
- return SELF::URI
- . '/journal/'
- . $this->getInput('journal-id');
- case 'Gallery':
- return SELF::URI
- . '/gallery/'
- . $this->getInput('username-gallery');
- case 'Scraps':
- return SELF::URI
- . '/scraps/'
- . $this->getInput('username-scraps');
- case 'Favorites':
- return SELF::URI
- . '/favorites/'
- . $this->getInput('username-favorites');
- case 'Gallery Folder':
- return SELF::URI
- . '/gallery/'
- . $this->getInput('username-folder')
- . '/folder/'
- . $this->getInput('folder-id');
- default: return parent::getURI();
- }
- }
-
- public function collectData() {
- switch($this->queriedContext) {
- case 'Search':
- $data = array(
- 'q' => $this->getInput('q'),
- 'perpage' => 72,
- 'rating-general' => ($this->getInput('rating-general') === true ? 'on' : 0),
- 'rating-mature' => ($this->getInput('rating-mature') === true ? 'on' : 0),
- 'rating-adult' => ($this->getInput('rating-adult') === true ? 'on' : 0),
- 'range' => $this->getInput('range'),
- 'type-art' => ($this->getInput('type-art') === true ? 'on' : 0),
- 'type-flash' => ($this->getInput('type-flash') === true ? 'on' : 0),
- 'type-photo' => ($this->getInput('type-photo') === true ? 'on' : 0),
- 'type-music' => ($this->getInput('type-music') === true ? 'on' : 0),
- 'type-story' => ($this->getInput('type-story') === true ? 'on' : 0),
- 'type-poetry' => ($this->getInput('type-poetry') === true ? 'on' : 0),
- 'mode' => $this->getInput('mode')
- );
- $html = $this->postFASimpleHTMLDOM($data);
- $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10);
- $this->itemsFromSubmissionList($html, $limit);
- break;
- case 'Browse':
- $data = array(
- 'cat' => $this->getInput('cat'),
- 'atype' => $this->getInput('atype'),
- 'species' => $this->getInput('species'),
- 'gender' => $this->getInput('gender'),
- 'perpage' => 72,
- 'rating_general' => ($this->getInput('rating_general') === true ? 'on' : 0),
- 'rating_mature' => ($this->getInput('rating_mature') === true ? 'on' : 0),
- 'rating_adult' => ($this->getInput('rating_adult') === true ? 'on' : 0)
- );
- $html = $this->postFASimpleHTMLDOM($data);
- $limit = (is_int($this->getInput('limit-browse')) ? $this->getInput('limit-browse') : 10);
- $this->itemsFromSubmissionList($html, $limit);
- break;
- case 'Journals':
- $html = $this->getFASimpleHTMLDOM($this->getURI());
- $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : -1);
- $this->itemsFromJournalList($html, $limit);
- break;
- case 'Single Journal':
- $html = $this->getFASimpleHTMLDOM($this->getURI());
- $this->itemsFromJournal($html);
- break;
- case 'Gallery':
- case 'Scraps':
- case 'Favorites':
- case 'Gallery Folder':
- $html = $this->getFASimpleHTMLDOM($this->getURI());
- $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10);
- $this->itemsFromSubmissionList($html, $limit);
- break;
- }
- }
-
- private function postFASimpleHTMLDOM($data) {
- $opts = array(
- CURLOPT_CUSTOMREQUEST => 'POST',
- CURLOPT_POSTFIELDS => http_build_query($data)
- );
- $header = array(
- 'Host: ' . parse_url(self::URI, PHP_URL_HOST),
- 'Content-Type: application/x-www-form-urlencoded',
- 'Cookie: ' . self::FA_AUTH_COOKIE
- );
-
- $html = getSimpleHTMLDOM($this->getURI(), $header, $opts);
- $html = defaultLinkTo($html, $this->getURI());
-
- return $html;
- }
-
- private function getFASimpleHTMLDOM($url, $cache = false) {
- $header = array(
- 'Cookie: ' . self::FA_AUTH_COOKIE
- );
-
- if($cache) {
- $html = getSimpleHTMLDOMCached($url, 86400, $header); // 24 hours
- } else {
- $html = getSimpleHTMLDOM($url, $header);
- }
-
- $html = defaultLinkTo($html, $url);
-
- return $html;
- }
-
- private function itemsFromJournalList($html, $limit) {
- foreach($html->find('table[id^=jid:]') as $journal) {
- # allows limit = -1 to mean 'unlimited'
- if($limit-- === 0) break;
-
- $item = array();
-
- $this->setReferrerPolicy($journal);
-
- $item['uri'] = $journal->find('a', 0)->href;
- $item['title'] = html_entity_decode($journal->find('a', 0)->plaintext);
- $item['author'] = $this->getInput('username-journals');
- $item['timestamp'] = strtotime(
- $journal->find('span.popup_date', 0)->plaintext);
- $item['content'] = $journal
- ->find('.alt1 table div.no_overflow', 0)
- ->innertext;
-
- $this->items[] = $item;
- }
- }
-
- private function itemsFromJournal($html) {
- $this->setReferrerPolicy($html);
- $item = array();
-
- $item['uri'] = $this->getURI();
-
- $title = $html->find('.journal-title-box .no_overflow', 0)->plaintext;
- $title = html_entity_decode($title);
- $title = trim($title, " \t\n\r\0\x0B" . chr(0xC2) . chr(0xA0));
- $item['title'] = $title;
-
- $item['author'] = $html->find('.journal-title-box a', 0)->plaintext;
- $item['timestamp'] = strtotime(
- $html->find('.journal-title-box span.popup_date', 0)->plaintext);
- $item['content'] = $html->find('.journal-body', 0)->innertext;
-
- $this->items[] = $item;
- }
-
- private function itemsFromSubmissionList($html, $limit) {
- $cache = ($this->getInput('cache') === true);
-
- foreach($html->find('section.gallery figure') as $figure) {
- # allows limit = -1 to mean 'unlimited'
- if($limit-- === 0) break;
-
- $item = array();
-
- $submissionURL = $figure->find('b u a', 0)->href;
- $imgURL = 'https:' . $figure->find('b u a img', 0)->src;
-
- $item['uri'] = $submissionURL;
- $item['title'] = html_entity_decode(
- $figure->find('figcaption p a[href*=/view/]', 0)->title);
- $item['author'] = $figure->find('figcaption p a[href*=/user/]', 0)->title;
-
- if($this->getInput('full') === true) {
- $submissionHTML = $this->getFASimpleHTMLDOM($submissionURL, $cache);
-
- $stats = $submissionHTML->find('.stats-container', 0);
- $item['timestamp'] = strtotime($stats->find('.popup_date', 0)->title);
- $item['enclosures'] = array(
- $submissionHTML->find('.actions a[href^=https://d.facdn]', 0)->href
- );
- foreach($stats->find('#keywords a') as $keyword) {
- $item['categories'][] = $keyword->plaintext;
- }
-
- $previewSrc = $submissionHTML->find('#submissionImg', 0)
- ->{'data-preview-src'};
- if($previewSrc) {
- $imgURL = 'https:' . $previewSrc;
- }
- $description = $submissionHTML
- ->find('.maintable .maintable tr td.alt1', -1);
- $this->setReferrerPolicy($description);
- $description = $description->innertext;
-
- $item['content'] = <<<EOD
+class FurAffinityBridge extends BridgeAbstract
+{
+ const NAME = 'FurAffinity Bridge';
+ const URI = 'https://www.furaffinity.net';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns posts from various sections of FurAffinity';
+ const MAINTAINER = 'Roliga';
+ const PARAMETERS = [
+ 'Search' => [
+ 'q' => [
+ 'name' => 'Query',
+ 'required' => true,
+ 'exampleValue' => 'dog',
+ ],
+ 'rating-general' => [
+ 'name' => 'General',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'rating-mature' => [
+ 'name' => 'Mature',
+ 'type' => 'checkbox',
+ ],
+ 'rating-adult' => [
+ 'name' => 'Adult',
+ 'type' => 'checkbox',
+ ],
+ 'range' => [
+ 'name' => 'Time range',
+ 'type' => 'list',
+ 'values' => [
+ 'A Day' => 'day',
+ '3 Days' => '3days',
+ 'A Week' => 'week',
+ 'A Month' => 'month',
+ 'All time' => 'all'
+ ],
+ 'defaultValue' => 'all'
+ ],
+ 'type-art' => [
+ 'name' => 'Art',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'type-flash' => [
+ 'name' => 'Flash',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'type-photo' => [
+ 'name' => 'Photography',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'type-music' => [
+ 'name' => 'Music',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'type-story' => [
+ 'name' => 'Story',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'type-poetry' => [
+ 'name' => 'Poetry',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'mode' => [
+ 'name' => 'Match mode',
+ 'type' => 'list',
+ 'values' => [
+ 'All of the words' => 'all',
+ 'Any of the words' => 'any',
+ 'Extended' => 'extended'
+ ],
+ 'defaultValue' => 'extended'
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => true,
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ],
+ 'full' => [
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'cache' => [
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ]
+ ],
+ 'Browse' => [
+ 'cat' => [
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => [
+ 'Visual Art' => [
+ 'All' => 1,
+ 'Artwork (Digital)' => 2,
+ 'Artwork (Traditional)' => 3,
+ 'Cellshading' => 4,
+ 'Crafting' => 5,
+ 'Designs' => 6,
+ 'Flash' => 7,
+ 'Fursuiting' => 8,
+ 'Icons' => 9,
+ 'Mosaics' => 10,
+ 'Photography' => 11,
+ 'Sculpting' => 12
+ ],
+ 'Readable Art' => [
+ 'Story' => 13,
+ 'Poetry' => 14,
+ 'Prose' => 15
+ ],
+ 'Audio Art' => [
+ 'Music' => 16,
+ 'Podcasts' => 17
+ ],
+ 'Downloadable' => [
+ 'Skins' => 18,
+ 'Handhelds' => 19,
+ 'Resources' => 20
+ ],
+ 'Other Stuff' => [
+ 'Adoptables' => 21,
+ 'Auctions' => 22,
+ 'Contests' => 23,
+ 'Current Events' => 24,
+ 'Desktops' => 25,
+ 'Stockart' => 26,
+ 'Screenshots' => 27,
+ 'Scraps' => 28,
+ 'Wallpaper' => 29,
+ 'YCH / Sale' => 30,
+ 'Other' => 31
+ ]
+ ],
+ 'defaultValue' => 1
+ ],
+ 'atype' => [
+ 'name' => 'Type',
+ 'type' => 'list',
+ 'values' => [
+ 'General Things' => [
+ 'All' => 1,
+ 'Abstract' => 2,
+ 'Animal related (non-anthro)' => 3,
+ 'Anime' => 4,
+ 'Comics' => 5,
+ 'Doodle' => 6,
+ 'Fanart' => 7,
+ 'Fantasy' => 8,
+ 'Human' => 9,
+ 'Portraits' => 10,
+ 'Scenery' => 11,
+ 'Still Life' => 12,
+ 'Tutorials' => 13,
+ 'Miscellaneous' => 14
+ ],
+ 'Fetish / Furry specialty' => [
+ 'Baby fur' => 101,
+ 'Bondage' => 102,
+ 'Digimon' => 103,
+ 'Fat Furs' => 104,
+ 'Fetish Other' => 105,
+ 'Fursuit' => 106,
+ 'Gore / Macabre Art' => 119,
+ 'Hyper' => 107,
+ 'Inflation' => 108,
+ 'Macro / Micro' => 109,
+ 'Muscle' => 110,
+ 'My Little Pony / Brony' => 111,
+ 'Paw' => 112,
+ 'Pokemon' => 113,
+ 'Pregnancy' => 114,
+ 'Sonic' => 115,
+ 'Transformation' => 116,
+ 'Vore' => 117,
+ 'Water Sports' => 118,
+ 'General Furry Art' => 100
+ ],
+ 'Music' => [
+ 'Techno' => 201,
+ 'Trance' => 202,
+ 'House' => 203,
+ '90s' => 204,
+ '80s' => 205,
+ '70s' => 206,
+ '60s' => 207,
+ 'Pre-60s' => 208,
+ 'Classical' => 209,
+ 'Game Music' => 210,
+ 'Rock' => 211,
+ 'Pop' => 212,
+ 'Rap' => 213,
+ 'Industrial' => 214,
+ 'Other Music' => 200
+ ]
+ ],
+ 'defaultValue' => 1
+ ],
+ 'species' => [
+ 'name' => 'Species',
+ 'type' => 'list',
+ 'values' => [
+ 'Unspecified / Any' => 1,
+ 'Amphibian' => [
+ 'Frog' => 1001,
+ 'Newt' => 1002,
+ 'Salamander' => 1003,
+ 'Amphibian (Other)' => 1000
+ ],
+ 'Aquatic' => [
+ 'Cephalopod' => 2001,
+ 'Dolphin' => 2002,
+ 'Fish' => 2005,
+ 'Porpoise' => 2004,
+ 'Seal' => 6068,
+ 'Shark' => 2006,
+ 'Whale' => 2003,
+ 'Aquatic (Other)' => 2000
+ ],
+ 'Avian' => [
+ 'Corvid' => 3001,
+ 'Crow' => 3002,
+ 'Duck' => 3003,
+ 'Eagle' => 3004,
+ 'Falcon' => 3005,
+ 'Goose' => 3006,
+ 'Gryphon' => 3007,
+ 'Hawk' => 3008,
+ 'Owl' => 3009,
+ 'Phoenix' => 3010,
+ 'Swan' => 3011,
+ 'Avian (Other)' => 3000
+ ],
+ 'Bears &amp; Ursines' => [
+ 'Bear' => 6002
+ ],
+ 'Camelids' => [
+ 'Camel' => 6074,
+ 'Llama' => 6036
+ ],
+ 'Canines &amp; Lupines' => [
+ 'Coyote' => 6008,
+ 'Doberman' => 6009,
+ 'Dog' => 6010,
+ 'Dingo' => 6011,
+ 'German Shepherd' => 6012,
+ 'Jackal' => 6013,
+ 'Husky' => 6014,
+ 'Wolf' => 6016,
+ 'Canine (Other)' => 6017
+ ],
+ 'Cervines' => [
+ 'Cervine (Other)' => 6018
+ ],
+ 'Cows &amp; Bovines' => [
+ 'Antelope' => 6004,
+ 'Cows' => 6003,
+ 'Gazelle' => 6005,
+ 'Goat' => 6006,
+ 'Bovines (General)' => 6007
+ ],
+ 'Dragons' => [
+ 'Eastern Dragon' => 4001,
+ 'Hydra' => 4002,
+ 'Serpent' => 4003,
+ 'Western Dragon' => 4004,
+ 'Wyvern' => 4005,
+ 'Dragon (Other)' => 4000
+ ],
+ 'Equestrians' => [
+ 'Donkey' => 6019,
+ 'Horse' => 6034,
+ 'Pony' => 6073,
+ 'Zebra' => 6071
+ ],
+ 'Exotic &amp; Mythicals' => [
+ 'Argonian' => 5002,
+ 'Chakat' => 5003,
+ 'Chocobo' => 5004,
+ 'Citra' => 5005,
+ 'Crux' => 5006,
+ 'Daemon' => 5007,
+ 'Digimon' => 5008,
+ 'Dracat' => 5009,
+ 'Draenei' => 5010,
+ 'Elf' => 5011,
+ 'Gargoyle' => 5012,
+ 'Iksar' => 5013,
+ 'Kaiju/Monster' => 5015,
+ 'Langurhali' => 5014,
+ 'Moogle' => 5017,
+ 'Naga' => 5016,
+ 'Orc' => 5018,
+ 'Pokemon' => 5019,
+ 'Satyr' => 5020,
+ 'Sergal' => 5021,
+ 'Tanuki' => 5022,
+ 'Unicorn' => 5023,
+ 'Xenomorph' => 5024,
+ 'Alien (Other)' => 5001,
+ 'Exotic (Other)' => 5000
+ ],
+ 'Felines' => [
+ 'Domestic Cat' => 6020,
+ 'Cheetah' => 6021,
+ 'Cougar' => 6022,
+ 'Jaguar' => 6023,
+ 'Leopard' => 6024,
+ 'Lion' => 6025,
+ 'Lynx' => 6026,
+ 'Ocelot' => 6027,
+ 'Panther' => 6028,
+ 'Tiger' => 6029,
+ 'Feline (Other)' => 6030
+ ],
+ 'Insects' => [
+ 'Arachnid' => 8000,
+ 'Mantid' => 8004,
+ 'Scorpion' => 8005,
+ 'Insect (Other)' => 8003
+ ],
+ 'Mammals (Other)' => [
+ 'Bat' => 6001,
+ 'Giraffe' => 6031,
+ 'Hedgehog' => 6032,
+ 'Hippopotamus' => 6033,
+ 'Hyena' => 6035,
+ 'Panda' => 6052,
+ 'Pig/Swine' => 6053,
+ 'Rabbit/Hare' => 6059,
+ 'Raccoon' => 6060,
+ 'Red Panda' => 6062,
+ 'Meerkat' => 6043,
+ 'Mongoose' => 6044,
+ 'Rhinoceros' => 6063,
+ 'Mammals (Other)' => 6000
+ ],
+ 'Marsupials' => [
+ 'Opossum' => 6037,
+ 'Kangaroo' => 6038,
+ 'Koala' => 6039,
+ 'Quoll' => 6040,
+ 'Wallaby' => 6041,
+ 'Marsupial (Other)' => 6042
+ ],
+ 'Mustelids' => [
+ 'Badger' => 6045,
+ 'Ferret' => 6046,
+ 'Mink' => 6048,
+ 'Otter' => 6047,
+ 'Skunk' => 6069,
+ 'Weasel' => 6049,
+ 'Mustelid (Other)' => 6051
+ ],
+ 'Primates' => [
+ 'Gorilla' => 6054,
+ 'Human' => 6055,
+ 'Lemur' => 6056,
+ 'Monkey' => 6057,
+ 'Primate (Other)' => 6058
+ ],
+ 'Reptillian' => [
+ 'Alligator &amp; Crocodile' => 7001,
+ 'Gecko' => 7003,
+ 'Iguana' => 7004,
+ 'Lizard' => 7005,
+ 'Snakes &amp; Serpents' => 7006,
+ 'Turtle' => 7007,
+ 'Reptilian (Other)' => 7000
+ ],
+ 'Rodents' => [
+ 'Beaver' => 6064,
+ 'Mouse' => 6065,
+ 'Rat' => 6061,
+ 'Squirrel' => 6070,
+ 'Rodent (Other)' => 6067
+ ],
+ 'Vulpines' => [
+ 'Fennec' => 6072,
+ 'Fox' => 6075,
+ 'Vulpine (Other)' => 6015
+ ],
+ 'Other' => [
+ 'Dinosaur' => 8001,
+ 'Wolverine' => 6050
+ ]
+ ],
+ 'defaultValue' => 1
+ ],
+ 'gender' => [
+ 'name' => 'Gender',
+ 'type' => 'list',
+ 'values' => [
+ 'Any' => 0,
+ 'Male' => 2,
+ 'Female' => 3,
+ 'Herm' => 4,
+ 'Transgender' => 5,
+ 'Multiple characters' => 6,
+ 'Other / Not Specified' => 7
+ ],
+ 'defaultValue' => 0
+ ],
+ 'rating_general' => [
+ 'name' => 'General',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'rating_mature' => [
+ 'name' => 'Mature',
+ 'type' => 'checkbox',
+ ],
+ 'rating_adult' => [
+ 'name' => 'Adult',
+ 'type' => 'checkbox',
+ ],
+ 'limit-browse' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => true,
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ],
+ 'full' => [
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'cache' => [
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ]
+
+ ],
+ 'Journals' => [
+ 'username-journals' => [
+ 'name' => 'Username',
+ 'required' => true,
+ 'exampleValue' => 'dhw',
+ 'title' => 'Lowercase username as seen in URLs'
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'defaultValue' => -1,
+ 'title' => 'Limit number of journals to return. -1 for unlimited.'
+ ]
+
+ ],
+ 'Single Journal' => [
+ 'journal-id' => [
+ 'name' => 'Journal ID',
+ 'required' => true,
+ 'exampleValue' => '10008853',
+ 'type' => 'number',
+ 'title' => 'Number seen in journal URL'
+ ]
+ ],
+ 'Gallery' => [
+ 'username-gallery' => [
+ 'name' => 'Username',
+ 'required' => true,
+ 'exampleValue' => 'dhw',
+ 'title' => 'Lowercase username as seen in URLs'
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => true,
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ],
+ 'full' => [
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'cache' => [
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ]
+ ],
+ 'Scraps' => [
+ 'username-scraps' => [
+ 'name' => 'Username',
+ 'required' => true,
+ 'exampleValue' => 'dhw',
+ 'title' => 'Lowercase username as seen in URLs'
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => true,
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ],
+ 'full' => [
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'cache' => [
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ]
+ ],
+ 'Favorites' => [
+ 'username-favorites' => [
+ 'name' => 'Username',
+ 'required' => true,
+ 'exampleValue' => 'dhw',
+ 'title' => 'Lowercase username as seen in URLs'
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => true,
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ],
+ 'full' => [
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'cache' => [
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ]
+ ],
+ 'Gallery Folder' => [
+ 'username-folder' => [
+ 'name' => 'Username',
+ 'required' => true,
+ 'exampleValue' => 'kopk',
+ 'title' => 'Lowercase username as seen in URLs'
+ ],
+ 'folder-id' => [
+ 'name' => 'Folder ID',
+ 'required' => true,
+ 'exampleValue' => '1031990',
+ 'type' => 'number',
+ 'title' => 'Number seen in folder URL'
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => true,
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ],
+ 'full' => [
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'cache' => [
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ]
+ ]
+ ];
+
+ /*
+ * This was aquired by creating a new user on FA then
+ * extracting the cookie from the browsers dev console.
+ */
+ const FA_AUTH_COOKIE = 'b=4ce65691-b50f-4742-a990-bf28d6de16ee; a=ca6e4566-9d81-4263-9444-653b142e35f8';
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ // Single journal
+ $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journal\/(\d+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['journal-id'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ // Journals
+ $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journals\/([^\/&?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['username-journals'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ // Gallery folder
+ $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/gallery\/([^\/&?\n]+)\/folder\/(\d+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['username-folder'] = urldecode($matches[3]);
+ $params['folder-id'] = urldecode($matches[4]);
+ $params['full'] = 'on';
+ return $params;
+ }
+
+ // Gallery (must be after gallery folder)
+ $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/(gallery|scraps|favorites)\/([^\/&?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['username-' . $matches[3]] = urldecode($matches[4]);
+ $params['full'] = 'on';
+ return $params;
+ }
+
+ return null;
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Search':
+ return 'Search For '
+ . $this->getInput('q');
+ case 'Browse':
+ return 'Browse';
+ case 'Journals':
+ return $this->getInput('username-journals');
+ case 'Single Journal':
+ return 'Journal '
+ . $this->getInput('journal-id');
+ case 'Gallery':
+ return $this->getInput('username-gallery');
+ case 'Scraps':
+ return $this->getInput('username-scraps');
+ case 'Favorites':
+ return $this->getInput('username-favorites');
+ case 'Gallery Folder':
+ return $this->getInput('username-folder')
+ . '\'s Folder '
+ . $this->getInput('folder-id');
+ default:
+ return parent::getName();
+ }
+ }
+
+ public function getDescription()
+ {
+ switch ($this->queriedContext) {
+ case 'Search':
+ return 'FurAffinity Search For '
+ . $this->getInput('q');
+ case 'Browse':
+ return 'FurAffinity Browse';
+ case 'Journals':
+ return 'FurAffinity Journals By '
+ . $this->getInput('username-journals');
+ case 'Single Journal':
+ return 'FurAffinity Journal '
+ . $this->getInput('journal-id');
+ case 'Gallery':
+ return 'FurAffinity Gallery By '
+ . $this->getInput('username-gallery');
+ case 'Scraps':
+ return 'FurAffinity Scraps By '
+ . $this->getInput('username-scraps');
+ case 'Favorites':
+ return 'FurAffinity Favorites By '
+ . $this->getInput('username-favorites');
+ case 'Gallery Folder':
+ return 'FurAffinity Gallery Folder '
+ . $this->getInput('folder-id')
+ . ' By '
+ . $this->getInput('username-folder');
+ default:
+ return parent::getDescription();
+ }
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Search':
+ return self::URI
+ . '/search';
+ case 'Browse':
+ return self::URI
+ . '/browse';
+ case 'Journals':
+ return self::URI
+ . '/journals/'
+ . $this->getInput('username-journals');
+ case 'Single Journal':
+ return self::URI
+ . '/journal/'
+ . $this->getInput('journal-id');
+ case 'Gallery':
+ return self::URI
+ . '/gallery/'
+ . $this->getInput('username-gallery');
+ case 'Scraps':
+ return self::URI
+ . '/scraps/'
+ . $this->getInput('username-scraps');
+ case 'Favorites':
+ return self::URI
+ . '/favorites/'
+ . $this->getInput('username-favorites');
+ case 'Gallery Folder':
+ return self::URI
+ . '/gallery/'
+ . $this->getInput('username-folder')
+ . '/folder/'
+ . $this->getInput('folder-id');
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function collectData()
+ {
+ switch ($this->queriedContext) {
+ case 'Search':
+ $data = [
+ 'q' => $this->getInput('q'),
+ 'perpage' => 72,
+ 'rating-general' => ($this->getInput('rating-general') === true ? 'on' : 0),
+ 'rating-mature' => ($this->getInput('rating-mature') === true ? 'on' : 0),
+ 'rating-adult' => ($this->getInput('rating-adult') === true ? 'on' : 0),
+ 'range' => $this->getInput('range'),
+ 'type-art' => ($this->getInput('type-art') === true ? 'on' : 0),
+ 'type-flash' => ($this->getInput('type-flash') === true ? 'on' : 0),
+ 'type-photo' => ($this->getInput('type-photo') === true ? 'on' : 0),
+ 'type-music' => ($this->getInput('type-music') === true ? 'on' : 0),
+ 'type-story' => ($this->getInput('type-story') === true ? 'on' : 0),
+ 'type-poetry' => ($this->getInput('type-poetry') === true ? 'on' : 0),
+ 'mode' => $this->getInput('mode')
+ ];
+ $html = $this->postFASimpleHTMLDOM($data);
+ $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10);
+ $this->itemsFromSubmissionList($html, $limit);
+ break;
+ case 'Browse':
+ $data = [
+ 'cat' => $this->getInput('cat'),
+ 'atype' => $this->getInput('atype'),
+ 'species' => $this->getInput('species'),
+ 'gender' => $this->getInput('gender'),
+ 'perpage' => 72,
+ 'rating_general' => ($this->getInput('rating_general') === true ? 'on' : 0),
+ 'rating_mature' => ($this->getInput('rating_mature') === true ? 'on' : 0),
+ 'rating_adult' => ($this->getInput('rating_adult') === true ? 'on' : 0)
+ ];
+ $html = $this->postFASimpleHTMLDOM($data);
+ $limit = (is_int($this->getInput('limit-browse')) ? $this->getInput('limit-browse') : 10);
+ $this->itemsFromSubmissionList($html, $limit);
+ break;
+ case 'Journals':
+ $html = $this->getFASimpleHTMLDOM($this->getURI());
+ $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : -1);
+ $this->itemsFromJournalList($html, $limit);
+ break;
+ case 'Single Journal':
+ $html = $this->getFASimpleHTMLDOM($this->getURI());
+ $this->itemsFromJournal($html);
+ break;
+ case 'Gallery':
+ case 'Scraps':
+ case 'Favorites':
+ case 'Gallery Folder':
+ $html = $this->getFASimpleHTMLDOM($this->getURI());
+ $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10);
+ $this->itemsFromSubmissionList($html, $limit);
+ break;
+ }
+ }
+
+ private function postFASimpleHTMLDOM($data)
+ {
+ $opts = [
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_POSTFIELDS => http_build_query($data)
+ ];
+ $header = [
+ 'Host: ' . parse_url(self::URI, PHP_URL_HOST),
+ 'Content-Type: application/x-www-form-urlencoded',
+ 'Cookie: ' . self::FA_AUTH_COOKIE
+ ];
+
+ $html = getSimpleHTMLDOM($this->getURI(), $header, $opts);
+ $html = defaultLinkTo($html, $this->getURI());
+
+ return $html;
+ }
+
+ private function getFASimpleHTMLDOM($url, $cache = false)
+ {
+ $header = [
+ 'Cookie: ' . self::FA_AUTH_COOKIE
+ ];
+
+ if ($cache) {
+ $html = getSimpleHTMLDOMCached($url, 86400, $header); // 24 hours
+ } else {
+ $html = getSimpleHTMLDOM($url, $header);
+ }
+
+ $html = defaultLinkTo($html, $url);
+
+ return $html;
+ }
+
+ private function itemsFromJournalList($html, $limit)
+ {
+ foreach ($html->find('table[id^=jid:]') as $journal) {
+ # allows limit = -1 to mean 'unlimited'
+ if ($limit-- === 0) {
+ break;
+ }
+
+ $item = [];
+
+ $this->setReferrerPolicy($journal);
+
+ $item['uri'] = $journal->find('a', 0)->href;
+ $item['title'] = html_entity_decode($journal->find('a', 0)->plaintext);
+ $item['author'] = $this->getInput('username-journals');
+ $item['timestamp'] = strtotime(
+ $journal->find('span.popup_date', 0)->plaintext
+ );
+ $item['content'] = $journal
+ ->find('.alt1 table div.no_overflow', 0)
+ ->innertext;
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function itemsFromJournal($html)
+ {
+ $this->setReferrerPolicy($html);
+ $item = [];
+
+ $item['uri'] = $this->getURI();
+
+ $title = $html->find('.journal-title-box .no_overflow', 0)->plaintext;
+ $title = html_entity_decode($title);
+ $title = trim($title, " \t\n\r\0\x0B" . chr(0xC2) . chr(0xA0));
+ $item['title'] = $title;
+
+ $item['author'] = $html->find('.journal-title-box a', 0)->plaintext;
+ $item['timestamp'] = strtotime(
+ $html->find('.journal-title-box span.popup_date', 0)->plaintext
+ );
+ $item['content'] = $html->find('.journal-body', 0)->innertext;
+
+ $this->items[] = $item;
+ }
+
+ private function itemsFromSubmissionList($html, $limit)
+ {
+ $cache = ($this->getInput('cache') === true);
+
+ foreach ($html->find('section.gallery figure') as $figure) {
+ # allows limit = -1 to mean 'unlimited'
+ if ($limit-- === 0) {
+ break;
+ }
+
+ $item = [];
+
+ $submissionURL = $figure->find('b u a', 0)->href;
+ $imgURL = 'https:' . $figure->find('b u a img', 0)->src;
+
+ $item['uri'] = $submissionURL;
+ $item['title'] = html_entity_decode(
+ $figure->find('figcaption p a[href*=/view/]', 0)->title
+ );
+ $item['author'] = $figure->find('figcaption p a[href*=/user/]', 0)->title;
+
+ if ($this->getInput('full') === true) {
+ $submissionHTML = $this->getFASimpleHTMLDOM($submissionURL, $cache);
+
+ $stats = $submissionHTML->find('.stats-container', 0);
+ $item['timestamp'] = strtotime($stats->find('.popup_date', 0)->title);
+ $item['enclosures'] = [
+ $submissionHTML->find('.actions a[href^=https://d.facdn]', 0)->href
+ ];
+ foreach ($stats->find('#keywords a') as $keyword) {
+ $item['categories'][] = $keyword->plaintext;
+ }
+
+ $previewSrc = $submissionHTML->find('#submissionImg', 0)
+ ->{'data-preview-src'};
+ if ($previewSrc) {
+ $imgURL = 'https:' . $previewSrc;
+ }
+
+ $description = $submissionHTML
+ ->find('.maintable .maintable tr td.alt1', -1);
+ $this->setReferrerPolicy($description);
+ $description = $description->innertext;
+
+ $item['content'] = <<<EOD
<a href="$submissionURL">
<img src="{$imgURL}" referrerpolicy="no-referrer" />
</a>
@@ -905,27 +927,28 @@ class FurAffinityBridge extends BridgeAbstract {
{$description}
</p>
EOD;
- } else {
- $item['content'] = <<<EOD
+ } else {
+ $item['content'] = <<<EOD
<a href="$submissionURL">
<img src="$imgURL" referrerpolicy="no-referrer" />
</a>
EOD;
- }
-
- $this->items[] = $item;
- }
- }
-
- private function setReferrerPolicy(&$html) {
- foreach($html->find('img') as $img) {
- /*
- * Note: Without the no-referrer policy their CDN sometimes denies requests.
- * We can't control this for enclosures sadly.
- * At least tt-rss adds the referrerpolicy on its own.
- * Alternatively we could not use https for images, but that's not ideal.
- */
- $img->referrerpolicy = 'no-referrer';
- }
- }
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function setReferrerPolicy(&$html)
+ {
+ foreach ($html->find('img') as $img) {
+ /*
+ * Note: Without the no-referrer policy their CDN sometimes denies requests.
+ * We can't control this for enclosures sadly.
+ * At least tt-rss adds the referrerpolicy on its own.
+ * Alternatively we could not use https for images, but that's not ideal.
+ */
+ $img->referrerpolicy = 'no-referrer';
+ }
+ }
}
diff --git a/bridges/FurAffinityUserBridge.php b/bridges/FurAffinityUserBridge.php
index d9214b84..fa10d7ae 100644
--- a/bridges/FurAffinityUserBridge.php
+++ b/bridges/FurAffinityUserBridge.php
@@ -1,58 +1,63 @@
<?php
-class FurAffinityUserBridge extends BridgeAbstract {
- const NAME = 'FurAffinity User Gallery';
- const URI = 'https://www.furaffinity.net';
- const MAINTAINER = 'CyberJacob';
- const DESCRIPTION = 'See https://rss-bridge.github.io/rss-bridge/Bridge_Specific/Furaffinityuser.html for explanation';
- const PARAMETERS = array(
- array(
- 'searchUsername' => array(
- 'name' => 'Search Username',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Username to fetch the gallery for',
- 'exampleValue' => 'armundy',
- ),
- 'aCookie' => array(
- 'name' => 'Login cookie \'a\'',
- 'type' => 'text',
- 'required' => true
- ),
- 'bCookie' => array(
- 'name' => 'Login cookie \'b\'',
- 'type' => 'text',
- 'required' => true
- )
- )
- );
-
- public function collectData() {
- $opt = array(CURLOPT_COOKIE => 'b=' . $this->getInput('bCookie') . '; a=' . $this->getInput('aCookie'));
-
- $url = self::URI . '/gallery/' . $this->getInput('searchUsername');
-
- $html = getSimpleHTMLDOM($url, array(), $opt)
- or returnServerError('Could not load the user\'s gallery page.');
-
- $submissions = $html->find('section[id=gallery-gallery]', 0)->find('figure');
- foreach($submissions as $submission) {
- $item = array();
- $item['title'] = $submission->find('figcaption', 0)->find('a', 0)->plaintext;
-
- $thumbnail = $submission->find('a', 0);
- $thumbnail->href = self::URI . $thumbnail->href;
-
- $item['content'] = $submission->find('a', 0);
-
- $this->items[] = $item;
- }
- }
-
- public function getName() {
- return self::NAME . ' for ' . $this->getInput('searchUsername');
- }
-
- public function getURI() {
- return self::URI . '/user/' . $this->getInput('searchUsername');
- }
+
+class FurAffinityUserBridge extends BridgeAbstract
+{
+ const NAME = 'FurAffinity User Gallery';
+ const URI = 'https://www.furaffinity.net';
+ const MAINTAINER = 'CyberJacob';
+ const DESCRIPTION = 'See https://rss-bridge.github.io/rss-bridge/Bridge_Specific/Furaffinityuser.html for explanation';
+ const PARAMETERS = [
+ [
+ 'searchUsername' => [
+ 'name' => 'Search Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Username to fetch the gallery for',
+ 'exampleValue' => 'armundy',
+ ],
+ 'aCookie' => [
+ 'name' => 'Login cookie \'a\'',
+ 'type' => 'text',
+ 'required' => true
+ ],
+ 'bCookie' => [
+ 'name' => 'Login cookie \'b\'',
+ 'type' => 'text',
+ 'required' => true
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $opt = [CURLOPT_COOKIE => 'b=' . $this->getInput('bCookie') . '; a=' . $this->getInput('aCookie')];
+
+ $url = self::URI . '/gallery/' . $this->getInput('searchUsername');
+
+ $html = getSimpleHTMLDOM($url, [], $opt)
+ or returnServerError('Could not load the user\'s gallery page.');
+
+ $submissions = $html->find('section[id=gallery-gallery]', 0)->find('figure');
+ foreach ($submissions as $submission) {
+ $item = [];
+ $item['title'] = $submission->find('figcaption', 0)->find('a', 0)->plaintext;
+
+ $thumbnail = $submission->find('a', 0);
+ $thumbnail->href = self::URI . $thumbnail->href;
+
+ $item['content'] = $submission->find('a', 0);
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName()
+ {
+ return self::NAME . ' for ' . $this->getInput('searchUsername');
+ }
+
+ public function getURI()
+ {
+ return self::URI . '/user/' . $this->getInput('searchUsername');
+ }
}
diff --git a/bridges/FuturaSciencesBridge.php b/bridges/FuturaSciencesBridge.php
index 541c4bac..97926bed 100644
--- a/bridges/FuturaSciencesBridge.php
+++ b/bridges/FuturaSciencesBridge.php
@@ -1,160 +1,170 @@
<?php
-class FuturaSciencesBridge extends FeedExpander {
- const MAINTAINER = 'ORelio';
- const NAME = 'Futura-Sciences Bridge';
- const URI = 'https://www.futura-sciences.com/';
- const DESCRIPTION = 'Returns the newest articles.';
+class FuturaSciencesBridge extends FeedExpander
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = 'Futura-Sciences Bridge';
+ const URI = 'https://www.futura-sciences.com/';
+ const DESCRIPTION = 'Returns the newest articles.';
- const PARAMETERS = array( array(
- 'feed' => array(
- 'name' => 'Feed',
- 'type' => 'list',
- 'values' => array(
- 'Les flux multi-magazines' => array(
- 'Les dernières actualités de Futura-Sciences' => 'actualites',
- 'Les dernières définitions de Futura-Sciences' => 'definitions',
- 'Les dernières photos de Futura-Sciences' => 'photos',
- 'Les dernières questions - réponses de Futura-Sciences' => 'questions-reponses',
- 'Les derniers dossiers de Futura-Sciences' => 'dossiers'
- ),
- 'Les flux Services' => array(
- 'Les cartes virtuelles de Futura-Sciences' => 'services/cartes-virtuelles',
- 'Les fonds d\'écran de Futura-Sciences' => 'services/fonds-ecran'
- ),
- 'Les flux Santé' => array(
- 'Les dernières actualités de Futura-Santé' => 'sante/actualites',
- 'Les dernières définitions de Futura-Santé' => 'sante/definitions',
- 'Les dernières questions-réponses de Futura-Santé' => 'sante/question-reponses',
- 'Les derniers dossiers de Futura-Santé' => 'sante/dossiers'
- ),
- 'Les flux High-Tech' => array(
- 'Les dernières actualités de Futura-High-Tech' => 'high-tech/actualites',
- 'Les dernières astuces de Futura-High-Tech' => 'high-tech/question-reponses',
- 'Les dernières définitions de Futura-High-Tech' => 'high-tech/definitions',
- 'Les derniers dossiers de Futura-High-Tech' => 'high-tech/dossiers'
- ),
- 'Les flux Espace' => array(
- 'Les dernières actualités de Futura-Espace' => 'espace/actualites',
- 'Les dernières définitions de Futura-Espace' => 'espace/definitions',
- 'Les dernières questions-réponses de Futura-Espace' => 'espace/question-reponses',
- 'Les derniers dossiers de Futura-Espace' => 'espace/dossiers'
- ),
- 'Les flux Environnement' => array(
- 'Les dernières actualités de Futura-Environnement' => 'environnement/actualites',
- 'Les dernières définitions de Futura-Environnement' => 'environnement/definitions',
- 'Les dernières questions-réponses de Futura-Environnement' => 'environnement/question-reponses',
- 'Les derniers dossiers de Futura-Environnement' => 'environnement/dossiers'
- ),
- 'Les flux Maison' => array(
- 'Les dernières actualités de Futura-Maison' => 'maison/actualites',
- 'Les dernières astuces de Futura-Maison' => 'maison/question-reponses',
- 'Les dernières définitions de Futura-Maison' => 'maison/definitions',
- 'Les derniers dossiers de Futura-Maison' => 'maison/dossiers'
- ),
- 'Les flux Nature' => array(
- 'Les dernières actualités de Futura-Nature' => 'nature/actualites',
- 'Les dernières définitions de Futura-Nature' => 'nature/definitions',
- 'Les dernières questions-réponses de Futura-Nature' => 'nature/question-reponses',
- 'Les derniers dossiers de Futura-Nature' => 'nature/dossiers'
- ),
- 'Les flux Terre' => array(
- 'Les dernières actualités de Futura-Terre' => 'terre/actualites',
- 'Les dernières définitions de Futura-Terre' => 'terre/definitions',
- 'Les dernières questions-réponses de Futura-Terre' => 'terre/question-reponses',
- 'Les derniers dossiers de Futura-Terre' => 'terre/dossiers'
- ),
- 'Les flux Matière' => array(
- 'Les dernières actualités de Futura-Matière' => 'matiere/actualites',
- 'Les dernières définitions de Futura-Matière' => 'matiere/definitions',
- 'Les dernières questions-réponses de Futura-Matière' => 'matiere/question-reponses',
- 'Les derniers dossiers de Futura-Matière' => 'matiere/dossiers'
- ),
- 'Les flux Mathématiques' => array(
- 'Les dernières actualités de Futura-Mathématiques' => 'mathematiques/actualites',
- 'Les derniers dossiers de Futura-Mathématiques' => 'mathematiques/dossiers'
- )
- )
- )
- ));
+ const PARAMETERS = [ [
+ 'feed' => [
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'values' => [
+ 'Les flux multi-magazines' => [
+ 'Les dernières actualités de Futura-Sciences' => 'actualites',
+ 'Les dernières définitions de Futura-Sciences' => 'definitions',
+ 'Les dernières photos de Futura-Sciences' => 'photos',
+ 'Les dernières questions - réponses de Futura-Sciences' => 'questions-reponses',
+ 'Les derniers dossiers de Futura-Sciences' => 'dossiers'
+ ],
+ 'Les flux Services' => [
+ 'Les cartes virtuelles de Futura-Sciences' => 'services/cartes-virtuelles',
+ 'Les fonds d\'écran de Futura-Sciences' => 'services/fonds-ecran'
+ ],
+ 'Les flux Santé' => [
+ 'Les dernières actualités de Futura-Santé' => 'sante/actualites',
+ 'Les dernières définitions de Futura-Santé' => 'sante/definitions',
+ 'Les dernières questions-réponses de Futura-Santé' => 'sante/question-reponses',
+ 'Les derniers dossiers de Futura-Santé' => 'sante/dossiers'
+ ],
+ 'Les flux High-Tech' => [
+ 'Les dernières actualités de Futura-High-Tech' => 'high-tech/actualites',
+ 'Les dernières astuces de Futura-High-Tech' => 'high-tech/question-reponses',
+ 'Les dernières définitions de Futura-High-Tech' => 'high-tech/definitions',
+ 'Les derniers dossiers de Futura-High-Tech' => 'high-tech/dossiers'
+ ],
+ 'Les flux Espace' => [
+ 'Les dernières actualités de Futura-Espace' => 'espace/actualites',
+ 'Les dernières définitions de Futura-Espace' => 'espace/definitions',
+ 'Les dernières questions-réponses de Futura-Espace' => 'espace/question-reponses',
+ 'Les derniers dossiers de Futura-Espace' => 'espace/dossiers'
+ ],
+ 'Les flux Environnement' => [
+ 'Les dernières actualités de Futura-Environnement' => 'environnement/actualites',
+ 'Les dernières définitions de Futura-Environnement' => 'environnement/definitions',
+ 'Les dernières questions-réponses de Futura-Environnement' => 'environnement/question-reponses',
+ 'Les derniers dossiers de Futura-Environnement' => 'environnement/dossiers'
+ ],
+ 'Les flux Maison' => [
+ 'Les dernières actualités de Futura-Maison' => 'maison/actualites',
+ 'Les dernières astuces de Futura-Maison' => 'maison/question-reponses',
+ 'Les dernières définitions de Futura-Maison' => 'maison/definitions',
+ 'Les derniers dossiers de Futura-Maison' => 'maison/dossiers'
+ ],
+ 'Les flux Nature' => [
+ 'Les dernières actualités de Futura-Nature' => 'nature/actualites',
+ 'Les dernières définitions de Futura-Nature' => 'nature/definitions',
+ 'Les dernières questions-réponses de Futura-Nature' => 'nature/question-reponses',
+ 'Les derniers dossiers de Futura-Nature' => 'nature/dossiers'
+ ],
+ 'Les flux Terre' => [
+ 'Les dernières actualités de Futura-Terre' => 'terre/actualites',
+ 'Les dernières définitions de Futura-Terre' => 'terre/definitions',
+ 'Les dernières questions-réponses de Futura-Terre' => 'terre/question-reponses',
+ 'Les derniers dossiers de Futura-Terre' => 'terre/dossiers'
+ ],
+ 'Les flux Matière' => [
+ 'Les dernières actualités de Futura-Matière' => 'matiere/actualites',
+ 'Les dernières définitions de Futura-Matière' => 'matiere/definitions',
+ 'Les dernières questions-réponses de Futura-Matière' => 'matiere/question-reponses',
+ 'Les derniers dossiers de Futura-Matière' => 'matiere/dossiers'
+ ],
+ 'Les flux Mathématiques' => [
+ 'Les dernières actualités de Futura-Mathématiques' => 'mathematiques/actualites',
+ 'Les derniers dossiers de Futura-Mathématiques' => 'mathematiques/dossiers'
+ ]
+ ]
+ ]
+ ]];
- public function collectData(){
- $url = self::URI . 'rss/' . $this->getInput('feed') . '.xml';
- $this->collectExpandableDatas($url, 10);
- }
+ public function collectData()
+ {
+ $url = self::URI . 'rss/' . $this->getInput('feed') . '.xml';
+ $this->collectExpandableDatas($url, 10);
+ }
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- $item['uri'] = str_replace('#xtor%3DRSS-8', '', $item['uri']);
- $article = getSimpleHTMLDOMCached($item['uri']);
- $item['content'] = $this->extractArticleContent($article);
- $author = $this->extractAuthor($article);
- if (!empty($author))
- $item['author'] = $author;
- return $item;
- }
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ $item['uri'] = str_replace('#xtor%3DRSS-8', '', $item['uri']);
+ $article = getSimpleHTMLDOMCached($item['uri']);
+ $item['content'] = $this->extractArticleContent($article);
+ $author = $this->extractAuthor($article);
+ if (!empty($author)) {
+ $item['author'] = $author;
+ }
+ return $item;
+ }
- private function extractArticleContent($article){
- $contents = $article->find('section.article-text', 1);
+ private function extractArticleContent($article)
+ {
+ $contents = $article->find('section.article-text', 1);
- foreach($contents->find('img') as $img) {
- if(!empty($img->getAttribute('data-src'))) {
- $img->src = $img->getAttribute('data-src');
- }
- }
+ foreach ($contents->find('img') as $img) {
+ if (!empty($img->getAttribute('data-src'))) {
+ $img->src = $img->getAttribute('data-src');
+ }
+ }
- foreach($contents->find('a.tooltip-link') as $a) {
- $a->outertext = $a->plaintext;
- }
+ foreach ($contents->find('a.tooltip-link') as $a) {
+ $a->outertext = $a->plaintext;
+ }
- foreach(array(
- 'clear',
- 'sharebar2',
- 'diaporamafullscreen',
- 'module.social-button',
- 'module.social-share',
- 'ficheprevnext',
- 'addthis_toolbox',
- 'noprint',
- 'hubbottom',
- 'hubbottom2'
- ) as $div_class_remove) {
- foreach($contents->find('div.' . $div_class_remove) as $div) {
- $keep_div = false;
- foreach(array(
- 'didyouknow'
- ) as $div_class_dont_remove) {
- if(strpos($div->getAttribute('class'), $div_class_dont_remove) !== false) {
- $keep_div = true;
- }
- }
- if(!$keep_div) {
- $div->outertext = '';
- }
- }
- }
+ foreach (
+ [
+ 'clear',
+ 'sharebar2',
+ 'diaporamafullscreen',
+ 'module.social-button',
+ 'module.social-share',
+ 'ficheprevnext',
+ 'addthis_toolbox',
+ 'noprint',
+ 'hubbottom',
+ 'hubbottom2'
+ ] as $div_class_remove
+ ) {
+ foreach ($contents->find('div.' . $div_class_remove) as $div) {
+ $keep_div = false;
+ foreach (
+ [
+ 'didyouknow'
+ ] as $div_class_dont_remove
+ ) {
+ if (strpos($div->getAttribute('class'), $div_class_dont_remove) !== false) {
+ $keep_div = true;
+ }
+ }
+ if (!$keep_div) {
+ $div->outertext = '';
+ }
+ }
+ }
- $contents = $contents->innertext;
+ $contents = $contents->innertext;
- $contents = stripWithDelimiters($contents, '<hr ', '/>');
- $contents = stripWithDelimiters($contents, '<p class="content-date', '</p>');
- $contents = stripWithDelimiters($contents, '<h1 class="content-title', '</h1>');
- $contents = stripWithDelimiters($contents, 'fs:definition="', '"');
- $contents = stripWithDelimiters($contents, 'fs:xt:clicktype="', '"');
- $contents = stripWithDelimiters($contents, 'fs:xt:clickname="', '"');
- $contents = StripWithDelimiters($contents, '<section class="module-toretain module-propal-nl', '</section>');
- $contents = stripWithDelimiters($contents, '<script ', '</script>');
- $contents = stripWithDelimiters($contents, '<script>', '</script>');
+ $contents = stripWithDelimiters($contents, '<hr ', '/>');
+ $contents = stripWithDelimiters($contents, '<p class="content-date', '</p>');
+ $contents = stripWithDelimiters($contents, '<h1 class="content-title', '</h1>');
+ $contents = stripWithDelimiters($contents, 'fs:definition="', '"');
+ $contents = stripWithDelimiters($contents, 'fs:xt:clicktype="', '"');
+ $contents = stripWithDelimiters($contents, 'fs:xt:clickname="', '"');
+ $contents = StripWithDelimiters($contents, '<section class="module-toretain module-propal-nl', '</section>');
+ $contents = stripWithDelimiters($contents, '<script ', '</script>');
+ $contents = stripWithDelimiters($contents, '<script>', '</script>');
- return trim($contents);
- }
+ return trim($contents);
+ }
- // Extracts the author from an article or element
- private function extractAuthor($article){
- $article_author = $article->find('h3.epsilon', 0);
- if($article_author) {
- return trim(str_replace(', Futura-Sciences', '', $article_author->plaintext));
- }
- return '';
- }
+ // Extracts the author from an article or element
+ private function extractAuthor($article)
+ {
+ $article_author = $article->find('h3.epsilon', 0);
+ if ($article_author) {
+ return trim(str_replace(', Futura-Sciences', '', $article_author->plaintext));
+ }
+ return '';
+ }
}
diff --git a/bridges/GBAtempBridge.php b/bridges/GBAtempBridge.php
index 79fe313f..98cafe7d 100644
--- a/bridges/GBAtempBridge.php
+++ b/bridges/GBAtempBridge.php
@@ -1,148 +1,156 @@
<?php
-class GBAtempBridge extends BridgeAbstract {
- const MAINTAINER = 'ORelio';
- const NAME = 'GBAtemp';
- const URI = 'https://gbatemp.net/';
- const DESCRIPTION = 'GBAtemp is a user friendly underground video game community.';
+class GBAtempBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = 'GBAtemp';
+ const URI = 'https://gbatemp.net/';
+ const DESCRIPTION = 'GBAtemp is a user friendly underground video game community.';
- const PARAMETERS = array( array(
- 'type' => array(
- 'name' => 'Type',
- 'type' => 'list',
- 'values' => array(
- 'News' => 'N',
- 'Reviews' => 'R',
- 'Tutorials' => 'T',
- 'Forum' => 'F'
- )
- )
- ));
+ const PARAMETERS = [ [
+ 'type' => [
+ 'name' => 'Type',
+ 'type' => 'list',
+ 'values' => [
+ 'News' => 'N',
+ 'Reviews' => 'R',
+ 'Tutorials' => 'T',
+ 'Forum' => 'F'
+ ]
+ ]
+ ]];
- private function buildItem($uri, $title, $author, $timestamp, $thumbnail, $content){
- $item = array();
- $item['uri'] = $uri;
- $item['title'] = $title;
- $item['author'] = $author;
- $item['timestamp'] = $timestamp;
- $item['content'] = $content;
- if (!empty($thumbnail)) {
- $item['enclosures'] = array($thumbnail);
- }
- return $item;
- }
+ private function buildItem($uri, $title, $author, $timestamp, $thumbnail, $content)
+ {
+ $item = [];
+ $item['uri'] = $uri;
+ $item['title'] = $title;
+ $item['author'] = $author;
+ $item['timestamp'] = $timestamp;
+ $item['content'] = $content;
+ if (!empty($thumbnail)) {
+ $item['enclosures'] = [$thumbnail];
+ }
+ return $item;
+ }
- private function decodeHtmlEntities($text) {
- $text = html_entity_decode($text);
- $convmap = array(0x0, 0x2FFFF, 0, 0xFFFF);
- return trim(mb_decode_numericentity($text, $convmap, 'UTF-8'));
- }
+ private function decodeHtmlEntities($text)
+ {
+ $text = html_entity_decode($text);
+ $convmap = [0x0, 0x2FFFF, 0, 0xFFFF];
+ return trim(mb_decode_numericentity($text, $convmap, 'UTF-8'));
+ }
- private function cleanupPostContent($content, $site_url){
- $content = defaultLinkTo($content, self::URI);
- $content = stripWithDelimiters($content, '<script', '</script>');
- $content = stripWithDelimiters($content, '<svg', '</svg>');
- $content = stripRecursiveHTMLSection($content, 'div', '<div class="reactionsBar');
- return $this->decodeHtmlEntities($content);
- }
+ private function cleanupPostContent($content, $site_url)
+ {
+ $content = defaultLinkTo($content, self::URI);
+ $content = stripWithDelimiters($content, '<script', '</script>');
+ $content = stripWithDelimiters($content, '<svg', '</svg>');
+ $content = stripRecursiveHTMLSection($content, 'div', '<div class="reactionsBar');
+ return $this->decodeHtmlEntities($content);
+ }
- private function findItemDate($item){
- $time = 0;
- $dateField = $item->find('time', 0);
- if (is_object($dateField)) {
- $time = strtotime($dateField->datetime);
- }
- return $time;
- }
+ private function findItemDate($item)
+ {
+ $time = 0;
+ $dateField = $item->find('time', 0);
+ if (is_object($dateField)) {
+ $time = strtotime($dateField->datetime);
+ }
+ return $time;
+ }
- private function findItemImage($item, $selector){
- $img = extractFromDelimiters($item->find($selector, 0)->style, 'url(', ')');
- $paramPos = strpos($img, '?');
- if ($paramPos !== false) {
- $img = substr($img, 0, $paramPos);
- }
- if (!str_ends_with($img, '.png') && !str_ends_with($img, '.jpg')) {
- $img = $img . '#.image';
- }
- return urljoin(self::URI, $img);
- }
+ private function findItemImage($item, $selector)
+ {
+ $img = extractFromDelimiters($item->find($selector, 0)->style, 'url(', ')');
+ $paramPos = strpos($img, '?');
+ if ($paramPos !== false) {
+ $img = substr($img, 0, $paramPos);
+ }
+ if (!str_ends_with($img, '.png') && !str_ends_with($img, '.jpg')) {
+ $img = $img . '#.image';
+ }
+ return urljoin(self::URI, $img);
+ }
- private function fetchPostContent($uri, $site_url){
- $html = getSimpleHTMLDOMCached($uri);
- if(!$html) {
- return 'Could not request GBAtemp: ' . $uri;
- }
+ private function fetchPostContent($uri, $site_url)
+ {
+ $html = getSimpleHTMLDOMCached($uri);
+ if (!$html) {
+ return 'Could not request GBAtemp: ' . $uri;
+ }
- $content = $html->find('article.message-body', 0)->innertext;
- return $this->cleanupPostContent($content, $site_url);
- }
+ $content = $html->find('article.message-body', 0)->innertext;
+ return $this->cleanupPostContent($content, $site_url);
+ }
- public function collectData(){
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
- $html = getSimpleHTMLDOM(self::URI);
+ switch ($this->getInput('type')) {
+ case 'N':
+ foreach ($html->find('li.news_item.full') as $newsItem) {
+ $url = urljoin(self::URI, $newsItem->find('a', 0)->href);
+ $img = $this->findItemImage($newsItem, 'a.news_image');
+ $time = $this->findItemDate($newsItem);
+ $author = $newsItem->find('a.username', 0)->plaintext;
+ $title = $this->decodeHtmlEntities($newsItem->find('h3.news_title', 0)->plaintext);
+ $content = $this->fetchPostContent($url, self::URI);
+ $this->items[] = $this->buildItem($url, $title, $author, $time, $img, $content);
+ unset($newsItem); // Some items are heavy, freeing the item proactively helps saving memory
+ }
+ break;
+ case 'R':
+ foreach ($html->find('li.portal_review') as $reviewItem) {
+ $url = urljoin(self::URI, $reviewItem->find('a.review_boxart', 0)->href);
+ $img = $this->findItemImage($reviewItem, 'a.review_boxart');
+ $title = $this->decodeHtmlEntities($reviewItem->find('h2.review_title', 0)->plaintext);
+ $content = getSimpleHTMLDOMCached($url);
+ $author = $content->find('span.author--name', 0)->plaintext;
+ $time = $this->findItemDate($content);
+ $intro = '<p><b>' . ($content->find('div#review_introduction', 0)->plaintext) . '</b></p>';
+ $review = $content->find('div#review_main', 0)->innertext;
+ $content = $this->cleanupPostContent($intro . $review, self::URI);
+ $this->items[] = $this->buildItem($url, $title, $author, $time, $img, $content);
+ unset($reviewItem); // Free up memory
+ }
+ break;
+ case 'T':
+ foreach ($html->find('li.portal-tutorial') as $tutorialItem) {
+ $url = urljoin(self::URI, $tutorialItem->find('a', 1)->href);
+ $title = $this->decodeHtmlEntities($tutorialItem->find('a', 1)->plaintext);
+ $time = $this->findItemDate($tutorialItem);
+ $author = $tutorialItem->find('a.username', 0)->plaintext;
+ $content = $this->fetchPostContent($url, self::URI);
+ $this->items[] = $this->buildItem($url, $title, $author, $time, null, $content);
+ unset($tutorialItem); // Free up memory
+ }
+ break;
+ case 'F':
+ foreach ($html->find('li.rc_item') as $postItem) {
+ $url = urljoin(self::URI, $postItem->find('a', 1)->href);
+ $title = $this->decodeHtmlEntities($postItem->find('a', 1)->plaintext);
+ $time = $this->findItemDate($postItem);
+ $author = $postItem->find('a.username', 0)->plaintext;
+ $content = $this->fetchPostContent($url, self::URI);
+ $this->items[] = $this->buildItem($url, $title, $author, $time, null, $content);
+ unset($postItem); // Free up memory
+ }
+ break;
+ }
+ }
- switch($this->getInput('type')) {
- case 'N':
- foreach($html->find('li.news_item.full') as $newsItem) {
- $url = urljoin(self::URI, $newsItem->find('a', 0)->href);
- $img = $this->findItemImage($newsItem, 'a.news_image');
- $time = $this->findItemDate($newsItem);
- $author = $newsItem->find('a.username', 0)->plaintext;
- $title = $this->decodeHtmlEntities($newsItem->find('h3.news_title', 0)->plaintext);
- $content = $this->fetchPostContent($url, self::URI);
- $this->items[] = $this->buildItem($url, $title, $author, $time, $img, $content);
- unset($newsItem); // Some items are heavy, freeing the item proactively helps saving memory
- }
- break;
- case 'R':
- foreach($html->find('li.portal_review') as $reviewItem) {
- $url = urljoin(self::URI, $reviewItem->find('a.review_boxart', 0)->href);
- $img = $this->findItemImage($reviewItem, 'a.review_boxart');
- $title = $this->decodeHtmlEntities($reviewItem->find('h2.review_title', 0)->plaintext);
- $content = getSimpleHTMLDOMCached($url);
- $author = $content->find('span.author--name', 0)->plaintext;
- $time = $this->findItemDate($content);
- $intro = '<p><b>' . ($content->find('div#review_introduction', 0)->plaintext) . '</b></p>';
- $review = $content->find('div#review_main', 0)->innertext;
- $content = $this->cleanupPostContent($intro . $review, self::URI);
- $this->items[] = $this->buildItem($url, $title, $author, $time, $img, $content);
- unset($reviewItem); // Free up memory
- }
- break;
- case 'T':
- foreach($html->find('li.portal-tutorial') as $tutorialItem) {
- $url = urljoin(self::URI, $tutorialItem->find('a', 1)->href);
- $title = $this->decodeHtmlEntities($tutorialItem->find('a', 1)->plaintext);
- $time = $this->findItemDate($tutorialItem);
- $author = $tutorialItem->find('a.username', 0)->plaintext;
- $content = $this->fetchPostContent($url, self::URI);
- $this->items[] = $this->buildItem($url, $title, $author, $time, null, $content);
- unset($tutorialItem); // Free up memory
- }
- break;
- case 'F':
- foreach($html->find('li.rc_item') as $postItem) {
- $url = urljoin(self::URI, $postItem->find('a', 1)->href);
- $title = $this->decodeHtmlEntities($postItem->find('a', 1)->plaintext);
- $time = $this->findItemDate($postItem);
- $author = $postItem->find('a.username', 0)->plaintext;
- $content = $this->fetchPostContent($url, self::URI);
- $this->items[] = $this->buildItem($url, $title, $author, $time, null, $content);
- unset($postItem); // Free up memory
- }
- break;
- }
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('type'))) {
+ $type = array_search(
+ $this->getInput('type'),
+ self::PARAMETERS[$this->queriedContext]['type']['values']
+ );
+ return 'GBAtemp ' . $type . ' Bridge';
+ }
- public function getName() {
- if(!is_null($this->getInput('type'))) {
- $type = array_search(
- $this->getInput('type'),
- self::PARAMETERS[$this->queriedContext]['type']['values']
- );
- return 'GBAtemp ' . $type . ' Bridge';
- }
-
- return parent::getName();
- }
+ return parent::getName();
+ }
}
diff --git a/bridges/GOGBridge.php b/bridges/GOGBridge.php
index f906d552..eacff97f 100644
--- a/bridges/GOGBridge.php
+++ b/bridges/GOGBridge.php
@@ -1,63 +1,62 @@
<?php
-class GOGBridge extends BridgeAbstract {
- const NAME = 'GOGBridge';
- const MAINTAINER = 'teromene';
- const URI = 'https://gog.com';
- const DESCRIPTION = 'Returns the latest releases from GOG.com';
-
- public function collectData() {
-
- $values = getContents('https://www.gog.com/games/ajax/filtered?limit=25&sort=new');
- $decodedValues = json_decode($values);
-
- $limit = 0;
- foreach($decodedValues->products as $game) {
-
- $item = array();
- $item['author'] = $game->developer . ' / ' . $game->publisher;
- $item['title'] = $game->title;
- $item['id'] = $game->id;
- $item['uri'] = self::URI . $game->url;
- $item['content'] = $this->buildGameContentPage($game);
- $item['timestamp'] = $game->globalReleaseDate;
-
- foreach($game->gallery as $image) {
- $item['enclosures'][] = $image . '.jpg';
- }
-
- $this->items[] = $item;
- $limit += 1;
-
- if($limit == 10) break;
-
- }
-
- }
-
- private function buildGameContentPage($game) {
-
- $gameDescriptionText = getContents('https://api.gog.com/products/' . $game->id . '?expand=description');
-
- $gameDescriptionValue = json_decode($gameDescriptionText);
-
- $content = 'Genres: ';
- $content .= implode(', ', $game->genres);
-
- $content .= '<br />Supported Platforms: ';
- if($game->worksOn->Windows) {
- $content .= 'Windows ';
- }
- if($game->worksOn->Mac) {
- $content .= 'Mac ';
- }
- if($game->worksOn->Linux) {
- $content .= 'Linux ';
- }
-
- $content .= '<br />' . $gameDescriptionValue->description->full;
-
- return $content;
-
- }
+class GOGBridge extends BridgeAbstract
+{
+ const NAME = 'GOGBridge';
+ const MAINTAINER = 'teromene';
+ const URI = 'https://gog.com';
+ const DESCRIPTION = 'Returns the latest releases from GOG.com';
+
+ public function collectData()
+ {
+ $values = getContents('https://www.gog.com/games/ajax/filtered?limit=25&sort=new');
+ $decodedValues = json_decode($values);
+
+ $limit = 0;
+ foreach ($decodedValues->products as $game) {
+ $item = [];
+ $item['author'] = $game->developer . ' / ' . $game->publisher;
+ $item['title'] = $game->title;
+ $item['id'] = $game->id;
+ $item['uri'] = self::URI . $game->url;
+ $item['content'] = $this->buildGameContentPage($game);
+ $item['timestamp'] = $game->globalReleaseDate;
+
+ foreach ($game->gallery as $image) {
+ $item['enclosures'][] = $image . '.jpg';
+ }
+
+ $this->items[] = $item;
+ $limit += 1;
+
+ if ($limit == 10) {
+ break;
+ }
+ }
+ }
+
+ private function buildGameContentPage($game)
+ {
+ $gameDescriptionText = getContents('https://api.gog.com/products/' . $game->id . '?expand=description');
+
+ $gameDescriptionValue = json_decode($gameDescriptionText);
+
+ $content = 'Genres: ';
+ $content .= implode(', ', $game->genres);
+
+ $content .= '<br />Supported Platforms: ';
+ if ($game->worksOn->Windows) {
+ $content .= 'Windows ';
+ }
+ if ($game->worksOn->Mac) {
+ $content .= 'Mac ';
+ }
+ if ($game->worksOn->Linux) {
+ $content .= 'Linux ';
+ }
+
+ $content .= '<br />' . $gameDescriptionValue->description->full;
+
+ return $content;
+ }
}
diff --git a/bridges/GQMagazineBridge.php b/bridges/GQMagazineBridge.php
index cacd6159..256cfeb5 100644
--- a/bridges/GQMagazineBridge.php
+++ b/bridges/GQMagazineBridge.php
@@ -9,131 +9,137 @@
*/
class GQMagazineBridge extends BridgeAbstract
{
- const MAINTAINER = 'Riduidel';
-
- const NAME = 'GQMagazine';
-
- // URI is no more valid, since we can address the whole gq galaxy
- const URI = 'https://www.gqmagazine.fr';
-
- const CACHE_TIMEOUT = 7200; // 2h
- const DESCRIPTION = 'GQMagazine section extractor bridge. This bridge allows you get only a specific section.';
-
- const DEFAULT_DOMAIN = 'www.gqmagazine.fr';
-
- const PARAMETERS = array( array(
- 'domain' => array(
- 'name' => 'Domain to use',
- 'required' => true,
- 'defaultValue' => self::DEFAULT_DOMAIN
- ),
- 'page' => array(
- 'name' => 'Initial page to load',
- 'required' => true,
- 'exampleValue' => 'sexe/news'
- ),
- 'limit' => self::LIMIT,
- ));
-
- const REPLACED_ATTRIBUTES = array(
- 'href' => 'href',
- 'src' => 'src',
- 'data-original' => 'src'
- );
-
- const POSSIBLE_TITLES = array(
- 'h2',
- 'h3'
- );
-
- private function getDomain() {
- $domain = $this->getInput('domain');
- if (empty($domain))
- $domain = self::DEFAULT_DOMAIN;
- if (strpos($domain, '://') === false)
- $domain = 'https://' . $domain;
- return $domain;
- }
-
- public function getURI()
- {
- return $this->getDomain() . '/' . $this->getInput('page');
- }
-
- private function findTitleOf($link) {
- foreach (self::POSSIBLE_TITLES as $tag) {
- $title = $link->parent()->find($tag, 0);
- if($title !== null) {
- if($title->plaintext !== null) {
- return $title->plaintext;
- }
- }
- }
- }
-
- public function collectData()
- {
- $html = getSimpleHTMLDOM($this->getURI());
-
- // Since GQ don't want simple class scrapping, let's do it the hard way and ... discover content !
- $main = $html->find('main', 0);
- $limit = $this->getInput('limit') ?? 10;
- foreach ($main->find('a') as $link) {
- if (count($this->items) >= $limit) {
- break;
- }
-
- $uri = $link->href;
- $date = $link->parent()->find('time', 0);
-
- $item = array();
- $author = $link->parent()->find('span[itemprop=name]', 0);
- if($author !== null) {
- $item['author'] = $author->plaintext;
- $item['title'] = $this->findTitleOf($link);
- switch(substr($uri, 0, 1)) {
- case 'h': // absolute uri
- $item['uri'] = $uri;
- break;
- case '/': // domain relative uri
- $item['uri'] = $this->getDomain() . $uri;
- break;
- default:
- $item['uri'] = $this->getDomain() . '/' . $uri;
- }
- $article = $this->loadFullArticle($item['uri']);
- if($article) {
- $item['content'] = $this->replaceUriInHtmlElement($article);
- } else {
- $item['content'] = "<strong>Article body couldn't be loaded</strong>. It must be a bug!";
- }
- $short_date = $date->datetime;
- $item['timestamp'] = strtotime($short_date);
- $this->items[] = $item;
- }
- }
- }
-
- /**
- * Loads the full article and returns the contents
- * @param $uri The article URI
- * @return The article content
- */
- private function loadFullArticle($uri){
- $html = getSimpleHTMLDOMCached($uri);
- return $html->find('article', 0);
- }
-
- /**
- * Replaces all relative URIs with absolute ones
- * @param $element A simplehtmldom element
- * @return The $element->innertext with all URIs replaced
- */
- private function replaceUriInHtmlElement($element){
- $returned = $element->innertext;
- foreach (self::REPLACED_ATTRIBUTES as $initial => $final) {
- $returned = str_replace($initial . '="/', $final . '="' . self::URI . '/', $returned);
- }
- return $returned;
- }
+ const MAINTAINER = 'Riduidel';
+
+ const NAME = 'GQMagazine';
+
+ // URI is no more valid, since we can address the whole gq galaxy
+ const URI = 'https://www.gqmagazine.fr';
+
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'GQMagazine section extractor bridge. This bridge allows you get only a specific section.';
+
+ const DEFAULT_DOMAIN = 'www.gqmagazine.fr';
+
+ const PARAMETERS = [ [
+ 'domain' => [
+ 'name' => 'Domain to use',
+ 'required' => true,
+ 'defaultValue' => self::DEFAULT_DOMAIN
+ ],
+ 'page' => [
+ 'name' => 'Initial page to load',
+ 'required' => true,
+ 'exampleValue' => 'sexe/news'
+ ],
+ 'limit' => self::LIMIT,
+ ]];
+
+ const REPLACED_ATTRIBUTES = [
+ 'href' => 'href',
+ 'src' => 'src',
+ 'data-original' => 'src'
+ ];
+
+ const POSSIBLE_TITLES = [
+ 'h2',
+ 'h3'
+ ];
+
+ private function getDomain()
+ {
+ $domain = $this->getInput('domain');
+ if (empty($domain)) {
+ $domain = self::DEFAULT_DOMAIN;
+ }
+ if (strpos($domain, '://') === false) {
+ $domain = 'https://' . $domain;
+ }
+ return $domain;
+ }
+
+ public function getURI()
+ {
+ return $this->getDomain() . '/' . $this->getInput('page');
+ }
+
+ private function findTitleOf($link)
+ {
+ foreach (self::POSSIBLE_TITLES as $tag) {
+ $title = $link->parent()->find($tag, 0);
+ if ($title !== null) {
+ if ($title->plaintext !== null) {
+ return $title->plaintext;
+ }
+ }
+ }
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ // Since GQ don't want simple class scrapping, let's do it the hard way and ... discover content !
+ $main = $html->find('main', 0);
+ $limit = $this->getInput('limit') ?? 10;
+ foreach ($main->find('a') as $link) {
+ if (count($this->items) >= $limit) {
+ break;
+ }
+
+ $uri = $link->href;
+ $date = $link->parent()->find('time', 0);
+
+ $item = [];
+ $author = $link->parent()->find('span[itemprop=name]', 0);
+ if ($author !== null) {
+ $item['author'] = $author->plaintext;
+ $item['title'] = $this->findTitleOf($link);
+ switch (substr($uri, 0, 1)) {
+ case 'h': // absolute uri
+ $item['uri'] = $uri;
+ break;
+ case '/': // domain relative uri
+ $item['uri'] = $this->getDomain() . $uri;
+ break;
+ default:
+ $item['uri'] = $this->getDomain() . '/' . $uri;
+ }
+ $article = $this->loadFullArticle($item['uri']);
+ if ($article) {
+ $item['content'] = $this->replaceUriInHtmlElement($article);
+ } else {
+ $item['content'] = "<strong>Article body couldn't be loaded</strong>. It must be a bug!";
+ }
+ $short_date = $date->datetime;
+ $item['timestamp'] = strtotime($short_date);
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ /**
+ * Loads the full article and returns the contents
+ * @param $uri The article URI
+ * @return The article content
+ */
+ private function loadFullArticle($uri)
+ {
+ $html = getSimpleHTMLDOMCached($uri);
+ return $html->find('article', 0);
+ }
+
+ /**
+ * Replaces all relative URIs with absolute ones
+ * @param $element A simplehtmldom element
+ * @return The $element->innertext with all URIs replaced
+ */
+ private function replaceUriInHtmlElement($element)
+ {
+ $returned = $element->innertext;
+ foreach (self::REPLACED_ATTRIBUTES as $initial => $final) {
+ $returned = str_replace($initial . '="/', $final . '="' . self::URI . '/', $returned);
+ }
+ return $returned;
+ }
}
diff --git a/bridges/GatesNotesBridge.php b/bridges/GatesNotesBridge.php
index bf456d26..8c988fcb 100644
--- a/bridges/GatesNotesBridge.php
+++ b/bridges/GatesNotesBridge.php
@@ -1,54 +1,55 @@
<?php
-class GatesNotesBridge extends FeedExpander {
-
- const MAINTAINER = 'corenting';
- const NAME = 'Gates Notes';
- const URI = 'https://www.gatesnotes.com';
- const DESCRIPTION = 'Returns the newest articles.';
- const CACHE_TIMEOUT = 21600; // 6h
-
- protected function parseItem($item){
- $item = parent::parseItem($item);
-
- $article_html = getSimpleHTMLDOMCached($item['uri']);
- if(!$article_html) {
- $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>';
- return $item;
- }
- $article_html = defaultLinkTo($article_html, $this->getURI());
-
- $top_description = '<p>' . $article_html->find('div.article_top_description', 0)->innertext . '</p>';
- $hero_image = '<img src=' . $article_html->find('img.article_top_DMT_Image', 0)->getAttribute('data-src') . '>';
-
- $article_body = $article_html->find('div.TGN_Article_ReadTimeSection', 0);
- // Convert iframe of Youtube videos to link
- foreach($article_body->find('iframe') as $found) {
-
- $iframeUrl = $found->getAttribute('src');
-
- if ($iframeUrl) {
- $text = 'Embedded Youtube video, click here to watch on Youtube.com';
- $found->outertext = '<p><a href="' . $iframeUrl . '">' . $text . '</a></p>';
- }
- }
- // Remove <link> CSS ressources
- foreach($article_body->find('link') as $found) {
-
- $linkedRessourceUrl = $found->getAttribute('href');
-
- if (str_ends_with($linkedRessourceUrl, '.css')) {
- $found->outertext = '';
- }
- }
- $article_body = sanitize($article_body->innertext);
-
- $item['content'] = $top_description . $hero_image . $article_body;
-
- return $item;
- }
-
- public function collectData(){
- $feed = static::URI . '/rss';
- $this->collectExpandableDatas($feed);
- }
+
+class GatesNotesBridge extends FeedExpander
+{
+ const MAINTAINER = 'corenting';
+ const NAME = 'Gates Notes';
+ const URI = 'https://www.gatesnotes.com';
+ const DESCRIPTION = 'Returns the newest articles.';
+ const CACHE_TIMEOUT = 21600; // 6h
+
+ protected function parseItem($item)
+ {
+ $item = parent::parseItem($item);
+
+ $article_html = getSimpleHTMLDOMCached($item['uri']);
+ if (!$article_html) {
+ $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>';
+ return $item;
+ }
+ $article_html = defaultLinkTo($article_html, $this->getURI());
+
+ $top_description = '<p>' . $article_html->find('div.article_top_description', 0)->innertext . '</p>';
+ $hero_image = '<img src=' . $article_html->find('img.article_top_DMT_Image', 0)->getAttribute('data-src') . '>';
+
+ $article_body = $article_html->find('div.TGN_Article_ReadTimeSection', 0);
+ // Convert iframe of Youtube videos to link
+ foreach ($article_body->find('iframe') as $found) {
+ $iframeUrl = $found->getAttribute('src');
+
+ if ($iframeUrl) {
+ $text = 'Embedded Youtube video, click here to watch on Youtube.com';
+ $found->outertext = '<p><a href="' . $iframeUrl . '">' . $text . '</a></p>';
+ }
+ }
+ // Remove <link> CSS ressources
+ foreach ($article_body->find('link') as $found) {
+ $linkedRessourceUrl = $found->getAttribute('href');
+
+ if (str_ends_with($linkedRessourceUrl, '.css')) {
+ $found->outertext = '';
+ }
+ }
+ $article_body = sanitize($article_body->innertext);
+
+ $item['content'] = $top_description . $hero_image . $article_body;
+
+ return $item;
+ }
+
+ public function collectData()
+ {
+ $feed = static::URI . '/rss';
+ $this->collectExpandableDatas($feed);
+ }
}
diff --git a/bridges/GelbooruBridge.php b/bridges/GelbooruBridge.php
index 7dcd44fc..93518843 100644
--- a/bridges/GelbooruBridge.php
+++ b/bridges/GelbooruBridge.php
@@ -1,88 +1,93 @@
<?php
-class GelbooruBridge extends BridgeAbstract {
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Gelbooru';
- const URI = 'https://gelbooru.com/';
- const DESCRIPTION = 'Returns images from given page';
+class GelbooruBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Gelbooru';
+ const URI = 'https://gelbooru.com/';
+ const DESCRIPTION = 'Returns images from given page';
- const PARAMETERS = array(
- 'global' => array(
- 'p' => array(
- 'name' => 'page',
- 'defaultValue' => 0,
- 'type' => 'number'
- ),
- 't' => array(
- 'name' => 'tags',
- 'exampleValue' => 'solo',
- 'title' => 'Tags to search for'
- ),
- 'l' => array(
- 'name' => 'limit',
- 'exampleValue' => 100,
- 'title' => 'How many posts to retrieve (hard limit of 1000)'
- )
- ),
- 0 => array()
- );
+ const PARAMETERS = [
+ 'global' => [
+ 'p' => [
+ 'name' => 'page',
+ 'defaultValue' => 0,
+ 'type' => 'number'
+ ],
+ 't' => [
+ 'name' => 'tags',
+ 'exampleValue' => 'solo',
+ 'title' => 'Tags to search for'
+ ],
+ 'l' => [
+ 'name' => 'limit',
+ 'exampleValue' => 100,
+ 'title' => 'How many posts to retrieve (hard limit of 1000)'
+ ]
+ ],
+ 0 => []
+ ];
- protected function getFullURI(){
- return $this->getURI()
- . 'index.php?&page=dapi&s=post&q=index&json=1&pid=' . $this->getInput('p')
- . '&limit=' . $this->getInput('l')
- . '&tags=' . urlencode($this->getInput('t'));
- }
+ protected function getFullURI()
+ {
+ return $this->getURI()
+ . 'index.php?&page=dapi&s=post&q=index&json=1&pid=' . $this->getInput('p')
+ . '&limit=' . $this->getInput('l')
+ . '&tags=' . urlencode($this->getInput('t'));
+ }
- /*
- This function is superfluous for GelbooruBridge, but useful
- for Bridges that inherit from it
- */
- protected function buildThumbnailURI($element){
- return $this->getURI() . 'thumbnails/' . $element->directory
- . '/thumbnail_' . $element->md5 . '.jpg';
- }
+ /*
+ This function is superfluous for GelbooruBridge, but useful
+ for Bridges that inherit from it
+ */
+ protected function buildThumbnailURI($element)
+ {
+ return $this->getURI() . 'thumbnails/' . $element->directory
+ . '/thumbnail_' . $element->md5 . '.jpg';
+ }
- protected function getItemFromElement($element){
- $item = array();
- $item['uri'] = $this->getURI() . 'index.php?page=post&s=view&id='
- . $element->id;
- $item['postid'] = $element->id;
- $item['author'] = $element->owner;
- $item['timestamp'] = date('d F Y H:i:s', $element->change);
- $item['tags'] = $element->tags;
- $item['title'] = $this->getName() . ' | ' . $item['postid'];
+ protected function getItemFromElement($element)
+ {
+ $item = [];
+ $item['uri'] = $this->getURI() . 'index.php?page=post&s=view&id='
+ . $element->id;
+ $item['postid'] = $element->id;
+ $item['author'] = $element->owner;
+ $item['timestamp'] = date('d F Y H:i:s', $element->change);
+ $item['tags'] = $element->tags;
+ $item['title'] = $this->getName() . ' | ' . $item['postid'];
- if (isset($element->preview_url)) {
- $thumbnailUri = $element->preview_url;
- } else{
- $thumbnailUri = $this->buildThumbnailURI($element);
- }
+ if (isset($element->preview_url)) {
+ $thumbnailUri = $element->preview_url;
+ } else {
+ $thumbnailUri = $this->buildThumbnailURI($element);
+ }
- $item['content'] = '<a href="' . $item['uri'] . '"><img src="'
- . $thumbnailUri . '" /></a><br><br><b>Tags:</b> '
- . $item['tags'] . '<br><br>' . $item['timestamp'];
+ $item['content'] = '<a href="' . $item['uri'] . '"><img src="'
+ . $thumbnailUri . '" /></a><br><br><b>Tags:</b> '
+ . $item['tags'] . '<br><br>' . $item['timestamp'];
- return $item;
- }
+ return $item;
+ }
- public function collectData(){
- $content = getContents($this->getFullURI());
- // $content is empty string
+ public function collectData()
+ {
+ $content = getContents($this->getFullURI());
+ // $content is empty string
- // Most other Gelbooru-based boorus put their content in the root of
- // the JSON. This check is here for Bridges that inherit from this one
- $posts = json_decode($content);
- if (isset($posts->post)) {
- $posts = $posts->post;
- }
+ // Most other Gelbooru-based boorus put their content in the root of
+ // the JSON. This check is here for Bridges that inherit from this one
+ $posts = json_decode($content);
+ if (isset($posts->post)) {
+ $posts = $posts->post;
+ }
- if (is_null($posts)) {
- returnServerError('No posts found.');
- }
+ if (is_null($posts)) {
+ returnServerError('No posts found.');
+ }
- foreach($posts as $post) {
- $this->items[] = $this->getItemFromElement($post);
- }
- }
+ foreach ($posts as $post) {
+ $this->items[] = $this->getItemFromElement($post);
+ }
+ }
}
diff --git a/bridges/GenshinImpactBridge.php b/bridges/GenshinImpactBridge.php
index 01ea12f0..cfa3dfe3 100644
--- a/bridges/GenshinImpactBridge.php
+++ b/bridges/GenshinImpactBridge.php
@@ -1,69 +1,73 @@
<?php
-class GenshinImpactBridge extends BridgeAbstract {
- const MAINTAINER = 'corenting';
- const NAME = 'Genshin Impact';
- const URI = 'https://genshin.mihoyo.com/en/news';
- const CACHE_TIMEOUT = 7200; // 2h
- const DESCRIPTION = 'News from the Genshin Impact website';
- const PARAMETERS = array(
- array(
- 'category' => array(
- 'name' => 'Category',
- 'type' => 'list',
- 'values' => array(
- 'Latest' => 10,
- 'Info' => 11,
- 'Updates' => 12,
- 'Events' => 13
- ),
- 'defaultValue' => 10
- )
- )
- );
+class GenshinImpactBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'corenting';
+ const NAME = 'Genshin Impact';
+ const URI = 'https://genshin.mihoyo.com/en/news';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'News from the Genshin Impact website';
+ const PARAMETERS = [
+ [
+ 'category' => [
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => [
+ 'Latest' => 10,
+ 'Info' => 11,
+ 'Updates' => 12,
+ 'Events' => 13
+ ],
+ 'defaultValue' => 10
+ ]
+ ]
+ ];
- public function collectData(){
- $category = $this->getInput('category');
+ public function collectData()
+ {
+ $category = $this->getInput('category');
- $url = 'https://genshin.mihoyo.com/content/yuanshen/getContentList';
- $url = $url . '?pageSize=3&pageNum=1&channelId=' . $category;
- $api_response = getContents($url);
- $json_list = json_decode($api_response, true);
+ $url = 'https://genshin.mihoyo.com/content/yuanshen/getContentList';
+ $url = $url . '?pageSize=3&pageNum=1&channelId=' . $category;
+ $api_response = getContents($url);
+ $json_list = json_decode($api_response, true);
- foreach($json_list['data']['list'] as $json_item) {
- $article_url = 'https://genshin.mihoyo.com/content/yuanshen/getContent';
- $article_url = $article_url . '?contentId=' . $json_item['contentId'];
- $article_res = getContents($article_url);
- $article_json = json_decode($article_res, true);
- $article_time = $article_json['data']['start_time'];
- $timezone = 'Asia/Shanghai';
- $article_timestamp = new DateTime($article_time, new DateTimeZone($timezone));
+ foreach ($json_list['data']['list'] as $json_item) {
+ $article_url = 'https://genshin.mihoyo.com/content/yuanshen/getContent';
+ $article_url = $article_url . '?contentId=' . $json_item['contentId'];
+ $article_res = getContents($article_url);
+ $article_json = json_decode($article_res, true);
+ $article_time = $article_json['data']['start_time'];
+ $timezone = 'Asia/Shanghai';
+ $article_timestamp = new DateTime($article_time, new DateTimeZone($timezone));
- $item = array();
+ $item = [];
- $item['title'] = $article_json['data']['title'];
- $item['timestamp'] = $article_timestamp->format('U');
- $item['content'] = $article_json['data']['content'];
- $item['uri'] = $this->getArticleUri($json_item);
- $item['id'] = $json_item['contentId'];
+ $item['title'] = $article_json['data']['title'];
+ $item['timestamp'] = $article_timestamp->format('U');
+ $item['content'] = $article_json['data']['content'];
+ $item['uri'] = $this->getArticleUri($json_item);
+ $item['id'] = $json_item['contentId'];
- // Picture
- foreach($article_json['data']['ext'] as $ext) {
- if ($ext['arrtName'] == 'banner' && count($ext['value']) == 1) {
- $item['enclosures'] = array($ext['value'][0]['url']);
- break;
- }
- }
+ // Picture
+ foreach ($article_json['data']['ext'] as $ext) {
+ if ($ext['arrtName'] == 'banner' && count($ext['value']) == 1) {
+ $item['enclosures'] = [$ext['value'][0]['url']];
+ break;
+ }
+ }
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
- public function getIcon() {
- return 'https://genshin.mihoyo.com/favicon.ico';
- }
+ public function getIcon()
+ {
+ return 'https://genshin.mihoyo.com/favicon.ico';
+ }
- private function getArticleUri($json_item) {
- return 'https://genshin.mihoyo.com/en/news/detail/' . $json_item['contentId'];
- }
+ private function getArticleUri($json_item)
+ {
+ return 'https://genshin.mihoyo.com/en/news/detail/' . $json_item['contentId'];
+ }
}
diff --git a/bridges/GettrBridge.php b/bridges/GettrBridge.php
index 5ecc5c83..2b019523 100644
--- a/bridges/GettrBridge.php
+++ b/bridges/GettrBridge.php
@@ -2,72 +2,72 @@
class GettrBridge extends BridgeAbstract
{
- const NAME = 'Gettr.com bridge';
- const URI = 'https://gettr.com';
- const DESCRIPTION = 'Fetches the latest posts from a GETTR user';
- const MAINTAINER = 'dvikan';
- const CACHE_TIMEOUT = 60 * 15; // 15m
- const PARAMETERS = [
- [
- 'user' => [
- 'name' => 'User',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'joerogan',
- ],
- 'limit' => [
- 'name' => 'Limit',
- 'type' => 'number',
- 'title' => 'Maximum number of items to return (maximum 20)',
- 'defaultValue' => 5,
- 'required' => true,
- ],
- ]
- ];
+ const NAME = 'Gettr.com bridge';
+ const URI = 'https://gettr.com';
+ const DESCRIPTION = 'Fetches the latest posts from a GETTR user';
+ const MAINTAINER = 'dvikan';
+ const CACHE_TIMEOUT = 60 * 15; // 15m
+ const PARAMETERS = [
+ [
+ 'user' => [
+ 'name' => 'User',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'joerogan',
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'title' => 'Maximum number of items to return (maximum 20)',
+ 'defaultValue' => 5,
+ 'required' => true,
+ ],
+ ]
+ ];
- public function collectData()
- {
- $api = sprintf(
- 'https://api.gettr.com/u/user/%s/posts?offset=0&max=%s&dir=fwd&incl=posts&fp=f_uo',
- $this->getInput('user'),
- min($this->getInput('limit'), 20)
- );
- $data = json_decode(getContents($api), false);
+ public function collectData()
+ {
+ $api = sprintf(
+ 'https://api.gettr.com/u/user/%s/posts?offset=0&max=%s&dir=fwd&incl=posts&fp=f_uo',
+ $this->getInput('user'),
+ min($this->getInput('limit'), 20)
+ );
+ $data = json_decode(getContents($api), false);
- foreach ($data->result->aux->post as $post) {
- $this->items[] = [
- 'title' => mb_substr($post->txt ?? $post->uid . '@gettr.com', 0, 100),
- 'uri' => sprintf('https://gettr.com/post/%s', $post->_id),
- 'author' => $post->uid,
- // Convert from ms to s
- 'timestamp' => substr($post->cdate, 0, strlen($post->cdate) - 3),
- 'uid' => $post->_id,
- // Hashtags found within post text
- 'categories' => $post->htgs ?? [],
- 'content' => $this->createContent($post),
- ];
- }
- }
+ foreach ($data->result->aux->post as $post) {
+ $this->items[] = [
+ 'title' => mb_substr($post->txt ?? $post->uid . '@gettr.com', 0, 100),
+ 'uri' => sprintf('https://gettr.com/post/%s', $post->_id),
+ 'author' => $post->uid,
+ // Convert from ms to s
+ 'timestamp' => substr($post->cdate, 0, strlen($post->cdate) - 3),
+ 'uid' => $post->_id,
+ // Hashtags found within post text
+ 'categories' => $post->htgs ?? [],
+ 'content' => $this->createContent($post),
+ ];
+ }
+ }
- /**
- * Collect text, image and video, if they exist
- */
- private function createContent(\stdClass $post): string
- {
- $content = '';
+ /**
+ * Collect text, image and video, if they exist
+ */
+ private function createContent(\stdClass $post): string
+ {
+ $content = '';
- // Text
- if (isset($post->txt)) {
- $isRepost = $this->getInput('user') !== $post->uid;
- if ($isRepost) {
- $content .= 'Reposted by ' . $this->getInput('user') . '@gettr.com<br><br>';
- }
- $content .= "$post->txt <br><br>";
- }
+ // Text
+ if (isset($post->txt)) {
+ $isRepost = $this->getInput('user') !== $post->uid;
+ if ($isRepost) {
+ $content .= 'Reposted by ' . $this->getInput('user') . '@gettr.com<br><br>';
+ }
+ $content .= "$post->txt <br><br>";
+ }
- // Preview image
- if (isset($post->previmg)) {
- $content .= <<<HTML
+ // Preview image
+ if (isset($post->previmg)) {
+ $content .= <<<HTML
<a href="$post->prevsrc" target="_blank">
<img
src='$post->previmg'
@@ -77,11 +77,11 @@ class GettrBridge extends BridgeAbstract
</a>
<br><br>
HTML;
- }
+ }
- // Images
- foreach ($post->imgs ?? [] as $imageUrl) {
- $content .= <<<HTML
+ // Images
+ foreach ($post->imgs ?? [] as $imageUrl) {
+ $content .= <<<HTML
<img
src='https://media.gettr.com/$imageUrl'
alt='Unable to load image'
@@ -89,13 +89,13 @@ HTML;
>
<br><br>
HTML;
- }
+ }
- // Video
- if (isset($post->ovid)) {
- $mainImage = $post->main;
+ // Video
+ if (isset($post->ovid)) {
+ $mainImage = $post->main;
- $content .= <<<HTML
+ $content .= <<<HTML
<video
style="max-width: 100%"
controls
@@ -106,30 +106,30 @@ HTML;
Your browser does not support the video element. Kindly update it to latest version.
</video >
HTML;
- // This is typically a m3u8 which I don't know how to present in a browser
- $streamingUrl = $post->vid;
- }
- $this->processMetadata($post);
+ // This is typically a m3u8 which I don't know how to present in a browser
+ $streamingUrl = $post->vid;
+ }
+ $this->processMetadata($post);
- return $content;
- }
+ return $content;
+ }
- public function getIcon()
- {
- return 'https://gettr.com/favicon.ico';
- }
+ public function getIcon()
+ {
+ return 'https://gettr.com/favicon.ico';
+ }
- /**
- * @param stdClass $post
- */
- private function processMetadata(stdClass $post): void
- {
- // Unused metadata, maybe used later
- $textLanguage = $post->txt_lang ?? 'en';
- $replies = $post->cm ?? 0;
- $likes = $post->lkbpst ?? 0;
- $reposts = $post->shbpst ?? 0;
- // I think a visibility of "p" means that it's public
- $visibility = $post->vis ?? 'p';
- }
+ /**
+ * @param stdClass $post
+ */
+ private function processMetadata(stdClass $post): void
+ {
+ // Unused metadata, maybe used later
+ $textLanguage = $post->txt_lang ?? 'en';
+ $replies = $post->cm ?? 0;
+ $likes = $post->lkbpst ?? 0;
+ $reposts = $post->shbpst ?? 0;
+ // I think a visibility of "p" means that it's public
+ $visibility = $post->vis ?? 'p';
+ }
}
diff --git a/bridges/GiphyBridge.php b/bridges/GiphyBridge.php
index f823246b..4692e183 100644
--- a/bridges/GiphyBridge.php
+++ b/bridges/GiphyBridge.php
@@ -1,56 +1,57 @@
<?php
-class GiphyBridge extends BridgeAbstract {
+class GiphyBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'dvikan';
+ const NAME = 'Giphy Bridge';
+ const URI = 'https://giphy.com/';
+ const CACHE_TIMEOUT = 60 * 60 * 8; // 8h
+ const DESCRIPTION = 'Bridge for giphy.com';
- const MAINTAINER = 'dvikan';
- const NAME = 'Giphy Bridge';
- const URI = 'https://giphy.com/';
- const CACHE_TIMEOUT = 60 * 60 * 8; // 8h
- const DESCRIPTION = 'Bridge for giphy.com';
+ const PARAMETERS = [ [
+ 's' => [
+ 'name' => 'search tag',
+ 'exampleValue' => 'bird',
+ 'required' => true
+ ],
+ 'noGif' => [
+ 'name' => 'Without gifs',
+ 'type' => 'checkbox',
+ 'title' => 'Exclude gifs from the results'
+ ],
+ 'noStick' => [
+ 'name' => 'Without stickers',
+ 'type' => 'checkbox',
+ 'title' => 'Exclude stickers from the results'
+ ],
+ 'n' => [
+ 'name' => 'max number of returned items (max 50)',
+ 'type' => 'number',
+ 'exampleValue' => 3,
+ ]
+ ]];
- const PARAMETERS = array( array(
- 's' => array(
- 'name' => 'search tag',
- 'exampleValue' => 'bird',
- 'required' => true
- ),
- 'noGif' => array(
- 'name' => 'Without gifs',
- 'type' => 'checkbox',
- 'title' => 'Exclude gifs from the results'
- ),
- 'noStick' => array(
- 'name' => 'Without stickers',
- 'type' => 'checkbox',
- 'title' => 'Exclude stickers from the results'
- ),
- 'n' => array(
- 'name' => 'max number of returned items (max 50)',
- 'type' => 'number',
- 'exampleValue' => 3,
- )
- ));
+ public function getName()
+ {
+ if (!is_null($this->getInput('s'))) {
+ return $this->getInput('s') . ' - ' . parent::getName();
+ }
- public function getName()
- {
- if (!is_null($this->getInput('s'))) {
- return $this->getInput('s') . ' - ' . parent::getName();
- }
+ return parent::getName();
+ }
- return parent::getName();
- }
+ protected function getGiphyItems($entries)
+ {
+ foreach ($entries as $entry) {
+ $createdAt = new \DateTime($entry->import_datetime);
- protected function getGiphyItems($entries){
- foreach($entries as $entry) {
- $createdAt = new \DateTime($entry->import_datetime);
-
- $this->items[] = array(
- 'id' => $entry->id,
- 'uri' => $entry->url,
- 'author' => $entry->username,
- 'timestamp' => $createdAt->format('U'),
- 'title' => $entry->title,
- 'content' => <<<HTML
+ $this->items[] = [
+ 'id' => $entry->id,
+ 'uri' => $entry->url,
+ 'author' => $entry->username,
+ 'timestamp' => $createdAt->format('U'),
+ 'title' => $entry->title,
+ 'content' => <<<HTML
<a href="{$entry->url}">
<img
loading="lazy"
@@ -59,48 +60,49 @@ class GiphyBridge extends BridgeAbstract {
height="{$entry->images->downsized->height}" />
</a>
HTML
- );
- }
- }
+ ];
+ }
+ }
- public function collectData() {
- /**
- * This uses Giphy's own undocumented public prod api key,
- * which should not have any rate limiting.
- * There is a documented public beta api key (dc6zaTOxFJmzC),
- * but it has severe rate limiting.
- *
- * https://giphy.api-docs.io/1.0/welcome/access-and-api-keys
- * https://developers.giphy.com/branch/master/docs/api/endpoint/#search
- */
- $apiKey = 'Gc7131jiJuvI7IdN0HZ1D7nh0ow5BU6g';
- $bundle = 'low_bandwidth';
- $limit = min($this->getInput('n') ?: 10, 50);
- $endpoints = array();
- if (empty($this->getInput('noGif'))) {
- $endpoints[] = 'gifs';
- }
- if (empty($this->getInput('noStick'))) {
- $endpoints[] = 'stickers';
- }
+ public function collectData()
+ {
+ /**
+ * This uses Giphy's own undocumented public prod api key,
+ * which should not have any rate limiting.
+ * There is a documented public beta api key (dc6zaTOxFJmzC),
+ * but it has severe rate limiting.
+ *
+ * https://giphy.api-docs.io/1.0/welcome/access-and-api-keys
+ * https://developers.giphy.com/branch/master/docs/api/endpoint/#search
+ */
+ $apiKey = 'Gc7131jiJuvI7IdN0HZ1D7nh0ow5BU6g';
+ $bundle = 'low_bandwidth';
+ $limit = min($this->getInput('n') ?: 10, 50);
+ $endpoints = [];
+ if (empty($this->getInput('noGif'))) {
+ $endpoints[] = 'gifs';
+ }
+ if (empty($this->getInput('noStick'))) {
+ $endpoints[] = 'stickers';
+ }
- foreach ($endpoints as $endpoint) {
- $uri = sprintf(
- 'https://api.giphy.com/v1/%s/search?q=%s&limit=%s&bundle=%s&api_key=%s',
- $endpoint,
- rawurlencode($this->getInput('s')),
- $limit,
- $bundle,
- $apiKey
- );
+ foreach ($endpoints as $endpoint) {
+ $uri = sprintf(
+ 'https://api.giphy.com/v1/%s/search?q=%s&limit=%s&bundle=%s&api_key=%s',
+ $endpoint,
+ rawurlencode($this->getInput('s')),
+ $limit,
+ $bundle,
+ $apiKey
+ );
- $result = json_decode(getContents($uri));
+ $result = json_decode(getContents($uri));
- $this->getGiphyItems($result->data);
- }
+ $this->getGiphyItems($result->data);
+ }
- usort($this->items, function ($a, $b) {
- return $a['timestamp'] < $b['timestamp'];
- });
- }
+ usort($this->items, function ($a, $b) {
+ return $a['timestamp'] < $b['timestamp'];
+ });
+ }
}
diff --git a/bridges/GitHubGistBridge.php b/bridges/GitHubGistBridge.php
index 5760d8a0..969ee3be 100644
--- a/bridges/GitHubGistBridge.php
+++ b/bridges/GitHubGistBridge.php
@@ -1,110 +1,106 @@
<?php
-class GitHubGistBridge extends BridgeAbstract {
-
- const NAME = 'GitHubGist comment bridge';
- const URI = 'https://gist.github.com';
- const DESCRIPTION = 'Generates feeds for Gist comments';
- const MAINTAINER = 'logmanoriginal';
- const CACHE_TIMEOUT = 3600;
-
- const PARAMETERS = array(array(
- 'id' => array(
- 'name' => 'Gist',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Insert Gist ID or URI',
- 'exampleValue' => '2646763'
- )
- ));
-
- private $filename;
-
- public function getURI() {
-
- $id = $this->getInput('id') ?: '';
-
- $urlpath = parse_url($id, PHP_URL_PATH);
-
- if($urlpath) {
-
- $components = explode('/', $urlpath);
- $id = end($components);
-
- }
-
- return static::URI . '/' . $id;
-
- }
-
- public function getName() {
- return $this->filename ? $this->filename . ' - ' . static::NAME : static::NAME;
- }
-
- public function collectData() {
-
- $html = getSimpleHTMLDOM($this->getURI(),
- null,
- null,
- true,
- true,
- DEFAULT_TARGET_CHARSET,
- false, // Do NOT remove line breaks
- DEFAULT_BR_TEXT,
- DEFAULT_SPAN_TEXT);
-
- $html = defaultLinkTo($html, $this->getURI());
-
- $fileinfo = $html->find('[class~="file-info"]', 0)
- or returnServerError('Could not find file info!');
-
- $this->filename = $fileinfo->plaintext;
-
- $comments = $html->find('div[class~="TimelineItem"]');
-
- if(is_null($comments)) { // no comments yet
- return;
- }
-
- foreach($comments as $comment) {
-
- $uri = $comment->find('a[href*=#gistcomment]', 0)
- or returnServerError('Could not find comment anchor!');
-
- $title = $comment->find('h3', 0);
-
- $datetime = $comment->find('[datetime]', 0)
- or returnServerError('Could not find comment datetime!');
-
- $author = $comment->find('a.author', 0)
- or returnServerError('Could not find author name!');
-
- $message = $comment->find('[class~="comment-body"]', 0)
- or returnServerError('Could not find comment body!');
-
- $item = array();
-
- $item['uri'] = $uri->href;
- $item['title'] = str_replace('commented', 'commented on', $title->plaintext ?? '');
- $item['timestamp'] = strtotime($datetime->datetime);
- $item['author'] = '<a href="' . $author->href . '">' . $author->plaintext . '</a>';
- $item['content'] = $this->fixContent($message);
- // $item['enclosures'] = array();
- // $item['categories'] = array();
-
- $this->items[] = $item;
-
- }
-
- }
-
- /** Removes all unnecessary tags and adds formatting */
- private function fixContent($content){
-
- // Restore code (inside <pre />) highlighting
- foreach($content->find('pre') as $pre) {
-
- $pre->style = <<<EOD
+class GitHubGistBridge extends BridgeAbstract
+{
+ const NAME = 'GitHubGist comment bridge';
+ const URI = 'https://gist.github.com';
+ const DESCRIPTION = 'Generates feeds for Gist comments';
+ const MAINTAINER = 'logmanoriginal';
+ const CACHE_TIMEOUT = 3600;
+
+ const PARAMETERS = [[
+ 'id' => [
+ 'name' => 'Gist',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert Gist ID or URI',
+ 'exampleValue' => '2646763'
+ ]
+ ]];
+
+ private $filename;
+
+ public function getURI()
+ {
+ $id = $this->getInput('id') ?: '';
+
+ $urlpath = parse_url($id, PHP_URL_PATH);
+
+ if ($urlpath) {
+ $components = explode('/', $urlpath);
+ $id = end($components);
+ }
+
+ return static::URI . '/' . $id;
+ }
+
+ public function getName()
+ {
+ return $this->filename ? $this->filename . ' - ' . static::NAME : static::NAME;
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(
+ $this->getURI(),
+ null,
+ null,
+ true,
+ true,
+ DEFAULT_TARGET_CHARSET,
+ false, // Do NOT remove line breaks
+ DEFAULT_BR_TEXT,
+ DEFAULT_SPAN_TEXT
+ );
+
+ $html = defaultLinkTo($html, $this->getURI());
+
+ $fileinfo = $html->find('[class~="file-info"]', 0)
+ or returnServerError('Could not find file info!');
+
+ $this->filename = $fileinfo->plaintext;
+
+ $comments = $html->find('div[class~="TimelineItem"]');
+
+ if (is_null($comments)) { // no comments yet
+ return;
+ }
+
+ foreach ($comments as $comment) {
+ $uri = $comment->find('a[href*=#gistcomment]', 0)
+ or returnServerError('Could not find comment anchor!');
+
+ $title = $comment->find('h3', 0);
+
+ $datetime = $comment->find('[datetime]', 0)
+ or returnServerError('Could not find comment datetime!');
+
+ $author = $comment->find('a.author', 0)
+ or returnServerError('Could not find author name!');
+
+ $message = $comment->find('[class~="comment-body"]', 0)
+ or returnServerError('Could not find comment body!');
+
+ $item = [];
+
+ $item['uri'] = $uri->href;
+ $item['title'] = str_replace('commented', 'commented on', $title->plaintext ?? '');
+ $item['timestamp'] = strtotime($datetime->datetime);
+ $item['author'] = '<a href="' . $author->href . '">' . $author->plaintext . '</a>';
+ $item['content'] = $this->fixContent($message);
+ // $item['enclosures'] = array();
+ // $item['categories'] = array();
+
+ $this->items[] = $item;
+ }
+ }
+
+ /** Removes all unnecessary tags and adds formatting */
+ private function fixContent($content)
+ {
+ // Restore code (inside <pre />) highlighting
+ foreach ($content->find('pre') as $pre) {
+ $pre->style = <<<EOD
padding: 16px;
overflow: auto;
font-size: 85%;
@@ -116,46 +112,40 @@ box-sizing: border-box;
margin-bottom: 16px;
EOD;
- $code = $pre->find('code', 0);
+ $code = $pre->find('code', 0);
- if($code) {
-
- $code->style = <<<EOD
+ if ($code) {
+ $code->style = <<<EOD
white-space: pre;
word-break: normal;
EOD;
+ }
+ }
- }
-
- }
+ // find <code /> not inside <pre /> (`inline-code`)
+ foreach ($content->find('code') as $code) {
+ if ($code->parent()->tag === 'pre') {
+ continue;
+ }
- // find <code /> not inside <pre /> (`inline-code`)
- foreach($content->find('code') as $code) {
-
- if($code->parent()->tag === 'pre') {
- continue;
- }
-
- $code->style = <<<EOD
+ $code->style = <<<EOD
background-color: rgba(27,31,35,0.05);
padding: 0.2em 0.4em;
border-radius: 3px;
EOD;
+ }
- }
-
- // restore text spacing
- foreach($content->find('p') as $p) {
- $p->style = 'margin-bottom: 16px;';
- }
-
- // Remove unnecessary tags
- $content = strip_tags(
- $content->innertext,
- '<p><a><img><ol><ul><li><table><tr><th><td><string><pre><code><br><hr><h>'
- );
+ // restore text spacing
+ foreach ($content->find('p') as $p) {
+ $p->style = 'margin-bottom: 16px;';
+ }
- return $content;
+ // Remove unnecessary tags
+ $content = strip_tags(
+ $content->innertext,
+ '<p><a><img><ol><ul><li><table><tr><th><td><string><pre><code><br><hr><h>'
+ );
- }
+ return $content;
+ }
}
diff --git a/bridges/GiteaBridge.php b/bridges/GiteaBridge.php
index f7b7b782..f7f426e9 100644
--- a/bridges/GiteaBridge.php
+++ b/bridges/GiteaBridge.php
@@ -1,305 +1,322 @@
<?php
+
/**
* Gitea is a community managed lightweight code hosting solution.
* https://docs.gitea.io/en-us/
*/
-class GiteaBridge extends BridgeAbstract {
-
- const NAME = 'Gitea';
- const URI = 'https://gitea.io';
- const DESCRIPTION = 'Returns the latest issues, commits or releases';
- const MAINTAINER = 'gileri';
- const CACHE_TIMEOUT = 300; // 5 minutes
-
- const PARAMETERS = array(
- 'global' => array(
- 'host' => array(
- 'name' => 'Host',
- 'exampleValue' => 'https://gitea.com',
- 'required' => true,
- 'title' => 'Host name with its protocol, without trailing slash',
- ),
- 'user' => array(
- 'name' => 'Username',
- 'exampleValue' => 'gitea',
- 'required' => true,
- 'title' => 'User name as it appears in the URL',
- ),
- 'project' => array(
- 'name' => 'Project name',
- 'exampleValue' => 'helm-chart',
- 'required' => true,
- 'title' => 'Project name as it appears in the URL',
- ),
- ),
- 'Commits' => array(
- 'branch' => array(
- 'name' => 'Branch name',
- 'defaultValue' => 'master',
- 'required' => true,
- 'title' => 'Branch name as it appears in the URL',
- ),
- ),
- 'Issues' => array(
- 'include_description' => array(
- 'name' => 'Include issue description',
- 'type' => 'checkbox',
- 'title' => 'Activate to include the issue description',
- ),
- ),
- 'Single issue' => array(
- 'issue' => array(
- 'name' => 'Issue number',
- 'type' => 'number',
- 'exampleValue' => 100,
- 'required' => true,
- 'title' => 'Issue number from the issues list',
- ),
- ),
- 'Single pull request' => array(
- 'pull_request' => array(
- 'name' => 'Pull request number',
- 'type' => 'number',
- 'exampleValue' => 100,
- 'required' => true,
- 'title' => 'Pull request number from the issues list',
- ),
- ),
- 'Pull requests' => array(
- 'include_description' => array(
- 'name' => 'Include pull request description',
- 'type' => 'checkbox',
- 'title' => 'Activate to include the pull request description',
- ),
- ),
- 'Releases' => array(),
- 'Tags' => array(),
- );
-
- private $title = '';
-
- public function getIcon() {
- return 'https://gitea.io/images/gitea.png';
- }
-
- public function getName() {
- switch($this->queriedContext) {
- case 'Commits':
- case 'Issues':
- case 'Pull requests':
- case 'Releases':
- case 'Tags': return $this->title . ' ' . $this->queriedContext;
- case 'Single issue': return 'Issue ' . $this->getInput('issue') . ': ' . $this->title;
- case 'Single pull request': return 'Pull request ' . $this->getInput('pull_request') . ': ' . $this->title;
- default: return parent::getName();
- }
- }
-
- public function getURI() {
- switch($this->queriedContext) {
- case 'Commits':
- return $this->getInput('host')
- . '/' . $this->getInput('user')
- . '/' . $this->getInput('project')
- . '/commits/' . $this->getInput('branch');
-
- case 'Issues':
- return $this->getInput('host')
- . '/' . $this->getInput('user')
- . '/' . $this->getInput('project')
- . '/issues/';
-
- case 'Single issue':
- return $this->getInput('host')
- . '/' . $this->getInput('user')
- . '/' . $this->getInput('project')
- . '/issues/' . $this->getInput('issue');
-
- case 'Releases':
- return $this->getInput('host')
- . '/' . $this->getInput('user')
- . '/' . $this->getInput('project')
- . '/releases/';
-
- case 'Tags':
- return $this->getInput('host')
- . '/' . $this->getInput('user')
- . '/' . $this->getInput('project')
- . '/tags/';
-
- case 'Pull requests':
- return $this->getInput('host')
- . '/' . $this->getInput('user')
- . '/' . $this->getInput('project')
- . '/pulls/';
-
- case 'Single pull request':
- return $this->getInput('host')
- . '/' . $this->getInput('user')
- . '/' . $this->getInput('project')
- . '/pulls/' . $this->getInput('pull_request');
-
- default: return parent::getURI();
- }
- }
-
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI())
- or returnServerError('Could not request ' . $this->getURI());
- $html = defaultLinkTo($html, $this->getURI());
-
- $this->title = $html->find('[property="og:title"]', 0)->content;
-
- switch($this->queriedContext) {
- case 'Commits':
- $this->collectCommitsData($html);
- break;
- case 'Issues':
- $this->collectIssuesData($html);
- break;
- case 'Pull requests':
- $this->collectPullRequestsData($html);
- break;
- case 'Single issue':
- $this->collectSingleIssueOrPrData($html);
- break;
- case 'Single pull request':
- $this->collectSingleIssueOrPrData($html);
- break;
- case 'Releases':
- $this->collectReleasesData($html);
- break;
- case 'Tags':
- $this->collectTagsData($html);
- break;
- }
- }
-
- protected function collectReleasesData($html) {
- $releases = $html->find('#release-list > li')
- or returnServerError('Unable to find releases');
-
- foreach($releases as $release) {
- $this->items[] = array(
- 'author' => $release->find('.author', 0)->plaintext,
- 'uri' => $release->find('a', 0)->href,
- 'title' => 'Release ' . $release->find('h4', 0)->plaintext,
- 'timestamp' => $release->find('.time-since', 0)->title,
- );
- }
- }
-
- protected function collectTagsData($html) {
- $tags = $html->find('table#tags-table > tbody > tr')
- or returnServerError('Unable to find tags');
-
- foreach($tags as $tag) {
- $this->items[] = array(
- 'uri' => $tag->find('a', 0)->href,
- 'title' => 'Tag ' . $tag->find('.release-tag-name > a', 0)->plaintext,
- );
- }
- }
-
- protected function collectCommitsData($html) {
- $commits = $html->find('#commits-table tbody tr')
- or returnServerError('Unable to find commits');
-
- foreach($commits as $commit) {
- $this->items[] = array(
- 'uri' => $commit->find('a.sha', 0)->href,
- 'title' => $commit->find('.message span', 0)->plaintext,
- 'author' => $commit->find('.author', 0)->plaintext,
- 'timestamp' => $commit->find('.time-since', 0)->title,
- 'uid' => $commit->find('.sha', 0)->plaintext,
- );
- }
- }
-
- protected function collectIssuesData($html) {
- $issues = $html->find('.issue.list li')
- or returnServerError('Unable to find issues');
-
- foreach($issues as $issue) {
- $uri = $issue->find('a', 0)->href;
-
- $item = array(
- 'uri' => $uri,
- 'title' => trim($issue->find('a.index', 0)->plaintext) . ' | ' . $issue->find('a.title', 0)->plaintext,
- 'author' => $issue->find('.desc a', 1)->plaintext,
- 'timestamp' => $issue->find('.time-since', 0)->title,
- );
-
- if($this->getInput('include_description')) {
- $issue_html = getSimpleHTMLDOMCached($uri, 3600)
- or returnServerError('Unable to load issue description');
-
- $issue_html = defaultLinkTo($issue_html, $uri);
-
- $item['content'] = $issue_html->find('.comment .markup', 0);
- }
-
- $this->items[] = $item;
- }
- }
-
- protected function collectSingleIssueOrPrData($html) {
- $comments = $html->find('.comment')
- or returnServerError('Unable to find comments');
-
- foreach($comments as $comment) {
- if (strpos($comment->getAttribute('class'), 'form') !== false
- || strpos($comment->getAttribute('class'), 'merge') !== false
- ) {
- // Ignore comment form and merge information
- continue;
- }
- $commentLink = $comment->find('a[href*="#issue"]', 0);
- $item = array(
- 'author' => $comment->find('a.author', 0)->plaintext,
- 'content' => $comment->find('.render-content', 0),
- );
- if ($commentLink !== null) {
- // Regular comment
- $item['uri'] = $commentLink->href;
- $item['title'] = str_replace($commentLink->plaintext, '', $comment->find('span', 0)->plaintext);
- $item['timestamp'] = $comment->find('.time-since', 0)->title;
- } else {
- // Change request comment
- $item['uri'] = $this->getURI() . '#' . $comment->getAttribute('id');
- $item['title'] = $comment->find('.comment-header .text', 0)->plaintext;
- }
- $this->items[] = $item;
- }
-
- $this->items = array_reverse($this->items);
- }
-
- protected function collectPullRequestsData($html) {
- $issues = $html->find('.issue.list li')
- or returnServerError('Unable to find pull requests');
-
- foreach($issues as $issue) {
- $uri = $issue->find('a', 0)->href;
-
- $item = array(
- 'uri' => $uri,
- 'title' => trim($issue->find('a.index', 0)->plaintext) . ' | ' . $issue->find('a.title', 0)->plaintext,
- 'author' => $issue->find('.desc a', 1)->plaintext,
- 'timestamp' => $issue->find('.time-since', 0)->title,
- );
-
- if($this->getInput('include_description')) {
- $issue_html = getSimpleHTMLDOMCached($uri, 3600)
- or returnServerError('Unable to load issue description');
-
- $issue_html = defaultLinkTo($issue_html, $uri);
-
- $item['content'] = $issue_html->find('.comment .markup', 0);
- }
-
- $this->items[] = $item;
- }
- }
+class GiteaBridge extends BridgeAbstract
+{
+ const NAME = 'Gitea';
+ const URI = 'https://gitea.io';
+ const DESCRIPTION = 'Returns the latest issues, commits or releases';
+ const MAINTAINER = 'gileri';
+ const CACHE_TIMEOUT = 300; // 5 minutes
+
+ const PARAMETERS = [
+ 'global' => [
+ 'host' => [
+ 'name' => 'Host',
+ 'exampleValue' => 'https://gitea.com',
+ 'required' => true,
+ 'title' => 'Host name with its protocol, without trailing slash',
+ ],
+ 'user' => [
+ 'name' => 'Username',
+ 'exampleValue' => 'gitea',
+ 'required' => true,
+ 'title' => 'User name as it appears in the URL',
+ ],
+ 'project' => [
+ 'name' => 'Project name',
+ 'exampleValue' => 'helm-chart',
+ 'required' => true,
+ 'title' => 'Project name as it appears in the URL',
+ ],
+ ],
+ 'Commits' => [
+ 'branch' => [
+ 'name' => 'Branch name',
+ 'defaultValue' => 'master',
+ 'required' => true,
+ 'title' => 'Branch name as it appears in the URL',
+ ],
+ ],
+ 'Issues' => [
+ 'include_description' => [
+ 'name' => 'Include issue description',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to include the issue description',
+ ],
+ ],
+ 'Single issue' => [
+ 'issue' => [
+ 'name' => 'Issue number',
+ 'type' => 'number',
+ 'exampleValue' => 100,
+ 'required' => true,
+ 'title' => 'Issue number from the issues list',
+ ],
+ ],
+ 'Single pull request' => [
+ 'pull_request' => [
+ 'name' => 'Pull request number',
+ 'type' => 'number',
+ 'exampleValue' => 100,
+ 'required' => true,
+ 'title' => 'Pull request number from the issues list',
+ ],
+ ],
+ 'Pull requests' => [
+ 'include_description' => [
+ 'name' => 'Include pull request description',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to include the pull request description',
+ ],
+ ],
+ 'Releases' => [],
+ 'Tags' => [],
+ ];
+
+ private $title = '';
+
+ public function getIcon()
+ {
+ return 'https://gitea.io/images/gitea.png';
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Commits':
+ case 'Issues':
+ case 'Pull requests':
+ case 'Releases':
+ case 'Tags':
+ return $this->title . ' ' . $this->queriedContext;
+ case 'Single issue':
+ return 'Issue ' . $this->getInput('issue') . ': ' . $this->title;
+ case 'Single pull request':
+ return 'Pull request ' . $this->getInput('pull_request') . ': ' . $this->title;
+ default:
+ return parent::getName();
+ }
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Commits':
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/commits/' . $this->getInput('branch');
+
+ case 'Issues':
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/issues/';
+
+ case 'Single issue':
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/issues/' . $this->getInput('issue');
+
+ case 'Releases':
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/releases/';
+
+ case 'Tags':
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/tags/';
+
+ case 'Pull requests':
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/pulls/';
+
+ case 'Single pull request':
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/pulls/' . $this->getInput('pull_request');
+
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request ' . $this->getURI());
+ $html = defaultLinkTo($html, $this->getURI());
+
+ $this->title = $html->find('[property="og:title"]', 0)->content;
+
+ switch ($this->queriedContext) {
+ case 'Commits':
+ $this->collectCommitsData($html);
+ break;
+ case 'Issues':
+ $this->collectIssuesData($html);
+ break;
+ case 'Pull requests':
+ $this->collectPullRequestsData($html);
+ break;
+ case 'Single issue':
+ $this->collectSingleIssueOrPrData($html);
+ break;
+ case 'Single pull request':
+ $this->collectSingleIssueOrPrData($html);
+ break;
+ case 'Releases':
+ $this->collectReleasesData($html);
+ break;
+ case 'Tags':
+ $this->collectTagsData($html);
+ break;
+ }
+ }
+
+ protected function collectReleasesData($html)
+ {
+ $releases = $html->find('#release-list > li')
+ or returnServerError('Unable to find releases');
+
+ foreach ($releases as $release) {
+ $this->items[] = [
+ 'author' => $release->find('.author', 0)->plaintext,
+ 'uri' => $release->find('a', 0)->href,
+ 'title' => 'Release ' . $release->find('h4', 0)->plaintext,
+ 'timestamp' => $release->find('.time-since', 0)->title,
+ ];
+ }
+ }
+
+ protected function collectTagsData($html)
+ {
+ $tags = $html->find('table#tags-table > tbody > tr')
+ or returnServerError('Unable to find tags');
+
+ foreach ($tags as $tag) {
+ $this->items[] = [
+ 'uri' => $tag->find('a', 0)->href,
+ 'title' => 'Tag ' . $tag->find('.release-tag-name > a', 0)->plaintext,
+ ];
+ }
+ }
+
+ protected function collectCommitsData($html)
+ {
+ $commits = $html->find('#commits-table tbody tr')
+ or returnServerError('Unable to find commits');
+
+ foreach ($commits as $commit) {
+ $this->items[] = [
+ 'uri' => $commit->find('a.sha', 0)->href,
+ 'title' => $commit->find('.message span', 0)->plaintext,
+ 'author' => $commit->find('.author', 0)->plaintext,
+ 'timestamp' => $commit->find('.time-since', 0)->title,
+ 'uid' => $commit->find('.sha', 0)->plaintext,
+ ];
+ }
+ }
+
+ protected function collectIssuesData($html)
+ {
+ $issues = $html->find('.issue.list li')
+ or returnServerError('Unable to find issues');
+
+ foreach ($issues as $issue) {
+ $uri = $issue->find('a', 0)->href;
+
+ $item = [
+ 'uri' => $uri,
+ 'title' => trim($issue->find('a.index', 0)->plaintext) . ' | ' . $issue->find('a.title', 0)->plaintext,
+ 'author' => $issue->find('.desc a', 1)->plaintext,
+ 'timestamp' => $issue->find('.time-since', 0)->title,
+ ];
+
+ if ($this->getInput('include_description')) {
+ $issue_html = getSimpleHTMLDOMCached($uri, 3600)
+ or returnServerError('Unable to load issue description');
+
+ $issue_html = defaultLinkTo($issue_html, $uri);
+
+ $item['content'] = $issue_html->find('.comment .markup', 0);
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ protected function collectSingleIssueOrPrData($html)
+ {
+ $comments = $html->find('.comment')
+ or returnServerError('Unable to find comments');
+
+ foreach ($comments as $comment) {
+ if (
+ strpos($comment->getAttribute('class'), 'form') !== false
+ || strpos($comment->getAttribute('class'), 'merge') !== false
+ ) {
+ // Ignore comment form and merge information
+ continue;
+ }
+ $commentLink = $comment->find('a[href*="#issue"]', 0);
+ $item = [
+ 'author' => $comment->find('a.author', 0)->plaintext,
+ 'content' => $comment->find('.render-content', 0),
+ ];
+ if ($commentLink !== null) {
+ // Regular comment
+ $item['uri'] = $commentLink->href;
+ $item['title'] = str_replace($commentLink->plaintext, '', $comment->find('span', 0)->plaintext);
+ $item['timestamp'] = $comment->find('.time-since', 0)->title;
+ } else {
+ // Change request comment
+ $item['uri'] = $this->getURI() . '#' . $comment->getAttribute('id');
+ $item['title'] = $comment->find('.comment-header .text', 0)->plaintext;
+ }
+ $this->items[] = $item;
+ }
+
+ $this->items = array_reverse($this->items);
+ }
+
+ protected function collectPullRequestsData($html)
+ {
+ $issues = $html->find('.issue.list li')
+ or returnServerError('Unable to find pull requests');
+
+ foreach ($issues as $issue) {
+ $uri = $issue->find('a', 0)->href;
+
+ $item = [
+ 'uri' => $uri,
+ 'title' => trim($issue->find('a.index', 0)->plaintext) . ' | ' . $issue->find('a.title', 0)->plaintext,
+ 'author' => $issue->find('.desc a', 1)->plaintext,
+ 'timestamp' => $issue->find('.time-since', 0)->title,
+ ];
+
+ if ($this->getInput('include_description')) {
+ $issue_html = getSimpleHTMLDOMCached($uri, 3600)
+ or returnServerError('Unable to load issue description');
+
+ $issue_html = defaultLinkTo($issue_html, $uri);
+
+ $item['content'] = $issue_html->find('.comment .markup', 0);
+ }
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/GithubIssueBridge.php b/bridges/GithubIssueBridge.php
index e3a0c73a..b90982c6 100644
--- a/bridges/GithubIssueBridge.php
+++ b/bridges/GithubIssueBridge.php
@@ -1,292 +1,301 @@
<?php
-class GithubIssueBridge extends BridgeAbstract {
-
- const MAINTAINER = 'Pierre Mazière';
- const NAME = 'Github Issue';
- const URI = 'https://github.com/';
- const CACHE_TIMEOUT = 0; // 10min
- const DESCRIPTION = 'Returns the issues or comments of an issue of a github project';
-
- const PARAMETERS = array(
- 'global' => array(
- 'u' => array(
- 'name' => 'User name',
- 'exampleValue' => 'RSS-Bridge',
- 'required' => true
- ),
- 'p' => array(
- 'name' => 'Project name',
- 'exampleValue' => 'rss-bridge',
- 'required' => true
- )
- ),
- 'Project Issues' => array(
- 'c' => array(
- 'name' => 'Show Issues Comments',
- 'type' => 'checkbox'
- ),
- 'q' => array(
- 'name' => 'Search Query',
- 'defaultValue' => 'is:issue is:open sort:updated-desc',
- 'required' => true
- )
- ),
- 'Issue comments' => array(
- 'i' => array(
- 'name' => 'Issue number',
- 'type' => 'number',
- 'exampleValue' => '2099',
- 'required' => true
- )
- )
- );
-
- // Allows generalization with GithubPullRequestBridge
- const BRIDGE_OPTIONS = array(0 => 'Project Issues', 1 => 'Issue comments');
- const URL_PATH = 'issues';
- const SEARCH_QUERY_PATH = 'issues';
-
- public function getName(){
- $name = $this->getInput('u') . '/' . $this->getInput('p');
- switch($this->queriedContext) {
- case static::BRIDGE_OPTIONS[0]: // Project Issues
- $prefix = static::NAME . 's for ';
- if($this->getInput('c')) {
- $prefix = static::NAME . 's comments for ';
- }
- $name = $prefix . $name;
- break;
- case static::BRIDGE_OPTIONS[1]: // Issue comments
- $name = static::NAME . ' ' . $name . ' #' . $this->getInput('i');
- break;
- default: return parent::getName();
- }
- return $name;
- }
-
- public function getURI() {
- if(null !== $this->getInput('u') && null !== $this->getInput('p')) {
- $uri = static::URI . $this->getInput('u') . '/'
- . $this->getInput('p') . '/';
- if($this->queriedContext === static::BRIDGE_OPTIONS[1]) {
- $uri .= static::URL_PATH . '/' . $this->getInput('i');
- } else {
- $uri .= static::SEARCH_QUERY_PATH . '?q=' . urlencode($this->getInput('q'));
- }
- return $uri;
- }
-
- return parent::getURI();
- }
-
- private function buildGitHubIssueCommentUri($issue_number, $comment_id) {
- // https://github.com/<user>/<project>/issues/<issue-number>#<id>
- return static::URI
- . $this->getInput('u')
- . '/'
- . $this->getInput('p')
- . '/' . static::URL_PATH . '/'
- . $issue_number
- . '#'
- . $comment_id;
- }
-
- private function extractIssueEvent($issueNbr, $title, $comment) {
-
- $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
-
- $author = $comment->find('.author, .avatar', 0);
- if ($author) {
- $author = trim($author->href, '/');
- } else {
- $author = '';
- }
-
- $title .= ' / '
- . trim(str_replace(
- array('octicon','-'), array(''),
- $comment->find('.octicon', 0)->getAttribute('class')
- ));
-
- $time = $comment->find('relative-time', 0);
- if ($time === null) {
- return;
- }
-
- foreach($comment->find('.Details-content--hidden, .btn') as $el) {
- $el->innertext = '';
- }
- $content = $comment->plaintext;
-
- $item = array();
- $item['author'] = $author;
- $item['uri'] = $uri;
- $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
- $item['timestamp'] = strtotime($time->getAttribute('datetime'));
- $item['content'] = $content;
- return $item;
- }
-
- private function extractIssueComment($issueNbr, $title, $comment) {
- $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
-
- $author = $comment->find('.author', 0)->plaintext;
-
- $header = $comment->find('.timeline-comment-header > h3', 0);
- $title .= ' / ' . ($header ? $header->plaintext : 'Activity');
-
- $time = $comment->find('relative-time', 0);
- if ($time === null) {
- return;
- }
-
- $content = $comment->find('.comment-body', 0)->innertext;
-
- $item = array();
- $item['author'] = $author;
- $item['uri'] = $uri;
- $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
- $item['timestamp'] = strtotime($time->getAttribute('datetime'));
- $item['content'] = $content;
- return $item;
- }
-
- private function extractIssueComments($issue) {
- $items = array();
- $title = $issue->find('.gh-header-title', 0)->plaintext;
- $issueNbr = trim(
- substr($issue->find('.gh-header-number', 0)->plaintext, 1)
- );
-
- $comments = $issue->find(
- '.comment, .TimelineItem-badge'
- );
-
- foreach($comments as $comment) {
- if ($comment->hasClass('comment')) {
- $comment = $comment->parent;
- $item = $this->extractIssueComment($issueNbr, $title, $comment);
- if ($item !== null) {
- $items[] = $item;
- }
- continue;
- } else {
- $comment = $comment->parent;
- $item = $this->extractIssueEvent($issueNbr, $title, $comment);
- if ($item !== null) {
- $items[] = $item;
- }
- }
-
- }
- return $items;
- }
-
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI());
-
- switch($this->queriedContext) {
- case static::BRIDGE_OPTIONS[1]: // Issue comments
- $this->items = $this->extractIssueComments($html);
- break;
- case static::BRIDGE_OPTIONS[0]: // Project Issues
- foreach($html->find('.js-active-navigation-container .js-navigation-item') as $issue) {
- $info = $issue->find('.opened-by', 0);
-
- preg_match('/\/([0-9]+)$/', $issue->find('a', 0)->href, $match);
- $issueNbr = $match[1];
-
- $item = array();
- $item['content'] = '';
-
- if($this->getInput('c')) {
- $uri = static::URI . $this->getInput('u')
- . '/' . $this->getInput('p') . '/' . static::URL_PATH . '/' . $issueNbr;
- $issue = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT);
- if($issue) {
- $this->items = array_merge(
- $this->items,
- $this->extractIssueComments($issue)
- );
- continue;
- }
- $item['content'] = 'Can not extract comments from ' . $uri;
- }
-
- $item['author'] = $info->find('a', 0)->plaintext;
- $item['timestamp'] = strtotime(
- $info->find('relative-time', 0)->getAttribute('datetime')
- );
- $item['title'] = html_entity_decode(
- $issue->find('.js-navigation-open', 0)->plaintext,
- ENT_QUOTES,
- 'UTF-8'
- );
-
- $comment_count = 0;
- if($span = $issue->find('a[aria-label*="comment"] span', 0)) {
- $comment_count = $span->plaintext;
- }
-
- $item['content'] .= "\n" . 'Comments: ' . $comment_count;
- $item['uri'] = self::URI
- . trim($issue->find('.js-navigation-open', 0)->getAttribute('href'), '/');
- $this->items[] = $item;
- }
- break;
- }
-
- array_walk($this->items, function(&$item){
- $item['content'] = preg_replace('/\s+/', ' ', $item['content']);
- $item['content'] = str_replace(
- 'href="/',
- 'href="' . static::URI,
- $item['content']
- );
- $item['content'] = str_replace(
- 'href="#',
- 'href="' . substr($item['uri'], 0, strpos($item['uri'], '#') + 1),
- $item['content']
- );
- $item['title'] = preg_replace('/\s+/', ' ', $item['title']);
- });
- }
-
- public function detectParameters($url) {
-
- if(filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false
- || strpos($url, self::URI) !== 0) {
- return null;
- }
-
- $url_components = parse_url($url);
- $path_segments = array_values(array_filter(explode('/', $url_components['path'])));
-
- switch(count($path_segments)) {
- case 2: // Project issues
- list($user, $project) = $path_segments;
- $show_comments = 'off';
- break;
- case 3: // Project issues with issue comments
- if($path_segments[2] !== static::URL_PATH) {
- return null;
- }
- list($user, $project) = $path_segments;
- $show_comments = 'on';
- break;
- case 4: // Issue comments
- list($user, $project, /* issues */, $issue) = $path_segments;
- break;
- default:
- return null;
- }
-
- return array(
- 'u' => $user,
- 'p' => $project,
- 'c' => isset($show_comments) ? $show_comments : null,
- 'i' => isset($issue) ? $issue : null,
- );
-
- }
+
+class GithubIssueBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Pierre Mazière';
+ const NAME = 'Github Issue';
+ const URI = 'https://github.com/';
+ const CACHE_TIMEOUT = 0; // 10min
+ const DESCRIPTION = 'Returns the issues or comments of an issue of a github project';
+
+ const PARAMETERS = [
+ 'global' => [
+ 'u' => [
+ 'name' => 'User name',
+ 'exampleValue' => 'RSS-Bridge',
+ 'required' => true
+ ],
+ 'p' => [
+ 'name' => 'Project name',
+ 'exampleValue' => 'rss-bridge',
+ 'required' => true
+ ]
+ ],
+ 'Project Issues' => [
+ 'c' => [
+ 'name' => 'Show Issues Comments',
+ 'type' => 'checkbox'
+ ],
+ 'q' => [
+ 'name' => 'Search Query',
+ 'defaultValue' => 'is:issue is:open sort:updated-desc',
+ 'required' => true
+ ]
+ ],
+ 'Issue comments' => [
+ 'i' => [
+ 'name' => 'Issue number',
+ 'type' => 'number',
+ 'exampleValue' => '2099',
+ 'required' => true
+ ]
+ ]
+ ];
+
+ // Allows generalization with GithubPullRequestBridge
+ const BRIDGE_OPTIONS = [0 => 'Project Issues', 1 => 'Issue comments'];
+ const URL_PATH = 'issues';
+ const SEARCH_QUERY_PATH = 'issues';
+
+ public function getName()
+ {
+ $name = $this->getInput('u') . '/' . $this->getInput('p');
+ switch ($this->queriedContext) {
+ case static::BRIDGE_OPTIONS[0]: // Project Issues
+ $prefix = static::NAME . 's for ';
+ if ($this->getInput('c')) {
+ $prefix = static::NAME . 's comments for ';
+ }
+ $name = $prefix . $name;
+ break;
+ case static::BRIDGE_OPTIONS[1]: // Issue comments
+ $name = static::NAME . ' ' . $name . ' #' . $this->getInput('i');
+ break;
+ default:
+ return parent::getName();
+ }
+ return $name;
+ }
+
+ public function getURI()
+ {
+ if (null !== $this->getInput('u') && null !== $this->getInput('p')) {
+ $uri = static::URI . $this->getInput('u') . '/'
+ . $this->getInput('p') . '/';
+ if ($this->queriedContext === static::BRIDGE_OPTIONS[1]) {
+ $uri .= static::URL_PATH . '/' . $this->getInput('i');
+ } else {
+ $uri .= static::SEARCH_QUERY_PATH . '?q=' . urlencode($this->getInput('q'));
+ }
+ return $uri;
+ }
+
+ return parent::getURI();
+ }
+
+ private function buildGitHubIssueCommentUri($issue_number, $comment_id)
+ {
+ // https://github.com/<user>/<project>/issues/<issue-number>#<id>
+ return static::URI
+ . $this->getInput('u')
+ . '/'
+ . $this->getInput('p')
+ . '/' . static::URL_PATH . '/'
+ . $issue_number
+ . '#'
+ . $comment_id;
+ }
+
+ private function extractIssueEvent($issueNbr, $title, $comment)
+ {
+ $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
+
+ $author = $comment->find('.author, .avatar', 0);
+ if ($author) {
+ $author = trim($author->href, '/');
+ } else {
+ $author = '';
+ }
+
+ $title .= ' / '
+ . trim(str_replace(
+ ['octicon','-'],
+ [''],
+ $comment->find('.octicon', 0)->getAttribute('class')
+ ));
+
+ $time = $comment->find('relative-time', 0);
+ if ($time === null) {
+ return;
+ }
+
+ foreach ($comment->find('.Details-content--hidden, .btn') as $el) {
+ $el->innertext = '';
+ }
+ $content = $comment->plaintext;
+
+ $item = [];
+ $item['author'] = $author;
+ $item['uri'] = $uri;
+ $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
+ $item['timestamp'] = strtotime($time->getAttribute('datetime'));
+ $item['content'] = $content;
+ return $item;
+ }
+
+ private function extractIssueComment($issueNbr, $title, $comment)
+ {
+ $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
+
+ $author = $comment->find('.author', 0)->plaintext;
+
+ $header = $comment->find('.timeline-comment-header > h3', 0);
+ $title .= ' / ' . ($header ? $header->plaintext : 'Activity');
+
+ $time = $comment->find('relative-time', 0);
+ if ($time === null) {
+ return;
+ }
+
+ $content = $comment->find('.comment-body', 0)->innertext;
+
+ $item = [];
+ $item['author'] = $author;
+ $item['uri'] = $uri;
+ $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
+ $item['timestamp'] = strtotime($time->getAttribute('datetime'));
+ $item['content'] = $content;
+ return $item;
+ }
+
+ private function extractIssueComments($issue)
+ {
+ $items = [];
+ $title = $issue->find('.gh-header-title', 0)->plaintext;
+ $issueNbr = trim(
+ substr($issue->find('.gh-header-number', 0)->plaintext, 1)
+ );
+
+ $comments = $issue->find(
+ '.comment, .TimelineItem-badge'
+ );
+
+ foreach ($comments as $comment) {
+ if ($comment->hasClass('comment')) {
+ $comment = $comment->parent;
+ $item = $this->extractIssueComment($issueNbr, $title, $comment);
+ if ($item !== null) {
+ $items[] = $item;
+ }
+ continue;
+ } else {
+ $comment = $comment->parent;
+ $item = $this->extractIssueEvent($issueNbr, $title, $comment);
+ if ($item !== null) {
+ $items[] = $item;
+ }
+ }
+ }
+ return $items;
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ switch ($this->queriedContext) {
+ case static::BRIDGE_OPTIONS[1]: // Issue comments
+ $this->items = $this->extractIssueComments($html);
+ break;
+ case static::BRIDGE_OPTIONS[0]: // Project Issues
+ foreach ($html->find('.js-active-navigation-container .js-navigation-item') as $issue) {
+ $info = $issue->find('.opened-by', 0);
+
+ preg_match('/\/([0-9]+)$/', $issue->find('a', 0)->href, $match);
+ $issueNbr = $match[1];
+
+ $item = [];
+ $item['content'] = '';
+
+ if ($this->getInput('c')) {
+ $uri = static::URI . $this->getInput('u')
+ . '/' . $this->getInput('p') . '/' . static::URL_PATH . '/' . $issueNbr;
+ $issue = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT);
+ if ($issue) {
+ $this->items = array_merge(
+ $this->items,
+ $this->extractIssueComments($issue)
+ );
+ continue;
+ }
+ $item['content'] = 'Can not extract comments from ' . $uri;
+ }
+
+ $item['author'] = $info->find('a', 0)->plaintext;
+ $item['timestamp'] = strtotime(
+ $info->find('relative-time', 0)->getAttribute('datetime')
+ );
+ $item['title'] = html_entity_decode(
+ $issue->find('.js-navigation-open', 0)->plaintext,
+ ENT_QUOTES,
+ 'UTF-8'
+ );
+
+ $comment_count = 0;
+ if ($span = $issue->find('a[aria-label*="comment"] span', 0)) {
+ $comment_count = $span->plaintext;
+ }
+
+ $item['content'] .= "\n" . 'Comments: ' . $comment_count;
+ $item['uri'] = self::URI
+ . trim($issue->find('.js-navigation-open', 0)->getAttribute('href'), '/');
+ $this->items[] = $item;
+ }
+ break;
+ }
+
+ array_walk($this->items, function (&$item) {
+ $item['content'] = preg_replace('/\s+/', ' ', $item['content']);
+ $item['content'] = str_replace(
+ 'href="/',
+ 'href="' . static::URI,
+ $item['content']
+ );
+ $item['content'] = str_replace(
+ 'href="#',
+ 'href="' . substr($item['uri'], 0, strpos($item['uri'], '#') + 1),
+ $item['content']
+ );
+ $item['title'] = preg_replace('/\s+/', ' ', $item['title']);
+ });
+ }
+
+ public function detectParameters($url)
+ {
+ if (
+ filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false
+ || strpos($url, self::URI) !== 0
+ ) {
+ return null;
+ }
+
+ $url_components = parse_url($url);
+ $path_segments = array_values(array_filter(explode('/', $url_components['path'])));
+
+ switch (count($path_segments)) {
+ case 2: // Project issues
+ list($user, $project) = $path_segments;
+ $show_comments = 'off';
+ break;
+ case 3: // Project issues with issue comments
+ if ($path_segments[2] !== static::URL_PATH) {
+ return null;
+ }
+ list($user, $project) = $path_segments;
+ $show_comments = 'on';
+ break;
+ case 4: // Issue comments
+ list($user, $project, /* issues */, $issue) = $path_segments;
+ break;
+ default:
+ return null;
+ }
+
+ return [
+ 'u' => $user,
+ 'p' => $project,
+ 'c' => isset($show_comments) ? $show_comments : null,
+ 'i' => isset($issue) ? $issue : null,
+ ];
+ }
}
diff --git a/bridges/GithubPullRequestBridge.php b/bridges/GithubPullRequestBridge.php
index 82f901d1..b508a919 100644
--- a/bridges/GithubPullRequestBridge.php
+++ b/bridges/GithubPullRequestBridge.php
@@ -1,44 +1,45 @@
<?php
-class GitHubPullRequestBridge extends GithubIssueBridge {
- const NAME = 'GitHub Pull Request';
- const DESCRIPTION = 'Returns the pull request or comments of a pull request of a GitHub project';
+class GitHubPullRequestBridge extends GithubIssueBridge
+{
+ const NAME = 'GitHub Pull Request';
+ const DESCRIPTION = 'Returns the pull request or comments of a pull request of a GitHub project';
- const PARAMETERS = array(
- 'global' => array(
- 'u' => array(
- 'name' => 'User name',
- 'exampleValue' => 'RSS-Bridge',
- 'required' => true
- ),
- 'p' => array(
- 'name' => 'Project name',
- 'exampleValue' => 'rss-bridge',
- 'required' => true
- )
- ),
- 'Project Pull Requests' => array(
- 'c' => array(
- 'name' => 'Show Pull Request Comments',
- 'type' => 'checkbox'
- ),
- 'q' => array(
- 'name' => 'Search Query',
- 'defaultValue' => 'is:pr is:open sort:created-desc',
- 'required' => true
- )
- ),
- 'Pull Request comments' => array(
- 'i' => array(
- 'name' => 'Pull Request number',
- 'type' => 'number',
- 'exampleValue' => '2100',
- 'required' => true
- )
- )
- );
+ const PARAMETERS = [
+ 'global' => [
+ 'u' => [
+ 'name' => 'User name',
+ 'exampleValue' => 'RSS-Bridge',
+ 'required' => true
+ ],
+ 'p' => [
+ 'name' => 'Project name',
+ 'exampleValue' => 'rss-bridge',
+ 'required' => true
+ ]
+ ],
+ 'Project Pull Requests' => [
+ 'c' => [
+ 'name' => 'Show Pull Request Comments',
+ 'type' => 'checkbox'
+ ],
+ 'q' => [
+ 'name' => 'Search Query',
+ 'defaultValue' => 'is:pr is:open sort:created-desc',
+ 'required' => true
+ ]
+ ],
+ 'Pull Request comments' => [
+ 'i' => [
+ 'name' => 'Pull Request number',
+ 'type' => 'number',
+ 'exampleValue' => '2100',
+ 'required' => true
+ ]
+ ]
+ ];
- const BRIDGE_OPTIONS = array(0 => 'Project Pull Requests', 1 => 'Pull Request comments');
- const URL_PATH = 'pull';
- const SEARCH_QUERY_PATH = 'pulls';
+ const BRIDGE_OPTIONS = [0 => 'Project Pull Requests', 1 => 'Pull Request comments'];
+ const URL_PATH = 'pull';
+ const SEARCH_QUERY_PATH = 'pulls';
}
diff --git a/bridges/GithubSearchBridge.php b/bridges/GithubSearchBridge.php
index fdabfc94..658c4d7c 100644
--- a/bridges/GithubSearchBridge.php
+++ b/bridges/GithubSearchBridge.php
@@ -1,67 +1,69 @@
<?php
-class GithubSearchBridge extends BridgeAbstract {
- const MAINTAINER = 'corenting';
- const NAME = 'Github Repositories Search';
- const URI = 'https://github.com/';
- const CACHE_TIMEOUT = 600; // 10min
- const DESCRIPTION = 'Returns a specified repositories search (sorted by recently updated)';
- const PARAMETERS = array( array(
- 's' => array(
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'rss-bridge',
- 'name' => 'Search query'
- )
- ));
+class GithubSearchBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'corenting';
+ const NAME = 'Github Repositories Search';
+ const URI = 'https://github.com/';
+ const CACHE_TIMEOUT = 600; // 10min
+ const DESCRIPTION = 'Returns a specified repositories search (sorted by recently updated)';
+ const PARAMETERS = [ [
+ 's' => [
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'rss-bridge',
+ 'name' => 'Search query'
+ ]
+ ]];
- public function collectData(){
- $params = array('utf8' => '✓',
- 'q' => urlencode($this->getInput('s')),
- 's' => 'updated',
- 'o' => 'desc',
- 'type' => 'Repositories');
- $url = self::URI . 'search?' . http_build_query($params);
+ public function collectData()
+ {
+ $params = ['utf8' => '✓',
+ 'q' => urlencode($this->getInput('s')),
+ 's' => 'updated',
+ 'o' => 'desc',
+ 'type' => 'Repositories'];
+ $url = self::URI . 'search?' . http_build_query($params);
- $html = getSimpleHTMLDOM($url);
+ $html = getSimpleHTMLDOM($url);
- foreach($html->find('li.repo-list-item') as $element) {
- $item = array();
+ foreach ($html->find('li.repo-list-item') as $element) {
+ $item = [];
- $uri = $element->find('.f4 a', 0)->href;
- $uri = substr(self::URI, 0, -1) . $uri;
- $item['uri'] = $uri;
+ $uri = $element->find('.f4 a', 0)->href;
+ $uri = substr(self::URI, 0, -1) . $uri;
+ $item['uri'] = $uri;
- $title = $element->find('.f4', 0)->plaintext;
- $item['title'] = $title;
+ $title = $element->find('.f4', 0)->plaintext;
+ $item['title'] = $title;
- // Description
- if (count($element->find('p.mb-1')) != 0) {
- $content = $element->find('p.mb-1', 0)->innertext;
- } else{
- $content = 'No description';
- }
+ // Description
+ if (count($element->find('p.mb-1')) != 0) {
+ $content = $element->find('p.mb-1', 0)->innertext;
+ } else {
+ $content = 'No description';
+ }
- // Tags
- $content = $content . '<br />';
- $tags = $element->find('a.topic-tag');
- $tags_array = array();
- if (count($tags) != 0) {
- $content = $content . 'Tags : ';
- foreach($tags as $tag_element) {
- $tag_link = 'https://github.com' . $tag_element->href;
- $tag_name = trim($tag_element->innertext);
- $content = $content . '<a href="' . $tag_link . '">' . $tag_name . '</a> ';
- array_push($tags_array, $tag_element->innertext);
- }
- }
+ // Tags
+ $content = $content . '<br />';
+ $tags = $element->find('a.topic-tag');
+ $tags_array = [];
+ if (count($tags) != 0) {
+ $content = $content . 'Tags : ';
+ foreach ($tags as $tag_element) {
+ $tag_link = 'https://github.com' . $tag_element->href;
+ $tag_name = trim($tag_element->innertext);
+ $content = $content . '<a href="' . $tag_link . '">' . $tag_name . '</a> ';
+ array_push($tags_array, $tag_element->innertext);
+ }
+ }
- $item['categories'] = $tags_array;
- $item['content'] = $content;
- $date = $element->find('relative-time', 0)->datetime;
- $item['timestamp'] = strtotime($date);
+ $item['categories'] = $tags_array;
+ $item['content'] = $content;
+ $date = $element->find('relative-time', 0)->datetime;
+ $item['timestamp'] = strtotime($date);
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/GithubTrendingBridge.php b/bridges/GithubTrendingBridge.php
index 16705b80..50d7d37d 100644
--- a/bridges/GithubTrendingBridge.php
+++ b/bridges/GithubTrendingBridge.php
@@ -1,630 +1,634 @@
<?php
-class GithubTrendingBridge extends BridgeAbstract {
- const MAINTAINER = 'liamka';
- const NAME = 'Github Trending';
- const URI = 'https://github.com/trending';
- const URI_ITEM = 'https://github.com';
- const CACHE_TIMEOUT = 43200; // 12hr
- const DESCRIPTION = 'See what the GitHub community is most excited repos.';
- const PARAMETERS = array(
- // If you are changing context and/or parameter names, change them also in getName().
- 'By language' => array(
- 'language' => array(
- 'name' => 'Select language',
- 'type' => 'list',
- 'values' => array(
- 'All languages' => '',
- 'HTML' => 'html',
- 'PHP' => 'php',
- 'Python' => 'python',
- 'Shell' => 'shell',
- 'Unknown languages' => 'unknown',
- '1C Enterprise' => '1c-enterprise',
- '4D' => '4d',
- 'ABAP' => 'abap',
- 'ABNF' => 'abnf',
- 'ActionScript' => 'actionscript',
- 'Ada' => 'ada',
- 'Adobe Font Metrics' => 'adobe-font-metrics',
- 'Agda' => 'agda',
- 'AGS Script' => 'ags-script',
- 'Alloy' => 'alloy',
- 'Alpine Abuild' => 'alpine-abuild',
- 'Altium Designer' => 'altium-designer',
- 'AMPL' => 'ampl',
- 'AngelScript' => 'angelscript',
- 'Ant Build System' => 'ant-build-system',
- 'ANTLR' => 'antlr',
- 'ApacheConf' => 'apacheconf',
- 'Apex' => 'apex',
- 'API Blueprint' => 'api-blueprint',
- 'APL' => 'apl',
- 'Apollo Guidance Computer' => 'apollo-guidance-computer',
- 'AppleScript' => 'applescript',
- 'Arc' => 'arc',
- 'AsciiDoc' => 'asciidoc',
- 'ASN.1' => 'asn.1',
- 'ASP' => 'asp',
- 'AspectJ' => 'aspectj',
- 'Assembly' => 'assembly',
- 'Asymptote' => 'asymptote',
- 'ATS' => 'ats',
- 'Augeas' => 'augeas',
- 'AutoHotkey' => 'autohotkey',
- 'AutoIt' => 'autoit',
- 'Awk' => 'awk',
- 'Ballerina' => 'ballerina',
- 'Batchfile' => 'batchfile',
- 'Befunge' => 'befunge',
- 'BibTeX' => 'bibtex',
- 'Bison' => 'bison',
- 'BitBake' => 'bitbake',
- 'Blade' => 'blade',
- 'BlitzBasic' => 'blitzbasic',
- 'BlitzMax' => 'blitzmax',
- 'Bluespec' => 'bluespec',
- 'Boo' => 'boo',
- 'Brainfuck' => 'brainfuck',
- 'Brightscript' => 'brightscript',
- 'Zeek' => 'zeek',
- 'C' => 'c',
- 'C#' => 'c%23', // already URL encoded
- 'C++' => 'c++',
- 'C-ObjDump' => 'c-objdump',
- 'C2hs Haskell' => 'c2hs-haskell',
- 'Cabal Config' => 'cabal-config',
- 'Cap\'n Proto' => 'cap\'n-proto',
- 'CartoCSS' => 'cartocss',
- 'Ceylon' => 'ceylon',
- 'Chapel' => 'chapel',
- 'Charity' => 'charity',
- 'ChucK' => 'chuck',
- 'Cirru' => 'cirru',
- 'Clarion' => 'clarion',
- 'Clean' => 'clean',
- 'Click' => 'click',
- 'CLIPS' => 'clips',
- 'Clojure' => 'clojure',
- 'Closure Templates' => 'closure-templates',
- 'Cloud Firestore Security Rules' => 'cloud-firestore-security-rules',
- 'CMake' => 'cmake',
- 'COBOL' => 'cobol',
- 'CodeQL' => 'codeql',
- 'CoffeeScript' => 'coffeescript',
- 'ColdFusion' => 'coldfusion',
- 'ColdFusion CFC' => 'coldfusion-cfc',
- 'COLLADA' => 'collada',
- 'Common Lisp' => 'common-lisp',
- 'Common Workflow Language' => 'common-workflow-language',
- 'Component Pascal' => 'component-pascal',
- 'CoNLL-U' => 'conll-u',
- 'Cool' => 'cool',
- 'Coq' => 'coq',
- 'Cpp-ObjDump' => 'cpp-objdump',
- 'Creole' => 'creole',
- 'Crystal' => 'crystal',
- 'CSON' => 'cson',
- 'Csound' => 'csound',
- 'Csound Document' => 'csound-document',
- 'Csound Score' => 'csound-score',
- 'CSS' => 'css',
- 'CSV' => 'csv',
- 'Cuda' => 'cuda',
- 'cURL Config' => 'curl-config',
- 'CWeb' => 'cweb',
- 'Cycript' => 'cycript',
- 'Cython' => 'cython',
- 'D' => 'd',
- 'D-ObjDump' => 'd-objdump',
- 'Darcs Patch' => 'darcs-patch',
- 'Dart' => 'dart',
- 'DataWeave' => 'dataweave',
- 'desktop' => 'desktop',
- 'Dhall' => 'dhall',
- 'Diff' => 'diff',
- 'DIGITAL Command Language' => 'digital-command-language',
- 'dircolors' => 'dircolors',
- 'DirectX 3D File' => 'directx-3d-file',
- 'DM' => 'dm',
- 'DNS Zone' => 'dns-zone',
- 'Dockerfile' => 'dockerfile',
- 'Dogescript' => 'dogescript',
- 'DTrace' => 'dtrace',
- 'Dylan' => 'dylan',
- 'E' => 'e',
- 'Eagle' => 'eagle',
- 'Easybuild' => 'easybuild',
- 'EBNF' => 'ebnf',
- 'eC' => 'ec',
- 'Ecere Projects' => 'ecere-projects',
- 'ECL' => 'ecl',
- 'ECLiPSe' => 'eclipse',
- 'EditorConfig' => 'editorconfig',
- 'Edje Data Collection' => 'edje-data-collection',
- 'edn' => 'edn',
- 'Eiffel' => 'eiffel',
- 'EJS' => 'ejs',
- 'Elixir' => 'elixir',
- 'Elm' => 'elm',
- 'Emacs Lisp' => 'emacs-lisp',
- 'EmberScript' => 'emberscript',
- 'EML' => 'eml',
- 'EQ' => 'eq',
- 'Erlang' => 'erlang',
- 'F#' => 'f%23', // already URL encoded
- 'F*' => 'f*',
- 'Factor' => 'factor',
- 'Fancy' => 'fancy',
- 'Fantom' => 'fantom',
- 'Faust' => 'faust',
- 'FIGlet Font' => 'figlet-font',
- 'Filebench WML' => 'filebench-wml',
- 'Filterscript' => 'filterscript',
- 'fish' => 'fish',
- 'FLUX' => 'flux',
- 'Formatted' => 'formatted',
- 'Forth' => 'forth',
- 'Fortran' => 'fortran',
- 'FreeMarker' => 'freemarker',
- 'Frege' => 'frege',
- 'G-code' => 'g-code',
- 'Game Maker Language' => 'game-maker-language',
- 'GAML' => 'gaml',
- 'GAMS' => 'gams',
- 'GAP' => 'gap',
- 'GCC Machine Description' => 'gcc-machine-description',
- 'GDB' => 'gdb',
- 'GDScript' => 'gdscript',
- 'Genie' => 'genie',
- 'Genshi' => 'genshi',
- 'Gentoo Ebuild' => 'gentoo-ebuild',
- 'Gentoo Eclass' => 'gentoo-eclass',
- 'Gerber Image' => 'gerber-image',
- 'Gettext Catalog' => 'gettext-catalog',
- 'Gherkin' => 'gherkin',
- 'Git Attributes' => 'git-attributes',
- 'Git Config' => 'git-config',
- 'GLSL' => 'glsl',
- 'Glyph' => 'glyph',
- 'Glyph Bitmap Distribution Format' => 'glyph-bitmap-distribution-format',
- 'GN' => 'gn',
- 'Gnuplot' => 'gnuplot',
- 'Go' => 'go',
- 'Golo' => 'golo',
- 'Gosu' => 'gosu',
- 'Grace' => 'grace',
- 'Gradle' => 'gradle',
- 'Grammatical Framework' => 'grammatical-framework',
- 'Graph Modeling Language' => 'graph-modeling-language',
- 'GraphQL' => 'graphql',
- 'Graphviz (DOT)' => 'graphviz-(dot)',
- 'Groovy' => 'groovy',
- 'Groovy Server Pages' => 'groovy-server-pages',
- 'Hack' => 'hack',
- 'Haml' => 'haml',
- 'Handlebars' => 'handlebars',
- 'HAProxy' => 'haproxy',
- 'Harbour' => 'harbour',
- 'Haskell' => 'haskell',
- 'Haxe' => 'haxe',
- 'HCL' => 'hcl',
- 'HiveQL' => 'hiveql',
- 'HLSL' => 'hlsl',
- 'HolyC' => 'holyc',
- 'HTML+Django' => 'html+django',
- 'HTML+ECR' => 'html+ecr',
- 'HTML+EEX' => 'html+eex',
- 'HTML+ERB' => 'html+erb',
- 'HTML+PHP' => 'html+php',
- 'HTML+Razor' => 'html+razor',
- 'HTTP' => 'http',
- 'HXML' => 'hxml',
- 'Hy' => 'hy',
- 'HyPhy' => 'hyphy',
- 'IDL' => 'idl',
- 'Idris' => 'idris',
- 'Ignore List' => 'ignore-list',
- 'IGOR Pro' => 'igor-pro',
- 'Inform 7' => 'inform-7',
- 'INI' => 'ini',
- 'Inno Setup' => 'inno-setup',
- 'Io' => 'io',
- 'Ioke' => 'ioke',
- 'IRC log' => 'irc-log',
- 'Isabelle' => 'isabelle',
- 'Isabelle ROOT' => 'isabelle-root',
- 'J' => 'j',
- 'Jasmin' => 'jasmin',
- 'Java' => 'java',
- 'Java Properties' => 'java-properties',
- 'Java Server Pages' => 'java-server-pages',
- 'JavaScript' => 'javascript',
- 'JavaScript+ERB' => 'javascript+erb',
- 'JFlex' => 'jflex',
- 'Jison' => 'jison',
- 'Jison Lex' => 'jison-lex',
- 'Jolie' => 'jolie',
- 'JSON' => 'json',
- 'JSON with Comments' => 'json-with-comments',
- 'JSON5' => 'json5',
- 'JSONiq' => 'jsoniq',
- 'JSONLD' => 'jsonld',
- 'Jsonnet' => 'jsonnet',
- 'JSX' => 'jsx',
- 'Julia' => 'julia',
- 'Jupyter Notebook' => 'jupyter-notebook',
- 'KiCad Layout' => 'kicad-layout',
- 'KiCad Legacy Layout' => 'kicad-legacy-layout',
- 'KiCad Schematic' => 'kicad-schematic',
- 'Kit' => 'kit',
- 'Kotlin' => 'kotlin',
- 'KRL' => 'krl',
- 'LabVIEW' => 'labview',
- 'Lasso' => 'lasso',
- 'Latte' => 'latte',
- 'Lean' => 'lean',
- 'Less' => 'less',
- 'Lex' => 'lex',
- 'LFE' => 'lfe',
- 'LilyPond' => 'lilypond',
- 'Limbo' => 'limbo',
- 'Linker Script' => 'linker-script',
- 'Linux Kernel Module' => 'linux-kernel-module',
- 'Liquid' => 'liquid',
- 'Literate Agda' => 'literate-agda',
- 'Literate CoffeeScript' => 'literate-coffeescript',
- 'Literate Haskell' => 'literate-haskell',
- 'LiveScript' => 'livescript',
- 'LLVM' => 'llvm',
- 'Logos' => 'logos',
- 'Logtalk' => 'logtalk',
- 'LOLCODE' => 'lolcode',
- 'LookML' => 'lookml',
- 'LoomScript' => 'loomscript',
- 'LSL' => 'lsl',
- 'LTspice Symbol' => 'ltspice-symbol',
- 'Lua' => 'lua',
- 'M' => 'm',
- 'M4' => 'm4',
- 'M4Sugar' => 'm4sugar',
- 'Makefile' => 'makefile',
- 'Mako' => 'mako',
- 'Markdown' => 'markdown',
- 'Marko' => 'marko',
- 'Mask' => 'mask',
- 'Mathematica' => 'mathematica',
- 'MATLAB' => 'matlab',
- 'Maven POM' => 'maven-pom',
- 'Max' => 'max',
- 'MAXScript' => 'maxscript',
- 'mcfunction' => 'mcfunction',
- 'MediaWiki' => 'mediawiki',
- 'Mercury' => 'mercury',
- 'Meson' => 'meson',
- 'Metal' => 'metal',
- 'Microsoft Developer Studio Project' => 'microsoft-developer-studio-project',
- 'MiniD' => 'minid',
- 'Mirah' => 'mirah',
- 'mIRC Script' => 'mirc-script',
- 'MLIR' => 'mlir',
- 'Modelica' => 'modelica',
- 'Modula-2' => 'modula-2',
- 'Modula-3' => 'modula-3',
- 'Module Management System' => 'module-management-system',
- 'Monkey' => 'monkey',
- 'Moocode' => 'moocode',
- 'MoonScript' => 'moonscript',
- 'Motorola 68K Assembly' => 'motorola-68k-assembly',
- 'MQL4' => 'mql4',
- 'MQL5' => 'mql5',
- 'MTML' => 'mtml',
- 'MUF' => 'muf',
- 'mupad' => 'mupad',
- 'Muse' => 'muse',
- 'Myghty' => 'myghty',
- 'nanorc' => 'nanorc',
- 'NASL' => 'nasl',
- 'NCL' => 'ncl',
- 'Nearley' => 'nearley',
- 'Nemerle' => 'nemerle',
- 'nesC' => 'nesc',
- 'NetLinx' => 'netlinx',
- 'NetLinx+ERB' => 'netlinx+erb',
- 'NetLogo' => 'netlogo',
- 'NewLisp' => 'newlisp',
- 'Nextflow' => 'nextflow',
- 'Nginx' => 'nginx',
- 'Nim' => 'nim',
- 'Ninja' => 'ninja',
- 'Nit' => 'nit',
- 'Nix' => 'nix',
- 'NL' => 'nl',
- 'NPM Config' => 'npm-config',
- 'NSIS' => 'nsis',
- 'Nu' => 'nu',
- 'NumPy' => 'numpy',
- 'ObjDump' => 'objdump',
- 'Object Data Instance Notation' => 'object-data-instance-notation',
- 'Objective-C' => 'objective-c',
- 'Objective-C++' => 'objective-c++',
- 'Objective-J' => 'objective-j',
- 'ObjectScript' => 'objectscript',
- 'OCaml' => 'ocaml',
- 'Odin' => 'odin',
- 'Omgrofl' => 'omgrofl',
- 'ooc' => 'ooc',
- 'Opa' => 'opa',
- 'Opal' => 'opal',
- 'Open Policy Agent' => 'open-policy-agent',
- 'OpenCL' => 'opencl',
- 'OpenEdge ABL' => 'openedge-abl',
- 'OpenQASM' => 'openqasm',
- 'OpenRC runscript' => 'openrc-runscript',
- 'OpenSCAD' => 'openscad',
- 'OpenStep Property List' => 'openstep-property-list',
- 'OpenType Feature File' => 'opentype-feature-file',
- 'Org' => 'org',
- 'Ox' => 'ox',
- 'Oxygene' => 'oxygene',
- 'Oz' => 'oz',
- 'P4' => 'p4',
- 'Pan' => 'pan',
- 'Papyrus' => 'papyrus',
- 'Parrot' => 'parrot',
- 'Parrot Assembly' => 'parrot-assembly',
- 'Parrot Internal Representation' => 'parrot-internal-representation',
- 'Pascal' => 'pascal',
- 'Pawn' => 'pawn',
- 'Pep8' => 'pep8',
- 'Perl' => 'perl',
- 'Pic' => 'pic',
- 'Pickle' => 'pickle',
- 'PicoLisp' => 'picolisp',
- 'PigLatin' => 'piglatin',
- 'Pike' => 'pike',
- 'PLpgSQL' => 'plpgsql',
- 'PLSQL' => 'plsql',
- 'Pod' => 'pod',
- 'Pod 6' => 'pod-6',
- 'PogoScript' => 'pogoscript',
- 'Pony' => 'pony',
- 'PostCSS' => 'postcss',
- 'PostScript' => 'postscript',
- 'POV-Ray SDL' => 'pov-ray-sdl',
- 'PowerBuilder' => 'powerbuilder',
- 'PowerShell' => 'powershell',
- 'Prisma' => 'prisma',
- 'Processing' => 'processing',
- 'Proguard' => 'proguard',
- 'Prolog' => 'prolog',
- 'Propeller Spin' => 'propeller-spin',
- 'Protocol Buffer' => 'protocol-buffer',
- 'Public Key' => 'public-key',
- 'Pug' => 'pug',
- 'Puppet' => 'puppet',
- 'Pure Data' => 'pure-data',
- 'PureBasic' => 'purebasic',
- 'PureScript' => 'purescript',
- 'Python console' => 'python-console',
- 'Python traceback' => 'python-traceback',
- 'q' => 'q',
- 'QMake' => 'qmake',
- 'QML' => 'qml',
- 'Quake' => 'quake',
- 'R' => 'r',
- 'Racket' => 'racket',
- 'Ragel' => 'ragel',
- 'Raku' => 'raku',
- 'RAML' => 'raml',
- 'Rascal' => 'rascal',
- 'Raw token data' => 'raw-token-data',
- 'RDoc' => 'rdoc',
- 'Readline Config' => 'readline-config',
- 'REALbasic' => 'realbasic',
- 'Reason' => 'reason',
- 'Rebol' => 'rebol',
- 'Red' => 'red',
- 'Redcode' => 'redcode',
- 'Regular Expression' => 'regular-expression',
- 'Ren\'Py' => 'ren\'py',
- 'RenderScript' => 'renderscript',
- 'reStructuredText' => 'restructuredtext',
- 'REXX' => 'rexx',
- 'RHTML' => 'rhtml',
- 'Rich Text Format' => 'rich-text-format',
- 'Ring' => 'ring',
- 'Riot' => 'riot',
- 'RMarkdown' => 'rmarkdown',
- 'RobotFramework' => 'robotframework',
- 'Roff' => 'roff',
- 'Roff Manpage' => 'roff-manpage',
- 'Rouge' => 'rouge',
- 'RPC' => 'rpc',
- 'RPM Spec' => 'rpm-spec',
- 'Ruby' => 'ruby',
- 'RUNOFF' => 'runoff',
- 'Rust' => 'rust',
- 'Sage' => 'sage',
- 'SaltStack' => 'saltstack',
- 'SAS' => 'sas',
- 'Sass' => 'sass',
- 'Scala' => 'scala',
- 'Scaml' => 'scaml',
- 'Scheme' => 'scheme',
- 'Scilab' => 'scilab',
- 'SCSS' => 'scss',
- 'sed' => 'sed',
- 'Self' => 'self',
- 'ShaderLab' => 'shaderlab',
- 'ShellSession' => 'shellsession',
- 'Shen' => 'shen',
- 'Slash' => 'slash',
- 'Slice' => 'slice',
- 'Slim' => 'slim',
- 'Smali' => 'smali',
- 'Smalltalk' => 'smalltalk',
- 'Smarty' => 'smarty',
- 'SmPL' => 'smpl',
- 'SMT' => 'smt',
- 'Solidity' => 'solidity',
- 'SourcePawn' => 'sourcepawn',
- 'SPARQL' => 'sparql',
- 'Spline Font Database' => 'spline-font-database',
- 'SQF' => 'sqf',
- 'SQL' => 'sql',
- 'SQLPL' => 'sqlpl',
- 'Squirrel' => 'squirrel',
- 'SRecode Template' => 'srecode-template',
- 'SSH Config' => 'ssh-config',
- 'Stan' => 'stan',
- 'Standard ML' => 'standard-ml',
- 'Starlark' => 'starlark',
- 'Stata' => 'stata',
- 'STON' => 'ston',
- 'Stylus' => 'stylus',
- 'SubRip Text' => 'subrip-text',
- 'SugarSS' => 'sugarss',
- 'SuperCollider' => 'supercollider',
- 'Svelte' => 'svelte',
- 'SVG' => 'svg',
- 'Swift' => 'swift',
- 'SWIG' => 'swig',
- 'SystemVerilog' => 'systemverilog',
- 'Tcl' => 'tcl',
- 'Tcsh' => 'tcsh',
- 'Tea' => 'tea',
- 'Terra' => 'terra',
- 'TeX' => 'tex',
- 'Texinfo' => 'texinfo',
- 'Text' => 'text',
- 'Textile' => 'textile',
- 'Thrift' => 'thrift',
- 'TI Program' => 'ti-program',
- 'TLA' => 'tla',
- 'TOML' => 'toml',
- 'TSQL' => 'tsql',
- 'TSX' => 'tsx',
- 'Turing' => 'turing',
- 'Turtle' => 'turtle',
- 'Twig' => 'twig',
- 'TXL' => 'txl',
- 'Type Language' => 'type-language',
- 'TypeScript' => 'typescript',
- 'Unified Parallel C' => 'unified-parallel-c',
- 'Unity3D Asset' => 'unity3d-asset',
- 'Unix Assembly' => 'unix-assembly',
- 'Uno' => 'uno',
- 'UnrealScript' => 'unrealscript',
- 'UrWeb' => 'urweb',
- 'V' => 'v',
- 'Vala' => 'vala',
- 'VBA' => 'vba',
- 'VBScript' => 'vbscript',
- 'VCL' => 'vcl',
- 'Verilog' => 'verilog',
- 'VHDL' => 'vhdl',
- 'Vim script' => 'vim-script',
- 'Vim Snippet' => 'vim-snippet',
- 'Visual Basic .NET' => 'visual-basic-.net',
- 'Visual Basic .NET' => 'visual-basic-.net',
- 'Volt' => 'volt',
- 'Vue' => 'vue',
- 'Wavefront Material' => 'wavefront-material',
- 'Wavefront Object' => 'wavefront-object',
- 'wdl' => 'wdl',
- 'Web Ontology Language' => 'web-ontology-language',
- 'WebAssembly' => 'webassembly',
- 'WebIDL' => 'webidl',
- 'WebVTT' => 'webvtt',
- 'Wget Config' => 'wget-config',
- 'Windows Registry Entries' => 'windows-registry-entries',
- 'wisp' => 'wisp',
- 'Wollok' => 'wollok',
- 'World of Warcraft Addon Data' => 'world-of-warcraft-addon-data',
- 'X BitMap' => 'x-bitmap',
- 'X Font Directory Index' => 'x-font-directory-index',
- 'X PixMap' => 'x-pixmap',
- 'X10' => 'x10',
- 'xBase' => 'xbase',
- 'XC' => 'xc',
- 'XCompose' => 'xcompose',
- 'XML' => 'xml',
- 'XML Property List' => 'xml-property-list',
- 'Xojo' => 'xojo',
- 'XPages' => 'xpages',
- 'XProc' => 'xproc',
- 'XQuery' => 'xquery',
- 'XS' => 'xs',
- 'XSLT' => 'xslt',
- 'Xtend' => 'xtend',
- 'Yacc' => 'yacc',
- 'YAML' => 'yaml',
- 'YANG' => 'yang',
- 'YARA' => 'yara',
- 'YASnippet' => 'yasnippet',
- 'ZAP' => 'zap',
- 'Zeek' => 'zeek',
- 'ZenScript' => 'zenscript',
- 'Zephir' => 'zephir',
- 'Zig' => 'zig',
- 'ZIL' => 'zil',
- 'Zimpl' => 'zimpl',
- ),
- 'defaultValue' => 'All languages'
- )
- ),
+class GithubTrendingBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'liamka';
+ const NAME = 'Github Trending';
+ const URI = 'https://github.com/trending';
+ const URI_ITEM = 'https://github.com';
+ const CACHE_TIMEOUT = 43200; // 12hr
+ const DESCRIPTION = 'See what the GitHub community is most excited repos.';
+ const PARAMETERS = [
+ // If you are changing context and/or parameter names, change them also in getName().
+ 'By language' => [
+ 'language' => [
+ 'name' => 'Select language',
+ 'type' => 'list',
+ 'values' => [
+ 'All languages' => '',
+ 'HTML' => 'html',
+ 'PHP' => 'php',
+ 'Python' => 'python',
+ 'Shell' => 'shell',
+ 'Unknown languages' => 'unknown',
+ '1C Enterprise' => '1c-enterprise',
+ '4D' => '4d',
+ 'ABAP' => 'abap',
+ 'ABNF' => 'abnf',
+ 'ActionScript' => 'actionscript',
+ 'Ada' => 'ada',
+ 'Adobe Font Metrics' => 'adobe-font-metrics',
+ 'Agda' => 'agda',
+ 'AGS Script' => 'ags-script',
+ 'Alloy' => 'alloy',
+ 'Alpine Abuild' => 'alpine-abuild',
+ 'Altium Designer' => 'altium-designer',
+ 'AMPL' => 'ampl',
+ 'AngelScript' => 'angelscript',
+ 'Ant Build System' => 'ant-build-system',
+ 'ANTLR' => 'antlr',
+ 'ApacheConf' => 'apacheconf',
+ 'Apex' => 'apex',
+ 'API Blueprint' => 'api-blueprint',
+ 'APL' => 'apl',
+ 'Apollo Guidance Computer' => 'apollo-guidance-computer',
+ 'AppleScript' => 'applescript',
+ 'Arc' => 'arc',
+ 'AsciiDoc' => 'asciidoc',
+ 'ASN.1' => 'asn.1',
+ 'ASP' => 'asp',
+ 'AspectJ' => 'aspectj',
+ 'Assembly' => 'assembly',
+ 'Asymptote' => 'asymptote',
+ 'ATS' => 'ats',
+ 'Augeas' => 'augeas',
+ 'AutoHotkey' => 'autohotkey',
+ 'AutoIt' => 'autoit',
+ 'Awk' => 'awk',
+ 'Ballerina' => 'ballerina',
+ 'Batchfile' => 'batchfile',
+ 'Befunge' => 'befunge',
+ 'BibTeX' => 'bibtex',
+ 'Bison' => 'bison',
+ 'BitBake' => 'bitbake',
+ 'Blade' => 'blade',
+ 'BlitzBasic' => 'blitzbasic',
+ 'BlitzMax' => 'blitzmax',
+ 'Bluespec' => 'bluespec',
+ 'Boo' => 'boo',
+ 'Brainfuck' => 'brainfuck',
+ 'Brightscript' => 'brightscript',
+ 'Zeek' => 'zeek',
+ 'C' => 'c',
+ 'C#' => 'c%23', // already URL encoded
+ 'C++' => 'c++',
+ 'C-ObjDump' => 'c-objdump',
+ 'C2hs Haskell' => 'c2hs-haskell',
+ 'Cabal Config' => 'cabal-config',
+ 'Cap\'n Proto' => 'cap\'n-proto',
+ 'CartoCSS' => 'cartocss',
+ 'Ceylon' => 'ceylon',
+ 'Chapel' => 'chapel',
+ 'Charity' => 'charity',
+ 'ChucK' => 'chuck',
+ 'Cirru' => 'cirru',
+ 'Clarion' => 'clarion',
+ 'Clean' => 'clean',
+ 'Click' => 'click',
+ 'CLIPS' => 'clips',
+ 'Clojure' => 'clojure',
+ 'Closure Templates' => 'closure-templates',
+ 'Cloud Firestore Security Rules' => 'cloud-firestore-security-rules',
+ 'CMake' => 'cmake',
+ 'COBOL' => 'cobol',
+ 'CodeQL' => 'codeql',
+ 'CoffeeScript' => 'coffeescript',
+ 'ColdFusion' => 'coldfusion',
+ 'ColdFusion CFC' => 'coldfusion-cfc',
+ 'COLLADA' => 'collada',
+ 'Common Lisp' => 'common-lisp',
+ 'Common Workflow Language' => 'common-workflow-language',
+ 'Component Pascal' => 'component-pascal',
+ 'CoNLL-U' => 'conll-u',
+ 'Cool' => 'cool',
+ 'Coq' => 'coq',
+ 'Cpp-ObjDump' => 'cpp-objdump',
+ 'Creole' => 'creole',
+ 'Crystal' => 'crystal',
+ 'CSON' => 'cson',
+ 'Csound' => 'csound',
+ 'Csound Document' => 'csound-document',
+ 'Csound Score' => 'csound-score',
+ 'CSS' => 'css',
+ 'CSV' => 'csv',
+ 'Cuda' => 'cuda',
+ 'cURL Config' => 'curl-config',
+ 'CWeb' => 'cweb',
+ 'Cycript' => 'cycript',
+ 'Cython' => 'cython',
+ 'D' => 'd',
+ 'D-ObjDump' => 'd-objdump',
+ 'Darcs Patch' => 'darcs-patch',
+ 'Dart' => 'dart',
+ 'DataWeave' => 'dataweave',
+ 'desktop' => 'desktop',
+ 'Dhall' => 'dhall',
+ 'Diff' => 'diff',
+ 'DIGITAL Command Language' => 'digital-command-language',
+ 'dircolors' => 'dircolors',
+ 'DirectX 3D File' => 'directx-3d-file',
+ 'DM' => 'dm',
+ 'DNS Zone' => 'dns-zone',
+ 'Dockerfile' => 'dockerfile',
+ 'Dogescript' => 'dogescript',
+ 'DTrace' => 'dtrace',
+ 'Dylan' => 'dylan',
+ 'E' => 'e',
+ 'Eagle' => 'eagle',
+ 'Easybuild' => 'easybuild',
+ 'EBNF' => 'ebnf',
+ 'eC' => 'ec',
+ 'Ecere Projects' => 'ecere-projects',
+ 'ECL' => 'ecl',
+ 'ECLiPSe' => 'eclipse',
+ 'EditorConfig' => 'editorconfig',
+ 'Edje Data Collection' => 'edje-data-collection',
+ 'edn' => 'edn',
+ 'Eiffel' => 'eiffel',
+ 'EJS' => 'ejs',
+ 'Elixir' => 'elixir',
+ 'Elm' => 'elm',
+ 'Emacs Lisp' => 'emacs-lisp',
+ 'EmberScript' => 'emberscript',
+ 'EML' => 'eml',
+ 'EQ' => 'eq',
+ 'Erlang' => 'erlang',
+ 'F#' => 'f%23', // already URL encoded
+ 'F*' => 'f*',
+ 'Factor' => 'factor',
+ 'Fancy' => 'fancy',
+ 'Fantom' => 'fantom',
+ 'Faust' => 'faust',
+ 'FIGlet Font' => 'figlet-font',
+ 'Filebench WML' => 'filebench-wml',
+ 'Filterscript' => 'filterscript',
+ 'fish' => 'fish',
+ 'FLUX' => 'flux',
+ 'Formatted' => 'formatted',
+ 'Forth' => 'forth',
+ 'Fortran' => 'fortran',
+ 'FreeMarker' => 'freemarker',
+ 'Frege' => 'frege',
+ 'G-code' => 'g-code',
+ 'Game Maker Language' => 'game-maker-language',
+ 'GAML' => 'gaml',
+ 'GAMS' => 'gams',
+ 'GAP' => 'gap',
+ 'GCC Machine Description' => 'gcc-machine-description',
+ 'GDB' => 'gdb',
+ 'GDScript' => 'gdscript',
+ 'Genie' => 'genie',
+ 'Genshi' => 'genshi',
+ 'Gentoo Ebuild' => 'gentoo-ebuild',
+ 'Gentoo Eclass' => 'gentoo-eclass',
+ 'Gerber Image' => 'gerber-image',
+ 'Gettext Catalog' => 'gettext-catalog',
+ 'Gherkin' => 'gherkin',
+ 'Git Attributes' => 'git-attributes',
+ 'Git Config' => 'git-config',
+ 'GLSL' => 'glsl',
+ 'Glyph' => 'glyph',
+ 'Glyph Bitmap Distribution Format' => 'glyph-bitmap-distribution-format',
+ 'GN' => 'gn',
+ 'Gnuplot' => 'gnuplot',
+ 'Go' => 'go',
+ 'Golo' => 'golo',
+ 'Gosu' => 'gosu',
+ 'Grace' => 'grace',
+ 'Gradle' => 'gradle',
+ 'Grammatical Framework' => 'grammatical-framework',
+ 'Graph Modeling Language' => 'graph-modeling-language',
+ 'GraphQL' => 'graphql',
+ 'Graphviz (DOT)' => 'graphviz-(dot)',
+ 'Groovy' => 'groovy',
+ 'Groovy Server Pages' => 'groovy-server-pages',
+ 'Hack' => 'hack',
+ 'Haml' => 'haml',
+ 'Handlebars' => 'handlebars',
+ 'HAProxy' => 'haproxy',
+ 'Harbour' => 'harbour',
+ 'Haskell' => 'haskell',
+ 'Haxe' => 'haxe',
+ 'HCL' => 'hcl',
+ 'HiveQL' => 'hiveql',
+ 'HLSL' => 'hlsl',
+ 'HolyC' => 'holyc',
+ 'HTML+Django' => 'html+django',
+ 'HTML+ECR' => 'html+ecr',
+ 'HTML+EEX' => 'html+eex',
+ 'HTML+ERB' => 'html+erb',
+ 'HTML+PHP' => 'html+php',
+ 'HTML+Razor' => 'html+razor',
+ 'HTTP' => 'http',
+ 'HXML' => 'hxml',
+ 'Hy' => 'hy',
+ 'HyPhy' => 'hyphy',
+ 'IDL' => 'idl',
+ 'Idris' => 'idris',
+ 'Ignore List' => 'ignore-list',
+ 'IGOR Pro' => 'igor-pro',
+ 'Inform 7' => 'inform-7',
+ 'INI' => 'ini',
+ 'Inno Setup' => 'inno-setup',
+ 'Io' => 'io',
+ 'Ioke' => 'ioke',
+ 'IRC log' => 'irc-log',
+ 'Isabelle' => 'isabelle',
+ 'Isabelle ROOT' => 'isabelle-root',
+ 'J' => 'j',
+ 'Jasmin' => 'jasmin',
+ 'Java' => 'java',
+ 'Java Properties' => 'java-properties',
+ 'Java Server Pages' => 'java-server-pages',
+ 'JavaScript' => 'javascript',
+ 'JavaScript+ERB' => 'javascript+erb',
+ 'JFlex' => 'jflex',
+ 'Jison' => 'jison',
+ 'Jison Lex' => 'jison-lex',
+ 'Jolie' => 'jolie',
+ 'JSON' => 'json',
+ 'JSON with Comments' => 'json-with-comments',
+ 'JSON5' => 'json5',
+ 'JSONiq' => 'jsoniq',
+ 'JSONLD' => 'jsonld',
+ 'Jsonnet' => 'jsonnet',
+ 'JSX' => 'jsx',
+ 'Julia' => 'julia',
+ 'Jupyter Notebook' => 'jupyter-notebook',
+ 'KiCad Layout' => 'kicad-layout',
+ 'KiCad Legacy Layout' => 'kicad-legacy-layout',
+ 'KiCad Schematic' => 'kicad-schematic',
+ 'Kit' => 'kit',
+ 'Kotlin' => 'kotlin',
+ 'KRL' => 'krl',
+ 'LabVIEW' => 'labview',
+ 'Lasso' => 'lasso',
+ 'Latte' => 'latte',
+ 'Lean' => 'lean',
+ 'Less' => 'less',
+ 'Lex' => 'lex',
+ 'LFE' => 'lfe',
+ 'LilyPond' => 'lilypond',
+ 'Limbo' => 'limbo',
+ 'Linker Script' => 'linker-script',
+ 'Linux Kernel Module' => 'linux-kernel-module',
+ 'Liquid' => 'liquid',
+ 'Literate Agda' => 'literate-agda',
+ 'Literate CoffeeScript' => 'literate-coffeescript',
+ 'Literate Haskell' => 'literate-haskell',
+ 'LiveScript' => 'livescript',
+ 'LLVM' => 'llvm',
+ 'Logos' => 'logos',
+ 'Logtalk' => 'logtalk',
+ 'LOLCODE' => 'lolcode',
+ 'LookML' => 'lookml',
+ 'LoomScript' => 'loomscript',
+ 'LSL' => 'lsl',
+ 'LTspice Symbol' => 'ltspice-symbol',
+ 'Lua' => 'lua',
+ 'M' => 'm',
+ 'M4' => 'm4',
+ 'M4Sugar' => 'm4sugar',
+ 'Makefile' => 'makefile',
+ 'Mako' => 'mako',
+ 'Markdown' => 'markdown',
+ 'Marko' => 'marko',
+ 'Mask' => 'mask',
+ 'Mathematica' => 'mathematica',
+ 'MATLAB' => 'matlab',
+ 'Maven POM' => 'maven-pom',
+ 'Max' => 'max',
+ 'MAXScript' => 'maxscript',
+ 'mcfunction' => 'mcfunction',
+ 'MediaWiki' => 'mediawiki',
+ 'Mercury' => 'mercury',
+ 'Meson' => 'meson',
+ 'Metal' => 'metal',
+ 'Microsoft Developer Studio Project' => 'microsoft-developer-studio-project',
+ 'MiniD' => 'minid',
+ 'Mirah' => 'mirah',
+ 'mIRC Script' => 'mirc-script',
+ 'MLIR' => 'mlir',
+ 'Modelica' => 'modelica',
+ 'Modula-2' => 'modula-2',
+ 'Modula-3' => 'modula-3',
+ 'Module Management System' => 'module-management-system',
+ 'Monkey' => 'monkey',
+ 'Moocode' => 'moocode',
+ 'MoonScript' => 'moonscript',
+ 'Motorola 68K Assembly' => 'motorola-68k-assembly',
+ 'MQL4' => 'mql4',
+ 'MQL5' => 'mql5',
+ 'MTML' => 'mtml',
+ 'MUF' => 'muf',
+ 'mupad' => 'mupad',
+ 'Muse' => 'muse',
+ 'Myghty' => 'myghty',
+ 'nanorc' => 'nanorc',
+ 'NASL' => 'nasl',
+ 'NCL' => 'ncl',
+ 'Nearley' => 'nearley',
+ 'Nemerle' => 'nemerle',
+ 'nesC' => 'nesc',
+ 'NetLinx' => 'netlinx',
+ 'NetLinx+ERB' => 'netlinx+erb',
+ 'NetLogo' => 'netlogo',
+ 'NewLisp' => 'newlisp',
+ 'Nextflow' => 'nextflow',
+ 'Nginx' => 'nginx',
+ 'Nim' => 'nim',
+ 'Ninja' => 'ninja',
+ 'Nit' => 'nit',
+ 'Nix' => 'nix',
+ 'NL' => 'nl',
+ 'NPM Config' => 'npm-config',
+ 'NSIS' => 'nsis',
+ 'Nu' => 'nu',
+ 'NumPy' => 'numpy',
+ 'ObjDump' => 'objdump',
+ 'Object Data Instance Notation' => 'object-data-instance-notation',
+ 'Objective-C' => 'objective-c',
+ 'Objective-C++' => 'objective-c++',
+ 'Objective-J' => 'objective-j',
+ 'ObjectScript' => 'objectscript',
+ 'OCaml' => 'ocaml',
+ 'Odin' => 'odin',
+ 'Omgrofl' => 'omgrofl',
+ 'ooc' => 'ooc',
+ 'Opa' => 'opa',
+ 'Opal' => 'opal',
+ 'Open Policy Agent' => 'open-policy-agent',
+ 'OpenCL' => 'opencl',
+ 'OpenEdge ABL' => 'openedge-abl',
+ 'OpenQASM' => 'openqasm',
+ 'OpenRC runscript' => 'openrc-runscript',
+ 'OpenSCAD' => 'openscad',
+ 'OpenStep Property List' => 'openstep-property-list',
+ 'OpenType Feature File' => 'opentype-feature-file',
+ 'Org' => 'org',
+ 'Ox' => 'ox',
+ 'Oxygene' => 'oxygene',
+ 'Oz' => 'oz',
+ 'P4' => 'p4',
+ 'Pan' => 'pan',
+ 'Papyrus' => 'papyrus',
+ 'Parrot' => 'parrot',
+ 'Parrot Assembly' => 'parrot-assembly',
+ 'Parrot Internal Representation' => 'parrot-internal-representation',
+ 'Pascal' => 'pascal',
+ 'Pawn' => 'pawn',
+ 'Pep8' => 'pep8',
+ 'Perl' => 'perl',
+ 'Pic' => 'pic',
+ 'Pickle' => 'pickle',
+ 'PicoLisp' => 'picolisp',
+ 'PigLatin' => 'piglatin',
+ 'Pike' => 'pike',
+ 'PLpgSQL' => 'plpgsql',
+ 'PLSQL' => 'plsql',
+ 'Pod' => 'pod',
+ 'Pod 6' => 'pod-6',
+ 'PogoScript' => 'pogoscript',
+ 'Pony' => 'pony',
+ 'PostCSS' => 'postcss',
+ 'PostScript' => 'postscript',
+ 'POV-Ray SDL' => 'pov-ray-sdl',
+ 'PowerBuilder' => 'powerbuilder',
+ 'PowerShell' => 'powershell',
+ 'Prisma' => 'prisma',
+ 'Processing' => 'processing',
+ 'Proguard' => 'proguard',
+ 'Prolog' => 'prolog',
+ 'Propeller Spin' => 'propeller-spin',
+ 'Protocol Buffer' => 'protocol-buffer',
+ 'Public Key' => 'public-key',
+ 'Pug' => 'pug',
+ 'Puppet' => 'puppet',
+ 'Pure Data' => 'pure-data',
+ 'PureBasic' => 'purebasic',
+ 'PureScript' => 'purescript',
+ 'Python console' => 'python-console',
+ 'Python traceback' => 'python-traceback',
+ 'q' => 'q',
+ 'QMake' => 'qmake',
+ 'QML' => 'qml',
+ 'Quake' => 'quake',
+ 'R' => 'r',
+ 'Racket' => 'racket',
+ 'Ragel' => 'ragel',
+ 'Raku' => 'raku',
+ 'RAML' => 'raml',
+ 'Rascal' => 'rascal',
+ 'Raw token data' => 'raw-token-data',
+ 'RDoc' => 'rdoc',
+ 'Readline Config' => 'readline-config',
+ 'REALbasic' => 'realbasic',
+ 'Reason' => 'reason',
+ 'Rebol' => 'rebol',
+ 'Red' => 'red',
+ 'Redcode' => 'redcode',
+ 'Regular Expression' => 'regular-expression',
+ 'Ren\'Py' => 'ren\'py',
+ 'RenderScript' => 'renderscript',
+ 'reStructuredText' => 'restructuredtext',
+ 'REXX' => 'rexx',
+ 'RHTML' => 'rhtml',
+ 'Rich Text Format' => 'rich-text-format',
+ 'Ring' => 'ring',
+ 'Riot' => 'riot',
+ 'RMarkdown' => 'rmarkdown',
+ 'RobotFramework' => 'robotframework',
+ 'Roff' => 'roff',
+ 'Roff Manpage' => 'roff-manpage',
+ 'Rouge' => 'rouge',
+ 'RPC' => 'rpc',
+ 'RPM Spec' => 'rpm-spec',
+ 'Ruby' => 'ruby',
+ 'RUNOFF' => 'runoff',
+ 'Rust' => 'rust',
+ 'Sage' => 'sage',
+ 'SaltStack' => 'saltstack',
+ 'SAS' => 'sas',
+ 'Sass' => 'sass',
+ 'Scala' => 'scala',
+ 'Scaml' => 'scaml',
+ 'Scheme' => 'scheme',
+ 'Scilab' => 'scilab',
+ 'SCSS' => 'scss',
+ 'sed' => 'sed',
+ 'Self' => 'self',
+ 'ShaderLab' => 'shaderlab',
+ 'ShellSession' => 'shellsession',
+ 'Shen' => 'shen',
+ 'Slash' => 'slash',
+ 'Slice' => 'slice',
+ 'Slim' => 'slim',
+ 'Smali' => 'smali',
+ 'Smalltalk' => 'smalltalk',
+ 'Smarty' => 'smarty',
+ 'SmPL' => 'smpl',
+ 'SMT' => 'smt',
+ 'Solidity' => 'solidity',
+ 'SourcePawn' => 'sourcepawn',
+ 'SPARQL' => 'sparql',
+ 'Spline Font Database' => 'spline-font-database',
+ 'SQF' => 'sqf',
+ 'SQL' => 'sql',
+ 'SQLPL' => 'sqlpl',
+ 'Squirrel' => 'squirrel',
+ 'SRecode Template' => 'srecode-template',
+ 'SSH Config' => 'ssh-config',
+ 'Stan' => 'stan',
+ 'Standard ML' => 'standard-ml',
+ 'Starlark' => 'starlark',
+ 'Stata' => 'stata',
+ 'STON' => 'ston',
+ 'Stylus' => 'stylus',
+ 'SubRip Text' => 'subrip-text',
+ 'SugarSS' => 'sugarss',
+ 'SuperCollider' => 'supercollider',
+ 'Svelte' => 'svelte',
+ 'SVG' => 'svg',
+ 'Swift' => 'swift',
+ 'SWIG' => 'swig',
+ 'SystemVerilog' => 'systemverilog',
+ 'Tcl' => 'tcl',
+ 'Tcsh' => 'tcsh',
+ 'Tea' => 'tea',
+ 'Terra' => 'terra',
+ 'TeX' => 'tex',
+ 'Texinfo' => 'texinfo',
+ 'Text' => 'text',
+ 'Textile' => 'textile',
+ 'Thrift' => 'thrift',
+ 'TI Program' => 'ti-program',
+ 'TLA' => 'tla',
+ 'TOML' => 'toml',
+ 'TSQL' => 'tsql',
+ 'TSX' => 'tsx',
+ 'Turing' => 'turing',
+ 'Turtle' => 'turtle',
+ 'Twig' => 'twig',
+ 'TXL' => 'txl',
+ 'Type Language' => 'type-language',
+ 'TypeScript' => 'typescript',
+ 'Unified Parallel C' => 'unified-parallel-c',
+ 'Unity3D Asset' => 'unity3d-asset',
+ 'Unix Assembly' => 'unix-assembly',
+ 'Uno' => 'uno',
+ 'UnrealScript' => 'unrealscript',
+ 'UrWeb' => 'urweb',
+ 'V' => 'v',
+ 'Vala' => 'vala',
+ 'VBA' => 'vba',
+ 'VBScript' => 'vbscript',
+ 'VCL' => 'vcl',
+ 'Verilog' => 'verilog',
+ 'VHDL' => 'vhdl',
+ 'Vim script' => 'vim-script',
+ 'Vim Snippet' => 'vim-snippet',
+ 'Visual Basic .NET' => 'visual-basic-.net',
+ 'Visual Basic .NET' => 'visual-basic-.net',
+ 'Volt' => 'volt',
+ 'Vue' => 'vue',
+ 'Wavefront Material' => 'wavefront-material',
+ 'Wavefront Object' => 'wavefront-object',
+ 'wdl' => 'wdl',
+ 'Web Ontology Language' => 'web-ontology-language',
+ 'WebAssembly' => 'webassembly',
+ 'WebIDL' => 'webidl',
+ 'WebVTT' => 'webvtt',
+ 'Wget Config' => 'wget-config',
+ 'Windows Registry Entries' => 'windows-registry-entries',
+ 'wisp' => 'wisp',
+ 'Wollok' => 'wollok',
+ 'World of Warcraft Addon Data' => 'world-of-warcraft-addon-data',
+ 'X BitMap' => 'x-bitmap',
+ 'X Font Directory Index' => 'x-font-directory-index',
+ 'X PixMap' => 'x-pixmap',
+ 'X10' => 'x10',
+ 'xBase' => 'xbase',
+ 'XC' => 'xc',
+ 'XCompose' => 'xcompose',
+ 'XML' => 'xml',
+ 'XML Property List' => 'xml-property-list',
+ 'Xojo' => 'xojo',
+ 'XPages' => 'xpages',
+ 'XProc' => 'xproc',
+ 'XQuery' => 'xquery',
+ 'XS' => 'xs',
+ 'XSLT' => 'xslt',
+ 'Xtend' => 'xtend',
+ 'Yacc' => 'yacc',
+ 'YAML' => 'yaml',
+ 'YANG' => 'yang',
+ 'YARA' => 'yara',
+ 'YASnippet' => 'yasnippet',
+ 'ZAP' => 'zap',
+ 'Zeek' => 'zeek',
+ 'ZenScript' => 'zenscript',
+ 'Zephir' => 'zephir',
+ 'Zig' => 'zig',
+ 'ZIL' => 'zil',
+ 'Zimpl' => 'zimpl',
+ ],
+ 'defaultValue' => 'All languages'
+ ]
+ ],
- 'global' => array(
- 'date_range' => array(
- 'name' => 'Date range',
- 'type' => 'list',
- 'values' => array(
- 'Today' => 'today',
- 'Weekly' => 'weekly',
- 'Monthly' => 'monthly',
- ),
- 'defaultValue' => 'today'
- )
- )
+ 'global' => [
+ 'date_range' => [
+ 'name' => 'Date range',
+ 'type' => 'list',
+ 'values' => [
+ 'Today' => 'today',
+ 'Weekly' => 'weekly',
+ 'Monthly' => 'monthly',
+ ],
+ 'defaultValue' => 'today'
+ ]
+ ]
- );
+ ];
- public function collectData(){
- $params = array('since' => urlencode($this->getInput('date_range')));
- $url = self::URI . '/' . $this->getInput('language') . '?' . http_build_query($params);
+ public function collectData()
+ {
+ $params = ['since' => urlencode($this->getInput('date_range'))];
+ $url = self::URI . '/' . $this->getInput('language') . '?' . http_build_query($params);
- $html = getSimpleHTMLDOM($url);
+ $html = getSimpleHTMLDOM($url);
- $this->items = array();
- foreach($html->find('.Box-row') as $element) {
- $item = array();
+ $this->items = [];
+ foreach ($html->find('.Box-row') as $element) {
+ $item = [];
- // URI
- $item['uri'] = self::URI_ITEM . $element->find('h1 a', 0)->href;
+ // URI
+ $item['uri'] = self::URI_ITEM . $element->find('h1 a', 0)->href;
- // Title
- $item['title'] = str_replace(' ', '', trim(strip_tags($element->find('h1 a', 0)->plaintext)));
+ // Title
+ $item['title'] = str_replace(' ', '', trim(strip_tags($element->find('h1 a', 0)->plaintext)));
- // Description
- $description = $element->find('p', 0);
- if ($description != null)
- $item['content'] = trim(strip_tags($description->innertext));
+ // Description
+ $description = $element->find('p', 0);
+ if ($description != null) {
+ $item['content'] = trim(strip_tags($description->innertext));
+ }
- // Time
- $item['timestamp'] = time();
+ // Time
+ $item['timestamp'] = time();
- // TODO: Proxy?
- $this->items[] = $item;
- }
- }
+ // TODO: Proxy?
+ $this->items[] = $item;
+ }
+ }
- public function getName(){
- if (!is_null($this->getInput('language'))) {
- $language = array_search($this->getInput('language'), self::PARAMETERS['By language']['language']['values']);
- return self::NAME . ': ' . $language;
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('language'))) {
+ $language = array_search($this->getInput('language'), self::PARAMETERS['By language']['language']['values']);
+ return self::NAME . ': ' . $language;
+ }
- return parent::getName();
- }
+ return parent::getName();
+ }
}
diff --git a/bridges/GitlabIssueBridge.php b/bridges/GitlabIssueBridge.php
index ce3ab08b..ebcdbb4c 100644
--- a/bridges/GitlabIssueBridge.php
+++ b/bridges/GitlabIssueBridge.php
@@ -1,205 +1,214 @@
<?php
-class GitlabIssueBridge extends BridgeAbstract {
-
- const MAINTAINER = 'Mynacol';
- const NAME = 'Gitlab Issue/Merge Request';
- const URI = 'https://gitlab.com/';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Returns comments of an issue/MR of a gitlab project';
-
- const PARAMETERS = array(
- 'global' => array(
- 'h' => array(
- 'name' => 'Gitlab instance host name',
- 'exampleValue' => 'gitlab.com',
- 'defaultValue' => 'gitlab.com',
- 'required' => true
- ),
- 'u' => array(
- 'name' => 'User/Organization name',
- 'exampleValue' => 'fdroid',
- 'required' => true
- ),
- 'p' => array(
- 'name' => 'Project name',
- 'exampleValue' => 'fdroidclient',
- 'required' => true
- )
-
- ),
- 'Issue comments' => array(
- 'i' => array(
- 'name' => 'Issue number',
- 'type' => 'number',
- 'exampleValue' => '2099',
- 'required' => true
- )
- ),
- 'Merge Request comments' => array(
- 'i' => array(
- 'name' => 'Merge Request number',
- 'type' => 'number',
- 'exampleValue' => '2099',
- 'required' => true
- )
- )
- );
-
- public function getName(){
- $name = $this->getInput('h') . '/' . $this->getInput('u') . '/' . $this->getInput('p');
- switch ($this->queriedContext) {
- case 'Issue comments':
- $name .= ' Issue #' . $this->getInput('i');
- break;
- case 'Merge Request comments':
- $name .= ' MR !' . $this->getInput('i');
- break;
- default:
- return parent::getName();
- }
- return $name;
- }
-
- public function getURI() {
- $host = $this->getInput('h') ?? 'gitlab.com';
- $uri = 'https://' . $host . '/' . $this->getInput('u') . '/'
- . $this->getInput('p') . '/';
- switch ($this->queriedContext) {
- case 'Issue comments':
- $uri .= '-/issues';
- break;
- case 'Merge Request comments':
- $uri .= '-/merge_requests';
- break;
- default:
- return $uri;
- }
- $uri .= '/' . $this->getInput('i');
- return $uri;
- }
-
- public function getIcon() {
- return 'https://' . $this->getInput('h') . '/favicon.ico';
- }
-
- public function collectData() {
- switch ($this->queriedContext) {
- case 'Issue comments':
- $this->items[] = $this->parseIssueDescription();
- break;
- case 'Merge Request comments':
- $this->items[] = $this->parseMergeRequestDescription();
- break;
- default:
- break;
- }
-
- /* parse issue/MR comments */
- $comments_uri = $this->getURI() . '/discussions.json';
- $comments = getContents($comments_uri);
- $comments = json_decode($comments, false);
-
- foreach ($comments as $value) {
- foreach ($value->notes as $comment) {
- $item = array();
- $item['uri'] = $comment->noteable_note_url;
- $item['uid'] = $item['uri'];
-
- // TODO fix invalid timestamps (fdroid bot)
- $item['timestamp'] = $comment->created_at ?? $comment->updated_at ?? $comment->last_edited_at;
- $author = $comment->author ?? $comment->last_edited_by;
- $item['author'] = '<img src="' . $author->avatar_url . '" width=24></img> <a href="https://' .
- $this->getInput('h') . $author->path . '">' . $author->name . ' @' . $author->username . '</a>';
-
- $content = '';
- if ($comment->system) {
- $content = $comment->note_html;
- if ($comment->type === 'StateNote') {
- $content .= ' the issue';
- } elseif ($comment->type === null) {
- // e.g. "added 900 commits\n800 from master\n175h4d - commit message\n..."
- $content = str_get_html($comment->note_html)->find('p', 0);
- }
- } else {
- // no switch-case to do strict comparison
- if ($comment->type === null || $comment->type === 'DiscussionNote') {
- $content = 'commented';
- } elseif ($comment->type === 'DiffNote') {
- $content = 'commented on a thread';
- } else {
- $content = $comment->note_html;
- }
- }
- $item['title'] = $author->name . " $content";
-
- $content = $this->fixImgSrc($comment->note_html);
- $item['content'] = defaultLinkTo($content, 'https://' . $this->getInput('h') . '/');
-
- $this->items[] = $item;
- }
- }
- }
-
- private function parseIssueDescription() {
- $description_uri = $this->getURI() . '.json';
- $description = getContents($description_uri);
- $description = json_decode($description, false);
- $description_html = getSimpleHtmlDomCached($this->getURI());
-
- $item = array();
- $item['uri'] = $this->getURI();
- $item['uid'] = $item['uri'];
-
- $item['timestamp'] = $description->created_at ?? $description->updated_at;
-
- $item['author'] = $this->parseAuthor($description_html);
-
- $item['title'] = $description->title;
- $item['content'] = markdownToHtml($description->description);
-
- return $item;
- }
-
- private function parseMergeRequestDescription() {
- $description_uri = $this->getURI() . '/cached_widget.json';
- $description = getContents($description_uri);
- $description = json_decode($description, false);
- $description_html = getSimpleHtmlDomCached($this->getURI());
-
- $item = array();
- $item['uri'] = $this->getURI();
- $item['uid'] = $item['uri'];
-
- $item['timestamp'] = $description_html->find('.merge-request-details time', 0)->datetime;
-
- $item['author'] = $this->parseAuthor($description_html);
-
- $item['title'] = 'Merge Request ' . $description->title;
- $item['content'] = markdownToHtml($description->description);
-
- return $item;
- }
-
- private function fixImgSrc($html) {
- if (is_string($html)) {
- $html = str_get_html($html);
- }
-
- foreach ($html->find('img') as $img) {
- $img->src = $img->getAttribute('data-src');
- }
- return $html;
- }
-
- private function parseAuthor($description_html) {
- $description_html = $this->fixImgSrc($description_html);
-
- $authors = $description_html->find('.issuable-meta a.author-link, .merge-request a.author-link');
- $editors = $description_html->find('.edited-text a.author-link');
- $author_str = implode(' ', $authors);
- if ($editors) {
- $author_str .= ', ' . implode(' ', $editors);
- }
- return defaultLinkTo($author_str, 'https://' . $this->getInput('h') . '/');
- }
+
+class GitlabIssueBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Mynacol';
+ const NAME = 'Gitlab Issue/Merge Request';
+ const URI = 'https://gitlab.com/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns comments of an issue/MR of a gitlab project';
+
+ const PARAMETERS = [
+ 'global' => [
+ 'h' => [
+ 'name' => 'Gitlab instance host name',
+ 'exampleValue' => 'gitlab.com',
+ 'defaultValue' => 'gitlab.com',
+ 'required' => true
+ ],
+ 'u' => [
+ 'name' => 'User/Organization name',
+ 'exampleValue' => 'fdroid',
+ 'required' => true
+ ],
+ 'p' => [
+ 'name' => 'Project name',
+ 'exampleValue' => 'fdroidclient',
+ 'required' => true
+ ]
+
+ ],
+ 'Issue comments' => [
+ 'i' => [
+ 'name' => 'Issue number',
+ 'type' => 'number',
+ 'exampleValue' => '2099',
+ 'required' => true
+ ]
+ ],
+ 'Merge Request comments' => [
+ 'i' => [
+ 'name' => 'Merge Request number',
+ 'type' => 'number',
+ 'exampleValue' => '2099',
+ 'required' => true
+ ]
+ ]
+ ];
+
+ public function getName()
+ {
+ $name = $this->getInput('h') . '/' . $this->getInput('u') . '/' . $this->getInput('p');
+ switch ($this->queriedContext) {
+ case 'Issue comments':
+ $name .= ' Issue #' . $this->getInput('i');
+ break;
+ case 'Merge Request comments':
+ $name .= ' MR !' . $this->getInput('i');
+ break;
+ default:
+ return parent::getName();
+ }
+ return $name;
+ }
+
+ public function getURI()
+ {
+ $host = $this->getInput('h') ?? 'gitlab.com';
+ $uri = 'https://' . $host . '/' . $this->getInput('u') . '/'
+ . $this->getInput('p') . '/';
+ switch ($this->queriedContext) {
+ case 'Issue comments':
+ $uri .= '-/issues';
+ break;
+ case 'Merge Request comments':
+ $uri .= '-/merge_requests';
+ break;
+ default:
+ return $uri;
+ }
+ $uri .= '/' . $this->getInput('i');
+ return $uri;
+ }
+
+ public function getIcon()
+ {
+ return 'https://' . $this->getInput('h') . '/favicon.ico';
+ }
+
+ public function collectData()
+ {
+ switch ($this->queriedContext) {
+ case 'Issue comments':
+ $this->items[] = $this->parseIssueDescription();
+ break;
+ case 'Merge Request comments':
+ $this->items[] = $this->parseMergeRequestDescription();
+ break;
+ default:
+ break;
+ }
+
+ /* parse issue/MR comments */
+ $comments_uri = $this->getURI() . '/discussions.json';
+ $comments = getContents($comments_uri);
+ $comments = json_decode($comments, false);
+
+ foreach ($comments as $value) {
+ foreach ($value->notes as $comment) {
+ $item = [];
+ $item['uri'] = $comment->noteable_note_url;
+ $item['uid'] = $item['uri'];
+
+ // TODO fix invalid timestamps (fdroid bot)
+ $item['timestamp'] = $comment->created_at ?? $comment->updated_at ?? $comment->last_edited_at;
+ $author = $comment->author ?? $comment->last_edited_by;
+ $item['author'] = '<img src="' . $author->avatar_url . '" width=24></img> <a href="https://' .
+ $this->getInput('h') . $author->path . '">' . $author->name . ' @' . $author->username . '</a>';
+
+ $content = '';
+ if ($comment->system) {
+ $content = $comment->note_html;
+ if ($comment->type === 'StateNote') {
+ $content .= ' the issue';
+ } elseif ($comment->type === null) {
+ // e.g. "added 900 commits\n800 from master\n175h4d - commit message\n..."
+ $content = str_get_html($comment->note_html)->find('p', 0);
+ }
+ } else {
+ // no switch-case to do strict comparison
+ if ($comment->type === null || $comment->type === 'DiscussionNote') {
+ $content = 'commented';
+ } elseif ($comment->type === 'DiffNote') {
+ $content = 'commented on a thread';
+ } else {
+ $content = $comment->note_html;
+ }
+ }
+ $item['title'] = $author->name . " $content";
+
+ $content = $this->fixImgSrc($comment->note_html);
+ $item['content'] = defaultLinkTo($content, 'https://' . $this->getInput('h') . '/');
+
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ private function parseIssueDescription()
+ {
+ $description_uri = $this->getURI() . '.json';
+ $description = getContents($description_uri);
+ $description = json_decode($description, false);
+ $description_html = getSimpleHtmlDomCached($this->getURI());
+
+ $item = [];
+ $item['uri'] = $this->getURI();
+ $item['uid'] = $item['uri'];
+
+ $item['timestamp'] = $description->created_at ?? $description->updated_at;
+
+ $item['author'] = $this->parseAuthor($description_html);
+
+ $item['title'] = $description->title;
+ $item['content'] = markdownToHtml($description->description);
+
+ return $item;
+ }
+
+ private function parseMergeRequestDescription()
+ {
+ $description_uri = $this->getURI() . '/cached_widget.json';
+ $description = getContents($description_uri);
+ $description = json_decode($description, false);
+ $description_html = getSimpleHtmlDomCached($this->getURI());
+
+ $item = [];
+ $item['uri'] = $this->getURI();
+ $item['uid'] = $item['uri'];
+
+ $item['timestamp'] = $description_html->find('.merge-request-details time', 0)->datetime;
+
+ $item['author'] = $this->parseAuthor($description_html);
+
+ $item['title'] = 'Merge Request ' . $description->title;
+ $item['content'] = markdownToHtml($description->description);
+
+ return $item;
+ }
+
+ private function fixImgSrc($html)
+ {
+ if (is_string($html)) {
+ $html = str_get_html($html);
+ }
+
+ foreach ($html->find('img') as $img) {
+ $img->src = $img->getAttribute('data-src');
+ }
+ return $html;
+ }
+
+ private function parseAuthor($description_html)
+ {
+ $description_html = $this->fixImgSrc($description_html);
+
+ $authors = $description_html->find('.issuable-meta a.author-link, .merge-request a.author-link');
+ $editors = $description_html->find('.edited-text a.author-link');
+ $author_str = implode(' ', $authors);
+ if ($editors) {
+ $author_str .= ', ' . implode(' ', $editors);
+ }
+ return defaultLinkTo($author_str, 'https://' . $this->getInput('h') . '/');
+ }
}
diff --git a/bridges/GizmodoBridge.php b/bridges/GizmodoBridge.php
index 7dc3de8f..64e2fc8a 100644
--- a/bridges/GizmodoBridge.php
+++ b/bridges/GizmodoBridge.php
@@ -1,79 +1,83 @@
<?php
-class GizmodoBridge extends FeedExpander {
- const MAINTAINER = 'polopollo';
- const NAME = 'Gizmodo';
- const URI = 'https://gizmodo.com';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Returns the newest posts from Gizmodo.';
-
- protected function parseItem($item) {
- $item = parent::parseItem($item);
-
- $html = getSimpleHTMLDOMCached($item['uri']);
-
- $html = defaultLinkTo($html, $this->getURI());
- $this->stripTags($html);
- $this->handleFigureTags($html);
- $this->handleIframeTags($html);
-
- // Get header image
- $image = $html->find('meta[property="og:image"]', 0)->content;
-
- $item['content'] = $html->find('div.js_post-content', 0)->innertext;
-
- // Get categories
- $categories = explode(',', $html->find('meta[name="keywords"]', 0)->content);
- $item['categories'] = array_map('trim', $categories);
-
- $item['enclosures'][] = $html->find('meta[property="og:image"]', 0)->content;
-
- return $item;
- }
-
- public function collectData() {
- $this->collectExpandableDatas(self::URI . '/rss', 20);
- }
-
- private function stripTags($html) {
- foreach ($html->find('aside') as $aside) {
- $aside->outertext = '';
- }
-
- foreach ($html->find('div.ad-unit') as $div) {
- $div->outertext = '';
- }
-
- foreach ($html->find('script') as $script) {
- $script->outertext = '';
- }
- }
-
- private function handleFigureTags($html) {
- foreach ($html->find('figure') as $index => $figure) {
-
- if (isset($figure->attr['data-id'])) {
- $id = $figure->attr['data-id'];
- $format = $figure->attr['data-format'];
-
- } else {
- $img = $figure->find('img', 0);
- $id = $img->attr['data-chomp-id'];
- $format = $img->attr['data-format'];
- $figure->find('div.img-permalink-sub-wrapper', 0)->style = '';
- }
-
- $imageUrl = 'https://i.kinja-img.com/gawker-media/image/upload/' . $id . '.' . $format;
-
- $figure->find('span', 0)->outertext = <<<EOD
+class GizmodoBridge extends FeedExpander
+{
+ const MAINTAINER = 'polopollo';
+ const NAME = 'Gizmodo';
+ const URI = 'https://gizmodo.com';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns the newest posts from Gizmodo.';
+
+ protected function parseItem($item)
+ {
+ $item = parent::parseItem($item);
+
+ $html = getSimpleHTMLDOMCached($item['uri']);
+
+ $html = defaultLinkTo($html, $this->getURI());
+ $this->stripTags($html);
+ $this->handleFigureTags($html);
+ $this->handleIframeTags($html);
+
+ // Get header image
+ $image = $html->find('meta[property="og:image"]', 0)->content;
+
+ $item['content'] = $html->find('div.js_post-content', 0)->innertext;
+
+ // Get categories
+ $categories = explode(',', $html->find('meta[name="keywords"]', 0)->content);
+ $item['categories'] = array_map('trim', $categories);
+
+ $item['enclosures'][] = $html->find('meta[property="og:image"]', 0)->content;
+
+ return $item;
+ }
+
+ public function collectData()
+ {
+ $this->collectExpandableDatas(self::URI . '/rss', 20);
+ }
+
+ private function stripTags($html)
+ {
+ foreach ($html->find('aside') as $aside) {
+ $aside->outertext = '';
+ }
+
+ foreach ($html->find('div.ad-unit') as $div) {
+ $div->outertext = '';
+ }
+
+ foreach ($html->find('script') as $script) {
+ $script->outertext = '';
+ }
+ }
+
+ private function handleFigureTags($html)
+ {
+ foreach ($html->find('figure') as $index => $figure) {
+ if (isset($figure->attr['data-id'])) {
+ $id = $figure->attr['data-id'];
+ $format = $figure->attr['data-format'];
+ } else {
+ $img = $figure->find('img', 0);
+ $id = $img->attr['data-chomp-id'];
+ $format = $img->attr['data-format'];
+ $figure->find('div.img-permalink-sub-wrapper', 0)->style = '';
+ }
+
+ $imageUrl = 'https://i.kinja-img.com/gawker-media/image/upload/' . $id . '.' . $format;
+
+ $figure->find('span', 0)->outertext = <<<EOD
<img src="{$imageUrl}">
EOD;
- }
- }
-
- private function handleIframeTags($html) {
- foreach($html->find('iframe') as $iframe) {
- $iframe->src = urljoin($this->getURI(), $iframe->src);
- }
- }
+ }
+ }
+
+ private function handleIframeTags($html)
+ {
+ foreach ($html->find('iframe') as $iframe) {
+ $iframe->src = urljoin($this->getURI(), $iframe->src);
+ }
+ }
}
diff --git a/bridges/GlassdoorBridge.php b/bridges/GlassdoorBridge.php
index 3358c74b..8c53cfa9 100644
--- a/bridges/GlassdoorBridge.php
+++ b/bridges/GlassdoorBridge.php
@@ -1,187 +1,197 @@
<?php
-class GlassdoorBridge extends BridgeAbstract {
-
- // Contexts
- const CONTEXT_BLOG = 'Blogs';
- const CONTEXT_REVIEW = 'Company Reviews';
- const CONTEXT_GLOBAL = 'global';
-
- // Global context parameters
- const PARAM_LIMIT = 'limit';
-
- // Blog context parameters
- const PARAM_BLOG_TYPE = 'blog_type';
- const PARAM_BLOG_FULL = 'full_article';
-
- const BLOG_TYPE_HOME = 'Home';
- const BLOG_TYPE_COMPANIES_HIRING = 'Companies Hiring';
- const BLOG_TYPE_CAREER_ADVICE = 'Career Advice';
- const BLOG_TYPE_INTERVIEWS = 'Interviews';
-
- // Review context parameters
- const PARAM_REVIEW_COMPANY = 'company';
-
- const MAINTAINER = 'logmanoriginal';
- const NAME = 'Glassdoor Bridge';
- const URI = 'https://www.glassdoor.com/';
- const DESCRIPTION = 'Returns feeds for blog posts and company reviews';
- const CACHE_TIMEOUT = 86400; // 24 hours
-
- const PARAMETERS = array(
- self::CONTEXT_BLOG => array(
- self::PARAM_BLOG_TYPE => array(
- 'name' => 'Blog type',
- 'type' => 'list',
- 'title' => 'Select the blog you want to follow',
- 'values' => array(
- self::BLOG_TYPE_HOME => 'blog/',
- self::BLOG_TYPE_COMPANIES_HIRING => 'blog/companies-hiring/',
- self::BLOG_TYPE_CAREER_ADVICE => 'blog/career-advice/',
- self::BLOG_TYPE_INTERVIEWS => 'blog/interviews/',
- )
- ),
- self::PARAM_BLOG_FULL => array(
- 'name' => 'Full article',
- 'type' => 'checkbox',
- 'title' => 'Enable to return the full article for each post'
- ),
- ),
- self::CONTEXT_REVIEW => array(
- self::PARAM_REVIEW_COMPANY => array(
- 'name' => 'Company URL',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Paste the company review page URL here!',
- 'exampleValue' => 'https://www.glassdoor.com/Reviews/GitHub-Reviews-E671945.htm'
- )
- ),
- self::CONTEXT_GLOBAL => array(
- self::PARAM_LIMIT => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'defaultValue' => -1,
- 'title' => 'Specifies the maximum number of items to return (default: All)'
- )
- )
- );
-
- public function getURI() {
- switch($this->queriedContext) {
- case self::CONTEXT_BLOG:
- return self::URI . $this->getInput(self::PARAM_BLOG_TYPE);
- case self::CONTEXT_REVIEW:
- return $this->filterCompanyURI($this->getInput(self::PARAM_REVIEW_COMPANY));
- }
-
- return parent::getURI();
- }
-
- public function collectData() {
- $url = $this->getURI();
- $html = getSimpleHTMLDOM($url);
- $html = defaultLinkTo($html, $url);
- $limit = $this->getInput(self::PARAM_LIMIT);
-
- switch($this->queriedContext) {
- case self::CONTEXT_BLOG:
- $this->collectBlogData($html, $limit);
- break;
- case self::CONTEXT_REVIEW:
- $this->collectReviewData($html, $limit);
- break;
- }
- }
-
- private function collectBlogData($html, $limit) {
- $posts = $html->find('div.post')
- or returnServerError('Unable to find blog posts!');
-
- foreach($posts as $post) {
- $item = [];
-
- $item['uri'] = $post->find('a', 0)->href;
- $item['title'] = $post->find('h3', 0)->plaintext;
- $item['content'] = $post->find('p', 0)->plaintext;
- $item['author'] = $post->find('p', -2)->plaintext;
- $item['timestamp'] = strtotime($post->find('p', -1)->plaintext);
-
- // TODO: fetch entire blog post content
- $this->items[] = $item;
-
- if ($limit > 0 && count($this->items) >= $limit) {
- return;
- }
- }
- }
-
- private function collectReviewData($html, $limit) {
- $reviews = $html->find('#ReviewsFeed li[id^="empReview]')
- or returnServerError('Unable to find reviews!');
-
- foreach($reviews as $review) {
- $item = [];
-
- $item['uri'] = $review->find('a.reviewLink', 0)->href;
-
- // Not all reviews have a title
- $item['title'] = $review->find('h2', 0)->plaintext ?? 'Glassdoor review';
-
- [$date, $author] = explode('-', $review->find('span.authorInfo', 0)->plaintext);
-
- $item['author'] = trim($author);
-
- $createdAt = DateTimeImmutable::createFromFormat('F m, Y', trim($date));
- if ($createdAt) {
- $item['timestamp'] = $createdAt->getTimestamp();
- }
-
- $item['content'] = $review->find('.px-std', 2)->text();
-
- $this->items[] = $item;
-
- if($limit > 0 && count($this->items) >= $limit) {
- return;
- }
- }
- }
-
- private function filterCompanyURI($uri) {
- /* Make sure the URI is a valid review page. Unfortunately there is no
- * simple way to determine if the URI is valid, because of automagic
- * redirection and strange naming conventions.
- */
- if(!filter_var($uri,
- FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) {
- returnClientError('The specified URL is invalid!');
- }
-
- $uri = filter_var($uri, FILTER_SANITIZE_URL);
- $path = parse_url($uri, PHP_URL_PATH);
- $parts = explode('/', $path);
-
- $allowed_strings = array(
- 'de-DE' => 'Bewertungen',
- 'en-AU' => 'Reviews',
- 'nl-BE' => 'Reviews',
- 'fr-BE' => 'Avis',
- 'en-CA' => 'Reviews',
- 'fr-CA' => 'Avis',
- 'fr-FR' => 'Avis',
- 'en-IN' => 'Reviews',
- 'en-IE' => 'Reviews',
- 'nl-NL' => 'Reviews',
- 'de-AT' => 'Bewertungen',
- 'de-CH' => 'Bewertungen',
- 'fr-CH' => 'Avis',
- 'en-GB' => 'Reviews',
- 'en' => 'Reviews'
- );
-
- if(!in_array($parts[1], $allowed_strings)) {
- returnClientError('Please specify a URL pointing to the companies review page!');
- }
-
- return $uri;
- }
+class GlassdoorBridge extends BridgeAbstract
+{
+ // Contexts
+ const CONTEXT_BLOG = 'Blogs';
+ const CONTEXT_REVIEW = 'Company Reviews';
+ const CONTEXT_GLOBAL = 'global';
+
+ // Global context parameters
+ const PARAM_LIMIT = 'limit';
+
+ // Blog context parameters
+ const PARAM_BLOG_TYPE = 'blog_type';
+ const PARAM_BLOG_FULL = 'full_article';
+
+ const BLOG_TYPE_HOME = 'Home';
+ const BLOG_TYPE_COMPANIES_HIRING = 'Companies Hiring';
+ const BLOG_TYPE_CAREER_ADVICE = 'Career Advice';
+ const BLOG_TYPE_INTERVIEWS = 'Interviews';
+
+ // Review context parameters
+ const PARAM_REVIEW_COMPANY = 'company';
+
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Glassdoor Bridge';
+ const URI = 'https://www.glassdoor.com/';
+ const DESCRIPTION = 'Returns feeds for blog posts and company reviews';
+ const CACHE_TIMEOUT = 86400; // 24 hours
+
+ const PARAMETERS = [
+ self::CONTEXT_BLOG => [
+ self::PARAM_BLOG_TYPE => [
+ 'name' => 'Blog type',
+ 'type' => 'list',
+ 'title' => 'Select the blog you want to follow',
+ 'values' => [
+ self::BLOG_TYPE_HOME => 'blog/',
+ self::BLOG_TYPE_COMPANIES_HIRING => 'blog/companies-hiring/',
+ self::BLOG_TYPE_CAREER_ADVICE => 'blog/career-advice/',
+ self::BLOG_TYPE_INTERVIEWS => 'blog/interviews/',
+ ]
+ ],
+ self::PARAM_BLOG_FULL => [
+ 'name' => 'Full article',
+ 'type' => 'checkbox',
+ 'title' => 'Enable to return the full article for each post'
+ ],
+ ],
+ self::CONTEXT_REVIEW => [
+ self::PARAM_REVIEW_COMPANY => [
+ 'name' => 'Company URL',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Paste the company review page URL here!',
+ 'exampleValue' => 'https://www.glassdoor.com/Reviews/GitHub-Reviews-E671945.htm'
+ ]
+ ],
+ self::CONTEXT_GLOBAL => [
+ self::PARAM_LIMIT => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'defaultValue' => -1,
+ 'title' => 'Specifies the maximum number of items to return (default: All)'
+ ]
+ ]
+ ];
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case self::CONTEXT_BLOG:
+ return self::URI . $this->getInput(self::PARAM_BLOG_TYPE);
+ case self::CONTEXT_REVIEW:
+ return $this->filterCompanyURI($this->getInput(self::PARAM_REVIEW_COMPANY));
+ }
+
+ return parent::getURI();
+ }
+
+ public function collectData()
+ {
+ $url = $this->getURI();
+ $html = getSimpleHTMLDOM($url);
+ $html = defaultLinkTo($html, $url);
+ $limit = $this->getInput(self::PARAM_LIMIT);
+
+ switch ($this->queriedContext) {
+ case self::CONTEXT_BLOG:
+ $this->collectBlogData($html, $limit);
+ break;
+ case self::CONTEXT_REVIEW:
+ $this->collectReviewData($html, $limit);
+ break;
+ }
+ }
+
+ private function collectBlogData($html, $limit)
+ {
+ $posts = $html->find('div.post')
+ or returnServerError('Unable to find blog posts!');
+
+ foreach ($posts as $post) {
+ $item = [];
+
+ $item['uri'] = $post->find('a', 0)->href;
+ $item['title'] = $post->find('h3', 0)->plaintext;
+ $item['content'] = $post->find('p', 0)->plaintext;
+ $item['author'] = $post->find('p', -2)->plaintext;
+ $item['timestamp'] = strtotime($post->find('p', -1)->plaintext);
+
+ // TODO: fetch entire blog post content
+ $this->items[] = $item;
+
+ if ($limit > 0 && count($this->items) >= $limit) {
+ return;
+ }
+ }
+ }
+
+ private function collectReviewData($html, $limit)
+ {
+ $reviews = $html->find('#ReviewsFeed li[id^="empReview]')
+ or returnServerError('Unable to find reviews!');
+
+ foreach ($reviews as $review) {
+ $item = [];
+
+ $item['uri'] = $review->find('a.reviewLink', 0)->href;
+
+ // Not all reviews have a title
+ $item['title'] = $review->find('h2', 0)->plaintext ?? 'Glassdoor review';
+
+ [$date, $author] = explode('-', $review->find('span.authorInfo', 0)->plaintext);
+
+ $item['author'] = trim($author);
+
+ $createdAt = DateTimeImmutable::createFromFormat('F m, Y', trim($date));
+ if ($createdAt) {
+ $item['timestamp'] = $createdAt->getTimestamp();
+ }
+
+ $item['content'] = $review->find('.px-std', 2)->text();
+
+ $this->items[] = $item;
+
+ if ($limit > 0 && count($this->items) >= $limit) {
+ return;
+ }
+ }
+ }
+
+ private function filterCompanyURI($uri)
+ {
+ /* Make sure the URI is a valid review page. Unfortunately there is no
+ * simple way to determine if the URI is valid, because of automagic
+ * redirection and strange naming conventions.
+ */
+ if (
+ !filter_var(
+ $uri,
+ FILTER_VALIDATE_URL,
+ FILTER_FLAG_PATH_REQUIRED
+ )
+ ) {
+ returnClientError('The specified URL is invalid!');
+ }
+
+ $uri = filter_var($uri, FILTER_SANITIZE_URL);
+ $path = parse_url($uri, PHP_URL_PATH);
+ $parts = explode('/', $path);
+
+ $allowed_strings = [
+ 'de-DE' => 'Bewertungen',
+ 'en-AU' => 'Reviews',
+ 'nl-BE' => 'Reviews',
+ 'fr-BE' => 'Avis',
+ 'en-CA' => 'Reviews',
+ 'fr-CA' => 'Avis',
+ 'fr-FR' => 'Avis',
+ 'en-IN' => 'Reviews',
+ 'en-IE' => 'Reviews',
+ 'nl-NL' => 'Reviews',
+ 'de-AT' => 'Bewertungen',
+ 'de-CH' => 'Bewertungen',
+ 'fr-CH' => 'Avis',
+ 'en-GB' => 'Reviews',
+ 'en' => 'Reviews'
+ ];
+
+ if (!in_array($parts[1], $allowed_strings)) {
+ returnClientError('Please specify a URL pointing to the companies review page!');
+ }
+
+ return $uri;
+ }
}
diff --git a/bridges/GlowficBridge.php b/bridges/GlowficBridge.php
index a3a85ef4..b51ead8d 100644
--- a/bridges/GlowficBridge.php
+++ b/bridges/GlowficBridge.php
@@ -1,90 +1,100 @@
<?php
-class GlowficBridge extends BridgeAbstract {
- const MAINTAINER = 'l1n';
- const NAME = 'Glowfic Bridge';
- const URI = 'https://www.glowfic.com';
- const CACHE_TIMEOUT = 3600; // 1 hour
- const DESCRIPTION = 'Returns the latest replies on a glowfic post.';
- const PARAMETERS = array(
- 'global' => array(),
- 'Thread' => array(
- 'post_id' => array(
- 'name' => 'Post ID',
- 'title' => 'https://www.glowfic.com/posts/POST ID',
- 'required' => true,
- 'exampleValue' => '2756',
- 'type' => 'number'
- ),
- 'start_page' => array(
- 'name' => 'Start Page',
- 'title' => 'To start from an offset page',
- 'type' => 'number'
- )
- )
- );
- public function collectData() {
- $url = $this->getAPIURI();
- $metadata = get_headers( $url . '/replies', true ) or returnClientError('Post did not return reply headers.');
- $metadata['Last-Page'] = ceil( $metadata['Total'] / $metadata['Per-Page'] );
- if(!is_null($this->getInput('start_page')) &&
- $this->getInput('start_page') < 1 && $metadata['Last-Page'] - $this->getInput('start_page') > 0) {
- $first_page = $metadata['Last-Page'] - $this->getInput('start_page');
- } else if(!is_null($this->getInput('start_page')) && $this->getInput('start_page') <= $metadata['Last-Page']) {
- $first_page = $this->getInput('start_page');
- } else {
- $first_page = 1;
- }
- for ($page_offset = $first_page; $page_offset <= $metadata['Last-Page']; $page_offset++) {
- $jsonContents = getContents($url . '/replies?page=' . $page_offset ) or
- returnClientError('Could not retrieve replies for page ' . $page_offset . '.');
- $replies = json_decode($jsonContents);
- foreach ($replies as $reply) {
- $item = array();
+class GlowficBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'l1n';
+ const NAME = 'Glowfic Bridge';
+ const URI = 'https://www.glowfic.com';
+ const CACHE_TIMEOUT = 3600; // 1 hour
+ const DESCRIPTION = 'Returns the latest replies on a glowfic post.';
+ const PARAMETERS = [
+ 'global' => [],
+ 'Thread' => [
+ 'post_id' => [
+ 'name' => 'Post ID',
+ 'title' => 'https://www.glowfic.com/posts/POST ID',
+ 'required' => true,
+ 'exampleValue' => '2756',
+ 'type' => 'number'
+ ],
+ 'start_page' => [
+ 'name' => 'Start Page',
+ 'title' => 'To start from an offset page',
+ 'type' => 'number'
+ ]
+ ]
+ ];
- $item['content'] = $reply->{'content'};
- $item['uri'] = $this->getURI() . '?page=' . $page_offset . '#reply-' . $reply->{'id'};
- if ($reply->{'icon'}) {
- $item['enclosures'] = array($reply->{'icon'}->{'url'});
- }
- $item['author'] = $reply->{'character'}->{'screenname'} . ' (' . $reply->{'character'}->{'name'} . ')';
- $item['timestamp'] = date('r', strtotime($reply->{'created_at'}));
- $item['title'] = 'Tag by ' . $reply->{'user'}->{'username'} . ' updated at ' . $reply->{'updated_at'};
- $this->items[] = $item;
- }
- }
- }
+ public function collectData()
+ {
+ $url = $this->getAPIURI();
+ $metadata = get_headers($url . '/replies', true) or returnClientError('Post did not return reply headers.');
+ $metadata['Last-Page'] = ceil($metadata['Total'] / $metadata['Per-Page']);
+ if (
+ !is_null($this->getInput('start_page')) &&
+ $this->getInput('start_page') < 1 && $metadata['Last-Page'] - $this->getInput('start_page') > 0
+ ) {
+ $first_page = $metadata['Last-Page'] - $this->getInput('start_page');
+ } elseif (!is_null($this->getInput('start_page')) && $this->getInput('start_page') <= $metadata['Last-Page']) {
+ $first_page = $this->getInput('start_page');
+ } else {
+ $first_page = 1;
+ }
+ for ($page_offset = $first_page; $page_offset <= $metadata['Last-Page']; $page_offset++) {
+ $jsonContents = getContents($url . '/replies?page=' . $page_offset) or
+ returnClientError('Could not retrieve replies for page ' . $page_offset . '.');
+ $replies = json_decode($jsonContents);
+ foreach ($replies as $reply) {
+ $item = [];
- private function getAPIURI() {
- $url = parent::getURI() . '/api/v1/posts/' . $this->getInput('post_id');
- return $url;
- }
+ $item['content'] = $reply->{'content'};
+ $item['uri'] = $this->getURI() . '?page=' . $page_offset . '#reply-' . $reply->{'id'};
+ if ($reply->{'icon'}) {
+ $item['enclosures'] = [$reply->{'icon'}->{'url'}];
+ }
+ $item['author'] = $reply->{'character'}->{'screenname'} . ' (' . $reply->{'character'}->{'name'} . ')';
+ $item['timestamp'] = date('r', strtotime($reply->{'created_at'}));
+ $item['title'] = 'Tag by ' . $reply->{'user'}->{'username'} . ' updated at ' . $reply->{'updated_at'};
+ $this->items[] = $item;
+ }
+ }
+ }
- public function getURI() {
- $url = parent::getURI() . '/posts/' . $this->getInput('post_id');
- return $url;
- }
+ private function getAPIURI()
+ {
+ $url = parent::getURI() . '/api/v1/posts/' . $this->getInput('post_id');
+ return $url;
+ }
- private function getPost() {
- $url = $this->getAPIURI();
- $jsonPost = getContents( $url ) or returnClientError('Could not retrieve post metadata.');
- $post = json_decode($jsonPost);
- return $post;
- }
+ public function getURI()
+ {
+ $url = parent::getURI() . '/posts/' . $this->getInput('post_id');
+ return $url;
+ }
- public function getName(){
- if(!is_null($this->getInput('post_id'))) {
- $post = $this->getPost();
- return $post->{'subject'} . ' - ' . parent::getName();
- }
- return parent::getName();
- }
+ private function getPost()
+ {
+ $url = $this->getAPIURI();
+ $jsonPost = getContents($url) or returnClientError('Could not retrieve post metadata.');
+ $post = json_decode($jsonPost);
+ return $post;
+ }
- public function getDescription(){
- if(!is_null($this->getInput('post_id'))) {
- $post = $this->getPost();
- return $post->{'content'};
- }
- return parent::getName();
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('post_id'))) {
+ $post = $this->getPost();
+ return $post->{'subject'} . ' - ' . parent::getName();
+ }
+ return parent::getName();
+ }
+
+ public function getDescription()
+ {
+ if (!is_null($this->getInput('post_id'))) {
+ $post = $this->getPost();
+ return $post->{'content'};
+ }
+ return parent::getName();
+ }
}
diff --git a/bridges/GoComicsBridge.php b/bridges/GoComicsBridge.php
index 9bca83e2..586e2a0d 100644
--- a/bridges/GoComicsBridge.php
+++ b/bridges/GoComicsBridge.php
@@ -1,60 +1,63 @@
<?php
-class GoComicsBridge extends BridgeAbstract {
-
- const MAINTAINER = 'sky';
- const NAME = 'GoComics Unofficial RSS';
- const URI = 'https://www.gocomics.com/';
- const CACHE_TIMEOUT = 21600; // 6h
- const DESCRIPTION = 'The Unofficial GoComics RSS';
- const PARAMETERS = array( array(
- 'comicname' => array(
- 'name' => 'comicname',
- 'type' => 'text',
- 'exampleValue' => 'heartofthecity',
- 'required' => true
- )
- ));
-
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI());
-
- //Get info from first page
- $author = preg_replace('/By /', '', $html->find('.media-subheading', 0)->plaintext);
-
- $link = self::URI . $html->find('.gc-deck--cta-0', 0)->find('a', 0)->href;
- for($i = 0; $i < 5; $i++) {
-
- $item = array();
-
- $page = getSimpleHTMLDOM($link);
- $imagelink = $page->find('.comic.container', 0)->getAttribute('data-image');
- $date = explode('/', $link);
-
- $item['id'] = $imagelink;
- $item['uri'] = $link;
- $item['author'] = $author;
- $item['title'] = 'GoComics ' . $this->getInput('comicname');
- $item['timestamp'] = DateTime::createFromFormat('Ymd', $date[5] . $date[6] . $date[7])->getTimestamp();
- $item['content'] = '<img src="' . $imagelink . '" />';
-
- $link = self::URI . $page->find('.js-previous-comic', 0)->href;
- $this->items[] = $item;
- }
- }
-
- public function getURI(){
- if(!is_null($this->getInput('comicname'))) {
- return self::URI . urlencode($this->getInput('comicname'));
- }
-
- return parent::getURI();
- }
-
- public function getName(){
- if(!is_null($this->getInput('comicname'))) {
- return $this->getInput('comicname') . ' - GoComics';
- }
-
- return parent::getName();
- }
+
+class GoComicsBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'sky';
+ const NAME = 'GoComics Unofficial RSS';
+ const URI = 'https://www.gocomics.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'The Unofficial GoComics RSS';
+ const PARAMETERS = [ [
+ 'comicname' => [
+ 'name' => 'comicname',
+ 'type' => 'text',
+ 'exampleValue' => 'heartofthecity',
+ 'required' => true
+ ]
+ ]];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ //Get info from first page
+ $author = preg_replace('/By /', '', $html->find('.media-subheading', 0)->plaintext);
+
+ $link = self::URI . $html->find('.gc-deck--cta-0', 0)->find('a', 0)->href;
+ for ($i = 0; $i < 5; $i++) {
+ $item = [];
+
+ $page = getSimpleHTMLDOM($link);
+ $imagelink = $page->find('.comic.container', 0)->getAttribute('data-image');
+ $date = explode('/', $link);
+
+ $item['id'] = $imagelink;
+ $item['uri'] = $link;
+ $item['author'] = $author;
+ $item['title'] = 'GoComics ' . $this->getInput('comicname');
+ $item['timestamp'] = DateTime::createFromFormat('Ymd', $date[5] . $date[6] . $date[7])->getTimestamp();
+ $item['content'] = '<img src="' . $imagelink . '" />';
+
+ $link = self::URI . $page->find('.js-previous-comic', 0)->href;
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('comicname'))) {
+ return self::URI . urlencode($this->getInput('comicname'));
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('comicname'))) {
+ return $this->getInput('comicname') . ' - GoComics';
+ }
+
+ return parent::getName();
+ }
}
diff --git a/bridges/GogsBridge.php b/bridges/GogsBridge.php
index c90c8c57..685e5ba2 100644
--- a/bridges/GogsBridge.php
+++ b/bridges/GogsBridge.php
@@ -1,205 +1,216 @@
<?php
-class GogsBridge extends BridgeAbstract {
-
- const NAME = 'Gogs';
- const URI = 'https://gogs.io';
- const DESCRIPTION = 'Returns the latest issues, commits or releases';
- const MAINTAINER = 'logmanoriginal';
- const CACHE_TIMEOUT = 300; // 5 minutes
-
- const PARAMETERS = array(
- 'global' => array(
- 'host' => array(
- 'name' => 'Host',
- 'exampleValue' => 'https://notabug.org',
- 'required' => true,
- 'title' => 'Host name with its protocol, without trailing slash',
- ),
- 'user' => array(
- 'name' => 'Username',
- 'exampleValue' => 'PDModdingCommunity',
- 'required' => true,
- 'title' => 'User name as it appears in the URL',
- ),
- 'project' => array(
- 'name' => 'Project name',
- 'exampleValue' => 'PD-Loader',
- 'required' => true,
- 'title' => 'Project name as it appears in the URL',
- ),
- ),
- 'Commits' => array(
- 'branch' => array(
- 'name' => 'Branch name',
- 'defaultValue' => 'master',
- 'required' => true,
- 'title' => 'Branch name as it appears in the URL',
- ),
- ),
- 'Issues' => array(
- 'include_description' => array(
- 'name' => 'Include issue description',
- 'type' => 'checkbox',
- 'title' => 'Activate to include the issue description',
- ),
- ),
- 'Single issue' => array(
- 'issue' => array(
- 'name' => 'Issue number',
- 'type' => 'number',
- 'exampleValue' => 100,
- 'required' => true,
- 'title' => 'Issue number from the issues list',
- ),
- ),
- 'Releases' => array(),
- );
-
- private $title = '';
-
- /**
- * Note: detectParamters doesn't make sense for this bridge because there is
- * no "single" host for this service. Anyone can host it.
- */
-
- public function getURI() {
- switch($this->queriedContext) {
- case 'Commits':
- return $this->getInput('host')
- . '/' . $this->getInput('user')
- . '/' . $this->getInput('project')
- . '/commits/' . $this->getInput('branch');
-
- case 'Issues':
- return $this->getInput('host')
- . '/' . $this->getInput('user')
- . '/' . $this->getInput('project')
- . '/issues/';
-
- case 'Single issue':
- return $this->getInput('host')
- . '/' . $this->getInput('user')
- . '/' . $this->getInput('project')
- . '/issues/' . $this->getInput('issue');
-
- case 'Releases':
- return $this->getInput('host')
- . '/' . $this->getInput('user')
- . '/' . $this->getInput('project')
- . '/releases/';
-
- default: return parent::getURI();
- }
- }
-
- public function getName() {
- switch($this->queriedContext) {
- case 'Commits':
- case 'Issues':
- case 'Releases': return $this->title . ' ' . $this->queriedContext;
- case 'Single issue': return $this->title . ' Issue ' . $this->getInput('issue');
- default: return parent::getName();
- }
- }
-
- public function getIcon() {
- return 'https://gogs.io/img/favicon.ico';
- }
-
- public function collectData() {
-
- $html = getSimpleHTMLDOM($this->getURI());
-
- $html = defaultLinkTo($html, $this->getURI());
-
- $this->title = $html->find('[property="og:title"]', 0)->content;
-
- switch($this->queriedContext) {
- case 'Commits':
- $this->collectCommitsData($html);
- break;
- case 'Issues':
- $this->collectIssuesData($html);
- break;
- case 'Single issue':
- $this->collectSingleIssueData($html);
- break;
- case 'Releases':
- $this->collectReleasesData($html);
- break;
- }
-
- }
-
- protected function collectCommitsData($html) {
- $commits = $html->find('#commits-table tbody tr')
- or returnServerError('Unable to find commits');
-
- foreach($commits as $commit) {
- $this->items[] = array(
- 'uri' => $commit->find('a.sha', 0)->href,
- 'title' => $commit->find('.message span', 0)->plaintext,
- 'author' => $commit->find('.author', 0)->plaintext,
- 'timestamp' => $commit->find('.time-since', 0)->title,
- 'uid' => $commit->find('.sha', 0)->plaintext,
- );
- }
- }
-
- protected function collectIssuesData($html) {
- $issues = $html->find('.issue.list li')
- or returnServerError('Unable to find issues');
-
- foreach($issues as $issue) {
- $uri = $issue->find('a', 0)->href;
-
- $item = array(
- 'uri' => $uri,
- 'title' => $issue->find('.label', 0)->plaintext . ' | ' . $issue->find('a.title', 0)->plaintext,
- 'author' => $issue->find('.desc a', 0)->plaintext,
- 'timestamp' => $issue->find('.time-since', 0)->title,
- 'uid' => $issue->find('.label', 0)->plaintext,
- );
-
- if($this->getInput('include_description')) {
- $issue_html = getSimpleHTMLDOMCached($uri, 3600)
- or returnServerError('Unable to load issue description');
-
- $issue_html = defaultLinkTo($issue_html, $uri);
-
- $item['content'] = $issue_html->find('.comment .markdown', 0);
- }
-
- $this->items[] = $item;
- }
- }
-
- protected function collectSingleIssueData($html) {
- $comments = $html->find('.comments .comment')
- or returnServerError('Unable to find comments');
-
- foreach($comments as $comment) {
- $this->items[] = array(
- 'uri' => $comment->find('a[href*="#issue"]', 0)->href,
- 'title' => $comment->find('span', 0)->plaintext,
- 'author' => $comment->find('.content a', 0)->plaintext,
- 'timestamp' => $comment->find('.time-since', 0)->title,
- 'content' => $comment->find('.markdown', 0),
- );
- }
-
- $this->items = array_reverse($this->items);
- }
-
- protected function collectReleasesData($html) {
- $releases = $html->find('#release-list li')
- or returnServerError('Unable to find releases');
-
- foreach($releases as $release) {
- $this->items[] = array(
- 'uri' => $release->find('a', 0)->href,
- 'title' => 'Release ' . $release->find('h4', 0)->plaintext,
- );
- }
- }
+
+class GogsBridge extends BridgeAbstract
+{
+ const NAME = 'Gogs';
+ const URI = 'https://gogs.io';
+ const DESCRIPTION = 'Returns the latest issues, commits or releases';
+ const MAINTAINER = 'logmanoriginal';
+ const CACHE_TIMEOUT = 300; // 5 minutes
+
+ const PARAMETERS = [
+ 'global' => [
+ 'host' => [
+ 'name' => 'Host',
+ 'exampleValue' => 'https://notabug.org',
+ 'required' => true,
+ 'title' => 'Host name with its protocol, without trailing slash',
+ ],
+ 'user' => [
+ 'name' => 'Username',
+ 'exampleValue' => 'PDModdingCommunity',
+ 'required' => true,
+ 'title' => 'User name as it appears in the URL',
+ ],
+ 'project' => [
+ 'name' => 'Project name',
+ 'exampleValue' => 'PD-Loader',
+ 'required' => true,
+ 'title' => 'Project name as it appears in the URL',
+ ],
+ ],
+ 'Commits' => [
+ 'branch' => [
+ 'name' => 'Branch name',
+ 'defaultValue' => 'master',
+ 'required' => true,
+ 'title' => 'Branch name as it appears in the URL',
+ ],
+ ],
+ 'Issues' => [
+ 'include_description' => [
+ 'name' => 'Include issue description',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to include the issue description',
+ ],
+ ],
+ 'Single issue' => [
+ 'issue' => [
+ 'name' => 'Issue number',
+ 'type' => 'number',
+ 'exampleValue' => 100,
+ 'required' => true,
+ 'title' => 'Issue number from the issues list',
+ ],
+ ],
+ 'Releases' => [],
+ ];
+
+ private $title = '';
+
+ /**
+ * Note: detectParamters doesn't make sense for this bridge because there is
+ * no "single" host for this service. Anyone can host it.
+ */
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Commits':
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/commits/' . $this->getInput('branch');
+
+ case 'Issues':
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/issues/';
+
+ case 'Single issue':
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/issues/' . $this->getInput('issue');
+
+ case 'Releases':
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/releases/';
+
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Commits':
+ case 'Issues':
+ case 'Releases':
+ return $this->title . ' ' . $this->queriedContext;
+ case 'Single issue':
+ return $this->title . ' Issue ' . $this->getInput('issue');
+ default:
+ return parent::getName();
+ }
+ }
+
+ public function getIcon()
+ {
+ return 'https://gogs.io/img/favicon.ico';
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $html = defaultLinkTo($html, $this->getURI());
+
+ $this->title = $html->find('[property="og:title"]', 0)->content;
+
+ switch ($this->queriedContext) {
+ case 'Commits':
+ $this->collectCommitsData($html);
+ break;
+ case 'Issues':
+ $this->collectIssuesData($html);
+ break;
+ case 'Single issue':
+ $this->collectSingleIssueData($html);
+ break;
+ case 'Releases':
+ $this->collectReleasesData($html);
+ break;
+ }
+ }
+
+ protected function collectCommitsData($html)
+ {
+ $commits = $html->find('#commits-table tbody tr')
+ or returnServerError('Unable to find commits');
+
+ foreach ($commits as $commit) {
+ $this->items[] = [
+ 'uri' => $commit->find('a.sha', 0)->href,
+ 'title' => $commit->find('.message span', 0)->plaintext,
+ 'author' => $commit->find('.author', 0)->plaintext,
+ 'timestamp' => $commit->find('.time-since', 0)->title,
+ 'uid' => $commit->find('.sha', 0)->plaintext,
+ ];
+ }
+ }
+
+ protected function collectIssuesData($html)
+ {
+ $issues = $html->find('.issue.list li')
+ or returnServerError('Unable to find issues');
+
+ foreach ($issues as $issue) {
+ $uri = $issue->find('a', 0)->href;
+
+ $item = [
+ 'uri' => $uri,
+ 'title' => $issue->find('.label', 0)->plaintext . ' | ' . $issue->find('a.title', 0)->plaintext,
+ 'author' => $issue->find('.desc a', 0)->plaintext,
+ 'timestamp' => $issue->find('.time-since', 0)->title,
+ 'uid' => $issue->find('.label', 0)->plaintext,
+ ];
+
+ if ($this->getInput('include_description')) {
+ $issue_html = getSimpleHTMLDOMCached($uri, 3600)
+ or returnServerError('Unable to load issue description');
+
+ $issue_html = defaultLinkTo($issue_html, $uri);
+
+ $item['content'] = $issue_html->find('.comment .markdown', 0);
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ protected function collectSingleIssueData($html)
+ {
+ $comments = $html->find('.comments .comment')
+ or returnServerError('Unable to find comments');
+
+ foreach ($comments as $comment) {
+ $this->items[] = [
+ 'uri' => $comment->find('a[href*="#issue"]', 0)->href,
+ 'title' => $comment->find('span', 0)->plaintext,
+ 'author' => $comment->find('.content a', 0)->plaintext,
+ 'timestamp' => $comment->find('.time-since', 0)->title,
+ 'content' => $comment->find('.markdown', 0),
+ ];
+ }
+
+ $this->items = array_reverse($this->items);
+ }
+
+ protected function collectReleasesData($html)
+ {
+ $releases = $html->find('#release-list li')
+ or returnServerError('Unable to find releases');
+
+ foreach ($releases as $release) {
+ $this->items[] = [
+ 'uri' => $release->find('a', 0)->href,
+ 'title' => 'Release ' . $release->find('h4', 0)->plaintext,
+ ];
+ }
+ }
}
diff --git a/bridges/GolemBridge.php b/bridges/GolemBridge.php
index 4bce9e4e..dd9b196e 100644
--- a/bridges/GolemBridge.php
+++ b/bridges/GolemBridge.php
@@ -1,125 +1,131 @@
<?php
-class GolemBridge extends FeedExpander {
- const MAINTAINER = 'Mynacol';
- const NAME = 'Golem Bridge';
- const URI = 'https://www.golem.de/';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Returns the full articles instead of only the intro';
- const PARAMETERS = array(array(
- 'category' => array(
- 'name' => 'Category',
- 'type' => 'list',
- 'values' => array(
- 'Alle News'
- => 'https://rss.golem.de/rss.php?feed=ATOM1.0',
- 'Audio/Video'
- => 'https://rss.golem.de/rss.php?ms=audio-video&feed=ATOM1.0',
- 'Auto'
- => 'https://rss.golem.de/rss.php?ms=auto&feed=ATOM1.0',
- 'Foto'
- => 'https://rss.golem.de/rss.php?ms=foto&feed=ATOM1.0',
- 'Games'
- => 'https://rss.golem.de/rss.php?ms=games&feed=ATOM1.0',
- 'Handy'
- => 'https://rss.golem.de/rss.php?ms=handy&feed=ATOM1.0',
- 'Internet'
- => 'https://rss.golem.de/rss.php?ms=internet&feed=ATOM1.0',
- 'Mobil'
- => 'https://rss.golem.de/rss.php?ms=mobil&feed=ATOM1.0',
- 'Open Source'
- => 'https://rss.golem.de/rss.php?ms=open-source&feed=ATOM1.0',
- 'Politik/Recht'
- => 'https://rss.golem.de/rss.php?ms=politik-recht&feed=ATOM1.0',
- 'Security'
- => 'https://rss.golem.de/rss.php?ms=security&feed=ATOM1.0',
- 'Desktop-Applikationen'
- => 'https://rss.golem.de/rss.php?ms=desktop-applikationen&feed=ATOM1.0',
- 'Software-Entwicklung'
- => 'https://rss.golem.de/rss.php?ms=softwareentwicklung&feed=ATOM1.0',
- 'Wirtschaft'
- => 'https://rss.golem.de/rss.php?ms=wirtschaft&feed=ATOM1.0',
- 'Wissenschaft'
- => 'https://rss.golem.de/rss.php?ms=wissenschaft&feed=ATOM1.0'
- )
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Specify number of full articles to return',
- 'defaultValue' => 5
- )
- ));
- const LIMIT = 5;
- const HEADERS = array('Cookie: golem_consent20=simple|220101;');
-
- public function collectData() {
- $this->collectExpandableDatas(
- $this->getInput('category'),
- $this->getInput('limit') ?: static::LIMIT
- );
- }
-
- protected function parseItem($item) {
- $item = parent::parseItem($item);
- $item['content'] = $item['content'] ?? '';
- $uri = $item['uri'];
-
- while ($uri) {
- $articlePage = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT, static::HEADERS);
-
- // URI without RSS feed reference
- $item['uri'] = $articlePage->find('head meta[name="twitter:url"]', 0)->content;
-
- $author = $articlePage->find('article header .authors .authors__name', 0);
- if ($author) {
- $item['author'] = $author->innertext;
- }
-
- $item['content'] .= $this->extractContent($articlePage);
-
- // next page
- $nextUri = $articlePage->find('link[rel="next"]', 0);
- $uri = $nextUri ? static::URI . $nextUri->href : null;
- }
-
- return $item;
- }
-
- private function extractContent($page) {
- $item = '';
-
- $article = $page->find('article', 0);
-
- // delete known bad elements
- foreach($article->find('div[id*="adtile"], #job-market, #seminars,
- div.gbox_affiliate, div.toc, .embedcontent') as $bad) {
- $bad->remove();
- }
- // reload html, as remove() is buggy
- $article = str_get_html($article->outertext);
-
- if ($pageHeader = $article->find('header.paged-cluster-header h1', 0)) {
- $item .= $pageHeader;
- }
-
- $header = $article->find('header', 0);
- foreach($header->find('p, figure') as $element) {
- $item .= $element;
- }
-
- $content = $article->find('div.formatted', 0);
-
- // full image quality
- foreach($content->find('img[data-src-full][src*="."]') as $img) {
- $img->src = $img->getAttribute('data-src-full');
- }
-
- foreach($content->find('p, h1, h3, img[src*="."]') as $element) {
- $item .= $element;
- }
-
- return $item;
- }
+class GolemBridge extends FeedExpander
+{
+ const MAINTAINER = 'Mynacol';
+ const NAME = 'Golem Bridge';
+ const URI = 'https://www.golem.de/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns the full articles instead of only the intro';
+ const PARAMETERS = [[
+ 'category' => [
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => [
+ 'Alle News'
+ => 'https://rss.golem.de/rss.php?feed=ATOM1.0',
+ 'Audio/Video'
+ => 'https://rss.golem.de/rss.php?ms=audio-video&feed=ATOM1.0',
+ 'Auto'
+ => 'https://rss.golem.de/rss.php?ms=auto&feed=ATOM1.0',
+ 'Foto'
+ => 'https://rss.golem.de/rss.php?ms=foto&feed=ATOM1.0',
+ 'Games'
+ => 'https://rss.golem.de/rss.php?ms=games&feed=ATOM1.0',
+ 'Handy'
+ => 'https://rss.golem.de/rss.php?ms=handy&feed=ATOM1.0',
+ 'Internet'
+ => 'https://rss.golem.de/rss.php?ms=internet&feed=ATOM1.0',
+ 'Mobil'
+ => 'https://rss.golem.de/rss.php?ms=mobil&feed=ATOM1.0',
+ 'Open Source'
+ => 'https://rss.golem.de/rss.php?ms=open-source&feed=ATOM1.0',
+ 'Politik/Recht'
+ => 'https://rss.golem.de/rss.php?ms=politik-recht&feed=ATOM1.0',
+ 'Security'
+ => 'https://rss.golem.de/rss.php?ms=security&feed=ATOM1.0',
+ 'Desktop-Applikationen'
+ => 'https://rss.golem.de/rss.php?ms=desktop-applikationen&feed=ATOM1.0',
+ 'Software-Entwicklung'
+ => 'https://rss.golem.de/rss.php?ms=softwareentwicklung&feed=ATOM1.0',
+ 'Wirtschaft'
+ => 'https://rss.golem.de/rss.php?ms=wirtschaft&feed=ATOM1.0',
+ 'Wissenschaft'
+ => 'https://rss.golem.de/rss.php?ms=wissenschaft&feed=ATOM1.0'
+ ]
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Specify number of full articles to return',
+ 'defaultValue' => 5
+ ]
+ ]];
+ const LIMIT = 5;
+ const HEADERS = ['Cookie: golem_consent20=simple|220101;'];
+
+ public function collectData()
+ {
+ $this->collectExpandableDatas(
+ $this->getInput('category'),
+ $this->getInput('limit') ?: static::LIMIT
+ );
+ }
+
+ protected function parseItem($item)
+ {
+ $item = parent::parseItem($item);
+ $item['content'] = $item['content'] ?? '';
+ $uri = $item['uri'];
+
+ while ($uri) {
+ $articlePage = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT, static::HEADERS);
+
+ // URI without RSS feed reference
+ $item['uri'] = $articlePage->find('head meta[name="twitter:url"]', 0)->content;
+
+ $author = $articlePage->find('article header .authors .authors__name', 0);
+ if ($author) {
+ $item['author'] = $author->innertext;
+ }
+
+ $item['content'] .= $this->extractContent($articlePage);
+
+ // next page
+ $nextUri = $articlePage->find('link[rel="next"]', 0);
+ $uri = $nextUri ? static::URI . $nextUri->href : null;
+ }
+
+ return $item;
+ }
+
+ private function extractContent($page)
+ {
+ $item = '';
+
+ $article = $page->find('article', 0);
+
+ // delete known bad elements
+ foreach (
+ $article->find('div[id*="adtile"], #job-market, #seminars,
+ div.gbox_affiliate, div.toc, .embedcontent') as $bad
+ ) {
+ $bad->remove();
+ }
+ // reload html, as remove() is buggy
+ $article = str_get_html($article->outertext);
+
+ if ($pageHeader = $article->find('header.paged-cluster-header h1', 0)) {
+ $item .= $pageHeader;
+ }
+
+ $header = $article->find('header', 0);
+ foreach ($header->find('p, figure') as $element) {
+ $item .= $element;
+ }
+
+ $content = $article->find('div.formatted', 0);
+
+ // full image quality
+ foreach ($content->find('img[data-src-full][src*="."]') as $img) {
+ $img->src = $img->getAttribute('data-src-full');
+ }
+
+ foreach ($content->find('p, h1, h3, img[src*="."]') as $element) {
+ $item .= $element;
+ }
+
+ return $item;
+ }
}
diff --git a/bridges/GoodreadsBridge.php b/bridges/GoodreadsBridge.php
index 4d92dd7f..ae1a865e 100644
--- a/bridges/GoodreadsBridge.php
+++ b/bridges/GoodreadsBridge.php
@@ -1,95 +1,95 @@
<?php
-class GoodreadsBridge extends BridgeAbstract {
-
- const MAINTAINER = 'captn3m0';
- const NAME = 'Goodreads Bridge';
- const URI = 'https://www.goodreads.com/';
- const CACHE_TIMEOUT = 0; // 30min
- const DESCRIPTION = 'Various RSS feeds from Goodreads';
-
- const CONTEXT_AUTHOR_BOOKS = 'Books by Author';
-
- // Using a specific context because I plan to expand this soon
- const PARAMETERS = array(
- 'Books by Author' => array(
- 'author_url' => array(
- 'name' => 'Link to author\'s page on Goodreads',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Should look somewhat like goodreads.com/author/show/',
- 'pattern' => '^(https:\/\/)?(www.)?goodreads\.com\/author\/show\/\d+\..*$',
- 'exampleValue' => 'https://www.goodreads.com/author/show/38550.Brandon_Sanderson'
- ),
- 'published_only' => array(
- 'name' => 'Show published books only',
- 'type' => 'checkbox',
- 'required' => false,
- 'title' => 'If left unchecked, this will return unpublished books as well',
- 'defaultValue' => 'checked',
- ),
- ),
- );
-
- private function collectAuthorBooks($url) {
-
- $regex = '/goodreads\.com\/author\/show\/(\d+)/';
-
- preg_match($regex, $url, $matches);
-
- $authorId = $matches[1];
-
- $authorListUrl = "https://www.goodreads.com/author/list/$authorId?sort=original_publication_year";
-
- $html = getSimpleHTMLDOMCached($authorListUrl, self::CACHE_TIMEOUT);
-
- foreach($html->find('tr[itemtype="http://schema.org/Book"]') as $row) {
- $dateSpan = $row->find('.uitext', 0)->plaintext;
- $date = null;
-
- // If book is not yet published, ignore for now
- if(preg_match('/published\s+(\d{4})/', $dateSpan, $matches) === 1) {
- // Goodreads doesn't give us exact publication date here, only a year
- // We are skipping future dates anyway, so this is def published
- // but we can't pick a dynamic date either to keep clients from getting
- // confused. So we pick a guaranteed date of 1st-Jan instead.
- $date = $matches[1] . '-01-01';
- } else if ($this->getInput('published_only') !== 'checked') {
- // We can return unpublished books as well
- $date = date('Y-01-01');
- } else {
- continue;
- }
-
- $row = defaultLinkTo($row, $this->getURI());
-
- $item['title'] = $row->find('.bookTitle', 0)->plaintext;
- $item['uri'] = $row->find('.bookTitle', 0)->getAttribute('href');
- $item['author'] = $row->find('.authorName', 0)->plaintext;
- $item['content'] = '<a href="'
- . $row->find('.bookTitle', 0)->getAttribute('href')
- . '"><img src="'
- . $row->find('.bookCover', 0)->getAttribute('src')
- . '"></a>';
- $item['timestamp'] = $date;
- $item['enclosures'] = array(
- $row->find('.bookCover', 0)->getAttribute('src')
- );
-
- $this->items[] = $item; // Add item to the list
- }
- }
-
- public function collectData() {
-
- switch ($this->queriedContext) {
- case self::CONTEXT_AUTHOR_BOOKS:
- $this->collectAuthorBooks($this->getInput('author_url'));
- break;
-
- default:
- throw new Exception('Invalid context', 1);
- break;
- }
- }
+class GoodreadsBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'captn3m0';
+ const NAME = 'Goodreads Bridge';
+ const URI = 'https://www.goodreads.com/';
+ const CACHE_TIMEOUT = 0; // 30min
+ const DESCRIPTION = 'Various RSS feeds from Goodreads';
+
+ const CONTEXT_AUTHOR_BOOKS = 'Books by Author';
+
+ // Using a specific context because I plan to expand this soon
+ const PARAMETERS = [
+ 'Books by Author' => [
+ 'author_url' => [
+ 'name' => 'Link to author\'s page on Goodreads',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Should look somewhat like goodreads.com/author/show/',
+ 'pattern' => '^(https:\/\/)?(www.)?goodreads\.com\/author\/show\/\d+\..*$',
+ 'exampleValue' => 'https://www.goodreads.com/author/show/38550.Brandon_Sanderson'
+ ],
+ 'published_only' => [
+ 'name' => 'Show published books only',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'title' => 'If left unchecked, this will return unpublished books as well',
+ 'defaultValue' => 'checked',
+ ],
+ ],
+ ];
+
+ private function collectAuthorBooks($url)
+ {
+ $regex = '/goodreads\.com\/author\/show\/(\d+)/';
+
+ preg_match($regex, $url, $matches);
+
+ $authorId = $matches[1];
+
+ $authorListUrl = "https://www.goodreads.com/author/list/$authorId?sort=original_publication_year";
+
+ $html = getSimpleHTMLDOMCached($authorListUrl, self::CACHE_TIMEOUT);
+
+ foreach ($html->find('tr[itemtype="http://schema.org/Book"]') as $row) {
+ $dateSpan = $row->find('.uitext', 0)->plaintext;
+ $date = null;
+
+ // If book is not yet published, ignore for now
+ if (preg_match('/published\s+(\d{4})/', $dateSpan, $matches) === 1) {
+ // Goodreads doesn't give us exact publication date here, only a year
+ // We are skipping future dates anyway, so this is def published
+ // but we can't pick a dynamic date either to keep clients from getting
+ // confused. So we pick a guaranteed date of 1st-Jan instead.
+ $date = $matches[1] . '-01-01';
+ } elseif ($this->getInput('published_only') !== 'checked') {
+ // We can return unpublished books as well
+ $date = date('Y-01-01');
+ } else {
+ continue;
+ }
+
+ $row = defaultLinkTo($row, $this->getURI());
+
+ $item['title'] = $row->find('.bookTitle', 0)->plaintext;
+ $item['uri'] = $row->find('.bookTitle', 0)->getAttribute('href');
+ $item['author'] = $row->find('.authorName', 0)->plaintext;
+ $item['content'] = '<a href="'
+ . $row->find('.bookTitle', 0)->getAttribute('href')
+ . '"><img src="'
+ . $row->find('.bookCover', 0)->getAttribute('src')
+ . '"></a>';
+ $item['timestamp'] = $date;
+ $item['enclosures'] = [
+ $row->find('.bookCover', 0)->getAttribute('src')
+ ];
+
+ $this->items[] = $item; // Add item to the list
+ }
+ }
+
+ public function collectData()
+ {
+ switch ($this->queriedContext) {
+ case self::CONTEXT_AUTHOR_BOOKS:
+ $this->collectAuthorBooks($this->getInput('author_url'));
+ break;
+
+ default:
+ throw new Exception('Invalid context', 1);
+ break;
+ }
+ }
}
diff --git a/bridges/GoogleGroupsBridge.php b/bridges/GoogleGroupsBridge.php
index 03152f5f..5bd7df47 100644
--- a/bridges/GoogleGroupsBridge.php
+++ b/bridges/GoogleGroupsBridge.php
@@ -1,67 +1,71 @@
<?php
-class GoogleGroupsBridge extends XPathAbstract {
- const NAME = 'Google Groups Bridge';
- const DESCRIPTION = 'Returns the latest posts on a Google Group';
- const URI = 'https://groups.google.com';
- const PARAMETERS = array( array(
- 'group' => array(
- 'name' => 'Group id',
- 'title' => 'The string that follows /g/ in the URL',
- 'exampleValue' => 'governance',
- 'required' => true
- ),
- 'account' => array(
- 'name' => 'Account id',
- 'title' => 'Some Google groups have an additional id following /a/ in the URL',
- 'exampleValue' => 'mozilla.org',
- 'required' => false
- )
- ));
- const CACHE_TIMEOUT = 3600;
+class GoogleGroupsBridge extends XPathAbstract
+{
+ const NAME = 'Google Groups Bridge';
+ const DESCRIPTION = 'Returns the latest posts on a Google Group';
+ const URI = 'https://groups.google.com';
+ const PARAMETERS = [ [
+ 'group' => [
+ 'name' => 'Group id',
+ 'title' => 'The string that follows /g/ in the URL',
+ 'exampleValue' => 'governance',
+ 'required' => true
+ ],
+ 'account' => [
+ 'name' => 'Account id',
+ 'title' => 'Some Google groups have an additional id following /a/ in the URL',
+ 'exampleValue' => 'mozilla.org',
+ 'required' => false
+ ]
+ ]];
+ const CACHE_TIMEOUT = 3600;
- const TEST_DETECT_PARAMETERS = array(
- 'https://groups.google.com/a/mozilla.org/g/announce' => array(
- 'account' => 'mozilla.org', 'group' => 'announce'
- ),
- 'https://groups.google.com/g/ansible-project' => array(
- 'account' => null, 'group' => 'ansible-project'
- ),
- );
+ const TEST_DETECT_PARAMETERS = [
+ 'https://groups.google.com/a/mozilla.org/g/announce' => [
+ 'account' => 'mozilla.org', 'group' => 'announce'
+ ],
+ 'https://groups.google.com/g/ansible-project' => [
+ 'account' => null, 'group' => 'ansible-project'
+ ],
+ ];
- const XPATH_EXPRESSION_ITEM = '//div[@class="yhgbKd"]';
- const XPATH_EXPRESSION_ITEM_TITLE = './/span[@class="o1DPKc"]';
- const XPATH_EXPRESSION_ITEM_CONTENT = './/span[@class="WzoK"]';
- const XPATH_EXPRESSION_ITEM_URI = './/a[@class="ZLl54"]/@href';
- const XPATH_EXPRESSION_ITEM_AUTHOR = './/span[@class="z0zUgf"][last()]';
- const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/div[@class="tRlaM"]';
- const XPATH_EXPRESSION_ITEM_ENCLOSURES = '';
- const XPATH_EXPRESSION_ITEM_CATEGORIES = '';
- const SETTING_FIX_ENCODING = true;
+ const XPATH_EXPRESSION_ITEM = '//div[@class="yhgbKd"]';
+ const XPATH_EXPRESSION_ITEM_TITLE = './/span[@class="o1DPKc"]';
+ const XPATH_EXPRESSION_ITEM_CONTENT = './/span[@class="WzoK"]';
+ const XPATH_EXPRESSION_ITEM_URI = './/a[@class="ZLl54"]/@href';
+ const XPATH_EXPRESSION_ITEM_AUTHOR = './/span[@class="z0zUgf"][last()]';
+ const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/div[@class="tRlaM"]';
+ const XPATH_EXPRESSION_ITEM_ENCLOSURES = '';
+ const XPATH_EXPRESSION_ITEM_CATEGORIES = '';
+ const SETTING_FIX_ENCODING = true;
- protected function getSourceUrl() {
- $source = self::URI;
+ protected function getSourceUrl()
+ {
+ $source = self::URI;
- $account = $this->getInput('account');
- if($account) {
- $source = $source . '/a/' . $account;
- }
- return $source . '/g/' . $this->getInput('group');
- }
+ $account = $this->getInput('account');
+ if ($account) {
+ $source = $source . '/a/' . $account;
+ }
+ return $source . '/g/' . $this->getInput('group');
+ }
- protected function provideWebsiteContent() {
- return defaultLinkTo(getContents($this->getSourceUrl()), self::URI);
- }
+ protected function provideWebsiteContent()
+ {
+ return defaultLinkTo(getContents($this->getSourceUrl()), self::URI);
+ }
- const URL_REGEX = '#^https://groups.google.com(?:/a/(?<account>\S+))?(?:/g/(?<group>\S+))#';
+ const URL_REGEX = '#^https://groups.google.com(?:/a/(?<account>\S+))?(?:/g/(?<group>\S+))#';
- public function detectParameters($url) {
- $params = array();
- if(preg_match(self::URL_REGEX, $url, $matches)) {
- $params['group'] = $matches['group'];
- $params['account'] = $matches['account'];
- return $params;
- }
- return null;
- }
+ public function detectParameters($url)
+ {
+ $params = [];
+ if (preg_match(self::URL_REGEX, $url, $matches)) {
+ $params['group'] = $matches['group'];
+ $params['account'] = $matches['account'];
+ return $params;
+ }
+ return null;
+ }
}
diff --git a/bridges/GooglePlayStoreBridge.php b/bridges/GooglePlayStoreBridge.php
index ec0c1090..d61be2c8 100644
--- a/bridges/GooglePlayStoreBridge.php
+++ b/bridges/GooglePlayStoreBridge.php
@@ -1,60 +1,64 @@
<?php
-class GooglePlayStoreBridge extends BridgeAbstract {
- const NAME = 'Google Play Store';
- const URI = 'https://play.google.com/store/apps';
- const CACHE_TIMEOUT = 3600; // 1h
- const DESCRIPTION = 'Returns the most recent version of an app with its changelog';
-
- const TEST_DETECT_PARAMETERS = array(
- 'https://play.google.com/store/apps/details?id=com.ichi2.anki' => array(
- 'id' => 'com.ichi2.anki'
- )
- );
-
- const PARAMETERS = array(array(
- 'id' => array(
- 'name' => 'Application ID',
- 'exampleValue' => 'com.ichi2.anki',
- 'required' => true
- )
- ));
-
- const INFORMATION_MAP = array(
- 'Updated' => 'timestamp',
- 'Current Version' => 'title',
- 'Offered By' => 'author'
- );
-
- public function collectData() {
- $appuri = static::URI . '/details?id=' . $this->getInput('id');
- $html = getSimpleHTMLDOM($appuri);
-
- $item = array();
- $item['uri'] = $appuri;
- $item['content'] = $html->find('div[itemprop=description]', 1)->innertext;
-
- // Find other fields from Additional Information section
- foreach($html->find('.hAyfc') as $info) {
- $index = self::INFORMATION_MAP[$info->first_child()->plaintext] ?? null;
- if (is_null($index)) {
- continue;
- }
- $item[$index] = $info->children(1)->plaintext;
- }
-
- $this->items[] = $item;
- }
-
- public function detectParameters($url) {
- // Example: https://play.google.com/store/apps/details?id=com.ichi2.anki
-
- $params = array();
- $regex = '/^(https?:\/\/)?play\.google\.com\/store\/apps\/details\?id=([^\/&?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['id'] = urldecode($matches[2]);
- return $params;
- }
-
- return null;
- }
+
+class GooglePlayStoreBridge extends BridgeAbstract
+{
+ const NAME = 'Google Play Store';
+ const URI = 'https://play.google.com/store/apps';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Returns the most recent version of an app with its changelog';
+
+ const TEST_DETECT_PARAMETERS = [
+ 'https://play.google.com/store/apps/details?id=com.ichi2.anki' => [
+ 'id' => 'com.ichi2.anki'
+ ]
+ ];
+
+ const PARAMETERS = [[
+ 'id' => [
+ 'name' => 'Application ID',
+ 'exampleValue' => 'com.ichi2.anki',
+ 'required' => true
+ ]
+ ]];
+
+ const INFORMATION_MAP = [
+ 'Updated' => 'timestamp',
+ 'Current Version' => 'title',
+ 'Offered By' => 'author'
+ ];
+
+ public function collectData()
+ {
+ $appuri = static::URI . '/details?id=' . $this->getInput('id');
+ $html = getSimpleHTMLDOM($appuri);
+
+ $item = [];
+ $item['uri'] = $appuri;
+ $item['content'] = $html->find('div[itemprop=description]', 1)->innertext;
+
+ // Find other fields from Additional Information section
+ foreach ($html->find('.hAyfc') as $info) {
+ $index = self::INFORMATION_MAP[$info->first_child()->plaintext] ?? null;
+ if (is_null($index)) {
+ continue;
+ }
+ $item[$index] = $info->children(1)->plaintext;
+ }
+
+ $this->items[] = $item;
+ }
+
+ public function detectParameters($url)
+ {
+ // Example: https://play.google.com/store/apps/details?id=com.ichi2.anki
+
+ $params = [];
+ $regex = '/^(https?:\/\/)?play\.google\.com\/store\/apps\/details\?id=([^\/&?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['id'] = urldecode($matches[2]);
+ return $params;
+ }
+
+ return null;
+ }
}
diff --git a/bridges/GoogleSearchBridge.php b/bridges/GoogleSearchBridge.php
index 5370804e..406cf2a9 100644
--- a/bridges/GoogleSearchBridge.php
+++ b/bridges/GoogleSearchBridge.php
@@ -1,102 +1,105 @@
<?php
-class GoogleSearchBridge extends BridgeAbstract {
+class GoogleSearchBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'sebsauvage';
+ const NAME = 'Google search';
+ const URI = 'https://www.google.com/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns max 100 results from the past year.';
- const MAINTAINER = 'sebsauvage';
- const NAME = 'Google search';
- const URI = 'https://www.google.com/';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Returns max 100 results from the past year.';
+ const PARAMETERS = [[
+ 'q' => [
+ 'name' => 'keyword',
+ 'required' => true,
+ 'exampleValue' => 'rss-bridge',
+ ],
+ 'verbatim' => [
+ 'name' => 'Verbatim',
+ 'type' => 'checkbox',
+ 'title' => 'Use literal keyword(s) without making improvements',
+ ],
+ ]];
- const PARAMETERS = array(array(
- 'q' => array(
- 'name' => 'keyword',
- 'required' => true,
- 'exampleValue' => 'rss-bridge',
- ),
- 'verbatim' => array(
- 'name' => 'Verbatim',
- 'type' => 'checkbox',
- 'title' => 'Use literal keyword(s) without making improvements',
- ),
- ));
+ public function collectData()
+ {
+ $dom = getSimpleHTMLDOM($this->getURI(), ['Accept-language: en-US']);
+ if (!$dom) {
+ returnServerError('No results for this query.');
+ }
+ $result = $dom->find('div[id=res]', 0);
- public function collectData(){
- $dom = getSimpleHTMLDOM($this->getURI(), ['Accept-language: en-US']);
- if (!$dom) {
- returnServerError('No results for this query.');
- }
- $result = $dom->find('div[id=res]', 0);
+ if (!$result) {
+ return;
+ }
- if(!$result) {
- return;
- }
+ foreach ($result->find('div[class~=g]') as $element) {
+ $item = [];
- foreach ($result->find('div[class~=g]') as $element) {
- $item = [];
+ $url = $element->find('a[href]', 0)->href;
+ $item['uri'] = htmlspecialchars_decode($url);
+ $item['title'] = $element->find('h3', 0)->plaintext;
- $url = $element->find('a[href]', 0)->href;
- $item['uri'] = htmlspecialchars_decode($url);
- $item['title'] = $element->find('h3', 0)->plaintext;
+ $resultDom = $element->find('div[data-content-feature=1]', 0);
+ if ($resultDom) {
+ // Split by — or ·
+ $resultParts = preg_split('/( — | · )/', $resultDom->plaintext);
+ $resultDate = trim($resultParts[0]);
+ $resultContent = trim($resultParts[1] ?? '');
+ } else {
+ // Some search results don't have this particular dom identifier
+ $resultDate = null;
+ $resultContent = null;
+ }
- $resultDom = $element->find('div[data-content-feature=1]', 0);
- if ($resultDom) {
- // Split by — or ·
- $resultParts = preg_split('/( — | · )/', $resultDom->plaintext);
- $resultDate = trim($resultParts[0]);
- $resultContent = trim($resultParts[1] ?? '');
- } else {
- // Some search results don't have this particular dom identifier
- $resultDate = null;
- $resultContent = null;
- }
+ if ($resultDate) {
+ try {
+ $createdAt = new \DateTime($resultDate);
+ // Set to midnight for consistent datetime
+ $createdAt->setTime(0, 0);
+ $item['timestamp'] = $createdAt->format('U');
+ } catch (\Exception $e) {
+ $item['timestamp'] = 0;
+ }
+ } else {
+ $item['timestamp'] = 0;
+ }
- if ($resultDate) {
- try {
- $createdAt = new \DateTime($resultDate);
- // Set to midnight for consistent datetime
- $createdAt->setTime(0, 0);
- $item['timestamp'] = $createdAt->format('U');
- } catch (\Exception $e) {
- $item['timestamp'] = 0;
- }
- } else {
- $item['timestamp'] = 0;
- }
+ if ($resultContent) {
+ $item['content'] = $resultContent;
+ }
- if ($resultContent) {
- $item['content'] = $resultContent;
- }
+ $this->items[] = $item;
+ }
+ // Sort by descending date
+ usort($this->items, function ($a, $b) {
+ return $b['timestamp'] <=> $a['timestamp'];
+ });
+ }
- $this->items[] = $item;
- }
- // Sort by descending date
- usort($this->items, function($a, $b) {
- return $b['timestamp'] <=> $a['timestamp'];
- });
- }
+ public function getURI()
+ {
+ if ($this->getInput('q')) {
+ $queryParameters = [
+ 'q' => $this->getInput('q'),
+ 'hl' => 'en',
+ 'num' => '100', // get 100 results
+ 'complete' => '0',
+ // in past year, sort by date, optionally verbatim
+ 'tbs' => 'qdr:y,sbd:1' . ($this->getInput('verbatim') ? ',li:1' : ''),
+ ];
+ return sprintf('https://www.google.com/search?%s', http_build_query($queryParameters));
+ }
- public function getURI() {
- if ($this->getInput('q')) {
- $queryParameters = [
- 'q' => $this->getInput('q'),
- 'hl' => 'en',
- 'num' => '100', // get 100 results
- 'complete' => '0',
- // in past year, sort by date, optionally verbatim
- 'tbs' => 'qdr:y,sbd:1' . ($this->getInput('verbatim') ? ',li:1' : ''),
- ];
- return sprintf('https://www.google.com/search?%s', http_build_query($queryParameters));
- }
+ return parent::getURI();
+ }
- return parent::getURI();
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('q'))) {
+ return $this->getInput('q') . ' - Google search';
+ }
- public function getName(){
- if(!is_null($this->getInput('q'))) {
- return $this->getInput('q') . ' - Google search';
- }
-
- return parent::getName();
- }
+ return parent::getName();
+ }
}
diff --git a/bridges/GrandComicsDatabaseBridge.php b/bridges/GrandComicsDatabaseBridge.php
index b4440870..7b83f84b 100644
--- a/bridges/GrandComicsDatabaseBridge.php
+++ b/bridges/GrandComicsDatabaseBridge.php
@@ -1,61 +1,61 @@
<?php
-class GrandComicsDatabaseBridge extends BridgeAbstract {
-
- const MAINTAINER = 'corenting';
- const NAME = 'Grand Comics Database Bridge';
- const URI = 'https://www.comics.org/';
- const CACHE_TIMEOUT = 7200; // 2h
- const DESCRIPTION = 'Returns the latest comics added to a series timeline';
- const PARAMETERS = array( array(
- 'series' => array(
- 'name' => 'Series id (from the timeline URL)',
- 'required' => true,
- 'exampleValue' => '63051',
- ),
- ));
-
- public function collectData(){
-
- $url = self::URI . 'series/' . $this->getInput('series') . '/details/timeline/';
- $html = getSimpleHTMLDOM($url);
-
- $table = $html->find('table', 0);
- $list = array_reverse($table->find('[class^=row_even]'));
- $seriesName = $html->find('span[id=series_name]', 0)->innertext;
-
- // Get row headers
- $rowHeaders = $table->find('th');
- foreach($list as $article) {
-
- // Skip empty rows
- $emptyRow = $article->find('td.empty_month');
- if (count($emptyRow) != 0) {
- continue;
- }
-
- $rows = $article->find('td');
- $key_date = $rows[0]->innertext;
-
- // Get URL too
- $uri = 'https://www.comics.org' . $article->find('a')[0]->href;
-
- // Build content
- $content = '';
- for($i = 0; $i < count($rowHeaders); $i++) {
- $headerItem = $rowHeaders[$i]->innertext;
- $rowItem = $rows[$i]->innertext;
- $content = $content . $headerItem . ': ' . $rowItem . '<br/>';
- }
-
- // Build final item
- $content = str_replace('href="/', 'href="' . static::URI, $content);
- $item = array();
- $item['title'] = $seriesName . ' - ' . $key_date;
- $item['timestamp'] = strtotime($key_date);
- $item['content'] = str_get_html($content);
- $item['uri'] = $uri;
-
- $this->items[] = $item;
- }
- }
+
+class GrandComicsDatabaseBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'corenting';
+ const NAME = 'Grand Comics Database Bridge';
+ const URI = 'https://www.comics.org/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Returns the latest comics added to a series timeline';
+ const PARAMETERS = [ [
+ 'series' => [
+ 'name' => 'Series id (from the timeline URL)',
+ 'required' => true,
+ 'exampleValue' => '63051',
+ ],
+ ]];
+
+ public function collectData()
+ {
+ $url = self::URI . 'series/' . $this->getInput('series') . '/details/timeline/';
+ $html = getSimpleHTMLDOM($url);
+
+ $table = $html->find('table', 0);
+ $list = array_reverse($table->find('[class^=row_even]'));
+ $seriesName = $html->find('span[id=series_name]', 0)->innertext;
+
+ // Get row headers
+ $rowHeaders = $table->find('th');
+ foreach ($list as $article) {
+ // Skip empty rows
+ $emptyRow = $article->find('td.empty_month');
+ if (count($emptyRow) != 0) {
+ continue;
+ }
+
+ $rows = $article->find('td');
+ $key_date = $rows[0]->innertext;
+
+ // Get URL too
+ $uri = 'https://www.comics.org' . $article->find('a')[0]->href;
+
+ // Build content
+ $content = '';
+ for ($i = 0; $i < count($rowHeaders); $i++) {
+ $headerItem = $rowHeaders[$i]->innertext;
+ $rowItem = $rows[$i]->innertext;
+ $content = $content . $headerItem . ': ' . $rowItem . '<br/>';
+ }
+
+ // Build final item
+ $content = str_replace('href="/', 'href="' . static::URI, $content);
+ $item = [];
+ $item['title'] = $seriesName . ' - ' . $key_date;
+ $item['timestamp'] = strtotime($key_date);
+ $item['content'] = str_get_html($content);
+ $item['uri'] = $uri;
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/GroupBundNaturschutzBridge.php b/bridges/GroupBundNaturschutzBridge.php
index d6c5cf11..2aa78578 100644
--- a/bridges/GroupBundNaturschutzBridge.php
+++ b/bridges/GroupBundNaturschutzBridge.php
@@ -2,106 +2,107 @@
class GroupBundNaturschutzBridge extends XPathAbstract
{
- const NAME = 'BUND Naturschutz in Bayern e.V. - Kreisgruppen';
- const URI = 'https://www.bund-naturschutz.de/ueber-uns/organisation/kreisgruppen-ortsgruppen';
- const DESCRIPTION = 'Returns the latest news from specified BUND Naturschutz in Bayern e.V. local group (Germany)';
- const MAINTAINER = 'dweipert';
+ const NAME = 'BUND Naturschutz in Bayern e.V. - Kreisgruppen';
+ const URI = 'https://www.bund-naturschutz.de/ueber-uns/organisation/kreisgruppen-ortsgruppen';
+ const DESCRIPTION = 'Returns the latest news from specified BUND Naturschutz in Bayern e.V. local group (Germany)';
+ const MAINTAINER = 'dweipert';
- const PARAMETERS = array(
- array(
- 'group' => array(
- 'name' => 'Group',
- 'type' => 'list',
- 'values' => array(
- // 'Aichach-Friedberg' => 'bn-aic.de', # non-uniform page
- 'Altötting' => 'altoetting',
- 'Amberg-Sulzbach' => 'amberg-sulzbach',
- 'Ansbach' => 'ansbach',
- 'Aschaffenburg' => 'aschaffenburg',
- 'Augsburg' => 'augsburg',
- 'Bad Kissingen' => 'bad-kissingen',
- 'Bad Tölz' => 'bad-toelz',
- 'Bamberg' => 'bamberg',
- 'Bayreuth' => 'bayreuth', # single entry # different layout
- 'Berchtesgadener Land' => 'berchtesgadener-land',
- 'Cham' => 'cham',
- // 'Coburg' => 'coburg', # no real entries # different layout
- 'Dachau' => 'dachau',
- 'Deggendorf' => 'Deggendorf',
- 'Dillingen' => 'dillingen',
- 'Dingolfing-Landau' => 'dingolfing-landau',
- 'Donau-Ries' => 'donauries',
- 'Ebersberg' => 'ebersberg',
- 'Eichstätt' => 'eichstaett', # single entry since 2020
- 'Erding' => 'erding',
- 'Erlangen' => 'erlangen',
- 'Forchheim' => 'forchheim',
- 'Freising' => 'freising',
- 'Freyung-Grafenau' => 'freyung-grafenau',
- 'Fürstenfeldbruck' => 'fuerstenfeldbruck',
- 'Fürth-Land' => 'fuerth-land',
- 'Fürth-Stadt' => 'fuerth',
- 'Garmisch-Partenkirchen' => 'garmisch-partenkirchen',
- 'Günzburg' => 'guenzburg',
- 'Hassberge' => 'hassberge',
- 'Höchstadt-Herzogenaurach' => 'hoechstadt-herzogenaurach',
- // 'Hof' => 'kreisgruppehof.bund-naturschutz.com', # non-uniform page
- 'Ingolstadt' => 'ingolstadt',
- 'Kelheim' => 'kelheim',
- 'Kempten' => 'kempten',
- 'Kitzingen' => 'kitzingen',
- 'Kronach' => 'kronach',
- 'Kulmbach' => 'kulmbach',
- 'Landsberg' => 'landsberg',
- 'Landshut' => 'landshut',
- 'Lichtenfeld' => 'lichtenfels',
- 'Lindau' => 'lindau',
- 'Main-Spessart' => 'main-spessart',
- 'Memmingen-Unterallgäu' => 'memmingen-unterallgaeu',
- 'Miesbach' => 'miesbach',
- 'Miltenberg' => 'miltenberg',
- 'Mühldorf am Inn' => 'muehldorf',
- // 'München' => 'bn-muenchen.de', # non-uniform page
- 'Neu-Ulm' => 'neu-ulm',
- 'Neuburg-Schrobenhausen' => 'neuburg-schrobenhausen',
- 'Neumarkt' => 'neumarkt',
- 'Neustadt/Aisch-Bad Windsheim' => 'neustadt-aisch',
- 'Neustadt/Waldnaab-Weiden' => 'neustadt-weiden',
- 'Nürnberg Stadt' => 'nuernberg-stadt',
- 'Nürnberger Land' => 'nuernberger-land',
- 'Ostallgäu-Kaufbeuren' => 'Ostallgäu-Kaufbeuren',
- 'Passau' => 'passau',
- 'Pfaffenhofen/Ilm' => 'pfaffenhofen',
- 'Regen' => 'regen',
- 'Regensburg' => 'regensburg',
- 'Rhön-Grabfeld' => 'rhoen-grabfeld',
- 'Rosenheim' => 'rosenheim',
- 'Roth' => 'roth',
- 'Rottal-Inn' => 'rottal-inn',
- 'Schwabach' => 'schwabach',
- 'Schwandorf' => 'schwandorf',
- 'Schweinfurt' => 'schweinfurt',
- 'Starnberg' => 'starnberg',
- 'Straubing-Bogen' => 'straubing',
- 'Tirschenreuth' => 'tirschenreuth',
- 'Traunstein' => 'traunstein',
- 'Weilheim-Schongau' => 'weilheim-schongau',
- 'Weißenburg-Gunzenhausen' => 'weissenburg-gunzenhausen',
- 'Wunsiedel' => 'wunsiedel',
- 'Würzburg' => 'wuerzburg',
- ),
- ),
- ),
- );
+ const PARAMETERS = [
+ [
+ 'group' => [
+ 'name' => 'Group',
+ 'type' => 'list',
+ 'values' => [
+ // 'Aichach-Friedberg' => 'bn-aic.de', # non-uniform page
+ 'Altötting' => 'altoetting',
+ 'Amberg-Sulzbach' => 'amberg-sulzbach',
+ 'Ansbach' => 'ansbach',
+ 'Aschaffenburg' => 'aschaffenburg',
+ 'Augsburg' => 'augsburg',
+ 'Bad Kissingen' => 'bad-kissingen',
+ 'Bad Tölz' => 'bad-toelz',
+ 'Bamberg' => 'bamberg',
+ 'Bayreuth' => 'bayreuth', # single entry # different layout
+ 'Berchtesgadener Land' => 'berchtesgadener-land',
+ 'Cham' => 'cham',
+ // 'Coburg' => 'coburg', # no real entries # different layout
+ 'Dachau' => 'dachau',
+ 'Deggendorf' => 'Deggendorf',
+ 'Dillingen' => 'dillingen',
+ 'Dingolfing-Landau' => 'dingolfing-landau',
+ 'Donau-Ries' => 'donauries',
+ 'Ebersberg' => 'ebersberg',
+ 'Eichstätt' => 'eichstaett', # single entry since 2020
+ 'Erding' => 'erding',
+ 'Erlangen' => 'erlangen',
+ 'Forchheim' => 'forchheim',
+ 'Freising' => 'freising',
+ 'Freyung-Grafenau' => 'freyung-grafenau',
+ 'Fürstenfeldbruck' => 'fuerstenfeldbruck',
+ 'Fürth-Land' => 'fuerth-land',
+ 'Fürth-Stadt' => 'fuerth',
+ 'Garmisch-Partenkirchen' => 'garmisch-partenkirchen',
+ 'Günzburg' => 'guenzburg',
+ 'Hassberge' => 'hassberge',
+ 'Höchstadt-Herzogenaurach' => 'hoechstadt-herzogenaurach',
+ // 'Hof' => 'kreisgruppehof.bund-naturschutz.com', # non-uniform page
+ 'Ingolstadt' => 'ingolstadt',
+ 'Kelheim' => 'kelheim',
+ 'Kempten' => 'kempten',
+ 'Kitzingen' => 'kitzingen',
+ 'Kronach' => 'kronach',
+ 'Kulmbach' => 'kulmbach',
+ 'Landsberg' => 'landsberg',
+ 'Landshut' => 'landshut',
+ 'Lichtenfeld' => 'lichtenfels',
+ 'Lindau' => 'lindau',
+ 'Main-Spessart' => 'main-spessart',
+ 'Memmingen-Unterallgäu' => 'memmingen-unterallgaeu',
+ 'Miesbach' => 'miesbach',
+ 'Miltenberg' => 'miltenberg',
+ 'Mühldorf am Inn' => 'muehldorf',
+ // 'München' => 'bn-muenchen.de', # non-uniform page
+ 'Neu-Ulm' => 'neu-ulm',
+ 'Neuburg-Schrobenhausen' => 'neuburg-schrobenhausen',
+ 'Neumarkt' => 'neumarkt',
+ 'Neustadt/Aisch-Bad Windsheim' => 'neustadt-aisch',
+ 'Neustadt/Waldnaab-Weiden' => 'neustadt-weiden',
+ 'Nürnberg Stadt' => 'nuernberg-stadt',
+ 'Nürnberger Land' => 'nuernberger-land',
+ 'Ostallgäu-Kaufbeuren' => 'Ostallgäu-Kaufbeuren',
+ 'Passau' => 'passau',
+ 'Pfaffenhofen/Ilm' => 'pfaffenhofen',
+ 'Regen' => 'regen',
+ 'Regensburg' => 'regensburg',
+ 'Rhön-Grabfeld' => 'rhoen-grabfeld',
+ 'Rosenheim' => 'rosenheim',
+ 'Roth' => 'roth',
+ 'Rottal-Inn' => 'rottal-inn',
+ 'Schwabach' => 'schwabach',
+ 'Schwandorf' => 'schwandorf',
+ 'Schweinfurt' => 'schweinfurt',
+ 'Starnberg' => 'starnberg',
+ 'Straubing-Bogen' => 'straubing',
+ 'Tirschenreuth' => 'tirschenreuth',
+ 'Traunstein' => 'traunstein',
+ 'Weilheim-Schongau' => 'weilheim-schongau',
+ 'Weißenburg-Gunzenhausen' => 'weissenburg-gunzenhausen',
+ 'Wunsiedel' => 'wunsiedel',
+ 'Würzburg' => 'wuerzburg',
+ ],
+ ],
+ ],
+ ];
- const XPATH_EXPRESSION_ITEM = '//div[@itemtype="http://schema.org/Article"]';
- const XPATH_EXPRESSION_ITEM_TITLE = './/*[@itemprop="headline"]';
- const XPATH_EXPRESSION_ITEM_CONTENT = './/*[@itemprop="description"]/text()';
- const XPATH_EXPRESSION_ITEM_URI = './/a/@href';
- const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/*[@itemprop="datePublished"]/@datetime';
- const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/img/@src';
+ const XPATH_EXPRESSION_ITEM = '//div[@itemtype="http://schema.org/Article"]';
+ const XPATH_EXPRESSION_ITEM_TITLE = './/*[@itemprop="headline"]';
+ const XPATH_EXPRESSION_ITEM_CONTENT = './/*[@itemprop="description"]/text()';
+ const XPATH_EXPRESSION_ITEM_URI = './/a/@href';
+ const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/*[@itemprop="datePublished"]/@datetime';
+ const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/img/@src';
- protected function getSourceUrl() {
- return 'https://' . $this->getInput('group') . '.bund-naturschutz.de/aktuelles';
- }
+ protected function getSourceUrl()
+ {
+ return 'https://' . $this->getInput('group') . '.bund-naturschutz.de/aktuelles';
+ }
}
diff --git a/bridges/HDWallpapersBridge.php b/bridges/HDWallpapersBridge.php
index c2dc5c20..ca5e251b 100644
--- a/bridges/HDWallpapersBridge.php
+++ b/bridges/HDWallpapersBridge.php
@@ -1,86 +1,91 @@
<?php
-class HDWallpapersBridge extends BridgeAbstract {
- const MAINTAINER = 'nel50n';
- const NAME = 'HD Wallpapers Bridge';
- const URI = 'https://www.hdwallpapers.in/';
- const CACHE_TIMEOUT = 43200; //12h
- const DESCRIPTION = 'Returns the latests wallpapers from HDWallpapers';
- const PARAMETERS = array( array(
- 'c' => array(
- 'name' => 'category',
- 'required' => true,
- 'defaultValue' => 'latest_wallpapers'
- ),
- 'm' => array(
- 'name' => 'max number of wallpapers'
- ),
- 'r' => array(
- 'name' => 'resolution',
- 'required' => true,
- 'defaultValue' => 'HD',
- 'title' => 'e.g=HD OR 1920x1200 OR 1680x1050'
- )
- ));
+class HDWallpapersBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'nel50n';
+ const NAME = 'HD Wallpapers Bridge';
+ const URI = 'https://www.hdwallpapers.in/';
+ const CACHE_TIMEOUT = 43200; //12h
+ const DESCRIPTION = 'Returns the latests wallpapers from HDWallpapers';
- public function collectData(){
- $category = $this->getInput('c');
- if(strrpos($category, 'wallpapers') !== strlen($category) - strlen('wallpapers')) {
- $category .= '-desktop-wallpapers';
- }
+ const PARAMETERS = [ [
+ 'c' => [
+ 'name' => 'category',
+ 'required' => true,
+ 'defaultValue' => 'latest_wallpapers'
+ ],
+ 'm' => [
+ 'name' => 'max number of wallpapers'
+ ],
+ 'r' => [
+ 'name' => 'resolution',
+ 'required' => true,
+ 'defaultValue' => 'HD',
+ 'title' => 'e.g=HD OR 1920x1200 OR 1680x1050'
+ ]
+ ]];
- $num = 0;
- $max = $this->getInput('m') ?: 14;
- $lastpage = 1;
+ public function collectData()
+ {
+ $category = $this->getInput('c');
+ if (strrpos($category, 'wallpapers') !== strlen($category) - strlen('wallpapers')) {
+ $category .= '-desktop-wallpapers';
+ }
- for($page = 1; $page <= $lastpage; $page++) {
- $link = self::URI . $category . '/page/' . $page;
- $html = getSimpleHTMLDOM($link);
+ $num = 0;
+ $max = $this->getInput('m') ?: 14;
+ $lastpage = 1;
- if($page === 1) {
- preg_match('/page\/(\d+)$/', $html->find('.pagination a', -2)->href, $matches);
- $lastpage = min($matches[1], ceil($max / 14));
- }
+ for ($page = 1; $page <= $lastpage; $page++) {
+ $link = self::URI . $category . '/page/' . $page;
+ $html = getSimpleHTMLDOM($link);
- $html = defaultLinkTo($html, self::URI);
+ if ($page === 1) {
+ preg_match('/page\/(\d+)$/', $html->find('.pagination a', -2)->href, $matches);
+ $lastpage = min($matches[1], ceil($max / 14));
+ }
- foreach($html->find('.wallpapers .wall a') as $element) {
- $thumbnail = $element->find('img', 0);
+ $html = defaultLinkTo($html, self::URI);
- $search = array(self::URI, 'wallpapers.html');
- $replace = array(self::URI . 'download/', $this->getInput('r') . '.jpg');
+ foreach ($html->find('.wallpapers .wall a') as $element) {
+ $thumbnail = $element->find('img', 0);
- $item = array();
- $item['uri'] = str_replace($search, $replace, $element->href);
+ $search = [self::URI, 'wallpapers.html'];
+ $replace = [self::URI . 'download/', $this->getInput('r') . '.jpg'];
- $item['timestamp'] = time();
- $item['title'] = $element->find('em1', 0)->text();
- $item['content'] = $item['title']
- . '<br><a href="'
- . $item['uri']
- . '"><img src="'
- . $thumbnail->src
- . '" /></a>';
+ $item = [];
+ $item['uri'] = str_replace($search, $replace, $element->href);
- $item['enclosures'] = array($item['uri']);
- $this->items[] = $item;
+ $item['timestamp'] = time();
+ $item['title'] = $element->find('em1', 0)->text();
+ $item['content'] = $item['title']
+ . '<br><a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $thumbnail->src
+ . '" /></a>';
- $num++;
- if ($num >= $max)
- break 2;
- }
- }
- }
+ $item['enclosures'] = [$item['uri']];
+ $this->items[] = $item;
- public function getName(){
- if(!is_null($this->getInput('c')) && !is_null($this->getInput('r'))) {
- return 'HDWallpapers - '
- . str_replace(array('__', '_'), array(' & ', ' '), $this->getInput('c'))
- . ' ['
- . $this->getInput('r')
- . ']';
- }
+ $num++;
+ if ($num >= $max) {
+ break 2;
+ }
+ }
+ }
+ }
- return parent::getName();
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('c')) && !is_null($this->getInput('r'))) {
+ return 'HDWallpapers - '
+ . str_replace(['__', '_'], [' & ', ' '], $this->getInput('c'))
+ . ' ['
+ . $this->getInput('r')
+ . ']';
+ }
+
+ return parent::getName();
+ }
}
diff --git a/bridges/HackerNewsUserThreadsBridge.php b/bridges/HackerNewsUserThreadsBridge.php
index 1b8410dd..fee96b61 100644
--- a/bridges/HackerNewsUserThreadsBridge.php
+++ b/bridges/HackerNewsUserThreadsBridge.php
@@ -1,48 +1,50 @@
<?php
-class HackerNewsUserThreadsBridge extends BridgeAbstract {
- const MAINTAINER = 'rakoo';
- const NAME = 'Hacker News User Threads';
- const URI = 'https://news.ycombinator.com';
- const CACHE_TIMEOUT = 7200; // 2 hours
- const DESCRIPTION = 'Hacker News threads for a user (at https://news.ycombinator.com/threads?id=xxx)';
- const PARAMETERS = array( array(
- 'user' => array(
- 'name' => 'User',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'nixcraft',
- 'title' => 'User whose threads you want to see'
- )
- ));
+class HackerNewsUserThreadsBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'rakoo';
+ const NAME = 'Hacker News User Threads';
+ const URI = 'https://news.ycombinator.com';
+ const CACHE_TIMEOUT = 7200; // 2 hours
+ const DESCRIPTION = 'Hacker News threads for a user (at https://news.ycombinator.com/threads?id=xxx)';
+ const PARAMETERS = [ [
+ 'user' => [
+ 'name' => 'User',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'nixcraft',
+ 'title' => 'User whose threads you want to see'
+ ]
+ ]];
- public function collectData(){
- $url = 'https://news.ycombinator.com/threads?id=' . $this->getInput('user');
- $html = getSimpleHTMLDOM($url);
- Debug::log('queried ' . $url);
- Debug::log('found ' . $html);
+ public function collectData()
+ {
+ $url = 'https://news.ycombinator.com/threads?id=' . $this->getInput('user');
+ $html = getSimpleHTMLDOM($url);
+ Debug::log('queried ' . $url);
+ Debug::log('found ' . $html);
- $item = array();
- $articles = $html->find('tr[class*="comtr"]');
- $story = '';
+ $item = [];
+ $articles = $html->find('tr[class*="comtr"]');
+ $story = '';
- foreach ($articles as $element) {
- $id = $element->getAttribute('id');
- $item['uri'] = 'https://news.ycombinator.com/item?id=' . $id;
+ foreach ($articles as $element) {
+ $id = $element->getAttribute('id');
+ $item['uri'] = 'https://news.ycombinator.com/item?id=' . $id;
- $author = $element->find('span[class*="comhead"]', 0)->find('a[class="hnuser"]', 0)->innertext;
- $newstory = $element->find('span[class*="comhead"]', 0)->find('span[class="onstory"]', 0);
- if (count($newstory->find('a')) > 0) {
- $story = $newstory->find('a', 0)->innertext;
- }
+ $author = $element->find('span[class*="comhead"]', 0)->find('a[class="hnuser"]', 0)->innertext;
+ $newstory = $element->find('span[class*="comhead"]', 0)->find('span[class="onstory"]', 0);
+ if (count($newstory->find('a')) > 0) {
+ $story = $newstory->find('a', 0)->innertext;
+ }
- $title = $author . ' | on ' . $story;
- $item['author'] = $author;
- $item['title'] = $title;
- $item['timestamp'] = $element->find('span[class*="age"]', 0)->find('a', 0)->innertext;
- $item['content'] = $element->find('span[class*="commtext"]', 0)->innertext;
+ $title = $author . ' | on ' . $story;
+ $item['author'] = $author;
+ $item['title'] = $title;
+ $item['timestamp'] = $element->find('span[class*="age"]', 0)->find('a', 0)->innertext;
+ $item['content'] = $element->find('span[class*="commtext"]', 0)->innertext;
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/HardwareInfoBridge.php b/bridges/HardwareInfoBridge.php
index ae79e0fd..e295984c 100644
--- a/bridges/HardwareInfoBridge.php
+++ b/bridges/HardwareInfoBridge.php
@@ -1,66 +1,67 @@
<?php
+
class HardwareInfoBridge extends FeedExpander
{
- const NAME = 'Hardware Info Bridge';
- const URI = 'https://nl.hardware.info/';
- const DESCRIPTION = 'Tech news from hardware.info (Dutch)';
- const MAINTAINER = 't0stiman';
-
- public function collectData()
- {
- $this->collectExpandableDatas('https://nl.hardware.info/updates/all.rss', 20);
- }
-
- protected function parseItem($feedItem)
- {
- $item = parent::parseItem($feedItem);
+ const NAME = 'Hardware Info Bridge';
+ const URI = 'https://nl.hardware.info/';
+ const DESCRIPTION = 'Tech news from hardware.info (Dutch)';
+ const MAINTAINER = 't0stiman';
- //get full article
- $articlePage = getSimpleHTMLDOMCached($feedItem->link);
+ public function collectData()
+ {
+ $this->collectExpandableDatas('https://nl.hardware.info/updates/all.rss', 20);
+ }
- $article = $articlePage->find('div.article__content', 0);
+ protected function parseItem($feedItem)
+ {
+ $item = parent::parseItem($feedItem);
- //everything under the social bar is not part of the article, remove it
- $reachedEndOfArticle = false;
+ //get full article
+ $articlePage = getSimpleHTMLDOMCached($feedItem->link);
- foreach($article->find('*') as $child) {
+ $article = $articlePage->find('div.article__content', 0);
- if(!$reachedEndOfArticle && isset($child->attr['class'])
- && $child->attr['class'] == 'article__content__social-bar') {
- $reachedEndOfArticle = true;
- }
+ //everything under the social bar is not part of the article, remove it
+ $reachedEndOfArticle = false;
- if($reachedEndOfArticle) {
- $child->outertext = '';
- }
- }
+ foreach ($article->find('*') as $child) {
+ if (
+ !$reachedEndOfArticle && isset($child->attr['class'])
+ && $child->attr['class'] == 'article__content__social-bar'
+ ) {
+ $reachedEndOfArticle = true;
+ }
- //get rid of some more elements we don't need
- $to_remove_selectors = array(
- 'script',
- 'div.incontent',
- 'div.article__content__social-bar',
- 'div#revealNewsTip',
- 'div.article__previous_next'
- );
+ if ($reachedEndOfArticle) {
+ $child->outertext = '';
+ }
+ }
- foreach($to_remove_selectors as $selector) {
- foreach($article->find($selector) as $found) {
- $found->outertext = '';
- }
- }
+ //get rid of some more elements we don't need
+ $to_remove_selectors = [
+ 'script',
+ 'div.incontent',
+ 'div.article__content__social-bar',
+ 'div#revealNewsTip',
+ 'div.article__previous_next'
+ ];
- // convert iframes to links. meant for embedded YouTube videos.
- foreach($article->find('iframe') as $found) {
+ foreach ($to_remove_selectors as $selector) {
+ foreach ($article->find($selector) as $found) {
+ $found->outertext = '';
+ }
+ }
- $iframeUrl = $found->getAttribute('src');
+ // convert iframes to links. meant for embedded YouTube videos.
+ foreach ($article->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>';
+ }
+ }
- $item['content'] = $article;
- return $item;
- }
+ $item['content'] = $article;
+ return $item;
+ }
}
diff --git a/bridges/HashnodeBridge.php b/bridges/HashnodeBridge.php
index 159510fb..ccfea547 100644
--- a/bridges/HashnodeBridge.php
+++ b/bridges/HashnodeBridge.php
@@ -1,46 +1,48 @@
<?php
-class HashnodeBridge extends BridgeAbstract {
+class HashnodeBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'liamka';
+ const NAME = 'Hashnode';
+ const URI = 'https://hashnode.com';
+ const CACHE_TIMEOUT = 3600; // 1hr
+ const DESCRIPTION = 'See trending or latest posts in Hashnode community.';
+ const LATEST_POSTS = 'https://hashnode.com/api/stories/recent?page=';
- const MAINTAINER = 'liamka';
- const NAME = 'Hashnode';
- const URI = 'https://hashnode.com';
- const CACHE_TIMEOUT = 3600; // 1hr
- const DESCRIPTION = 'See trending or latest posts in Hashnode community.';
- const LATEST_POSTS = 'https://hashnode.com/api/stories/recent?page=';
+ public function collectData()
+ {
+ $this->items = [];
+ for ($i = 0; $i < 5; $i++) {
+ $url = self::LATEST_POSTS . $i;
+ $content = getContents($url);
+ $array = json_decode($content, true);
- public function collectData(){
- $this->items = [];
- for ($i = 0; $i < 5; $i++) {
- $url = self::LATEST_POSTS . $i;
- $content = getContents($url);
- $array = json_decode($content, true);
+ if ($array['posts'] != null) {
+ foreach ($array['posts'] as $post) {
+ $item = [];
+ $item['title'] = $post['title'];
+ $item['content'] = nl2br(htmlspecialchars($post['brief']));
+ $item['timestamp'] = $post['dateAdded'];
+ if ($post['partOfPublication'] === true) {
+ $item['uri'] = sprintf(
+ 'https://%s.hashnode.dev/%s',
+ $post['publication']['username'],
+ $post['slug']
+ );
+ } else {
+ $item['uri'] = sprintf('https://hashnode.com/post/%s', $post['slug']);
+ }
+ if (!isset($item['uri'])) {
+ continue;
+ }
+ $this->items[] = $item;
+ }
+ }
+ }
+ }
- if($array['posts'] != null) {
- foreach($array['posts'] as $post) {
- $item = [];
- $item['title'] = $post['title'];
- $item['content'] = nl2br(htmlspecialchars($post['brief']));
- $item['timestamp'] = $post['dateAdded'];
- if($post['partOfPublication'] === true) {
- $item['uri'] = sprintf(
- 'https://%s.hashnode.dev/%s',
- $post['publication']['username'],
- $post['slug']
- );
- } else {
- $item['uri'] = sprintf('https://hashnode.com/post/%s', $post['slug']);
- }
- if(!isset($item['uri'])) {
- continue;
- }
- $this->items[] = $item;
- }
- }
- }
- }
-
- public function getName(){
- return self::NAME . ': Recent posts';
- }
+ public function getName()
+ {
+ return self::NAME . ': Recent posts';
+ }
}
diff --git a/bridges/HaveIBeenPwnedBridge.php b/bridges/HaveIBeenPwnedBridge.php
index da1e3938..e8a5e4f9 100644
--- a/bridges/HaveIBeenPwnedBridge.php
+++ b/bridges/HaveIBeenPwnedBridge.php
@@ -1,4 +1,5 @@
<?php
+
/**
* Uses the API as documented here:
* https://haveibeenpwned.com/API/v3#AllBreaches
@@ -6,149 +7,148 @@
* Gets the latest breaches by the date of the breach or when it was added to
* HIBP.
* */
-class HaveIBeenPwnedBridge extends BridgeAbstract {
- const NAME = 'Have I Been Pwned (HIBP) Bridge';
- const URI = 'https://haveibeenpwned.com';
- const DESCRIPTION = 'Returns list of Pwned websites';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(array(
- 'order' => array(
- 'name' => 'Order by',
- 'type' => 'list',
- 'values' => array(
- 'Breach date' => 'breachDate',
- 'Date added to HIBP' => 'dateAdded',
- ),
- 'defaultValue' => 'dateAdded',
- ),
- 'item_limit' => array(
- 'name' => 'Limit number of returned items',
- 'type' => 'number',
- 'required' => true,
- 'defaultValue' => 20,
- )
- ));
- const API_URI = 'https://haveibeenpwned.com/api/v3';
-
- const CACHE_TIMEOUT = 3600;
-
- private $breaches = array();
-
- public function collectData() {
-
- $data = json_decode(getContents(self::API_URI . '/breaches'), true);
-
- foreach($data as $breach) {
- $item = array();
-
- $pwnCount = number_format($breach['PwnCount']);
- $item['title'] = $breach['Title'] . ' - '
- . $pwnCount . ' breached accounts';
- $item['dateAdded'] = $breach['AddedDate'];
- $item['breachDate'] = $breach['BreachDate'];
- $item['uri'] = self::URI . '/PwnedWebsites#' . $breach['Name'];
-
- $item['content'] = '<p>' . $breach['Description'] . '</p>';
- $item['content'] .= '<p>' . $this->breachType($breach) . '</p>';
-
- $breachDate = date('j F Y', strtotime($breach['BreachDate']));
- $addedDate = date('j F Y', strtotime($breach['AddedDate']));
- $compData = implode(', ', $breach['DataClasses']);
-
- $item['content'] .= <<<EOD
+class HaveIBeenPwnedBridge extends BridgeAbstract
+{
+ const NAME = 'Have I Been Pwned (HIBP) Bridge';
+ const URI = 'https://haveibeenpwned.com';
+ const DESCRIPTION = 'Returns list of Pwned websites';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [[
+ 'order' => [
+ 'name' => 'Order by',
+ 'type' => 'list',
+ 'values' => [
+ 'Breach date' => 'breachDate',
+ 'Date added to HIBP' => 'dateAdded',
+ ],
+ 'defaultValue' => 'dateAdded',
+ ],
+ 'item_limit' => [
+ 'name' => 'Limit number of returned items',
+ 'type' => 'number',
+ 'required' => true,
+ 'defaultValue' => 20,
+ ]
+ ]];
+ const API_URI = 'https://haveibeenpwned.com/api/v3';
+
+ const CACHE_TIMEOUT = 3600;
+
+ private $breaches = [];
+
+ public function collectData()
+ {
+ $data = json_decode(getContents(self::API_URI . '/breaches'), true);
+
+ foreach ($data as $breach) {
+ $item = [];
+
+ $pwnCount = number_format($breach['PwnCount']);
+ $item['title'] = $breach['Title'] . ' - '
+ . $pwnCount . ' breached accounts';
+ $item['dateAdded'] = $breach['AddedDate'];
+ $item['breachDate'] = $breach['BreachDate'];
+ $item['uri'] = self::URI . '/PwnedWebsites#' . $breach['Name'];
+
+ $item['content'] = '<p>' . $breach['Description'] . '</p>';
+ $item['content'] .= '<p>' . $this->breachType($breach) . '</p>';
+
+ $breachDate = date('j F Y', strtotime($breach['BreachDate']));
+ $addedDate = date('j F Y', strtotime($breach['AddedDate']));
+ $compData = implode(', ', $breach['DataClasses']);
+
+ $item['content'] .= <<<EOD
<p>
<strong>Breach date:</strong> {$breachDate}<br>
<strong>Date added to HIBP:</strong> {$addedDate}<br>
<strong>Compromised accounts:</strong> {$pwnCount}<br>
<strong>Compromised data:</strong> {$compData}<br>
EOD;
- $item['uid'] = $breach['Name'];
- $this->breaches[] = $item;
- }
-
- $this->orderBreaches();
- $this->createItems();
- }
-
- private const BREACH_TYPES = array(
- 'IsVerified' => array(
- false => 'Unverified breach, may be sourced from elsewhere'
- ),
- 'IsFabricated' => array(
- true => 'Fabricated breach, likely not legitimate'
- ),
- 'IsSensitive' => array(
- true => 'Sensitive breach, not publicly searchable'
- ),
- 'IsRetired' => array(
- true => 'Retired breach, removed from system'
- ),
- 'IsSpamList' => array(
- true => 'Spam list, used for spam marketing'
- ),
- 'IsMalware' => array(
- true => 'Malware breach'
- ),
- );
-
- /**
- * Extract data breach type(s)
- */
- private function breachType($breach) {
-
- $content = '';
-
- foreach (self::BREACH_TYPES as $type => $message) {
- if (isset($message[$breach[$type]])) {
- $content .= $message[$breach[$type]] . '.<br>';
- }
- }
-
- return $content;
-
- }
-
- /**
- * Order Breaches by date added or date breached
- */
- private function orderBreaches() {
-
- $sortBy = $this->getInput('order');
- $sort = array();
-
- foreach ($this->breaches as $key => $item) {
- $sort[$key] = $item[$sortBy];
- }
-
- array_multisort($sort, SORT_DESC, $this->breaches);
-
- }
-
- /**
- * Create items from breaches array
- */
- private function createItems() {
-
- $limit = $this->getInput('item_limit');
-
- if ($limit < 1) {
- $limit = 20;
- }
-
- foreach ($this->breaches as $breach) {
- $item = array();
-
- $item['title'] = $breach['title'];
- $item['timestamp'] = $breach[$this->getInput('order')];
- $item['uri'] = $breach['uri'];
- $item['content'] = $breach['content'];
- $item['uid'] = $breach['uid'];
-
- $this->items[] = $item;
-
- if (count($this->items) >= $limit) {
- break;
- }
- }
- }
+ $item['uid'] = $breach['Name'];
+ $this->breaches[] = $item;
+ }
+
+ $this->orderBreaches();
+ $this->createItems();
+ }
+
+ private const BREACH_TYPES = [
+ 'IsVerified' => [
+ false => 'Unverified breach, may be sourced from elsewhere'
+ ],
+ 'IsFabricated' => [
+ true => 'Fabricated breach, likely not legitimate'
+ ],
+ 'IsSensitive' => [
+ true => 'Sensitive breach, not publicly searchable'
+ ],
+ 'IsRetired' => [
+ true => 'Retired breach, removed from system'
+ ],
+ 'IsSpamList' => [
+ true => 'Spam list, used for spam marketing'
+ ],
+ 'IsMalware' => [
+ true => 'Malware breach'
+ ],
+ ];
+
+ /**
+ * Extract data breach type(s)
+ */
+ private function breachType($breach)
+ {
+ $content = '';
+
+ foreach (self::BREACH_TYPES as $type => $message) {
+ if (isset($message[$breach[$type]])) {
+ $content .= $message[$breach[$type]] . '.<br>';
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * Order Breaches by date added or date breached
+ */
+ private function orderBreaches()
+ {
+ $sortBy = $this->getInput('order');
+ $sort = [];
+
+ foreach ($this->breaches as $key => $item) {
+ $sort[$key] = $item[$sortBy];
+ }
+
+ array_multisort($sort, SORT_DESC, $this->breaches);
+ }
+
+ /**
+ * Create items from breaches array
+ */
+ private function createItems()
+ {
+ $limit = $this->getInput('item_limit');
+
+ if ($limit < 1) {
+ $limit = 20;
+ }
+
+ foreach ($this->breaches as $breach) {
+ $item = [];
+
+ $item['title'] = $breach['title'];
+ $item['timestamp'] = $breach[$this->getInput('order')];
+ $item['uri'] = $breach['uri'];
+ $item['content'] = $breach['content'];
+ $item['uid'] = $breach['uid'];
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= $limit) {
+ break;
+ }
+ }
+ }
}
diff --git a/bridges/HeiseBridge.php b/bridges/HeiseBridge.php
index b73e5124..5f5092e3 100644
--- a/bridges/HeiseBridge.php
+++ b/bridges/HeiseBridge.php
@@ -1,79 +1,87 @@
<?php
-class HeiseBridge extends FeedExpander {
- const MAINTAINER = 'Dreckiger-Dan';
- const NAME = 'Heise Online Bridge';
- const URI = 'https://heise.de/';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Returns the full articles instead of only the intro';
- const PARAMETERS = array(array(
- 'category' => array(
- 'name' => 'Category',
- 'type' => 'list',
- 'values' => array(
- 'Alle News'
- => 'https://www.heise.de/newsticker/heise-atom.xml',
- 'Top-News'
- => 'https://www.heise.de/newsticker/heise-top-atom.xml',
- 'Internet-Störungen'
- => 'https://www.heise.de/netze/netzwerk-tools/imonitor-internet-stoerungen/feed/aktuelle-meldungen/',
- 'Alle News von heise Developer'
- => 'https://www.heise.de/developer/rss/news-atom.xml'
- )
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Specify number of full articles to return',
- 'defaultValue' => 5
- )
- ));
- const LIMIT = 5;
+class HeiseBridge extends FeedExpander
+{
+ const MAINTAINER = 'Dreckiger-Dan';
+ const NAME = 'Heise Online Bridge';
+ const URI = 'https://heise.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://www.heise.de/newsticker/heise-atom.xml',
+ 'Top-News'
+ => 'https://www.heise.de/newsticker/heise-top-atom.xml',
+ 'Internet-Störungen'
+ => 'https://www.heise.de/netze/netzwerk-tools/imonitor-internet-stoerungen/feed/aktuelle-meldungen/',
+ 'Alle News von heise Developer'
+ => 'https://www.heise.de/developer/rss/news-atom.xml'
+ ]
+ ],
+ '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
- );
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas(
+ $this->getInput('category'),
+ $this->getInput('limit') ?: static::LIMIT
+ );
+ }
- protected function parseItem($feedItem) {
- $item = parent::parseItem($feedItem);
- $item['uri'] = explode('?', $item['uri'])[0] . '?seite=all';
+ protected function parseItem($feedItem)
+ {
+ $item = parent::parseItem($feedItem);
+ $item['uri'] = explode('?', $item['uri'])[0] . '?seite=all';
- if (strpos($item['uri'], 'https://www.heise.de') !== 0) {
- return $item;
- }
+ if (strpos($item['uri'], 'https://www.heise.de') !== 0) {
+ return $item;
+ }
- $article = getSimpleHTMLDOMCached($item['uri']);
+ $article = getSimpleHTMLDOMCached($item['uri']);
- if ($article) {
- $article = defaultLinkTo($article, $item['uri']);
- $item = $this->addArticleToItem($item, $article);
- }
+ if ($article) {
+ $article = defaultLinkTo($article, $item['uri']);
+ $item = $this->addArticleToItem($item, $article);
+ }
- return $item;
- }
+ return $item;
+ }
- private function addArticleToItem($item, $article) {
- $authors = $article->find('.a-creator__names', 0)->find('.a-creator__name');
- if ($authors)
- $item['author'] = implode(', ', array_map(function ($e) { return $e->plaintext; }, $authors ));
+ private function addArticleToItem($item, $article)
+ {
+ $authors = $article->find('.a-creator__names', 0)->find('.a-creator__name');
+ if ($authors) {
+ $item['author'] = implode(', ', array_map(function ($e) {
+ return $e->plaintext;
+ }, $authors));
+ }
- $content = $article->find('div[class*="article-content"]', 0);
+ $content = $article->find('div[class*="article-content"]', 0);
- if ($content == null)
- $content = $article->find('#article_content', 0);
+ if ($content == null) {
+ $content = $article->find('#article_content', 0);
+ }
- foreach($content->find('p, h3, ul, table, pre, img') as $element) {
- $item['content'] .= $element;
- }
+ foreach ($content->find('p, h3, ul, table, pre, img') as $element) {
+ $item['content'] .= $element;
+ }
- foreach($content->find('img') as $img) {
- $item['enclosures'][] = $img->src;
- }
+ foreach ($content->find('img') as $img) {
+ $item['enclosures'][] = $img->src;
+ }
- return $item;
- }
+ return $item;
+ }
}
diff --git a/bridges/HotUKDealsBridge.php b/bridges/HotUKDealsBridge.php
index 45cc6637..7328ca04 100644
--- a/bridges/HotUKDealsBridge.php
+++ b/bridges/HotUKDealsBridge.php
@@ -1,3336 +1,3335 @@
<?php
-class HotUKDealsBridge extends PepperBridgeAbstract {
+class HotUKDealsBridge extends PepperBridgeAbstract
+{
+ const NAME = 'HotUKDeals bridge';
+ const URI = 'https://www.hotukdeals.com/';
+ const DESCRIPTION = 'Return the HotUKDeals search result using keywords';
+ const MAINTAINER = 'sysadminstory';
+ const PARAMETERS = [
+ 'Search by keyword(s))' => [
+ 'q' => [
+ 'name' => 'Keyword(s)',
+ 'type' => 'text',
+ 'exampleValue' => 'lamp',
+ 'required' => true
+ ],
+ 'hide_expired' => [
+ 'name' => 'Hide expired deals',
+ 'type' => 'checkbox',
+ ],
+ 'hide_local' => [
+ 'name' => 'Hide local deals',
+ 'type' => 'checkbox',
+ 'title' => 'Hide deals in physical store',
+ ],
+ 'priceFrom' => [
+ 'name' => 'Minimal Price',
+ 'type' => 'text',
+ 'title' => 'Minmal Price in Pounds',
+ 'required' => false
+ ],
+ 'priceTo' => [
+ 'name' => 'Maximum Price',
+ 'type' => 'text',
+ 'title' => 'Maximum Price in Pounds',
+ 'required' => false
+ ],
+ ],
- const NAME = 'HotUKDeals bridge';
- const URI = 'https://www.hotukdeals.com/';
- const DESCRIPTION = 'Return the HotUKDeals search result using keywords';
- const MAINTAINER = 'sysadminstory';
- const PARAMETERS = array(
- 'Search by keyword(s))' => array (
- 'q' => array(
- 'name' => 'Keyword(s)',
- 'type' => 'text',
- 'exampleValue' => 'lamp',
- 'required' => true
- ),
- 'hide_expired' => array(
- 'name' => 'Hide expired deals',
- 'type' => 'checkbox',
- ),
- 'hide_local' => array(
- 'name' => 'Hide local deals',
- 'type' => 'checkbox',
- 'title' => 'Hide deals in physical store',
- ),
- 'priceFrom' => array(
- 'name' => 'Minimal Price',
- 'type' => 'text',
- 'title' => 'Minmal Price in Pounds',
- 'required' => false
- ),
- 'priceTo' => array(
- 'name' => 'Maximum Price',
- 'type' => 'text',
- 'title' => 'Maximum Price in Pounds',
- 'required' => false
- ),
- ),
+ 'Deals per group' => [
+ 'group' => [
+ 'name' => 'Group',
+ 'type' => 'list',
+ 'title' => 'Group whose deals must be displayed',
+ 'values' => [
+ '3D Blu-ray' => '3d-bluray',
+ '3D Printer' => '3d-printer',
+ '3D TV' => '3d-tv',
+ '4K Blu-ray' => '4k-bluray',
+ '4K Monitor' => '4k-monitor',
+ '4K TV' => '4k-tv',
+ '5G Phones' => '5g-phones',
+ '7 Up' => '7up',
+ '8K TV' => '8k-tv',
+ '32 inch TV' => '32-inch-tv',
+ '40 inch TV' => '40-inch-tv',
+ '55 inch TV' => '55-inch-tv',
+ '65 inch TV' => '65-inch-tv',
+ '75 inch TV' => '75-inch-tv',
+ '144Hz Monitor' => '144hz',
+ 'A4 Paper' => 'a4-paper',
+ 'AAA Battery' => 'aaa',
+ 'AA Battery' => 'aa',
+ 'Abercrombie' => 'abercrombie',
+ 'Aberlour' => 'aberlour',
+ 'Accommodation' => 'accomodation',
+ 'Accurist' => 'accurist',
+ 'Ace Combat 7: Skies Unknown' => 'ace-combat-7',
+ 'Acer' => 'acer',
+ 'Acer Aspire' => 'acer-aspire',
+ 'Acer Laptop' => 'acer-laptop',
+ 'Acer PC Monitor' => 'acer-pc-monitor',
+ 'Acer Predator' => 'acer-predator',
+ 'Action Camera' => 'action-camera',
+ 'Action Figure &amp; Playsets' => 'playsets',
+ 'Activewear' => 'sports-clothes',
+ 'Activia' => 'activia',
+ 'adidas' => 'adidas',
+ 'adidas Continental' => 'continental',
+ 'Adidas Gazelle' => 'gazelle',
+ 'Adidas Originals' => 'adidas-originals',
+ 'Adidas Samba' => 'samba',
+ 'Adidas Stan Smith' => 'stan-smith',
+ 'Adidas Superstar' => 'adidas-superstar',
+ 'Adidas Trainers' => 'adidas-shoes',
+ 'Adidas Ultraboost' => 'adidas-ultraboost',
+ 'Adidas ZX Flux' => 'adidas-zx-flux',
+ 'Adobe' => 'adobe',
+ 'Adobe Lightroom' => 'lightroom',
+ 'Adobe Photoshop' => 'photoshop',
+ 'Adult Products' => 'adult',
+ 'Advent Calendar' => 'advent-calendar',
+ 'Adventure Time' => 'adventure-time',
+ 'AEG' => 'aeg',
+ 'Aftershave' => 'aftershave',
+ 'Age Of Empires' => 'age-of-empires',
+ 'Air Bed' => 'air-bed',
+ 'Air Conditioner' => 'air-con',
+ 'Airer' => 'airer',
+ 'Airfix' => 'airfix',
+ 'Air Fryer' => 'air-fryer',
+ 'Airline' => 'airline',
+ 'Airport' => 'airport',
+ 'Airport Parking' => 'airport-parking',
+ 'Air Purifier' => 'air-purifier',
+ 'AirTag' => 'airtag',
+ 'Air Treatment' => 'air-treatment',
+ 'AKG' => 'akg',
+ 'Alarm Clock' => 'alarm-clock',
+ 'Alarm System' => 'alarm-system',
+ 'Alcatel' => 'alcatel',
+ 'Alcohol' => 'alcohol',
+ 'Alesis' => 'alesis',
+ 'Alien: Isolation' => 'alien-isolation',
+ 'Alienware' => 'alienware',
+ 'All-in-One PC' => 'all-in-one-pc',
+ 'All-in-One Printer' => 'all-in-one-printer',
+ 'Alloy Wheel' => 'alloy-wheels',
+ 'All Saints' => 'all-saints',
+ 'Almonds' => 'almonds',
+ 'Alpro' => 'alpro',
+ 'Alton Towers' => 'alton-towers',
+ 'Amazfit' => 'xiaomi-amazfit',
+ 'Amazfit Bip' => 'xiaomi-amazfit-bip',
+ 'Amazfit GTS' => 'amazfit-gts',
+ 'Amazfit Verge' => 'amazfit-verge',
+ 'Amazfit Verge Lite' => 'amazfit-verge-lite',
+ 'Amazfit Watch' => 'amazfit-watch',
+ 'Amazon Add On Item' => 'add-on-item',
+ 'Amazon Business' => 'amazon-business',
+ 'Amazon Echo' => 'amazon-echo',
+ 'Amazon Echo Dot' => 'amazon-echo-dot',
+ 'Amazon Echo Plus' => 'amazon-echo-plus',
+ 'Amazon Echo Show' => 'amazon-echo-show',
+ 'Amazon Echo Show 5' => 'echo-show-5',
+ 'Amazon Echo Show 8' => 'amazon-echo-show-8',
+ 'Amazon Echo Spot' => 'amazon-echo-spot',
+ 'Amazon Fire 7' => 'amazon-fire-7',
+ 'Amazon Fire HD 8' => 'amazon-fire-hd-7',
+ 'Amazon Fire HD 10 Tablet' => 'amazon-fire-hd-10',
+ 'Amazon Fire Tablet' => 'amazon-tablet',
+ 'Amazon Fire TV Cube' => 'fire-tv-cube',
+ 'Amazon Fire TV Stick' => 'amazon-fire-stick',
+ 'Amazon Pantry' => 'amazon-pantry',
+ 'Amazon Prime' => 'amazon-prime',
+ 'Amazon Prime Video' => 'amazon-video',
+ 'Amazon Warehouse' => 'amazon-warehouse',
+ 'AMD' => 'amd',
+ 'AMD Radeon' => 'radeon',
+ 'AMD Ryzen' => 'amd-ryzen',
+ 'AMD Ryzen 5 5600X' => 'amd-ryzen-5-5600x',
+ 'AMD Ryzen 7 5800X' => 'amd-ryzen-7-5800x',
+ 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x',
+ 'AMD Ryzen 9 5950X' => 'amd-ryzen-9-5950x',
+ 'Amex' => 'amex',
+ 'Amiibo' => 'amiibo',
+ 'Amplifier' => 'amplifier',
+ 'Anchor Butter' => 'anchor-butter',
+ 'Andrex' => 'andrex',
+ 'Android Apps' => 'android-app',
+ 'Android Smartphone' => 'android-smartphone',
+ 'Android Tablet' => 'android-tablet',
+ 'Angelcare' => 'angelcare',
+ 'Angle Grinder' => 'grinder',
+ 'Anglepoise' => 'anglepoise',
+ 'Angry Birds' => 'angry-birds',
+ 'Animal Crossing' => 'animal-crossing',
+ 'Anime' => 'anime',
+ 'Anker' => 'anker',
+ 'Ankle Boots' => 'ankle-boots',
+ 'Anno 1800' => 'anno-1800',
+ 'Anthem' => 'anthem',
+ 'Antibacterial Hand Gel' => 'hand-gel',
+ 'Antibacterial Wipes' => 'cleaning-wipes',
+ 'Antivirus' => 'antivirus',
+ 'Antler' => 'antler',
+ 'AOC' => 'aoc',
+ 'Apex Legends' => 'apex-legends',
+ 'A Plague Tale: Innocence' => 'a-plague-tale-innocence',
+ 'App' => 'app',
+ 'Apple' => 'apple',
+ 'Apple AirPods' => 'apple-airpods',
+ 'Apple Airpods 2' => 'airpods-2',
+ 'Apple Airpods Max' => 'airpods-max',
+ 'Apple Airpods Pro' => 'airpods-pro',
+ 'Apple EarPods' => 'earpods',
+ 'Apple Headphones' => 'apple-headphones',
+ 'Apple HomePod' => 'apple-homepod',
+ 'Apple HomePod mini' => 'apple-homepod-mini',
+ 'Apple Keyboard' => 'apple-keyboard',
+ 'Apple Pencil' => 'apple-pencil',
+ 'Apple TV' => 'apple-tv',
+ 'Apple TV 4K' => 'apple-tv-4k',
+ 'Apple Watch' => 'apple-watch',
+ 'Apple Watch 3' => 'apple-watch-3',
+ 'Apple Watch 4' => 'apple-watch-4',
+ 'Apple Watch 5' => 'apple-watch-5',
+ 'Apple Watch 6' => 'apple-watch-6',
+ 'Apple Watch SE' => 'apple-watch-se',
+ 'Apron' => 'apron',
+ 'Aquadoodle' => 'aquadoodle',
+ 'Aqua Optima' => 'aqua-optima',
+ 'Aquarium' => 'aquarium',
+ 'Aramis' => 'aramis',
+ 'Argan Oil' => 'argan-oil',
+ 'Ariel' => 'ariel',
+ 'Ark' => 'ark',
+ 'Armani' => 'armani',
+ 'Armchair' => 'armchair',
+ 'Armed Forces Discount' => 'armed-forces',
+ 'Arsenal F. C.' => 'arsenal',
+ 'Arts and Crafts' => 'craft',
+ 'Asics' => 'asics',
+ 'Ask' => 'ask',
+ 'ASRock' => 'asrock',
+ 'Assassin&#039;s Creed' => 'assassins-creed',
+ 'Assassin&#039;s Creed: Odyssey' => 'assassins-creed-odyssey',
+ 'Assassin&#039;s Creed: Origins' => 'assassins-creed-origins',
+ 'Assassin&#039;s Creed: Unity' => 'assassins-creed-unity',
+ 'Assassin&#039;s Creed: Valhalla' => 'assasins-creed-valhalla',
+ 'Astral Chain' => 'astral-chain',
+ 'ASTRO Gaming' => 'astro-gaming',
+ 'Astro Gaming A40' => 'astro-gaming-a40',
+ 'Astro Gaming A50' => 'astro-gaming-a50',
+ 'Asus' => 'asus',
+ 'ASUS Laptop' => 'asus-laptop',
+ 'ASUS Monitor' => 'asus-monitor',
+ 'ASUS ROG' => 'asus-rog',
+ 'Asus ROG Phone' => 'asus-rog-phone',
+ 'Asus ROG Phone 2' => 'asus-rog-phone-2',
+ 'ASUS Router' => 'asus-router',
+ 'Asus Smartphone' => 'asus-smartphone',
+ 'ASUS Vivobook' => 'asus-vivobook',
+ 'ASUS Zenbook' => 'zenbook',
+ 'Asus ZenFone 6' => 'asus-zenfone-6',
+ 'Atari' => 'atari',
+ 'Audi' => 'audi',
+ 'Audio &amp; Hi-Fi' => 'audio',
+ 'Audio Accessories' => 'audio-accessories',
+ 'Audiobook' => 'audiobook',
+ 'Audio Technica' => 'audio-technica',
+ 'Aukey' => 'aukey',
+ 'Aussie' => 'aussie',
+ 'Autoglym' => 'autoglym',
+ 'Aveeno' => 'aveeno',
+ 'Avengers' => 'avengers',
+ 'AVG' => 'avg',
+ 'Aviva' => 'aviva',
+ 'Avon' => 'avon',
+ 'AV Receiver' => 'av-receiver',
+ 'Axe' => 'axe',
+ 'Baby Annabell' => 'baby-annabell',
+ 'Baby Bath' => 'baby-bath',
+ 'Baby Born' => 'baby-born',
+ 'Baby Bottle' => 'baby-bottles',
+ 'Baby Bouncer' => 'bouncer',
+ 'Baby Carrier' => 'baby-carrier',
+ 'Baby Clothes' => 'baby-clothes',
+ 'Baby Food' => 'baby-food',
+ 'Baby Gym' => 'baby-gym',
+ 'Baby Jogger' => 'baby-jogger',
+ 'Babyliss' => 'babyliss',
+ 'Baby Monitor' => 'baby-monitor',
+ 'Baby Shoes' => 'baby-shoes',
+ 'Baby Swing' => 'baby-swing',
+ 'Baby Walker' => 'baby-walker',
+ 'Baby Wipes' => 'wipes',
+ 'Bacardi' => 'bacardi',
+ 'Backpack' => 'backpack',
+ 'Back to the Future' => 'back-to-the-future',
+ 'Bacon' => 'bacon',
+ 'Badminton' => 'badminton',
+ 'Bag' => 'bag',
+ 'Bagless Vacuum Cleaner' => 'bagless-vacuum-cleaner',
+ 'Bahco' => 'bahco',
+ 'Baileys' => 'baileys',
+ 'Baked Beans' => 'baked-beans',
+ 'Bakery Products' => 'bakery-products',
+ 'Baking' => 'baking',
+ 'Ball Pit' => 'ball-pit',
+ 'Ballpoint Pen' => 'pen',
+ 'Band of Brothers' => 'band-of-brothers',
+ 'Bang &amp; Olufsen' => 'bang-olufsen',
+ 'Bank' => 'bank',
+ 'Bank Account' => 'bank-account',
+ 'Banks &amp; Credit Cards' => 'bank-credit-card',
+ 'Barbell' => 'barbell',
+ 'Barbie' => 'barbie',
+ 'Barbour' => 'barbour',
+ 'Barclaycard' => 'barclaycard',
+ 'Barclays' => 'barclays',
+ 'Barebones PC' => 'barebones',
+ 'bareMinerals' => 'bareminerals',
+ 'Barry M' => 'barry-m',
+ 'Bar Stools' => 'bar-stools',
+ 'Base Layer' => 'base-layer',
+ 'Basket' => 'basket',
+ 'Basketball' => 'basketball',
+ 'Basmati Rice' => 'basmati-rice',
+ 'Bath Mat' => 'bath-mat',
+ 'Bathroom Accessories' => 'bathroom',
+ 'Bathroom Cabinet' => 'bathroom-cabinet',
+ 'Bathroom Scale' => 'bathroom-scales',
+ 'Bathroom Tap' => 'tap',
+ 'Batman' => 'batman',
+ 'Battery' => 'battery',
+ 'Battleborn' => 'battleborn',
+ 'Battlefield' => 'battlefield',
+ 'Battlefield 1' => 'battlefield-1',
+ 'Battlefield 4' => 'battlefield-4',
+ 'Battlefield 5' => 'battlefield-5',
+ 'Battlestar Galactica' => 'battlestar-galactica',
+ 'Baylis &amp; Harding' => 'baylis-and-harding',
+ 'Bayonetta' => 'bayonetta',
+ 'Bayonetta 2' => 'bayonetta-2',
+ 'Baywatch' => 'baywatch',
+ 'BB-8' => 'bb-8',
+ 'BBC' => 'bbc',
+ 'BBQ Food' => 'bbq',
+ 'BBQs and Grills' => 'grill',
+ 'Bean Bag' => 'bean-bag',
+ 'Beanie Hat' => 'beanie-hat',
+ 'Bean to Cup Machine' => 'bean-to-cup',
+ 'Beard Trimmer' => 'beard-trimmer',
+ 'Beats by Dre' => 'beats-by-dre',
+ 'Beats Solo 3' => 'beats-solo-3',
+ 'Beats Studio 3' => 'beats-studio-3',
+ 'Beauty' => 'beauty-care',
+ 'Beauty and the Beast' => 'beauty-and-the-beast',
+ 'Becks' => 'becks',
+ 'Bed' => 'bed',
+ 'Bedding' => 'bedding',
+ 'Bedding &amp; Linens' => 'bedding-linens',
+ 'Bed Frame' => 'bed-frame',
+ 'Bedroom' => 'bedroom-furniture',
+ 'Beef' => 'beef',
+ 'Beer' => 'beer',
+ 'Beer Advent Calendar' => 'beer-advent-calendar',
+ 'Beko' => 'beko',
+ 'Belkin' => 'belkin',
+ 'Belstaff' => 'belstaff',
+ 'Belt' => 'belt',
+ 'BelVita' => 'belvita',
+ 'Ben &amp; Jerry&#039;s' => 'ben-jerrys',
+ 'Benefit Cosmetics' => 'benefit-cosmetics',
+ 'BenQ' => 'benq',
+ 'BenQ Monitor' => 'benq-monitor',
+ 'Ben Sherman' => 'ben-sherman',
+ 'BeoPlay Headphones' => 'beoplay-headphones',
+ 'Beoplay Speakers' => 'beoplay',
+ 'Berghaus' => 'berghaus',
+ 'Bestway' => 'bestway',
+ 'Betting' => 'betting',
+ 'Beyerdynamic' => 'beyerdynamic',
+ 'Bic' => 'bic',
+ 'Bike' => 'bike',
+ 'Bike Accessories' => 'bike-accessories',
+ 'Bike Brake' => 'brakes',
+ 'Bike Computer' => 'bike-computer',
+ 'Bike Helmet' => 'bicycle-helmet',
+ 'Bike Inner Tube' => 'inner-tube',
+ 'Bike Lights' => 'bike-lights',
+ 'Bike Lock' => 'bike-lock',
+ 'Bike Parts' => 'bike-parts',
+ 'Bike Pump' => 'bike-pump',
+ 'Biker Equipment' => 'biker-equipment',
+ 'Bike Saddle' => 'saddle',
+ 'Biking &amp; Urban Sports' => 'biking-urban-sports',
+ 'Bikini' => 'bikini',
+ 'Billabong' => 'billabong',
+ 'Bin' => 'bin',
+ 'Binatone' => 'binatone',
+ 'Bingo' => 'bingo',
+ 'Binoculars' => 'binoculars',
+ 'Bio Oil' => 'bio-oil',
+ 'Bioshock' => 'bioshock',
+ 'Birds Eye' => 'birds-eye',
+ 'Birkenstock' => 'birkenstock',
+ 'Biscuits' => 'biscuits',
+ 'Bissell' => 'bissell',
+ 'Bistro Set' => 'bistro-set',
+ 'Bitdefender' => 'bitdefender',
+ 'Black &amp; Decker' => 'black-decker',
+ 'Blackberry Smartphone' => 'blackberry',
+ 'Blanket' => 'blanket',
+ 'Blaupunkt' => 'blaupunkt',
+ 'Blazer' => 'blazer',
+ 'Bleach' => 'bleach',
+ 'Blended Malt' => 'malt',
+ 'Blender' => 'blender',
+ 'Blinds' => 'blinds',
+ 'Blink XT2 Smart Security Camera' => 'blink-xt2',
+ 'Blizzard' => 'blizzard',
+ 'Blood &amp; Truth' => 'blood-and-truth',
+ 'Bloodborne' => 'bloodborne',
+ 'Blood Pressure Monitor' => 'blood-pressure',
+ 'Blu-ray' => 'blu-ray',
+ 'Blu-ray Player' => 'blu-ray-player',
+ 'Bluetooth Headphones' => 'bluetooth-headphones',
+ 'Bluetooth Speaker' => 'bluetooth-speaker',
+ 'BMW' => 'bmw',
+ 'BMW Mini Cooper' => 'mini-cooper',
+ 'BMX' => 'bmx',
+ 'Board Game' => 'board-game',
+ 'Boardman' => 'boardman',
+ 'Boat Shoes' => 'boat-shoes',
+ 'Bodum' => 'bodum',
+ 'Bogof' => 'bogof',
+ 'Boiler' => 'boiler',
+ 'Bold' => 'bold',
+ 'Bombay Sapphire' => 'bombay-sapphire',
+ 'Bomber Jacket' => 'bomber-jacket',
+ 'Bonne Maman' => 'bonne-maman',
+ 'Bonsai' => 'bonsai',
+ 'Book' => 'book',
+ 'Bookcase' => 'bookcase',
+ 'Books &amp; Magazines' => 'books-magazines',
+ 'Booster Seat' => 'booster-seat',
+ 'Boots' => 'boots',
+ 'Borderlands' => 'borderlands',
+ 'Borderlands 3' => 'borderlands-3',
+ 'Bosch' => 'bosch',
+ 'Bosch Dishwasher' => 'bosch-dishwasher',
+ 'Bosch Drill' => 'bosch-drill',
+ 'Bosch Fridge' => 'bosch-fridge',
+ 'Bosch Rotak' => 'rotak',
+ 'Bosch Washing Machine' => 'bosch-washing-machine',
+ 'Bose' => 'bose',
+ 'Bose Headphones' => 'bose-headphones',
+ 'Bose Noise Cancelling Headphones 700' => 'bose-headphones-700',
+ 'Bose QuietComfort' => 'bose-quietcomfort',
+ 'Bose QuietComfort 35 II' => 'bose-quietcomfort-35-ii',
+ 'Bose SoundLink' => 'bose-soundlink',
+ 'Bose SoundLink Around-Ear II' => 'bose-soundlink-2',
+ 'Bose SoundTouch' => 'bose-soundtouch',
+ 'BOSS' => 'hugo-boss',
+ 'Boss Bottled' => 'boss-bottled',
+ 'Bouncy Castle' => 'bouncy-castle',
+ 'Bourbon' => 'bourbon',
+ 'Bourjois' => 'bourjois',
+ 'Bowers &amp; Wilkins' => 'bowers-wilkins',
+ 'Bowling' => 'bowling',
+ 'Bowmore' => 'bowmore',
+ 'Boxers' => 'boxers',
+ 'Boxing' => 'boxing',
+ 'Boxing Gloves' => 'boxing-gloves',
+ 'Boy&#039;s Clothes' => 'clothes-for-boys',
+ 'Bra' => 'bra',
+ 'Brabantia' => 'brabantia',
+ 'Bracelet' => 'bracelet',
+ 'Brands' => 'brand',
+ 'Brandy' => 'brandy',
+ 'Branston' => 'branston',
+ 'Branston Beans' => 'branston-beans',
+ 'Braun' => 'braun',
+ 'Braun Series 3' => 'braun-series-3',
+ 'Braun Series 5' => 'braun-series-5',
+ 'Braun Series 7' => 'braun-series-7',
+ 'Braun Series 9' => 'braun-series-9',
+ 'Braun Shaver' => 'braun-shaver',
+ 'Bread' => 'bread',
+ 'Breadmaker' => 'breadmaker',
+ 'Breakdown Cover' => 'breakdown',
+ 'Breaking Bad' => 'breaking-bad',
+ 'Breast Pump' => 'breast-pump',
+ 'Breville' => 'breville',
+ 'Breville Blend Active' => 'blendactive',
+ 'Brewdog' => 'brewdog',
+ 'Bridge Camera' => 'bridge-camera',
+ 'Briefcase' => 'briefcase',
+ 'Brita' => 'brita',
+ 'Britax' => 'britax',
+ 'British Airways' => 'british-airways',
+ 'Broadband' => 'broadband',
+ 'Broadband &amp; Phone Contracts' => 'broadband-phone-service',
+ 'Brogues' => 'brogues',
+ 'Brother' => 'brother',
+ 'Brother Printer' => 'brother-printer',
+ 'Brownie' => 'brownie',
+ 'BT' => 'bt',
+ 'BT Sport' => 'bt-sport',
+ 'Budweiser' => 'budweiser',
+ 'Buffalo' => 'buffalo',
+ 'Bugaboo' => 'bugaboo',
+ 'Buggy' => 'buggy',
+ 'Build-A-Bear' => 'build-a-bear',
+ 'Bulb' => 'bulbs',
+ 'Bulletstorm' => 'bulletstorm',
+ 'Bulmers' => 'bulmers',
+ 'Bulova' => 'bulova',
+ 'Burberry' => 'burberry',
+ 'Burger' => 'burger',
+ 'Burnout Paradise' => 'burnout-paradise',
+ 'Burt&#039;s Bees' => 'burts-bees',
+ 'Bus and Coach Ticket' => 'bus',
+ 'Bush' => 'bush',
+ 'Bushmills' => 'bushmills',
+ 'Butter' => 'butter',
+ 'Buying From Abroad' => 'buying-from-abroad',
+ 'Bvlgari' => 'bvlgari',
+ 'Cabin Case' => 'cabin-case',
+ 'Cabinet' => 'cabinet',
+ 'Cable Reel' => 'cable-reel',
+ 'Cables' => 'cables',
+ 'Cadbury&#039;s' => 'cadbury',
+ 'Café Rouge' => 'cafe-rouge',
+ 'Cafetière' => 'cafetiere',
+ 'Caffè Nero' => 'cafe-nero',
+ 'Cake' => 'cake',
+ 'Calculator' => 'calculator',
+ 'Calendar' => 'calendar',
+ 'Call of Duty' => 'call-of-duty',
+ 'Call of Duty: Black Ops' => 'black-ops',
+ 'Call of Duty: Black Ops 3' => 'black-ops-3',
+ 'Call of Duty: Black Ops 4' => 'black-ops-4',
+ 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war',
+ 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare',
+ 'Call of Duty: Modern Warfare' => 'modern-warfare',
+ 'Call of Duty: WW2' => 'call-of-duty-ww2',
+ 'Calpol' => 'calpol',
+ 'Calvin Klein' => 'calvin-klein',
+ 'Camcorder' => 'camcorder',
+ 'Camelbak' => 'camelbak',
+ 'Camera' => 'camera',
+ 'Camera Accessories' => 'camera-accessories',
+ 'Camera Bag' => 'camera-bag',
+ 'Camera Lens' => 'lens',
+ 'Camping' => 'camping',
+ 'Campingaz' => 'campingaz',
+ 'Candle' => 'candle',
+ 'Cannondale' => 'cannondale',
+ 'Canon' => 'canon',
+ 'Canon Camera' => 'canon-camera',
+ 'Canon EOS' => 'canon-eos',
+ 'Canon Lens' => 'canon-lens',
+ 'Canon Pixma' => 'canon-pixma',
+ 'Canon PowerShot' => 'canon-powershot',
+ 'Canon PowerShot SX430 IS' => 'canon-powershot-sx430-is',
+ 'Canon Printer' => 'canon-printer',
+ 'Canterbury' => 'canterbury',
+ 'Canton' => 'canton',
+ 'Canvas Print' => 'canvas-print',
+ 'Cap' => 'cap',
+ 'Capsule Machine' => 'capsule-machine',
+ 'Captain America' => 'captain-america',
+ 'Captain Morgan' => 'captain-morgan',
+ 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker',
+ 'Car' => 'car',
+ 'Car &amp; Motorcycle' => 'car-motorcycle',
+ 'Car Accessories' => 'car-accessories',
+ 'Caravan' => 'caravan',
+ 'Car Battery' => 'car-battery',
+ 'Carbon Monoxide Detector' => 'carbon-monoxide',
+ 'Car Care' => 'car-care',
+ 'Car Charger' => 'car-charger',
+ 'Cardhu' => 'cardhu',
+ 'Cardigan' => 'cardigan',
+ 'Card Reader' => 'card-reader',
+ 'Carex' => 'carex',
+ 'Carhartt' => 'carhartt',
+ 'Car Hire' => 'car-hire',
+ 'Car Insurance' => 'car-insurance',
+ 'Car Leasing' => 'car-lease',
+ 'Carling' => 'carling',
+ 'Car Lock' => 'lock',
+ 'Carlsberg' => 'carlsberg',
+ 'Car Mats' => 'car-mats',
+ 'Carolina Herrera' => 'carolina-herrera',
+ 'Car Parts' => 'car-parts',
+ 'Carpet' => 'carpet',
+ 'Carpet Cleaner' => 'carpet-cleaner',
+ 'CarPlan' => 'carplan',
+ 'Car Polish' => 'car-polish',
+ 'Carrera Bikes' => 'carrera',
+ 'Car Seat' => 'car-seat',
+ 'Car Service' => 'car-service',
+ 'Car Stereo' => 'car-stereo',
+ 'Car Wash' => 'car-wash',
+ 'Car Wax' => 'car-wax',
+ 'Casio' => 'casio',
+ 'Casio Eco-Drive' => 'eco-drive',
+ 'Casio Edifice' => 'edifice',
+ 'Casio G-Shock' => 'g-shock',
+ 'Casserole' => 'casserole',
+ 'Cast Iron Pots and Pans' => 'cast-iron',
+ 'Castrol' => 'castrol',
+ 'Caterpillar' => 'caterpillar',
+ 'Cat Flap' => 'cat-flap',
+ 'Cat Food' => 'cat-food',
+ 'Cath Kidston' => 'cath-kidston',
+ 'Cat Supplies' => 'cat-supplies',
+ 'CCTV' => 'cctv',
+ 'CD' => 'cd',
+ 'CD Player' => 'cd-player',
+ 'Ceiling Light' => 'ceiling-light',
+ 'Celebrations' => 'celebrations',
+ 'Cereal' => 'cereal',
+ 'Cetirizine' => 'cetirizine',
+ 'Chad Valley' => 'chad-valley',
+ 'Chainsaw' => 'chainsaw',
+ 'Champagne' => 'champagne',
+ 'Champneys' => 'champneys',
+ 'Chanel' => 'chanel',
+ 'Chanel Coco Mademoiselle' => 'coco-mademoiselle',
+ 'Changing Bag' => 'changing-bag',
+ 'Channel 4' => 'channel-4',
+ 'Charger' => 'charger',
+ 'Cheese' => 'cheese',
+ 'Chelsea Boots' => 'chelsea-boots',
+ 'Chelsea F. C.' => 'chelsea',
+ 'Chess' => 'chess',
+ 'Chessington' => 'chessington',
+ 'Chest Freezer' => 'chest-freezer',
+ 'Chest of Drawers' => 'chest-of-drawers',
+ 'Chicco' => 'chicco',
+ 'Chicken' => 'chicken',
+ 'Childcare' => 'baby',
+ 'Children&#039;s Books' => 'childrens-books',
+ 'Chino' => 'chino',
+ 'Chisel' => 'chisel',
+ 'Chloe' => 'chloe',
+ 'Chocolate' => 'chocolate',
+ 'Chocolate Advent Calendar' => 'chocolate-advent-calendar',
+ 'Chopper' => 'chopper',
+ 'Chopping Board' => 'chopping-board',
+ 'Christmas Card' => 'christmas-card',
+ 'Christmas Decoration' => 'christmas-decorations',
+ 'Christmas Gift' => 'christmas-gifts',
+ 'Christmas Jumper' => 'christmas-jumper',
+ 'Christmas Lights' => 'christmas-lights',
+ 'Christmas Stocking Fillers' => 'christmas-stocking-fillers',
+ 'Christmas Toys' => 'christmas-toys',
+ 'Christmas Tree' => 'christmas-tree',
+ 'Chromebook' => 'chromebook',
+ 'Chromecast' => 'chromecast',
+ 'Chromecast Ultra' => 'chromecast-ultra',
+ 'Chromecast with Google TV' => 'chromecast-google-tv',
+ 'Chronograph' => 'chronograph',
+ 'Chupa Chups' => 'chupa-chups',
+ 'Chuwi' => 'chuwi',
+ 'Cider' => 'cider',
+ 'Cinema' => 'cinema',
+ 'Cineworld' => 'cineworld',
+ 'Circular Saw' => 'circular-saw',
+ 'Circulon' => 'circulon',
+ 'Ciroc' => 'ciroc',
+ 'Cities Skylines' => 'cities-skylines',
+ 'Citizen' => 'citizen',
+ 'Citroen' => 'citroen',
+ 'City Break' => 'city-breaks',
+ 'Civilization' => 'civilization',
+ 'Clarins' => 'clarins',
+ 'Clarks' => 'clarks',
+ 'Clearance' => 'clearance',
+ 'Climbing' => 'climbing',
+ 'Climbing Frame' => 'climbing-frame',
+ 'Clinique' => 'clinique',
+ 'Clothes' => 'clothes',
+ 'Cloud Service' => 'cloud',
+ 'Clutch Bag' => 'clutch',
+ 'Coat' => 'coat',
+ 'Coca Cola' => 'coke',
+ 'Cocktail' => 'cocktail',
+ 'Coconut Oil' => 'coconut',
+ 'Coffee' => 'coffee',
+ 'Coffee Beans' => 'coffee-beans',
+ 'Coffee Machine' => 'coffee-machine',
+ 'Coffee Pods' => 'coffee-pods',
+ 'Coffee Table' => 'coffee-table',
+ 'Cognac' => 'cognac',
+ 'Cola' => 'cola',
+ 'Coleman' => 'coleman',
+ 'Colgate' => 'colgate',
+ 'Combi Drill' => 'combi',
+ 'Comfort' => 'comfort',
+ 'Comic' => 'comic',
+ 'Command &amp; Conquer' => 'command-and-conquer',
+ 'Compact Camera' => 'compact-camera',
+ 'Compact Flash' => 'compact-flash',
+ 'Competitions' => 'competitions',
+ 'Compost' => 'compost',
+ 'Compressor' => 'compressor',
+ 'Computer Accessories' => 'computer-accessories',
+ 'Computers &amp; Tablets' => 'computers',
+ 'Concert' => 'concert',
+ 'Condé Nast' => 'conde-nast',
+ 'Conditioner' => 'conditioner',
+ 'Condom' => 'condom',
+ 'Connectors' => 'connectors',
+ 'Contact Lenses' => 'contact-lenses',
+ 'Contents Insurance' => 'contents-insurance',
+ 'Controller' => 'controller',
+ 'Converse' => 'converse',
+ 'Converse Chuck Taylor' => 'chuck-taylor',
+ 'Cooker' => 'cooker',
+ 'Cooking Oil' => 'cooking-oil',
+ 'Cookware' => 'cooking',
+ 'Cookware Set' => 'cookware-set',
+ 'Cookworks' => 'cookworks',
+ 'Cool Box' => 'cool-box',
+ 'Coors Light' => 'coors-light',
+ 'Cordless Drill' => 'cordless-drill',
+ 'Cordless Phone' => 'cordless-phone',
+ 'Cornetto' => 'cornetto',
+ 'Corona Beer' => 'corona',
+ 'Corsair' => 'corsair',
+ 'Cosatto' => 'cosatto',
+ 'Costa Coffee' => 'costa-coffee',
+ 'Costume' => 'costume',
+ 'Cot' => 'cot',
+ 'Counter Strike' => 'counter-strike',
+ 'Courses and Training' => 'education',
+ 'Cow &amp; Gate' => 'cow-and-gate',
+ 'Cozy Coupe' => 'cozy-coupe',
+ 'CPU' => 'cpu',
+ 'CPU Cooler' => 'cpu-cooler',
+ 'Craghoppers' => 'craghoppers',
+ 'Crash Bandicoot' => 'crash-bandicoot',
+ 'Crash Team Racing Nitro-Fueled' => 'crash-team-racing-nitro-fueled',
+ 'Crayola' => 'crayola',
+ 'Creatine' => 'creatine',
+ 'Credit Card' => 'credit-card',
+ 'Creme Egg' => 'creme-egg',
+ 'Cricket' => 'cricket',
+ 'Crisps' => 'crisps',
+ 'Crocs' => 'crocs',
+ 'Cross Trainer' => 'cross-trainer',
+ 'Crown Paint' => 'crown',
+ 'Crucial' => 'crucial',
+ 'Cruelty Free Makeup' => 'cruelty-free-makeup',
+ 'Cruises' => 'cruise',
+ 'Cube Bikes' => 'cube',
+ 'Cubot' => 'cubot',
+ 'Cufflinks' => 'cufflinks',
+ 'Culture &amp; Leisure' => 'entertainment',
+ 'Cuphead' => 'cuphead',
+ 'Cuprinol' => 'cuprinol',
+ 'Curling Wand' => 'curling-wand',
+ 'Curtain' => 'curtain',
+ 'Cushelle' => 'cushelle',
+ 'Cushion' => 'cushion',
+ 'Cutlery' => 'cutlery',
+ 'CyberLink' => 'cyberlink',
+ 'Cyberpunk 2077' => 'cyberpunk-2077',
+ 'Cybex' => 'cybex',
+ 'Cycling' => 'cycling',
+ 'Cycling Jacket' => 'cycling-jacket',
+ 'D-Link' => 'd-link',
+ 'DAB Radio' => 'dab-radio',
+ 'Dacia' => 'dacia',
+ 'Daily Mail' => 'daily-mail',
+ 'Dairy Milk' => 'dairy-milk',
+ 'Darksiders' => 'darksiders',
+ 'Dark Souls' => 'dark-souls',
+ 'Dark Souls 3' => 'dark-souls-3',
+ 'Dartboard' => 'dartboard',
+ 'Darts' => 'darts',
+ 'Dash Cam' => 'dash-cam',
+ 'Data Storage' => 'storage',
+ 'Davidoff' => 'davidoff',
+ 'Days Gone' => 'days-gone',
+ 'Days Out' => 'days-out',
+ 'Daz' => 'daz',
+ 'DC Comic' => 'dc',
+ 'DDR3' => 'ddr3',
+ 'DDR4' => 'ddr4',
+ 'Dead Island' => 'dead-island',
+ 'Dead or Alive 6' => 'dead-or-alive-6',
+ 'Deadpool' => 'deadpool',
+ 'Dead Rising' => 'dead-rising',
+ 'Death Stranding' => 'death-stranding',
+ 'Deezer' => 'deezer',
+ 'Dehumidifier' => 'dehumidifier',
+ 'Dell' => 'dell',
+ 'Dell Laptop' => 'dell-laptop',
+ 'Dell Monitor' => 'dell-monitor',
+ 'Dell XPS' => 'xps',
+ 'Delonghi' => 'delonghi',
+ 'Demon&#039;s Souls' => 'demon-souls',
+ 'Denby' => 'denby',
+ 'Denon' => 'denon',
+ 'Deodorant' => 'deodorant',
+ 'Desk' => 'desk',
+ 'Desperados Beer' => 'desperados',
+ 'Despicable Me' => 'despicable-me',
+ 'Destiny' => 'destiny',
+ 'Destiny 2' => 'destiny-2',
+ 'Detergent' => 'detergent',
+ 'Detroit: Become Human' => 'detroit-become-human',
+ 'Dettol' => 'dettol',
+ 'Deus Ex' => 'deus-ex',
+ 'Deus Ex: Mankind Divided' => 'deus-ex-mankind-divided',
+ 'Development Boards' => 'development-boards',
+ 'Devil May Cry 5' => 'devil-may-cry-5',
+ 'DeWalt' => 'dewalt',
+ 'DFDS' => 'dfds',
+ 'Diablo 3' => 'diablo-3',
+ 'Diary' => 'diary',
+ 'Dickies' => 'dickies',
+ 'Diesel' => 'diesel',
+ 'Diet' => 'diet',
+ 'Diggerland' => 'diggerland',
+ 'Digihome' => 'digihome',
+ 'Digimon' => 'digimon',
+ 'Digital Camera' => 'digital-camera',
+ 'Digital Watch' => 'digital-watch',
+ 'Dildo' => 'dildo',
+ 'Dimplex' => 'dimplex',
+ 'Dining Room' => 'dining-room',
+ 'Dining Room Chair' => 'chair',
+ 'Dining Set' => 'dining-set',
+ 'Dining Table' => 'dining-table',
+ 'Dinner Plate' => 'plates',
+ 'Dinner Set' => 'dinner-set',
+ 'Dinosaur' => 'dinosaur',
+ 'Dior' => 'dior',
+ 'Dior Sauvage' => 'dior-sauvage',
+ 'Dirt' => 'dirt',
+ 'Dirt 4' => 'dirt-4',
+ 'DIRT 5' => 'dirt-5',
+ 'Dirt Rally 2.0' => 'dirt-rally-2',
+ 'Disaronno' => 'disaronno',
+ 'Discord Nitro' => 'discord-nitro',
+ 'Disgaea' => 'disgaea',
+ 'Dishonored' => 'dishonored',
+ 'Dishonored 2' => 'dishonored-2',
+ 'Dishwasher' => 'dishwasher',
+ 'Dishwasher Tablets' => 'dishwasher-tablets',
+ 'Disinfectants' => 'disinfectants',
+ 'Disney' => 'disney',
+ 'Disney&#039;s Cars' => 'disney-cars',
+ 'Disney&#039;s Frozen' => 'disney-frozen',
+ 'Disney+' => 'disney-plus',
+ 'Disney Infinity' => 'disney-infinity',
+ 'Disneyland' => 'disneyland',
+ 'Disney Princess' => 'disney-princess',
+ 'Disney Tsum Tsum' => 'tsum-tsum',
+ 'Disney World' => 'disney-world',
+ 'Divan' => 'divan',
+ 'DIY' => 'diy',
+ 'DJ Equipment' => 'dj',
+ 'DJI Phantom' => 'dji-phantom',
+ 'DKNY' => 'dkny',
+ 'Doctor Who' => 'doctor-who',
+ 'Dog Bed' => 'dog-bed',
+ 'Dog Food' => 'dog-food',
+ 'Dog Supplies' => 'dog',
+ 'Dolce &amp; Gabbana' => 'dolce',
+ 'Dolce Gusto' => 'dolce-gusto',
+ 'Dolce Gusto Coffee Machine' => 'dolce-gusto-coffee-machine',
+ 'Doll' => 'doll',
+ 'Dolls House' => 'dolls-house',
+ 'Domain Service' => 'domain',
+ 'Doogee' => 'doogee',
+ 'Doom' => 'doom',
+ 'Door' => 'door',
+ 'Doorbell' => 'doorbell',
+ 'Door Handles' => 'door-handles',
+ 'Doormat' => 'doormat',
+ 'Doritos' => 'doritos',
+ 'Dove' => 'dove',
+ 'Down Jacket' => 'down-jacket',
+ 'Downton Abbey' => 'downton-abbey',
+ 'Dr. Martens' => 'dr-martens',
+ 'Dragon Age' => 'dragon-age',
+ 'Dragon Ball' => 'dragon-ball',
+ 'Dragon Ball: FighterZ' => 'dragon-ball-fighterz',
+ 'Dragon Quest' => 'dragon-quest',
+ 'Dragon Quest Builders' => 'dragon-quest-builders',
+ 'Dragon Quest Builders 2' => 'dragon-quest-builders-2',
+ 'Dragon Quest XI: Echoes of an Elusive Age' => 'dragon-quest-xi',
+ 'Draper' => 'draper',
+ 'Drayton Manor' => 'drayton-manor',
+ 'Dreame T20' => 'dreame-t20',
+ 'Dreame V9' => 'dreame-v9',
+ 'Dreame V9P' => 'dreame-v9p',
+ 'Dreame V10' => 'dreame-v10',
+ 'Dreame V11' => 'dreame-v11',
+ 'Dreame Vacuum Cleaner' => 'xiaomi-vacuum-cleaner',
+ 'Dremel' => 'dremel',
+ 'Dress' => 'dress',
+ 'Dressing Gown' => 'dressing-gown',
+ 'Drill' => 'drill',
+ 'Drill Driver' => 'driver',
+ 'Drinks' => 'drinks',
+ 'Driveclub' => 'driveclub',
+ 'Driving Lessons' => 'driving-lessons',
+ 'Drone' => 'drone',
+ 'Dryer' => 'dryer',
+ 'DSLR Camera' => 'dslr',
+ 'Dual Fuel Cooker' => 'dual-fuel',
+ 'Dualit' => 'dualit',
+ 'Dual Sim' => 'sim',
+ 'Dulux' => 'dulux',
+ 'Duracell' => 'duracell',
+ 'Durex' => 'durex',
+ 'Duvet' => 'duvet',
+ 'DVD' => 'dvd',
+ 'DVD Player' => 'dvd-player',
+ 'Dying Light' => 'dying-light',
+ 'Dymo' => 'dymo',
+ 'Dyson' => 'dyson',
+ 'Dyson Supersonic' => 'dyson-supersonic',
+ 'Dyson V6' => 'dyson-v6',
+ 'Dyson V7' => 'dyson-v7',
+ 'Dyson V8' => 'dyson-v8',
+ 'Dyson V10' => 'dyson-v10',
+ 'Dyson V11' => 'dyson-v11',
+ 'Dyson Vacuum Cleaner' => 'dyson-vacuum-cleaner',
+ 'e-Reader' => 'ereader',
+ 'EA' => 'ea',
+ 'EA Access' => 'ea-access',
+ 'Earphones' => 'earphones',
+ 'Earrings' => 'earrings',
+ 'EA Sports' => 'ea-sports',
+ 'EA Sports UFC' => 'ufc',
+ 'Easter Eggs' => 'egg',
+ 'Eastpak' => 'eastpak',
+ 'eBook' => 'ebook',
+ 'Ecovacs' => 'ecovacs',
+ 'Ecover' => 'ecover',
+ 'Educational Toys' => 'educational-toys',
+ 'EE' => 'ee',
+ 'eFootball PES 2021' => 'pes-2021',
+ 'ELC Happyland' => 'happyland',
+ 'Electrical Accessories' => 'electrical-accessories',
+ 'Electric Bike' => 'electric-bike',
+ 'Electric Blanket' => 'electric-blanket',
+ 'Electric Cooker' => 'electric-cooker',
+ 'Electric Fires' => 'electric-fire',
+ 'Electric Scooter' => 'electric-scooter',
+ 'Electric Shower' => 'electric-shower',
+ 'Electric Toothbrush' => 'electric-toothbrush',
+ 'Electronic Accessories' => 'electronics-accessories',
+ 'Electronics' => 'electronics',
+ 'Elemis' => 'elemis',
+ 'Elephone' => 'elephone',
+ 'Elgato' => 'elgato',
+ 'Elite Dangerous' => 'elite-dangerous',
+ 'Elizabeth Arden' => 'elizabeth-arden',
+ 'Emirates' => 'emirates',
+ 'Endura' => 'endura',
+ 'Eneloop' => 'eneloop',
+ 'Energizer' => 'energizer',
+ 'Energy' => 'energy',
+ 'Energy, Heating &amp; Gas' => 'energy-heating-gas',
+ 'Energy Drinks' => 'energy-drinks',
+ 'Engine Oil' => 'engine-oil',
+ 'Epilator' => 'epilator',
+ 'Epson' => 'epson',
+ 'Epson Printer' => 'epson-printer',
+ 'Espresso' => 'espresso',
+ 'Espresso Machine' => 'espresso-machine',
+ 'Esprit' => 'esprit',
+ 'Estée Lauder' => 'estee-lauder',
+ 'Ethernet' => 'ethernet',
+ 'Etnies' => 'etnies',
+ 'Eurostar Ticket' => 'eurostar',
+ 'Eurotunnel' => 'eurotunnel',
+ 'Everton F. C.' => 'everton',
+ 'EVGA' => 'evga',
+ 'Evian' => 'evian',
+ 'Exercise Equipment' => 'exercise-equipment',
+ 'Exercise Weights' => 'weight',
+ 'Extension Lead' => 'extension-lead',
+ 'External Hard Drive' => 'external-hard-drive',
+ 'F1' => 'formula-one',
+ 'F1 2017' => 'f1-2017',
+ 'F1 2018' => 'f1-2018',
+ 'F1 2019' => 'f1-2019',
+ 'F1 2020' => 'f1-2020',
+ 'Fabric Conditioner' => 'fabric-conditioner',
+ 'Face Cream' => 'face-cream',
+ 'Face Mask' => 'face-mask',
+ 'Fairy' => 'fairy',
+ 'Fairy Light' => 'fairy-light',
+ 'Fallout' => 'fallout',
+ 'Fallout 4' => 'fallout-4',
+ 'Fallout 76' => 'fallout-76',
+ 'Family &amp; Kids' => 'kids',
+ 'Family Break' => 'family-break',
+ 'Family Guy' => 'family-guy',
+ 'Famous Grouse' => 'famous-grouse',
+ 'Fancy Dress' => 'fancy-dress',
+ 'Fans' => 'fan',
+ 'Fanta' => 'fanta',
+ 'Far Cry' => 'far-cry',
+ 'Far Cry 4' => 'far-cry-4',
+ 'Far Cry 5' => 'far-cry-5',
+ 'Far Cry New Dawn' => 'far-cry-new-dawn',
+ 'Far Cry Primal' => 'far-cry-primal',
+ 'Farming Simulator' => 'farming-simulator',
+ 'Fashion &amp; Accessories' => 'fashion',
+ 'Fashion Accessories' => 'fashion-accessories',
+ 'Fashion for Men' => 'mens-clothing',
+ 'Fashion for Women' => 'womens-clothes',
+ 'Fast and Furious' => 'fast-and-furious',
+ 'Father&#039;s Day' => 'fathers-day',
+ 'FatMax' => 'fatmax',
+ 'FC Barcelona' => 'fc-barcelona',
+ 'Felix' => 'felix',
+ 'Fence' => 'fence',
+ 'Fender Guitar' => 'fender',
+ 'Ferrero Rocher' => 'ferrero-rocher',
+ 'Ferry' => 'ferry',
+ 'Festival' => 'festival',
+ 'Fever Thermometer' => 'thermometer',
+ 'Fiat' => 'fiat',
+ 'Fidget Spinner' => 'spinner',
+ 'FIFA' => 'fifa',
+ 'FIFA 17' => 'fifa-17',
+ 'FIFA 18' => 'fifa-18',
+ 'FIFA 19' => 'fifa-19',
+ 'FIFA 20' => 'fifa-20',
+ 'FIFA 21' => 'fifa-21',
+ 'FightStick' => 'fightstick',
+ 'Figures' => 'figures',
+ 'Fila Trainers' => 'fila-trainers',
+ 'Filing Cabinet' => 'filing-cabinet',
+ 'Final Fantasy' => 'final-fantasy',
+ 'Final Fantasy 15' => 'final-fantasy-15',
+ 'Finance &amp; Insurance' => 'personal-finance',
+ 'Finish' => 'finish',
+ 'Finlux' => 'finlux',
+ 'Fiorelli' => 'fiorelli',
+ 'Fire Emblem' => 'fire-emblem',
+ 'Fire Pit' => 'fire-pit',
+ 'Fireplace' => 'fireplace',
+ 'Firewall: Zero Hour' => 'firewall-zero-hour',
+ 'First Aid' => 'first-aid',
+ 'Fish &amp; Seafood' => 'fish-and-seafood',
+ 'Fish and Aquatic Pet Supplies' => 'fish',
+ 'Fisher Price' => 'fisher-price',
+ 'Fisher Price Imaginext' => 'imaginext',
+ 'Fisher Price Jumperoo' => 'jumperoo',
+ 'Fisher Price Little People' => 'little-people',
+ 'Fishing' => 'fishing',
+ 'Fiskars' => 'fiskars',
+ 'Fitbit' => 'fitbit',
+ 'Fitbit Alta' => 'fitbit-alta',
+ 'Fitbit Blaze' => 'fitbit-blaze',
+ 'Fitbit Charge 2' => 'fitbit-charge-2',
+ 'Fitbit Inspire' => 'fitbit-inspire',
+ 'Fitbit Versa' => 'fitbit-versa',
+ 'Fitness &amp; Running' => 'fitness',
+ 'Fitness App' => 'fitness-app',
+ 'Fitness Tracker' => 'fitness-tracker',
+ 'Flamingo Land' => 'flamingo-land',
+ 'Flea Treatment' => 'flea',
+ 'Fleece Clothing' => 'fleece',
+ 'Flights' => 'flight',
+ 'Flip Flops' => 'flip-flops',
+ 'Floodlight' => 'floodlight',
+ 'Flooring' => 'flooring',
+ 'Flowers' => 'flowers',
+ 'Flymo' => 'flymo',
+ 'FM Transmitter' => 'fm-transmitter',
+ 'Food' => 'food',
+ 'Food Containers' => 'food-containers',
+ 'Food Processor' => 'food-processor',
+ 'Food Server' => 'food-server',
+ 'Football' => 'football',
+ 'Football Boots' => 'football-boots',
+ 'Football Manager' => 'football-manager',
+ 'Football Matches' => 'football-matches',
+ 'Football Shirt' => 'football-shirt',
+ 'Foot Pump' => 'foot-pump',
+ 'Ford' => 'ford',
+ 'For Honor' => 'for-honor',
+ 'Fortnite' => 'fortnite',
+ 'Fortnite: Darkfire' => 'fortnite-darkfire',
+ 'Forza' => 'forza',
+ 'Forza 7' => 'forza-7',
+ 'Forza Horizon' => 'forza-horizon',
+ 'Forza Horizon 3' => 'forza-horizon-3',
+ 'Forza Horizon 4' => 'forza-horizon-4',
+ 'Forza Motorsport' => 'forza-motorsport',
+ 'Foscam' => 'foscam',
+ 'Fossil' => 'fossil',
+ 'Foster&#039;s' => 'fosters',
+ 'Foundation' => 'foundation',
+ 'Fountain Pen' => 'fountain-pen',
+ 'Fred Perry' => 'fred-perry',
+ 'Freesat' => 'freesat',
+ 'Freeview' => 'freeview',
+ 'Freezer' => 'freezer',
+ 'Fridge' => 'fridge',
+ 'Fridge Freezer' => 'fridge-freezer',
+ 'Frontline' => 'frontline',
+ 'Frozen Food' => 'frozen',
+ 'Fruit' => 'fruit',
+ 'Fruit and Vegetables' => 'fruit-and-vegetable',
+ 'Fruit of the Loom' => 'fruit-of-the-loom',
+ 'Fryer' => 'fryer',
+ 'Frying Pan' => 'frying-pan',
+ 'Fujifilm' => 'fuji',
+ 'Fujitsu' => 'fujitsu',
+ 'Funko Pop' => 'funko-pop',
+ 'Furby' => 'furby',
+ 'Furniture' => 'furniture',
+ 'G-Star' => 'g-star',
+ 'G-Sync Monitor' => 'g-sync',
+ 'Gaggia' => 'gaggia',
+ 'Gambling' => 'gambling',
+ 'Game App' => 'game-app',
+ 'Game of Thrones' => 'game-of-thrones',
+ 'Games &amp; Board Games' => 'board-games',
+ 'Games Consoles' => 'console',
+ 'Gaming' => 'gaming',
+ 'Gaming Accessories' => 'gaming-accessories',
+ 'Gaming Chair' => 'gaming-chair',
+ 'Gaming Headset' => 'gaming-headset',
+ 'Gaming Keyboard' => 'gaming-keyboard',
+ 'Gaming Laptop' => 'gaming-laptop',
+ 'Gaming Monitor' => 'gaming-monitor',
+ 'Gaming Mouse' => 'gaming-mouse',
+ 'Gaming PC' => 'gaming-pc',
+ 'Gant' => 'gant',
+ 'Garage' => 'garage',
+ 'Garage &amp; Service' => 'garage-service',
+ 'Garden' => 'garden',
+ 'Garden &amp; Do It Yourself' => 'garden-diy',
+ 'Garden Furniture' => 'garden-furniture',
+ 'Gardening' => 'gardening',
+ 'Garden Storage' => 'garden-storage',
+ 'Garden Table' => 'table',
+ 'Garmin' => 'garmin',
+ 'Garmin Fenix' => 'garmin-fenix',
+ 'Garmin Fenix 6' => 'garmin-fenix-6',
+ 'Garmin Fenix 6 Pro' => 'garmin-fenix-6-pro',
+ 'Garmin Forerunner' => 'garmin-forerunner',
+ 'Garmin Vivoactive' => 'garmin-vivoactive',
+ 'Garmin Watch' => 'garmin-watch',
+ 'Garnier' => 'garnier',
+ 'Gas' => 'gas',
+ 'Gas Canister' => 'butane',
+ 'Gas Cooker' => 'gas-cooker',
+ 'Gatwick' => 'gatwick',
+ 'Gazebo' => 'gazebo',
+ 'GBK' => 'gbk',
+ 'Gears 5' => 'gears-5',
+ 'Gears of War' => 'gears-of-war',
+ 'Gears of War 4' => 'gears-of-war-4',
+ 'George Foreman' => 'george-foreman',
+ 'Geox' => 'geox',
+ 'GHD' => 'ghd',
+ 'Ghostbusters' => 'ghostbusters',
+ 'Ghostbusters: The Video Game Remastered' => 'ghostbusters-the-video-game',
+ 'Ghost of Tsushima' => 'ghost-of-tsushima',
+ 'Gibson Guitar' => 'gibson',
+ 'giffgaff' => 'giffgaff',
+ 'Gift Card' => 'gift-card',
+ 'Gift Hamper' => 'hamper',
+ 'Gifts' => 'gifts',
+ 'Gift Set' => 'gift-set',
+ 'GIGABYTE' => 'gigabyte',
+ 'Gigaset' => 'gigaset',
+ 'Gilet' => 'gilet',
+ 'Gillette Fusion' => 'fusion',
+ 'Gillette Mach3' => 'mach-3',
+ 'Gillette Razor' => 'gillette',
+ 'Gimbal' => 'gimbal',
+ 'Gin' => 'gin',
+ 'Girl&#039;s Clothes' => 'girls-clothes',
+ 'Glasses' => 'glasses',
+ 'Glassware' => 'glassware',
+ 'Glenfiddich' => 'glenfiddich',
+ 'Glenlivet' => 'glenlivet',
+ 'Glenmorangie' => 'glenmorangie',
+ 'Gloves' => 'gloves',
+ 'Glue' => 'glue',
+ 'Glue Gun' => 'glue-gun',
+ 'Gluten-Free' => 'gluten-free',
+ 'God of War' => 'god-of-war',
+ 'Go Kart' => 'go-kart',
+ 'Golf' => 'golf',
+ 'Golf Balls' => 'golf-balls',
+ 'Golf Clubs' => 'golf-clubs',
+ 'Goodfellas' => 'goodfellas',
+ 'Goodmans' => 'goodmans',
+ 'Goodyear' => 'goodyear',
+ 'Google' => 'google',
+ 'Google Home' => 'google-home',
+ 'Google Home Max' => 'google-home-max',
+ 'Google Home Mini' => 'google-home-mini',
+ 'Google Nest' => 'nest',
+ 'Google Nest Audio' => 'google-nest-audio',
+ 'Google Nest Hub' => 'google-home-hub',
+ 'Google Nest Mini' => 'nest-mini',
+ 'Google Nest Protect' => 'google-nest-protect',
+ 'Google Nexus' => 'nexus',
+ 'Google Pixel' => 'google-pixel',
+ 'Google Pixel 2' => 'google-pixel-2',
+ 'Google Pixel 2 XL' => 'google-pixel-2-xl',
+ 'Google Pixel 3' => 'google-pixel-3',
+ 'Google Pixel 3 XL' => 'google-pixel-3-xl',
+ 'Google Pixel 3a' => 'google-pixel-3a',
+ 'Google Pixel 3a XL' => 'google-pixel-3a-xl',
+ 'Google Pixel 4' => 'google-pixel-4',
+ 'Google Pixel 4 XL' => 'google-pixel-4-xl',
+ 'Google Pixel 4a' => 'google-pixel-4a',
+ 'Google Pixel 4a 5G' => 'google-pixel-4a-5g',
+ 'Google Pixel 5' => 'google-pixel-5',
+ 'Google Pixelbook' => 'google-pixelbook',
+ 'Google Pixel XL' => 'google-pixel-xl',
+ 'Google Smartphone' => 'google-smartphone',
+ 'Google Stadia' => 'google-stadia',
+ 'GoPro' => 'gopro',
+ 'GoPro HERO 6' => 'gopro-hero-6',
+ 'GoPro HERO 7' => 'gopro-hero-7',
+ 'GoPro HERO 8' => 'gopro-hero-8',
+ 'GoPro HERO 9' => 'gopro-hero-9',
+ 'Gore-Tex Clothing and Shoes' => 'gore-tex',
+ 'Graco' => 'graco',
+ 'Grand National' => 'grand-national',
+ 'Gran Turismo' => 'gran-turismo',
+ 'Gran Turismo Sport' => 'gran-turismo-sport',
+ 'Graphics Card' => 'graphics-card',
+ 'Gravity Rush' => 'gravity-rush',
+ 'Graze' => 'graze',
+ 'GreedFall' => 'greedfall',
+ 'Greenhouse' => 'greenhouse',
+ 'Greeting Cards and Wrapping Paper' => 'wrapping-paper-and-cards',
+ 'Greggs' => 'greggs',
+ 'Grey Goose' => 'grey-goose',
+ 'Griffin Technology' => 'griffin',
+ 'GroBag' => 'grobag',
+ 'Groceries' => 'groceries',
+ 'Gruffalo' => 'gruffalo',
+ 'Grundig' => 'grundig',
+ 'GTA' => 'gta',
+ 'GTA V' => 'gta-v',
+ 'GTX 970' => 'gtx-970',
+ 'GTX 980' => 'gtx-980',
+ 'GTX 1060' => 'gtx-1060',
+ 'GTX 1070' => 'gtx-1070',
+ 'GTX 1080' => 'gtx-1080',
+ 'GTX 1080 Ti' => 'gtx-1080-ti',
+ 'GTX 1660' => 'gtx-1660',
+ 'GTX 1660 Ti' => 'gtx-1660-ti',
+ 'Guardians of the Galaxy' => 'guardians-of-the-galaxy',
+ 'Gucci' => 'gucci',
+ 'Guinness' => 'guinness',
+ 'Guitar' => 'guitar',
+ 'Guitar Amp' => 'guitar-amp',
+ 'Guitar Hero' => 'guitar-hero',
+ 'Gulliver&#039;s' => 'gullivers',
+ 'Gym' => 'gym',
+ 'Gym Membership' => 'gym-membership',
+ 'H1Z1' => 'h1z1',
+ 'Häagen Dazs' => 'haagen-dazs',
+ 'Habitat' => 'habitat',
+ 'Hacksaw' => 'hacksaw',
+ 'Hair Brush' => 'hair-brush',
+ 'Hair Care' => 'hair',
+ 'Hair Clipper' => 'hair-clipper',
+ 'Hair Colour' => 'hair-colour',
+ 'Haircut' => 'haircut',
+ 'Hair Dryer' => 'hair-dryer',
+ 'Hair Dye' => 'hair-dye',
+ 'Hair Removal Devices' => 'hair-removal-devices',
+ 'Halifax' => 'halifax',
+ 'Hall' => 'hall',
+ 'Halloween' => 'halloween',
+ 'Halo' => 'halo',
+ 'Halo 5' => 'halo-5',
+ 'Ham' => 'ham',
+ 'Hammer' => 'hammer',
+ 'Hammer Drill' => 'hammer-drill',
+ 'Hammock' => 'hammock',
+ 'Handbag' => 'handbag',
+ 'Hand Blender' => 'hand-blender',
+ 'Hand Cream' => 'hand-cream',
+ 'Hand Mixer' => 'hand-mixer',
+ 'Hand Tools' => 'hand-tools',
+ 'Handwash' => 'handwash',
+ 'Hard Drive' => 'hard-drive',
+ 'Haribo' => 'haribo',
+ 'Harman Kardon' => 'harman-kardon',
+ 'Harry Potter' => 'harry-potter',
+ 'Hasbro' => 'hasbro',
+ 'Hat' => 'hat',
+ 'Hatchimals' => 'hatchimals',
+ 'Hats &amp; Caps' => 'hats-caps',
+ 'Hauck' => 'hauck',
+ 'Hayfever Remedies' => 'hayfever',
+ 'Headboard' => 'headboard',
+ 'Headphones' => 'headphones',
+ 'Headset' => 'headset',
+ 'Health &amp; Beauty' => 'beauty',
+ 'Healthcare' => 'health-care',
+ 'Heart Rate Monitor' => 'heart-rate-monitor',
+ 'Heater' => 'heater',
+ 'Heating' => 'heating',
+ 'Heating Appliances' => 'heating-appliances',
+ 'Hedge Trimmer' => 'hedge-trimmer',
+ 'Heineken' => 'heineken',
+ 'Heinz' => 'heinz',
+ 'Heinz Beanz' => 'heinz-baked-beans',
+ 'Hello Kitty' => 'hello-kitty',
+ 'Hello Neighbour' => 'hello-neighbour',
+ 'Helly Hansen' => 'helly-hansen',
+ 'Henry Hoover' => 'henry-hoover',
+ 'Hermes' => 'hermes',
+ 'High5' => 'high-5',
+ 'Highchair' => 'highchair',
+ 'Hiking' => 'hiking',
+ 'Hilton' => 'hilton',
+ 'Hisense' => 'hisense',
+ 'Hisense TVs' => 'hisense-tv',
+ 'Hitachi' => 'hitachi',
+ 'Hitman' => 'hitman',
+ 'Hive' => 'hive',
+ 'Hive Active Heating' => 'hive-active-heating',
+ 'Hob' => 'hob',
+ 'Hobbit' => 'hobbit',
+ 'Hockey' => 'hockey',
+ 'Holiday Inn' => 'holiday-inn',
+ 'Holiday Park' => 'holiday-parks',
+ 'Holidays and Trips' => 'holidays-and-trips',
+ 'Hollow Knight' => 'hollow-knight',
+ 'Home &amp; Living' => 'home',
+ 'Home Accessories' => 'home-accessories',
+ 'Home Appliances' => 'home-appliances',
+ 'Home Care' => 'home-care',
+ 'Home Cinema' => 'home-cinema',
+ 'HoMedics' => 'homedics',
+ 'Homefront' => 'homefront',
+ 'Home Networking' => 'network',
+ 'Homeplug' => 'homeplug',
+ 'Home Security' => 'home-security',
+ 'Homeware' => 'homeware',
+ 'Honda' => 'honda',
+ 'Honey' => 'honey',
+ 'Honeywell' => 'honeywell',
+ 'Honor 6X' => 'honor-6x',
+ 'Honor 7' => 'honor-7',
+ 'Honor 8S' => 'honor-8s',
+ 'Honor 8X' => 'honor-8x',
+ 'Honor 8X Max' => 'honor-8x-max',
+ 'Honor 9' => 'honor-9',
+ 'Honor 9X' => 'honor-9x',
+ 'Honor 10' => 'honor-10',
+ 'Honor Band 5' => 'honor-band-5',
+ 'Honor Play' => 'honor-play',
+ 'Honor Smartphone' => 'honor',
+ 'Honor View 20' => 'honor-view-20',
+ 'Hoodie' => 'hoodie',
+ 'Hoover' => 'hoover',
+ 'Hori' => 'hori',
+ 'Horizon: Zero Dawn' => 'horizon-zero-dawn',
+ 'Hornby' => 'hornby',
+ 'Horse Races' => 'horse-races',
+ 'Hose' => 'hose',
+ 'HOTAS' => 'hotas',
+ 'Hotel' => 'hotel',
+ 'Hotpoint' => 'hotpoint',
+ 'Hotspot' => 'hotspot',
+ 'Hot Tub' => 'hot-tub',
+ 'Hot Water Bottle' => 'hot-water-bottle',
+ 'Hot Wheels' => 'hot-wheels',
+ 'Hozelock' => 'hozelock',
+ 'HP' => 'hp',
+ 'HP Envy' => 'hp-envy',
+ 'HP Laptop' => 'hp-laptop',
+ 'HP Omen' => 'hp-omen',
+ 'HP Printer' => 'hp-printer',
+ 'HTC' => 'htc',
+ 'HTC 10' => 'htc-10',
+ 'HTC Desire' => 'htc-desire',
+ 'HTC One' => 'htc-one',
+ 'HTC Smartphone' => 'htc-smartphone',
+ 'HTC U11' => 'htc-u11',
+ 'HTC Vive' => 'htc-vive',
+ 'Huawei' => 'huawei',
+ 'Huawei Freebuds 3' => 'huawei-freebuds-3',
+ 'Huawei Headphones' => 'huawei-headphones',
+ 'Huawei Mate 20' => 'huawei-mate-20',
+ 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro',
+ 'Huawei Mate 30' => 'huawei-mate-30',
+ 'Huawei Mate 30 Lite' => 'huawei-mate-30-lite',
+ 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro',
+ 'Huawei Matebook' => 'huawei-matebook',
+ 'Huawei MediaPad M3' => 'huawei-mediapad-m3',
+ 'Huawei MediaPad M5' => 'huawei-mediapad-m5',
+ 'Huawei MediaPad T3' => 'huawei-mediapad-t3',
+ 'Huawei MediaPad T5' => 'huawei-mediapad-t5',
+ 'Huawei P9' => 'huawei-p9',
+ 'Huawei P10' => 'huawei-p10',
+ 'Huawei P20' => 'huawei-p20',
+ 'Huawei P20 Lite' => 'huawei-p20-lite',
+ 'Huawei P20 Pro' => 'huawei-p20-pro',
+ 'Huawei P30' => 'huawei-p30',
+ 'Huawei P30 Lite' => 'huawei-p30-lite',
+ 'Huawei P30 Pro' => 'huawei-p30-pro',
+ 'Huawei P40' => 'huawei-p40',
+ 'Huawei P40 Lite' => 'huawei-p40-lite',
+ 'Huawei P40 Pro' => 'huawei-p40-pro',
+ 'Huawei P Smart' => 'huawei-p-smart',
+ 'Huawei Smartphone' => 'huawei-smartphone',
+ 'Huawei Smartwatch' => 'huawei-smartwatch',
+ 'Huawei Tablet' => 'huawei-tablet',
+ 'Huawei Watch 2' => 'huawei-watch-2',
+ 'Huawei Watch GT' => 'huawei-watch-gt',
+ 'Huawei Watch GT2' => 'huawei-watch-gt2',
+ 'Huawei Watch GT 2 Pro' => 'huawei-watch-gt-2-pro',
+ 'Huawei Y7' => 'huawei-y7',
+ 'Huggies' => 'huggies',
+ 'Hulk' => 'hulk',
+ 'Humax' => 'humax',
+ 'Humidifier' => 'humidifier',
+ 'Hunter' => 'hunter',
+ 'HyperX' => 'hyperx',
+ 'Hyrule Warriors' => 'hyrule-warriors',
+ 'Hyundai' => 'hyundai',
+ 'IAMS' => 'iams',
+ 'iCandy' => 'icandy',
+ 'Ice-Watch' => 'ice-watch',
+ 'Ice Cream' => 'ice-cream',
+ 'Ice Cream Maker' => 'ice-cream-maker',
+ 'iMac' => 'apple-imac',
+ 'iMac 2021' => 'imac-2021',
+ 'Impact Driver' => 'impact-driver',
+ 'Indesit' => 'indesit',
+ 'Inflatable Boats' => 'boat',
+ 'Inflatable Toys' => 'inflatable',
+ 'Injustice' => 'injustice',
+ 'Injustice 2' => 'injustice-2',
+ 'Ink Cartridge' => 'ink',
+ 'Inkjet Printer' => 'inkjet-printer',
+ 'Innocent' => 'innocent',
+ 'Instant Cameras' => 'instant-cameras',
+ 'Instant Ink' => 'instant-ink',
+ 'Instax Mini 9' => 'instax-mini-9',
+ 'Insulation' => 'insulation',
+ 'Insurance' => 'insurance',
+ 'Intel' => 'intel',
+ 'Intel Atom' => 'atom',
+ 'Intel i3' => 'i3',
+ 'Intel i5' => 'i5',
+ 'Intel i7' => 'i7',
+ 'Intel i9' => 'intel-i9',
+ 'Internet' => 'internet',
+ 'Internet Security' => 'internet-security',
+ 'In the Night Garden' => 'in-the-night-garden',
+ 'Intimate Care' => 'intimate-care',
+ 'Introduce Yourself' => 'introduce-yourself',
+ 'iOS Apps' => 'ios-apps',
+ 'iPad' => 'ipad',
+ 'iPad 2019' => 'ipad-2019',
+ 'iPad 2020' => 'ipad-2020',
+ 'iPad Air' => 'ipad-air',
+ 'iPad Air 2019' => 'ipad-air-2019',
+ 'iPad Air 2020' => 'ipad-air-2020',
+ 'iPad Case' => 'ipad-case',
+ 'iPad mini' => 'ipad-mini',
+ 'iPad Pro' => 'ipad-pro',
+ 'iPad Pro 11' => 'ipad-pro-11',
+ 'iPad Pro 12.9' => 'ipad-pro-12-9',
+ 'iPad Pro 2020' => 'ipad-pro-2020',
+ 'iPad Pro 2021' => 'ipad-pro-2021',
+ 'IP Camera' => 'ip-camera',
+ 'iPhone' => 'iphone',
+ 'iPhone 5s' => 'iphone-5s',
+ 'iPhone 6' => 'iphone-6',
+ 'iPhone 6 Plus' => 'iphone-6-plus',
+ 'iPhone 6s' => 'iphone-6s',
+ 'iPhone 6s Plus' => 'iphone-6s-plus',
+ 'iPhone 7' => 'iphone-7',
+ 'iPhone 7 Plus' => 'iphone-7-plus',
+ 'iPhone 8' => 'iphone-8',
+ 'iPhone 8 Plus' => 'iphone-8-plus',
+ 'iPhone 11' => 'iphone-11',
+ 'iPhone 11 Pro' => 'iphone-11-pro',
+ 'iPhone 11 Pro Max' => 'iphone-11-pro-max',
+ 'iPhone 12' => 'iphone-12',
+ 'iPhone 12 mini' => 'iphone-12-mini',
+ 'iPhone 12 Pro' => 'iphone-12-pro',
+ 'iPhone 12 Pro Max' => 'iphone-12-pro-max',
+ 'iPhone Accessories' => 'iphone-accessories',
+ 'iPhone Case' => 'iphone-case',
+ 'iPhone SE' => 'iphone-se',
+ 'iPhone X' => 'iphone-x',
+ 'iPhone Xr' => 'iphone-xr',
+ 'iPhone Xs' => 'iphone-xs',
+ 'iPhone Xs Max' => 'iphone-xs-max',
+ 'iPod' => 'ipod',
+ 'iPod Nano' => 'ipod-nano',
+ 'iPod Shuffle' => 'ipod-shuffle',
+ 'iPod Touch' => 'ipod-touch',
+ 'Irish Whiskey' => 'irish-whisky',
+ 'Irn Bru' => 'irn-bru',
+ 'iRobot' => 'irobot',
+ 'Iron' => 'iron',
+ 'Ironing' => 'ironing',
+ 'Ironing Board' => 'ironing-board',
+ 'Iron Man' => 'iron-man',
+ 'Issey Miyake' => 'issey-miyake',
+ 'ITV' => 'itv',
+ 'Jabra' => 'jabra',
+ 'Jabra Elite 85h' => 'jabra-elite-85h',
+ 'Jabra Elite Active 65t' => 'jabra-elite-active-65t',
+ 'Jabra Elite Active 75t' => 'jabra-elite-active-75t',
+ 'Jabra Headphones' => 'jabra-headphones',
+ 'Jack &amp; Jones' => 'jack-and-jones',
+ 'Jack Daniel&#039;s' => 'jack-daniels',
+ 'Jacket' => 'jacket',
+ 'Jack Wills' => 'jack-wills',
+ 'Jack Wolfskin' => 'jack-wolfskin',
+ 'Jaffa Cakes' => 'jaffa-cakes',
+ 'Jägermeister' => 'jagermeister',
+ 'Jameson' => 'jameson',
+ 'Jamie Oliver' => 'jamie-oliver',
+ 'Jaybird' => 'jaybird',
+ 'JBL' => 'jbl',
+ 'JBL Flip' => 'jbl-flip',
+ 'JBL GO' => 'jbl-go',
+ 'JBL Headphones' => 'jbl-headphones',
+ 'JBL Link' => 'jbl-link',
+ 'JBL Live' => 'jbl-live',
+ 'JBL Tune' => 'jbl-tune',
+ 'JCB' => 'jcb',
+ 'Jean Paul Gaultier' => 'jean-paul-gautier',
+ 'Jean Paul Gaultier Le Male' => 'le-male',
+ 'Jeans' => 'jeans',
+ 'Jelly Belly' => 'jelly-belly',
+ 'Jewellery' => 'jewellery',
+ 'Jigsaw' => 'jigsaw',
+ 'Jim Beam' => 'jim-beam',
+ 'Jimmy Choo' => 'jimmy-choo',
+ 'JML' => 'jml',
+ 'Jogging Bottoms' => 'jogging-bottoms',
+ 'Johnnie Walker' => 'johnnie-walker',
+ 'Johnson&#039;s' => 'johnsons',
+ 'John West' => 'john-west',
+ 'John Wick' => 'john-wick',
+ 'JoJo Siwa' => 'jojo',
+ 'Joop' => 'joop',
+ 'Joseph Joseph' => 'joseph-joseph',
+ 'Joules' => 'joules',
+ 'Juice' => 'juice',
+ 'Juicer' => 'juicer',
+ 'Jumper' => 'jumper',
+ 'Jurassic World' => 'jurassic-world',
+ 'Jura Whisky' => 'jura',
+ 'Just Cause' => 'just-cause',
+ 'Just Cause 3' => 'just-cause-3',
+ 'Just Cause 4' => 'just-cause-4',
+ 'Just Dance' => 'just-dance',
+ 'JVC' => 'jvc',
+ 'K-Swiss' => 'k-swiss',
+ 'Karcher' => 'karcher',
+ 'Karcher Window Vacuum' => 'karcher-window-cleaner',
+ 'Karen Millen' => 'karen-millen',
+ 'Karrimor' => 'karrimor',
+ 'Kaspersky' => 'kaspersky',
+ 'Kayak' => 'kayak',
+ 'Keg' => 'keg',
+ 'Kellogg&#039;s' => 'kelloggs',
+ 'Kellogg&#039;s Cornflakes' => 'cornflakes',
+ 'Kellogg&#039;s Crunchy Nut' => 'crunchy-nut',
+ 'Kenco' => 'kenco',
+ 'Kenwood' => 'kenwood',
+ 'Kenwood kMix' => 'kmix',
+ 'Kenzo' => 'kenzo',
+ 'Ketchup' => 'ketchup',
+ 'Keter' => 'keter',
+ 'Kettle' => 'kettle',
+ 'Kettlebell' => 'kettlebell',
+ 'Keyboard' => 'keyboard',
+ 'KIA' => 'kia',
+ 'Kickers' => 'kickers',
+ 'Kid&#039;s Bike' => 'kids-bike',
+ 'Kid&#039;s Clothes' => 'kids-clothes',
+ 'Kid&#039;s Room' => 'kids-rooms',
+ 'Kid&#039;s Shoes' => 'kids-shoes',
+ 'Kidizoom' => 'kidizoom',
+ 'Killzone' => 'killzone',
+ 'Kilner' => 'kilner',
+ 'Kinder' => 'kinder',
+ 'Kindle' => 'kindle',
+ 'Kindle Book' => 'kindle-book',
+ 'Kindle Fire' => 'kindle-fire',
+ 'Kindle Oasis' => 'kindle-oasis',
+ 'Kindle Paperwhite' => 'kindle-paperwhite',
+ 'Kingdom Come: Deliverance' => 'kingdom-come-deliverance',
+ 'Kingdom Hearts' => 'kingdom-hearts',
+ 'Kingdom Hearts 3' => 'kingdom-hearts-3',
+ 'Kingdom Hearts: The Story So Far' => 'kingdom-hearts-the-story-so-far',
+ 'King Kong' => 'king-kong',
+ 'King Size Bed' => 'king-size',
+ 'Kingsmill' => 'kingsmill',
+ 'Kingston' => 'kingston',
+ 'Kitchen' => 'kitchen',
+ 'KitchenAid' => 'kitchenaid',
+ 'Kitchen Appliances' => 'kitchen-appliances',
+ 'Kitchen Knife' => 'knife',
+ 'Kitchen Roll' => 'kitchen-roll',
+ 'Kitchen Scale' => 'kitchen-scales',
+ 'Kitchen Tap' => 'kitchen-tap',
+ 'Kitchen Utensils' => 'kitchen-utensils',
+ 'Kite' => 'kite',
+ 'KitSound' => 'kitsound',
+ 'Knickers' => 'knickers',
+ 'Kobo' => 'kobo',
+ 'Kodak' => 'kodak',
+ 'Kodi' => 'kodi',
+ 'Kohinoor' => 'kohinoor',
+ 'Kopparberg' => 'kopparberg',
+ 'Kraken' => 'kraken',
+ 'Krispy Kreme' => 'krispy-kreme',
+ 'Krups' => 'krups',
+ 'KTC' => 'ktc',
+ 'Kurt Geiger' => 'kurt-geiger',
+ 'L&#039;Occitane' => 'loccitane',
+ 'L.O.L. Surprise!' => 'lol-surprise',
+ 'Lacoste' => 'lacoste',
+ 'Ladder' => 'ladder',
+ 'Lamaze' => 'lamaze',
+ 'Lamb' => 'lamb',
+ 'Laminate' => 'laminate',
+ 'Laminator' => 'laminator',
+ 'Lamp' => 'lamp',
+ 'Lancôme' => 'lancome',
+ 'Landmann' => 'landmann',
+ 'Lantern' => 'lantern',
+ 'Laphroaig' => 'laphroaig',
+ 'Laptop' => 'laptop',
+ 'Laptop Accessories' => 'laptop-accessories',
+ 'Laptop Case' => 'laptop-case',
+ 'Laptop Sleeve' => 'laptop-sleeve',
+ 'Laser Printer' => 'laser-printer',
+ 'Last Minute' => 'last-minute',
+ 'Laundry Basket' => 'laundry-basket',
+ 'Laura Ashley' => 'laura-ashley',
+ 'Lavazza' => 'lavazza',
+ 'Lavender' => 'lavender',
+ 'Lawnmower' => 'lawnmower',
+ 'Lay-Z-Spa' => 'lay-z-spa',
+ 'LeapFrog' => 'leapfrog',
+ 'Le Creuset' => 'le-creuset',
+ 'LED Bulb' => 'led-bulbs',
+ 'LED Light' => 'led-light',
+ 'LED Strip Lights' => 'led-strip-lights',
+ 'LED TV' => 'led-tv',
+ 'Lee Stafford' => 'lee-stafford',
+ 'Leffe' => 'leffe',
+ 'Leggings' => 'leggings',
+ 'Lego' => 'lego',
+ 'Lego Advent Calendar' => 'lego-advent-calendar',
+ 'Lego Architecture' => 'lego-architecture',
+ 'Lego Art' => 'lego-art',
+ 'Lego Batman' => 'lego-batman',
+ 'Lego BrickHeadz' => 'lego-brickheadz',
+ 'Lego City' => 'lego-city',
+ 'Lego Classic' => 'lego-classic',
+ 'Lego Creator' => 'lego-creator',
+ 'Lego Dimensions' => 'lego-dimensions',
+ 'Lego Disney' => 'lego-disney',
+ 'Lego Dots' => 'lego-dots',
+ 'Lego Duplo' => 'lego-duplo',
+ 'Lego Friends' => 'lego-friends',
+ 'LEGO Harry Potter' => 'lego-harry-potter',
+ 'Lego Hidden Side' => 'lego-hidden-side',
+ 'Legoland' => 'legoland',
+ 'Lego Marvel' => 'lego-marvel',
+ 'Lego Mindstorms' => 'lego-mindstorms',
+ 'Lego Nexo Knights' => 'lego-nexo-knights',
+ 'Lego Ninjago' => 'lego-ninjago',
+ 'Lego Porsche' => 'lego-porsche',
+ 'Lego Simpsons' => 'lego-simpsons',
+ 'Lego Speed Champions' => 'lego-speed-champions',
+ 'Lego Star Wars' => 'lego-star-wars',
+ 'Lego Star Wars Millennium Falcon' => 'lego-star-wars-millennium-falcon',
+ 'Lego Super Mario' => 'lego-mario',
+ 'Lego Technic' => 'lego-technic',
+ 'Lego VIDIYO' => 'lego-vidiyo',
+ 'Lemonade' => 'lemonade',
+ 'Lenor' => 'lenor',
+ 'Lenovo' => 'lenovo',
+ 'Lenovo IdeaPad' => 'lenovo-ideapad',
+ 'Lenovo Laptop' => 'lenovo-laptop',
+ 'Lenovo Tablet' => 'lenovo-tablet',
+ 'Lenovo Thinkpad' => 'thinkpad',
+ 'Lenovo Yoga Laptop' => 'lenovo-yoga-laptop',
+ 'Lenovo Yoga Tablet' => 'lenovo-yoga',
+ 'Les Paul' => 'les-paul',
+ 'Levi&#039;s' => 'levi',
+ 'Lexar' => 'lexar',
+ 'LG' => 'lg',
+ 'LG G3' => 'lg-g3',
+ 'LG G5' => 'lg-g5',
+ 'LG G6' => 'lg-g6',
+ 'LG G7' => 'lg-g7',
+ 'LG G8S ThinQ' => 'lg-g8s-thinq',
+ 'LG OLED TV' => 'lg-oled-tv',
+ 'LG Smartphone' => 'lg-smartphone',
+ 'LG TV' => 'lg-tv',
+ 'LG V30' => 'lg-v30',
+ 'LG V40 ThinQ' => 'lg-v40-thinq',
+ 'Life Insurance' => 'life-insurance',
+ 'Life is Strange' => 'life-is-strange',
+ 'Light Box' => 'light-box',
+ 'Lighting' => 'lighting',
+ 'Lightning Cable' => 'lightning-cable',
+ 'Lightsaber' => 'lightsaber',
+ 'Lindor' => 'lindor',
+ 'Lindt' => 'lindt',
+ 'Lingerie' => 'lingerie',
+ 'Linksys' => 'linksys',
+ 'Linx' => 'linx',
+ 'Lion King' => 'lion-king',
+ 'Lipstick' => 'lipstick',
+ 'Lipsy' => 'lipsy',
+ 'Little Tikes' => 'little-tikes',
+ 'Liverpool F. C.' => 'liverpool-fc',
+ 'Living Room' => 'living-room',
+ 'Local Traffic' => 'local-traffic',
+ 'Lodge' => 'lodge',
+ 'Loft' => 'loft',
+ 'Logitech' => 'logitech',
+ 'Logitech G430' => 'logitech-g430',
+ 'Logitech G703' => 'logitech-g703',
+ 'Logitech G903' => 'logitech-g903',
+ 'Logitech Harmony' => 'harmony',
+ 'Logitech Keyboard' => 'logitech-keyboard',
+ 'Logitech Mouse' => 'logitech-mouse',
+ 'Logitech MX Master' => 'logitech-mx-master',
+ 'Logitech MX Master 2S' => 'logitech-mx-master-2s',
+ 'London Eye' => 'london-eye',
+ 'London Zoo' => 'london-zoo',
+ 'Longleat' => 'longleat',
+ 'Long Sleeve' => 'long-sleeve',
+ 'Lord of the Rings' => 'lord-of-the-rings',
+ 'Lottery' => 'lottery',
+ 'Lounger' => 'lounger',
+ 'Lowepro' => 'lowepro',
+ 'Lucozade' => 'lucozade',
+ 'Luigi' => 'luigi',
+ 'Luigi&#039;s Mansion' => 'luigis-manison',
+ 'Luigi&#039;s Mansion 3' => 'luigis-mansion-3',
+ 'Lunch Bag' => 'lunch-bag',
+ 'Lunch Box' => 'lunch-box',
+ 'Lurpak' => 'lurpak',
+ 'Luton' => 'luton',
+ 'Lyle &amp; Scott' => 'lyle-and-scott',
+ 'Lynx' => 'lynx',
+ 'M.2 SSD' => 'm2-ssd',
+ 'MacBook' => 'macbook',
+ 'MacBook Air' => 'macbook-air',
+ 'MacBook Pro' => 'macbook-pro',
+ 'MacBook Pro 13' => 'macbook-pro-13',
+ 'MacBook Pro 15' => 'macbook-pro-15',
+ 'MacBook Pro 16' => 'macbook-pro-16',
+ 'Maclaren' => 'maclaren',
+ 'Mac mini' => 'mac-mini',
+ 'Madame Tussauds' => 'madame-tussauds',
+ 'Mad Catz' => 'madcatz',
+ 'Madden NFL' => 'madden',
+ 'Madden NFL 20' => 'madden-nfl-20',
+ 'Mad Max' => 'mad-max',
+ 'Mafia 3' => 'mafia-3',
+ 'Magazine' => 'magazine',
+ 'Magimix' => 'magimix',
+ 'Magners' => 'magners',
+ 'Magnum' => 'magnum',
+ 'Make Up' => 'make-up',
+ 'Makeup Advent Calendar' => 'makeup-advent-calendar',
+ 'Make Up Brush' => 'make-up-brush',
+ 'Makita' => 'makita',
+ 'Makita Drill' => 'makita-drill',
+ 'Malibu' => 'malibu',
+ 'Maltesers' => 'maltesers',
+ 'MAM' => 'mam',
+ 'Mamas &amp; Papas' => 'mamas-and-papas',
+ 'Manchester United' => 'manchester-united',
+ 'Manfrotto' => 'manfrotto',
+ 'Manga' => 'manga',
+ 'Manuka Honey' => 'manuka-honey',
+ 'Marantz' => 'marantz',
+ 'Marc Jacobs' => 'marc-jacobs',
+ 'Marc Jacobs Daisy' => 'daisy',
+ 'Mario &amp; Sonic at the Olympic Games: Tokyo 2020' => 'mario-and-sonic-tokyo-2020',
+ 'Mario + Rabbids Kingdom Battle' => 'mario-rabbids-kingdom-battle',
+ 'Mario Kart' => 'mario-kart',
+ 'Mario Kart 8' => 'mario-kart-8',
+ 'Mario Kart 8 Deluxe' => 'mario-kart-8-deluxe',
+ 'Marmite' => 'marmite',
+ 'Mars' => 'mars',
+ 'Marshall' => 'marshall',
+ 'Marshall Headphones' => 'marshall-headphones',
+ 'Marvel' => 'marvel',
+ 'Marvel&#039;s Spider-Man (PS4)' => 'spider-man-2018',
+ 'Marvel&#039;s Spider-Man: Miles Morales' => 'spiderman-miles-morales',
+ 'Mascara' => 'mascara',
+ 'Massage' => 'massage',
+ 'Mass Effect' => 'mass-effect',
+ 'Mass Effect: Andromeda' => 'mass-effect-andromeda',
+ 'Mastercard' => 'mastercard',
+ 'Masterplug' => 'masterplug',
+ 'Maternity &amp; Pregnancy' => 'maternity',
+ 'Mattress' => 'mattress',
+ 'Mattress Protector' => 'mattress-protector',
+ 'Mattress Topper' => 'mattress-topper',
+ 'Mavic' => 'mavic',
+ 'Max Factor' => 'max-factor',
+ 'Maxi Cosi' => 'maxi-cosi',
+ 'Maximuscle' => 'maximuscle',
+ 'Maxtor' => 'maxtor',
+ 'Maybelline' => 'maybelline',
+ 'Mayo' => 'mayo',
+ 'Mazda' => 'mazda',
+ 'McAfee' => 'mcafee',
+ 'Meat &amp; Sausages' => 'meat',
+ 'Meccano' => 'meccano',
+ 'Mechanical Keyboard' => 'mechanical-keyboard',
+ 'Medal of Honor' => 'medal-of-honor',
+ 'Medela' => 'medela',
+ 'Media Player' => 'media-player',
+ 'Medievil' => 'medievil',
+ 'Medion' => 'medion',
+ 'Mega Bloks' => 'mega-bloks',
+ 'Megathread' => 'megathread',
+ 'Melissa &amp; Doug' => 'melissa',
+ 'Memory Cards' => 'memory-cards',
+ 'Memory Foam Mattress' => 'memory-foam',
+ 'Men&#039;s Boots' => 'mens-boots',
+ 'Men&#039;s Fragrance' => 'mens-fragrance',
+ 'Men&#039;s Shoes' => 'mens-shoes',
+ 'Men&#039;s Suit' => 'suit',
+ 'Mercedes' => 'mercedes',
+ 'Meridian' => 'meridian',
+ 'Merlin' => 'merlin',
+ 'Merrell' => 'merrell',
+ 'Messenger Bag' => 'messenger-bag',
+ 'Metal Gear Solid' => 'metal-gear-solid',
+ 'Metro Exodus' => 'metro-exodus',
+ 'Metroid' => 'metroid',
+ 'Metro Series' => 'metro-series',
+ 'Michael Kors' => 'michael-kors',
+ 'Michelin' => 'michelin',
+ 'Microphone' => 'microphone',
+ 'Micro SD Card' => 'micro-sd',
+ 'Micro SDHC' => 'micro-sdhc',
+ 'Micro SDXC' => 'micro-sdxc',
+ 'Microserver' => 'microserver',
+ 'Microsoft' => 'microsoft',
+ 'Microsoft Flight Simulator' => 'microsoft-flight-simulator',
+ 'Microsoft Office' => 'microsoft-office',
+ 'Microsoft Points' => 'microsoft-points',
+ 'Microsoft Software' => 'microsoft-software',
+ 'Microsoft Surface Book' => 'surface-book',
+ 'Microsoft Surface Laptop' => 'surface',
+ 'Microsoft Surface Pro 6' => 'surface-pro-6',
+ 'Microsoft Surface Pro 7' => 'surface-pro-7',
+ 'Microsoft Surface Tablet' => 'microsoft-surface-tablet',
+ 'Microwave' => 'microwave',
+ 'Middle Earth' => 'middle-earth',
+ 'Middle Earth: Shadow of Mordor' => 'shadow-of-mordor',
+ 'Middle Earth: Shadow of War' => 'middle-earth-shadow-of-war',
+ 'Miele' => 'miele',
+ 'Miele Vacuum Cleaner' => 'miele-vacuum-cleaner',
+ 'Milk' => 'milk',
+ 'Milk Frother' => 'milk-frother',
+ 'Milk Tray' => 'milk-tray',
+ 'Milwaukee' => 'milwaukee',
+ 'Mince' => 'mince',
+ 'Minecraft Game' => 'minecraft',
+ 'Mineral Water' => 'mineral-water',
+ 'Mini Fridge' => 'mini-fridge',
+ 'Minions' => 'minions',
+ 'Mini PC' => 'mini-pc',
+ 'Minky' => 'minky',
+ 'Mira' => 'mira',
+ 'Mirror' => 'mirror',
+ 'Mirror&#039;s Edge' => 'mirrors-edge',
+ 'Misc' => 'misc',
+ 'Misfit' => 'misfit',
+ 'Mitre Saw' => 'mitre-saw',
+ 'Mitsubishi' => 'mitsubishi',
+ 'Mixer &amp; Blender' => 'mixer-and-blender',
+ 'Mobile Contracts' => 'mobile-contract',
+ 'Mobile Phone' => 'mobile-phone',
+ 'Model Building' => 'model-building',
+ 'Moët' => 'moet',
+ 'Molton Brown' => 'molton-brown',
+ 'Money Saving Tips and Tricks' => 'money-saving-tips',
+ 'Monitor' => 'monitor',
+ 'Monopoly' => 'monopoly',
+ 'Monsoon' => 'monsoon',
+ 'Monster Energy' => 'monster-energy',
+ 'Monster High' => 'monster-high',
+ 'Monster Hunter' => 'monster-hunter',
+ 'Monster Hunter World' => 'monster-hunter-world',
+ 'Mont Blanc' => 'mont-blanc',
+ 'Mop' => 'mop',
+ 'Morphy Richards' => 'morphy-richards',
+ 'Mortal Kombat' => 'mortal-kombat',
+ 'Mortal Kombat 11' => 'mortal-kombat-11',
+ 'Mortgage' => 'mortgage',
+ 'Moschino' => 'moschino',
+ 'Moses Basket' => 'moses-basket',
+ 'MOT' => 'mot',
+ 'Motherboard' => 'motherboard',
+ 'Moto 360' => 'moto-360',
+ 'Moto E' => 'moto-e',
+ 'Moto G' => 'moto-g',
+ 'Moto G4' => 'moto-g4',
+ 'Moto G5' => 'moto-g5',
+ 'Moto G6' => 'moto-g6',
+ 'Moto G7' => 'moto-g7',
+ 'Motorcycle' => 'motorcycle',
+ 'Motorcycle Accessories' => 'motorcycle-accessories',
+ 'Motorcycle Helmet' => 'motorcycle-helmet',
+ 'Motorola' => 'motorola',
+ 'Motorola Smartphone' => 'motorola-smartphone',
+ 'Moto X' => 'moto-x',
+ 'Moto Z' => 'moto-z',
+ 'Mountain Bike' => 'mountain-bike',
+ 'Mouse &amp; Keyboard Bundles' => 'mouse-and-keyboard-bundle',
+ 'Mouse Mat' => 'mouse-mat',
+ 'Mouthwash' => 'mouthwash',
+ 'Movie and TV Box Set' => 'box-set',
+ 'Movies &amp; Series' => 'movie',
+ 'MP3 Player' => 'mp3-player',
+ 'Mr Kipling' => 'mr-kipling',
+ 'Mr Men' => 'mr-men',
+ 'MSI' => 'msi',
+ 'MSI Laptop' => 'msi-laptop',
+ 'Muc-Off' => 'muc-off',
+ 'Mug' => 'mug',
+ 'Muller' => 'muller',
+ 'Multi-Room Audio System' => 'multi-room-audio-system',
+ 'Multitool' => 'multitool',
+ 'Museums' => 'museums',
+ 'Music' => 'music',
+ 'Musical Instruments' => 'musical-instrument',
+ 'Music App' => 'music-app',
+ 'Music Streaming' => 'music-streaming',
+ 'My Little Pony' => 'my-little-pony',
+ 'Nail Gun' => 'nail-gun',
+ 'Nail Polish' => 'nail-polish',
+ 'Nails' => 'nails',
+ 'Nails Inc.' => 'nails-inc',
+ 'Nakd' => 'nakd',
+ 'Nando&#039;s' => 'nandos',
+ 'Nappy' => 'nappy',
+ 'NAS' => 'nas',
+ 'National Express Ticket' => 'national-express',
+ 'National Trust' => 'national-trust',
+ 'Nature Observation' => 'nature-observation',
+ 'NatWest' => 'natwest',
+ 'NBA 2K' => 'nba-2k',
+ 'NBA Live' => 'nba',
+ 'Necklace' => 'necklace',
+ 'Need for Speed' => 'need-for-speed',
+ 'Need for Speed: Payback' => 'need-for-speed-payback',
+ 'Need for Speed Heat' => 'need-for-speed-heat',
+ 'Neff' => 'neff',
+ 'Nerf Guns' => 'nerf',
+ 'Nescafé Azera' => 'azera',
+ 'Nescafé Coffee' => 'nescafe',
+ 'Nespresso' => 'nespresso',
+ 'Nespresso Coffee Machine' => 'nespresso-coffee-machine',
+ 'Nest Hello' => 'nest-hello',
+ 'Nestlé' => 'nestle',
+ 'Nest Learning Thermostat' => 'nest-learning-thermostat',
+ 'Nestlé Cheerios' => 'cheerios',
+ 'Nestlé Shreddies' => 'shreddies',
+ 'Netatmo' => 'netatmo',
+ 'Netflix' => 'netflix',
+ 'Netgear' => 'netgear',
+ 'Netgear Arlo' => 'arlo',
+ 'New Balance' => 'new-balance',
+ 'New Balance Trainers' => 'new-balance-trainers',
+ 'New Look' => 'new-look',
+ 'Newspapers' => 'newspapers',
+ 'Nextbase' => 'nextbase',
+ 'NFL' => 'nfl',
+ 'NHL' => 'nhl',
+ 'NHL 20' => 'nhl-20',
+ 'NHS' => 'nhs',
+ 'NieR: Automata' => 'nier',
+ 'Night Light' => 'night-light',
+ 'Nike' => 'nike',
+ 'Nike Air Max' => 'nike-air-max',
+ 'Nike Air Max 200' => 'nike-air-max-200',
+ 'Nike Air Max 270' => 'nike-air-max-270',
+ 'Nike Air Max 720' => 'nike-air-max-720',
+ 'Nike Free' => 'nike-free',
+ 'Nike Huarache' => 'nike-huarache',
+ 'Nike Jordan' => 'jordan',
+ 'Nike Presto' => 'nike-presto',
+ 'Nike Roshe' => 'nike-roshe',
+ 'Nike Trainers' => 'nike-shoes',
+ 'Nikon' => 'nikon',
+ 'Nikon Camera' => 'nikon-camera',
+ 'Nikon Coolpix' => 'nikon-coolpix',
+ 'Nikon D3400' => 'nikon-d3400',
+ 'Nikon Lens' => 'nikon-lens',
+ 'Nilfisk' => 'nilfisk',
+ 'Ni No Kuni' => 'ni-no-kuni',
+ 'Ni No Kuni: Wrath of the White Witch' => 'ni-no-kuni-white-witch',
+ 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-2',
+ 'Nintendo' => 'nintendo',
+ 'Nintendo 2DS' => '2ds',
+ 'Nintendo 3DS' => '3ds',
+ 'Nintendo 3DS Game' => '3ds-games',
+ 'Nintendo 3DS XL' => 'nintendo-3ds-xl',
+ 'Nintendo Accessories' => 'nintendo-accessories',
+ 'Nintendo Classic Mini' => 'nintendo-classic-mini',
+ 'Nintendo DS Game' => 'ds-games',
+ 'Nintendo Labo' => 'switch-labo',
+ 'Nintendo Switch' => 'nintendo-switch',
+ 'Nintendo Switch Accessories' => 'switch-accessories',
+ 'Nintendo Switch Case' => 'switch-case',
+ 'Nintendo Switch Controller' => 'switch-controller',
+ 'Nintendo Switch Game' => 'switch-game',
+ 'Nintendo Switch Joy-Con' => 'switch-joy-con',
+ 'Nintendo Switch Lite' => 'nintendo-switch-lite',
+ 'Nintendo Switch Pro Controller' => 'switch-pro-controller',
+ 'Nioh' => 'nioh',
+ 'Nissan' => 'nissan',
+ 'Nivea' => 'nivea',
+ 'No7' => 'no7',
+ 'Noise Cancelling Headphones' => 'noise-cancelling-headphones',
+ 'Nokia' => 'nokia',
+ 'Nokia Smartphones' => 'nokia-mobile',
+ 'No Man&#039;s Sky' => 'no-man-s-sky',
+ 'Noodles' => 'noodles',
+ 'Norton' => 'norton',
+ 'Now' => 'now-tv',
+ 'Numatic' => 'numatic',
+ 'Nursery' => 'nursery',
+ 'Nutella' => 'nutella',
+ 'NutriBullet' => 'nutribullet',
+ 'Nutri Ninja' => 'nutri-ninja',
+ 'Nuts' => 'nuts',
+ 'Nvidia' => 'nvidia',
+ 'Nvidia GeForce' => 'geforce',
+ 'Nvidia Shield' => 'nvidia-shield',
+ 'NYX' => 'nyx',
+ 'NZXT' => 'nzxt',
+ 'O2' => 'o2',
+ 'O2 Refresh' => 'o2-refresh',
+ 'Oakley' => 'oakley',
+ 'Octonauts' => 'octonauts',
+ 'Oculus Game' => 'oculus-game',
+ 'Oculus Go' => 'oculus-go',
+ 'Oculus Quest' => 'oculus-quest',
+ 'Oculus Rift' => 'oculus',
+ 'Oculus Rift S' => 'oculus-rift-s',
+ 'Odeon' => 'odeon',
+ 'Office' => 'office',
+ 'Office Chair' => 'office-chair',
+ 'Official Announcements' => 'official-announcements',
+ 'Olay' => 'olay',
+ 'OLED TV' => 'oled',
+ 'Olive Oil' => 'olive-oil',
+ 'Olympus' => 'olympus',
+ 'Omega Seamaster' => 'omega-seamaster',
+ 'Omega Speedmaster' => 'omega-speedmaster',
+ 'Omega Watches' => 'omega-watch',
+ 'OnePlus 3' => 'oneplus-3',
+ 'OnePlus 5' => 'oneplus-5',
+ 'OnePlus 6' => 'oneplus-6',
+ 'OnePlus 6T' => 'oneplus-6t',
+ 'OnePlus 7' => 'oneplus-7',
+ 'OnePlus 7 Pro' => 'oneplus-7-pro',
+ 'OnePlus 7T' => 'oneplus-7t',
+ 'OnePlus 7T Pro' => 'one-plus-7t-pro',
+ 'OnePlus 8' => 'oneplus-8',
+ 'OnePlus 8 Pro' => 'oneplus-8-pro',
+ 'OnePlus 8T' => 'oneplus-8t',
+ 'OnePlus 9' => 'oneplus-9',
+ 'OnePlus 9 Pro' => 'oneplus-9-pro',
+ 'OnePlus Nord' => 'oneplus-nord',
+ 'OnePlus Nord N10 5G' => 'oneplus-n10',
+ 'OnePlus Nord N100' => 'oneplus-n100',
+ 'OnePlus Smartphone' => 'oneplus',
+ 'Onesie' => 'onesie',
+ 'Onkyo' => 'onkyo',
+ 'Online Courses' => 'online-courses',
+ 'Operating System' => 'operating-system',
+ 'Oppo Find X2 Lite' => 'oppo-find-x2-lite',
+ 'Oppo Find X2 Neo' => 'oppo-find-x2-neo',
+ 'Oppo Find X2 Pro' => 'oppo-find-x2-pro',
+ 'Oppo Reno' => 'oppo-reno',
+ 'Oppo Reno4 5G' => 'oppo-reno4',
+ 'Oppo Reno4 Z 5G' => 'oppo-reno4-z',
+ 'Oppo Smartphone' => 'oppo-smartphone',
+ 'Opticians' => 'opticians',
+ 'Optoma' => 'optoma',
+ 'Oral-B' => 'oral-b',
+ 'Oral-B Toothbrush' => 'oral-b-toothbrush',
+ 'Oreo' => 'oreo',
+ 'Origin' => 'origin',
+ 'Original Penguin' => 'penguin',
+ 'Orla Kiely' => 'orla-kiely',
+ 'Osprey' => 'osprey',
+ 'Osram' => 'osram',
+ 'Other' => 'other-deals',
+ 'Ottoman' => 'ottoman',
+ 'Oukitel' => 'oukitel',
+ 'Outdoor Clothing' => 'outdoor-clothing',
+ 'Outdoor Lighting' => 'outdoor-lighting',
+ 'Outdoor Sports &amp; Camping' => 'outdoor',
+ 'Outdoor Toys' => 'outdoor-toys',
+ 'Outlast' => 'outlast',
+ 'Outlet' => 'outlet',
+ 'Outwell' => 'outwell',
+ 'Oven' => 'oven',
+ 'Overcooked' => 'overcooked',
+ 'Overcooked 2' => 'overcooked-2',
+ 'Overwatch' => 'overwatch',
+ 'Oyster Card' => 'oyster',
+ 'Package Holidays' => 'holiday',
+ 'Paco Rabanne' => 'paco-rabanne',
+ 'Paco Rabanne 1 Million' => 'paco-rabanne-1-million',
+ 'Paco Rabanne Lady Million' => 'lady-million',
+ 'Paddling Pool' => 'paddling-pool',
+ 'Padlock' => 'padlock',
+ 'Paint' => 'paint',
+ 'Paint Brush' => 'paint-brush',
+ 'Pampers' => 'pampers',
+ 'Panasonic' => 'panasonic',
+ 'Panasonic Camera' => 'panasonic-camera',
+ 'Panasonic Lumix' => 'lumix',
+ 'Panasonic TV' => 'panasonic-tv',
+ 'Pandora' => 'pandora',
+ 'Panini' => 'panini',
+ 'Panini Stickers' => 'panini-stickers',
+ 'Papa Johns' => 'papa-johns',
+ 'Paper Mario' => 'paper-mario',
+ 'Parasol' => 'parasol',
+ 'Parcel and Delivery Services' => 'parcel',
+ 'Parka' => 'parka',
+ 'Parking' => 'parking',
+ 'Parrot' => 'parrot',
+ 'Paul Smith' => 'paul-smith',
+ 'PAW Patrol' => 'paw-patrol',
+ 'Payday' => 'payday',
+ 'Payday 2' => 'payday-2',
+ 'PAYG' => 'payg',
+ 'Pay Monthly' => 'pay-monthly',
+ 'PC' => 'pc',
+ 'PC Case' => 'pc-case',
+ 'PC Game' => 'pc-game',
+ 'PC Gaming Accessories' => 'pc-gaming-accessories',
+ 'PC Gaming Systems' => 'pc-gaming-systems',
+ 'PC Mouse' => 'mouse',
+ 'PC Parts' => 'pc-parts',
+ 'Peanut Butter' => 'peanut-butter',
+ 'Peanuts' => 'peanuts',
+ 'Pedometer' => 'pedometer',
+ 'Pentax' => 'pentax',
+ 'Peppa Pig' => 'peppa-pig',
+ 'PepperBonus' => 'pepperbonus',
+ 'Pepsi' => 'pepsi',
+ 'Perfume' => 'perfume',
+ 'Persil' => 'persil',
+ 'Persona' => 'persona',
+ 'Persona 5' => 'persona-5',
+ 'Personal Care &amp; Hygiene' => 'personal-care-hygiene',
+ 'Petrol and Diesel' => 'petrol',
+ 'Pet Supplies' => 'pets',
+ 'Peugeot' => 'peugeot',
+ 'PG Tips' => 'pg-tips',
+ 'Philips' => 'philips',
+ 'Philips Alarm Clock' => 'philips-alarm-clock',
+ 'Philips Avent' => 'avent',
+ 'Philips Hue' => 'philips-hue',
+ 'Philips Lumea' => 'lumea',
+ 'Philips OneBlade' => 'philips-one-blade',
+ 'Philips Senseo' => 'philips-senseo',
+ 'Philips Senseo Coffee Machine' => 'philips-senseo-coffee-machine',
+ 'Philips Shaver' => 'philips-shaver',
+ 'Philips Sonicare' => 'sonicare',
+ 'Philips TV' => 'philips-tv',
+ 'Phone Holder' => 'phone-holder',
+ 'Phones &amp; Accessories' => 'phone',
+ 'Photo &amp; Cameras' => 'photo-video',
+ 'Photo &amp; Video App' => 'photo-video-app',
+ 'Photo Editing' => 'photo-editing',
+ 'Photo Frame' => 'photo-frame',
+ 'Photo Paper' => 'photo-paper',
+ 'Piano' => 'piano',
+ 'Picnic &amp; Outdoor Cooking' => 'picnic',
+ 'Pikmin 3 Deluxe' => 'pikmin-3-deluxe',
+ 'Pillow' => 'pillow',
+ 'Pimm&#039;s' => 'pimms',
+ 'Pioneer' => 'pioneer',
+ 'Pirate Toys' => 'pirates',
+ 'PIR Lights' => 'pir',
+ 'Pixel C' => 'pixel-c',
+ 'Piz Buin' => 'piz-buin',
+ 'Pizza' => 'pizza',
+ 'Pizza Stone' => 'pizza-stone',
+ 'Planer' => 'planer',
+ 'Planet Earth' => 'planet-earth',
+ 'Plant' => 'plant',
+ 'Plant Pot' => 'plant-pots',
+ 'Plants vs. Zombies: Battle for Neighborville' => 'battle-for-neighborville',
+ 'Plants vs Zombies' => 'plants-vs-zombies',
+ 'Play-Doh' => 'play-doh',
+ 'PlayerUnknown&#039;s Battlegrounds' => 'playerunknown-s-battlegrounds',
+ 'Playhouse' => 'playhouse',
+ 'Playing Cards' => 'playing-cards',
+ 'Playmat' => 'playmat',
+ 'Playmobil' => 'playmobil',
+ 'Playmobil Advent Calendar' => 'playmobil-advent-calendar',
+ 'PlayStation' => 'playstation',
+ 'PlayStation 5 DualSense Controller' => 'ps5-controller',
+ 'PlayStation Accessories' => 'playstation-accessories',
+ 'PlayStation Classic' => 'playstation-classic',
+ 'PlayStation Move' => 'playstation-move',
+ 'PlayStation Now' => 'playstation-now',
+ 'PlayStation Plus' => 'playstation-plus',
+ 'PlayStation VR' => 'playstation-vr',
+ 'PlayStation VR Aim Controller' => 'aim-controller-ps4',
+ 'Pliers' => 'pliers',
+ 'Plumbing &amp; Fittings' => 'plumbing-and-fitting',
+ 'Plus Size' => 'plus-size',
+ 'PNY' => 'pny',
+ 'POCO F2 Pro' => 'poco-f2-pro',
+ 'POCO F3' => 'poco-f3',
+ 'Poco M3' => 'poco-m3',
+ 'POCO X3' => 'poco-x3',
+ 'POCO X3 Pro' => 'poco-x3-pro',
+ 'Pokémon' => 'pokemon',
+ 'Pokémon: Let&#039;s Go' => 'pokemon-lets-go',
+ 'Pokémon Go' => 'pokemon-go',
+ 'Pokemon Sword and Shield' => 'pokemon-sword-and-shield',
+ 'Pokémon Ultra Sun and Ultra Moon' => 'pokemon-ultra-sun-ultra-moon',
+ 'Poker' => 'poker',
+ 'Pokken Tournament' => 'pokken-tournament',
+ 'Polaroid' => 'polaroid',
+ 'Police Toys' => 'police',
+ 'Polo Shirt' => 'polo-shirt',
+ 'Pool' => 'pool',
+ 'Pool &amp; Snooker' => 'pool-table',
+ 'Popcorn' => 'popcorn',
+ 'Pork' => 'pork',
+ 'Porridge &amp; Oats' => 'porridge-and-oats',
+ 'Portable Wireless Speaker' => 'wireless-speaker',
+ 'Poster' => 'poster',
+ 'Pots and Pans' => 'pan',
+ 'Potty' => 'potty',
+ 'Power Bank' => 'power-bank',
+ 'Powerbeats Pro' => 'powerbeats-pro',
+ 'Power Dental Flosser' => 'floss',
+ 'Powerline' => 'powerline',
+ 'Power Rangers' => 'power-rangers',
+ 'Power Tool' => 'power-tool',
+ 'Prada' => 'prada',
+ 'Pram' => 'pram',
+ 'Pregnancy' => 'pregnancy',
+ 'Prescription Glasses' => 'prescription-glasses',
+ 'Pressure Cooker' => 'pressure-cooker',
+ 'Pressure Washer' => 'pressure-washer',
+ 'Price Glitch' => 'price-glitch',
+ 'Prime Gaming' => 'twitch',
+ 'Pringles' => 'pringles',
+ 'Printer &amp; Printer Supplies' => 'printer',
+ 'Printer Supplies' => 'printer-supplies',
+ 'Productivity App' => 'productivity-app',
+ 'Pro Evolution Soccer' => 'pro-evolution-soccer',
+ 'Pro Evolution Soccer 2018' => 'pro-evolution-soccer-2018',
+ 'Pro Evolution Soccer 2019' => 'pro-evolution-soccer-2019',
+ 'Pro Evolution Soccer 2020' => 'pes-2020',
+ 'Project Cars' => 'project-cars',
+ 'Project Cars 2' => 'project-cars-2',
+ 'Projector' => 'projector',
+ 'Protein' => 'protein',
+ 'Protein Bars' => 'protein-bars',
+ 'Protein Shaker' => 'shaker',
+ 'PS4' => 'ps4-slim',
+ 'PS4 Camera' => 'ps4-camera',
+ 'PS4 Controller' => 'ps4-controller',
+ 'PS4 Games' => 'ps4-games',
+ 'PS4 Headset' => 'ps4-headset',
+ 'PS4 Pro' => 'ps4-pro',
+ 'PS5' => 'ps5',
+ 'PS5 Games' => 'ps5-game',
+ 'PSU' => 'psu',
+ 'Public Transport' => 'public-transport',
+ 'Pukka' => 'pukka',
+ 'Pulse Light Epilator' => 'pulse-light-epilator',
+ 'Puma' => 'puma',
+ 'Puma Trainers' => 'puma-trainers',
+ 'Puppy Supplies' => 'puppy',
+ 'Purse' => 'purse',
+ 'Pushchair' => 'pushchair',
+ 'Pushchairs and Strollers' => 'baby-transport',
+ 'Puzzle' => 'puzzle',
+ 'PVR' => 'pvr',
+ 'Pyjamas' => 'pyjamas',
+ 'Pyrex' => 'pyrex',
+ 'Q Acoustics' => 'q-acoustics',
+ 'QNAP' => 'qnap',
+ 'Qualcast' => 'qualcast',
+ 'Quality Street' => 'quality-street',
+ 'Quantum Break' => 'quantum-break',
+ 'Quechua' => 'quechua',
+ 'Quick Charge' => 'quick-charge',
+ 'Quiksilver' => 'quiksilver',
+ 'Quinny' => 'quinny',
+ 'Quorn' => 'quorn',
+ 'Rab' => 'rab',
+ 'Radeon RX 480' => 'rx-480',
+ 'Radeon RX 5700' => 'radeon-rx-5700',
+ 'Radeon RX 5700 XT' => 'radeon-rx-5700-xt',
+ 'Radeon RX 6800' => 'radeon-rx-6800',
+ 'Radeon RX 6800 XT' => 'radeon-rx-6800-xt',
+ 'Radeon RX 6900 XT' => 'radeon-rx-6900-xt',
+ 'Radiator' => 'radiator',
+ 'Radio' => 'radio',
+ 'Radley' => 'radley',
+ 'Rage 2' => 'rage-2',
+ 'Railcard' => 'railcard',
+ 'Rainbow Six' => 'rainbow-six',
+ 'Rake' => 'rake',
+ 'Ralph Lauren' => 'ralph-lauren',
+ 'RAM' => 'ram',
+ 'Raspberry Pi' => 'raspberry-pi',
+ 'Ratchet' => 'ratchet',
+ 'Ratchet and Clank' => 'ratchet-and-clank',
+ 'Rattan Garden Furniture' => 'rattan',
+ 'RAVPower' => 'ravpower',
+ 'Ray Ban' => 'ray-ban',
+ 'Razer' => 'razer',
+ 'Razor' => 'razor',
+ 'Razor Blade' => 'razor-blade',
+ 'Real Madrid' => 'real-madrid',
+ 'Realme Smartphones' => 'realme-smartphone',
+ 'Real Techniques' => 'real-techniques',
+ 'Recliner' => 'recliner',
+ 'ReCore' => 'recore',
+ 'Recreational Sports' => 'recreational-sports',
+ 'Red Bull' => 'red-bull',
+ 'Red Dead Redemption' => 'red-dead-redemption',
+ 'Red Dead Redemption 2' => 'red-dead-redemption-2',
+ 'Redex' => 'redex',
+ 'Red Kite' => 'red-kite',
+ 'Reebok' => 'reebok',
+ 'Reese&#039;s' => 'reeses',
+ 'Regatta' => 'regatta',
+ 'Regina' => 'regina',
+ 'Remington' => 'remington',
+ 'Remote Control Car' => 'remote-control-car',
+ 'Renault' => 'renault',
+ 'Resident Evil' => 'resident-evil',
+ 'Resident Evil 2' => 'resident-evil-2',
+ 'Resident Evil 7' => 'resident-evil-7',
+ 'Restaurant, Café &amp; Pub' => 'restaurant',
+ 'Retailer Offers and Issues' => 'retailer-offers-and-issues',
+ 'Ribena' => 'ribena',
+ 'Rice' => 'rice',
+ 'Rice Cooker' => 'rice-cooker',
+ 'Rick and Morty' => 'rick-and-morty',
+ 'Ricoh' => 'ricoh',
+ 'Ride On' => 'ride-on',
+ 'Ring' => 'ring',
+ 'Ring Door View Cam' => 'ring-door-view-cam',
+ 'Ring Fit Adventures' => 'ring-fit-adventures',
+ 'Ring Stick Up Cam' => 'ring-stick-up-cam',
+ 'Ring Video Doorbell' => 'ring-video-doorbell',
+ 'Ring Video Doorbell 2' => 'ring-video-doorbell-2',
+ 'Ring Video Doorbell 3' => 'ring-video-doorbell-3',
+ 'Ring Video Doorbell Pro' => 'ring-video-doorbell-pro',
+ 'Road Bike' => 'road-bike',
+ 'Roaming' => 'roaming',
+ 'Robinsons' => 'robinsons',
+ 'Robotic Lawnmower' => 'robotic-lawnmower',
+ 'Robot Vacuum Cleaner' => 'robot-vacuum-cleaner',
+ 'Rock Band' => 'rock-band',
+ 'Rocket League' => 'rocket-league',
+ 'Rocking Horse' => 'rocking-horse',
+ 'Rogue One: A Star Wars Story' => 'rogue-one',
+ 'Roku' => 'roku',
+ 'Rolex' => 'rolex',
+ 'Rollerskates' => 'skate',
+ 'Ronseal' => 'ronseal',
+ 'Roof Box' => 'roof-box',
+ 'Roses' => 'roses',
+ 'Rotary' => 'rotary',
+ 'Router' => 'router',
+ 'Rowenta' => 'rowenta',
+ 'RTX 2060' => 'rtx-2060',
+ 'RTX 2070' => 'rtx-2070',
+ 'RTX 2080' => 'rtx-2080',
+ 'RTX 2080 Ti' => 'rtx-2080-ti',
+ 'RTX 3070' => 'rtx-3070',
+ 'RTX 3080' => 'rtx-3080',
+ 'RTX 3090' => 'rtx-3090',
+ 'Rug' => 'rug',
+ 'Rugby' => 'rugby',
+ 'Rum' => 'rum',
+ 'Running' => 'running',
+ 'Running Shoes' => 'running-shoes',
+ 'Russell Hobbs' => 'russell-hobbs',
+ 'RX 570' => 'rx-570',
+ 'RX 580' => 'rx-580',
+ 'RX 590' => 'rx-590',
+ 'RX Vega 56' => 'rx-vega-56',
+ 'RX Vega 64' => 'rx-vega-64',
+ 'Ryanair' => 'ryanair',
+ 'Ryobi' => 'ryobi',
+ 'Safari' => 'safari',
+ 'Safety Boots' => 'safety-boots',
+ 'Sage by Heston Blumenthal' => 'sage',
+ 'Saints Row' => 'saints-row',
+ 'Saitek' => 'saitek',
+ 'Sale' => 'sale',
+ 'Salmon' => 'salmon',
+ 'Salomon' => 'salomon',
+ 'Salter' => 'salter',
+ 'Samsonite' => 'samsonite',
+ 'Samsung' => 'samsung',
+ 'Samsung Ecobubble' => 'ecobubble',
+ 'Samsung Fridge' => 'samsung-fridge',
+ 'Samsung Galaxy' => 'samsung-galaxy',
+ 'Samsung Galaxy A10' => 'samsung-galaxy-a10',
+ 'Samsung Galaxy A20e' => 'samsung-galaxy-a20e',
+ 'Samsung Galaxy A40' => 'samsung-galaxy-a40',
+ 'Samsung Galaxy A42 5G' => 'samsung-galaxy-a42-5g',
+ 'Samsung Galaxy A50' => 'samsung-galaxy-a50',
+ 'Samsung Galaxy A51' => 'samsung-galaxy-a51',
+ 'Samsung Galaxy A52 5G' => 'samsung-galaxy-a52',
+ 'Samsung Galaxy A60' => 'samsung-galaxy-a60',
+ 'Samsung Galaxy A70' => 'samsung-galaxy-a70',
+ 'Samsung Galaxy A71' => 'samsung-galaxy-a71',
+ 'Samsung Galaxy A72' => 'samsung-galaxy-a72',
+ 'Samsung Galaxy A80' => 'samsung-galaxy-a80',
+ 'Samsung Galaxy A90' => 'samsung-galaxy-a90',
+ 'Samsung Galaxy Buds' => 'samsung-galaxy-buds',
+ 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus',
+ 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live',
+ 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro',
+ 'Samsung Galaxy Fold' => 'samsung-galaxy-fold',
+ 'Samsung Galaxy J5' => 'galaxy-j5',
+ 'Samsung Galaxy Note' => 'samsung-galaxy-note',
+ 'Samsung Galaxy Note 8' => 'samsung-galaxy-note-8',
+ 'Samsung Galaxy Note 9' => 'samsung-galaxy-note-9',
+ 'Samsung Galaxy Note 10' => 'samsung-galaxy-note-10',
+ 'Samsung Galaxy Note 10+' => 'samsung-galaxy-note-10-plus',
+ 'Samsung Galaxy Note20' => 'samsung-galaxy-note20',
+ 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note20-ultra',
+ 'Samsung Galaxy S6' => 'samsung-galaxy-s6',
+ 'Samsung Galaxy S7' => 'samsung-galaxy-s7',
+ 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge',
+ 'Samsung Galaxy S8' => 'samsung-galaxy-s8',
+ 'Samsung Galaxy S8+' => 'samsung-s8-plus',
+ 'Samsung Galaxy S9' => 'samsung-galaxy-s9',
+ 'Samsung Galaxy S9 Plus' => 'samsung-s9-plus',
+ 'Samsung Galaxy S10' => 'samsung-galaxy-s10',
+ 'Samsung Galaxy S10 Lite' => 'samsung-galaxy-s10-lite',
+ 'Samsung Galaxy S10 Plus' => 'samsung-galaxy-s10-plus',
+ 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e',
+ 'Samsung Galaxy S20' => 'samsung-galaxy-s20',
+ 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe',
+ 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra',
+ 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus',
+ 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g',
+ 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g',
+ 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g',
+ 'Samsung Galaxy Tab' => 'samsung-galaxy-tab',
+ 'Samsung Galaxy Tab A' => 'samsung-galaxy-tab-a',
+ 'Samsung Galaxy Tab A7' => 'samsung-galaxy-tab-a7',
+ 'Samsung Galaxy Tab S' => 'samsung-galaxy-tab-s',
+ 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4',
+ 'Samsung Galaxy Tab S5e' => 'samsung-galaxy-tab-s5e',
+ 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6',
+ 'Samsung Galaxy Watch' => 'samsung-galaxy-watch',
+ 'Samsung Galaxy Watch3' => 'samsung-galaxy-watch3',
+ 'Samsung Galaxy Watch Active2' => 'samsung-galaxy-watch-active-2',
+ 'Samsung Gear' => 'samsung-gear',
+ 'Samsung Gear S3' => 'gear-s3',
+ 'Samsung Gear VR' => 'samsung-gear-vr',
+ 'Samsung Headphones' => 'samsung-headphones',
+ 'Samsung Monitor' => 'samsung-monitor',
+ 'Samsung QLED TVs' => 'samsung-qled-tv',
+ 'Samsung Smartphone' => 'samsung-smartphone',
+ 'Samsung SSD' => 'samsung-ssd',
+ 'Samsung The Frame TV' => 'samsung-the-frame',
+ 'Samsung TV' => 'samsung-tv',
+ 'Samsung Washing Machine' => 'samsung-washing-machine',
+ 'Samsung Watch' => 'samsung-watch',
+ 'Sandals' => 'sandals',
+ 'Sander' => 'sander',
+ 'SanDisk' => 'sandisk',
+ 'SanDisk SSD' => 'sandisk-ssd',
+ 'Sand Pit' => 'sand-pit',
+ 'Sandwich Maker' => 'sandwich',
+ 'San Miguel' => 'san-miguel',
+ 'Santander' => 'santander',
+ 'Satchel' => 'satchel',
+ 'Sat Nav' => 'sat-nav',
+ 'Sauce' => 'sauce',
+ 'Saw' => 'saw',
+ 'Scalextric' => 'scalextric',
+ 'Scanner' => 'scanner',
+ 'School Bag' => 'school-bag',
+ 'School Supplies' => 'school',
+ 'School Uniform' => 'school-uniform',
+ 'Schwalbe' => 'schwalbe',
+ 'Scooby Doo' => 'scooby-doo',
+ 'Scooter' => 'scooter',
+ 'Scotch Whisky' => 'scotch',
+ 'Scrabble' => 'scrabble',
+ 'Screen Protector' => 'screen-protector',
+ 'Screenwash' => 'screenwash',
+ 'Screwdriver' => 'screwdriver',
+ 'Screws' => 'screws',
+ 'SD Cards' => 'sd-card',
+ 'SDHC' => 'sdhc',
+ 'SDXC' => 'sdxc',
+ 'Seagate' => 'seagate',
+ 'Sea Life' => 'sea-life',
+ 'Sea of Thieves' => 'sea-of-thieves',
+ 'Season Pass' => 'season-pass',
+ 'Seaworld' => 'seaworld',
+ 'Security Camera' => 'security-camera',
+ 'Seeds &amp; Bulbs' => 'seeds-and-bulbs',
+ 'Sega' => 'sega',
+ 'SEGA Mega Drive Mini' => 'sega-mega-drive-mini',
+ 'Segway' => 'segway',
+ 'Seiko' => 'seiko',
+ 'Sekiro: Shadows Die Twice' => 'sekiro',
+ 'Sekonda' => 'sekonda',
+ 'Selfie Stick' => 'selfie-stick',
+ 'Sennheiser' => 'sennheiser',
+ 'Sennheiser Headphones' => 'sennheiser-headphones',
+ 'Sensodyne' => 'sensodyne',
+ 'Server' => 'server',
+ 'Services &amp; Contracts' => 'services-contracts',
+ 'Services and Subscriptions' => 'service-contract',
+ 'Sewing' => 'sewing',
+ 'Sewing Machine' => 'sewing-machine',
+ 'Sex Toys' => 'sex-toys',
+ 'Shadow of the Tomb Raider' => 'shadow-of-the-tomb-raider',
+ 'Shampoo' => 'shampoo',
+ 'Shark' => 'shark',
+ 'Shark DuoClean' => 'shark-duoclean',
+ 'Shark Vacuum Cleaner' => 'shark-vacuum-cleaner',
+ 'Sharp' => 'sharp',
+ 'Sharpener' => 'sharpener',
+ 'Sharpie' => 'sharpie',
+ 'Shaver' => 'shaver',
+ 'Shaving &amp; Beard Care' => 'shaving',
+ 'Shaving, Trimming, &amp; Hair Removal' => 'hair-removal',
+ 'Shaving Foam' => 'shaving-foam',
+ 'Shears' => 'shears',
+ 'Sheba' => 'sheba',
+ 'Shed' => 'shed',
+ 'Shelter' => 'shelter',
+ 'Shelves' => 'shelves',
+ 'Shenmue I &amp; II' => 'shenmue-one-and-two',
+ 'Shenmue III' => 'shenmue-3',
+ 'Shenmue Series' => 'shenmue-series',
+ 'Shimano' => 'shimano',
+ 'Shirt' => 'shirt',
+ 'Shoe Rack' => 'shoe-rack',
+ 'Shoes' => 'shoe',
+ 'Shopkins' => 'shopkins',
+ 'Shortbread' => 'shortbread',
+ 'Shorts' => 'shorts',
+ 'Short Trip' => 'break',
+ 'Shoulder Bag' => 'shoulder-bag',
+ 'Shovel' => 'shovel',
+ 'Shower Curtain' => 'shower-curtain',
+ 'Shower Enclosure' => 'shower-enclosure',
+ 'Shower Fittings' => 'shower',
+ 'Shower Gel' => 'shower-gel',
+ 'Shower Head' => 'shower-head',
+ 'Shredder' => 'shredder',
+ 'Side-by-Side-Fridge' => 'side-by-side-fridge',
+ 'Sideboard' => 'sideboard',
+ 'Sid Meier&#039;s Civilization VI' => 'civilization-vi',
+ 'Siemens' => 'siemens',
+ 'Siemens Washing Machine' => 'siemens-washing-machine',
+ 'Sigma' => 'sigma',
+ 'Silentnight' => 'silentnight',
+ 'Silvercrest' => 'silvercrest',
+ 'Silver Cross' => 'silver-cross',
+ 'Sim Free' => 'sim-free',
+ 'Sim Only' => 'sim-only',
+ 'Simplehuman' => 'simplehuman',
+ 'Simpsons' => 'simpsons',
+ 'Single Malt' => 'single-malt',
+ 'Sink' => 'sink',
+ 'Sistema' => 'sistema',
+ 'Skateboard' => 'skateboard',
+ 'Skating' => 'skating',
+ 'Skechers' => 'skechers',
+ 'Skiing' => 'ski',
+ 'Skin Care' => 'skincare',
+ 'Skittles' => 'skittles',
+ 'Skoda' => 'skoda',
+ 'Skullcandy' => 'skullcandy',
+ 'Sky' => 'sky',
+ 'Sky Cinema' => 'sky-cinema',
+ 'Skylanders' => 'skylanders',
+ 'Skylanders Battlecast' => 'skylanders-battlecast',
+ 'Skylanders Imaginators' => 'skylanders-imaginators',
+ 'Sleeping Bag' => 'sleeping-bag',
+ 'Sleeping Dogs' => 'sleeping-dogs',
+ 'Sleepwear' => 'sleepwear',
+ 'Slide' => 'slide',
+ 'Slimming World' => 'slimming-world',
+ 'Slippers' => 'slippers',
+ 'Slow Cooker' => 'slow-cooker',
+ 'Smart Clock' => 'clock',
+ 'Smart Doorbells' => 'smart-doorbell',
+ 'Smart Home' => 'smart-home',
+ 'Smart Light' => 'smart-light',
+ 'Smart Lock' => 'smart-lock',
+ 'Smartphone Accessories' => 'smartphone-accessories',
+ 'Smartphone Case' => 'smartphone-case',
+ 'Smartphone under £200' => 'smartphone-under-200-pounds',
+ 'Smartphone under £400' => 'smartphone-under-400-pounds',
+ 'Smart Plugs' => 'smart-plugs',
+ 'Smart Speaker' => 'smart-speaker',
+ 'Smart Tech &amp; Gadgets' => 'smart-tech',
+ 'Smart Thermostat' => 'thermostat',
+ 'SmartThings' => 'smartthings',
+ 'Smart TV' => 'smart-tv',
+ 'Smart Watch' => 'smartwatch',
+ 'Smeg' => 'smeg',
+ 'Smirnoff' => 'smirnoff',
+ 'Smoke Alarm' => 'smoke-alarm',
+ 'Smoothie' => 'smoothie',
+ 'Smoothie Maker' => 'smoothie-maker',
+ 'Snacks' => 'snacks',
+ 'Sneakers' => 'sneakers',
+ 'SNES Nintendo Classic Mini' => 'snes-nintendo-classic',
+ 'Snickers' => 'snickers',
+ 'Sniper Elite' => 'sniper-elite',
+ 'Snowboard' => 'snowboard',
+ 'Snow Boots' => 'snow-boots',
+ 'Soap' => 'soap',
+ 'Soap and Glory' => 'soap-and-glory',
+ 'Socket Set' => 'socket-set',
+ 'Socks' => 'socks',
+ 'SodaStream' => 'soda-stream',
+ 'Sofa' => 'sofa',
+ 'Soft Drinks' => 'soft-drinks',
+ 'Soft Toy' => 'soft-toy',
+ 'Software' => 'software',
+ 'Software &amp; Apps' => 'software-apps',
+ 'Solar Lights' => 'solar-lights',
+ 'Soldering Iron' => 'soldering',
+ 'Sonic' => 'sonic',
+ 'Sonos' => 'sonos',
+ 'Sonos Beam' => 'sonos-beam',
+ 'Sonos Move' => 'sonos-move',
+ 'Sonos One' => 'sonos-one',
+ 'Sonos PLAY:1' => 'sonos-play-1',
+ 'Sonos PLAY:3' => 'sonos-play-3',
+ 'Sonos PLAY:5' => 'sonos-play-5',
+ 'Sonos PLAYBAR' => 'sonos-playbar',
+ 'Sonos PLAYBASE' => 'sonos-playbase',
+ 'Sony' => 'sony',
+ 'Sony Camera' => 'sony-camera',
+ 'Sony Headphones' => 'sony-headphones',
+ 'Sony Pulse 3D Wireless Headset' => 'pulse-3d-wireless-headsets',
+ 'Sony TV' => 'sony-tv',
+ 'Sony WF-1000XM3' => 'sony-wf1000xm3',
+ 'Sony WH-1000XM3' => 'sony-wh-1000xm3',
+ 'Sony WH-1000XM4' => 'sony-wh1000xm4',
+ 'Sony Xperia' => 'xperia',
+ 'Sony Xperia 5' => 'sony-xperia-5',
+ 'Sony Xperia 10' => 'sony-xperia-10',
+ 'Sony Xperia Xa' => 'sony-xperia-xa',
+ 'Sony Xperia Z3' => 'xperia-z3',
+ 'Sony Xperia Z5' => 'xperia-z5',
+ 'Soulcalibur' => 'soulcalibur',
+ 'Soundbar' => 'soundbar',
+ 'Soundbase' => 'soundbase',
+ 'Sound Card' => 'sound-card',
+ 'Soundmagic' => 'soundmagic',
+ 'Soup' => 'soup',
+ 'Soup Maker' => 'soup-maker',
+ 'Sous-Vide' => 'sousvide',
+ 'Southern Comfort' => 'southern-comfort',
+ 'South Park' => 'south-park',
+ 'Spa' => 'spa',
+ 'Spade' => 'spade',
+ 'Spanner' => 'spanner',
+ 'Speaker' => 'speakers',
+ 'Specialized' => 'specialized',
+ 'Speedo' => 'speedo',
+ 'Sphero' => 'sphero',
+ 'Spice Rack' => 'spice-rack',
+ 'Spiderman' => 'spiderman',
+ 'Spiralizer' => 'spiralizer',
+ 'Spirit &amp; Liqueur' => 'spirits',
+ 'Spirit Level' => 'spirit-level',
+ 'Splatoon' => 'splatoon',
+ 'Sports &amp; Outdoors' => 'sports-fitness',
+ 'Sports Events' => 'sports-events',
+ 'Sports Nutrition' => 'nutrition',
+ 'Spreads' => 'spreads',
+ 'Spyro Reignited Trilogy' => 'spyro-reignited-trilogy',
+ 'SSD' => 'ssd',
+ 'SSHD' => 'sshd',
+ 'Staedtler' => 'staedtler',
+ 'Stair Gate' => 'stair-gate',
+ 'Stanley' => 'stanley',
+ 'Stapler' => 'stapler',
+ 'Starbucks' => 'starbucks',
+ 'Starlink: Battle for Atlas' => 'starlink-battle-for-atlas',
+ 'Star Ocean' => 'star-ocean',
+ 'Star Trek' => 'star-trek',
+ 'Star Wars' => 'star-wars',
+ 'Star Wars: Battlefront' => 'star-wars-battlefront',
+ 'Star Wars: Battlefront II' => 'star-wars-battlefront-2',
+ 'Star Wars: Squadrons' => 'star-wars-squadrons',
+ 'Star Wars Jedi: Fallen Order' => 'star-wars-jedi-fallen-order',
+ 'Stationery' => 'stationery',
+ 'Stationery &amp; Office Supplies' => 'stationery-office-supplies',
+ 'Staycation' => 'staycation',
+ 'Steak' => 'steak',
+ 'Steam Cleaner' => 'steam-cleaner',
+ 'Steam Controller' => 'steam-controller',
+ 'Steamer' => 'steamer',
+ 'Steam Gaming' => 'steam',
+ 'Steam Iron' => 'steam-iron',
+ 'Steam Link' => 'steam-link',
+ 'Steam Mop' => 'steam-mop',
+ 'SteelSeries' => 'steelseries',
+ 'Steering Wheel' => 'steering-wheel',
+ 'Stella' => 'stella',
+ 'Stool' => 'stool',
+ 'Storage Box' => 'storage-box',
+ 'Stormtrooper' => 'stormtrooper',
+ 'Straightener' => 'straightener',
+ 'Streaming' => 'streaming',
+ 'Street Fighter' => 'street-fighter',
+ 'Street Fighter V' => 'street-fighter-v',
+ 'Streetwear' => 'streetwear',
+ 'Strimmer' => 'strimmer',
+ 'Strongbow' => 'strongbow',
+ 'Student Discount' => 'student-discount',
+ 'Subwoofer' => 'subwoofer',
+ 'Suitcase' => 'suitcase',
+ 'Suncare' => 'suncare',
+ 'Sun Cream' => 'sun-cream',
+ 'Sunglasses' => 'sunglasses',
+ 'Superdry' => 'superdry',
+ 'Superfast Broadband' => 'superfast-broadband',
+ 'Superking' => 'superking',
+ 'Super Mario' => 'mario',
+ 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars',
+ 'Super Mario 3D World' => 'super-mario-3d-world',
+ 'Super Mario Maker 2' => 'super-mario-maker-2',
+ 'Super Mario Odyssey' => 'super-mario-odyssey',
+ 'Super Mario Party' => 'mario-party',
+ 'Supermarket' => 'supermarket',
+ 'Super Smash Bros.' => 'super-smash-bros',
+ 'Surf' => 'surf',
+ 'Swarovski' => 'swarovski',
+ 'Sweets' => 'sweets',
+ 'Swimming' => 'swimming',
+ 'Swimming Goggles' => 'goggles',
+ 'Swimwear' => 'swimwear',
+ 'Swing' => 'swing',
+ 'Swingball' => 'swingball',
+ 'Syberia' => 'syberia',
+ 'Sylvanian' => 'sylvanian',
+ 'Synology' => 'synology',
+ 'T-Mobile' => 't-mobile',
+ 'T-Shirt' => 't-shirt',
+ 'Table Lamp' => 'table-lamp',
+ 'Tablet' => 'tablet',
+ 'Tablet Accessories' => 'tablet-accessories',
+ 'Table Tennis' => 'table-tennis',
+ 'Tableware' => 'tableware',
+ 'Tacx' => 'tacx',
+ 'Tado' => 'tado',
+ 'Tag Heuer' => 'tag-heuer',
+ 'Takeaway and Food Delivery' => 'takeaway',
+ 'Tales of Vesperia: Definitive Edition' => 'tales-of-vesperia-definitive-edition',
+ 'Talisker' => 'talisker',
+ 'Talkmobile' => 'talkmobile',
+ 'Tamron' => 'tamron',
+ 'Tangle Teezer' => 'tangle-teezer',
+ 'Tank Top' => 'tank-top',
+ 'Tannoy' => 'tannoy',
+ 'Tanqueray' => 'tanqueray',
+ 'Tape' => 'tape',
+ 'Tassimo' => 'tassimo',
+ 'Tassimo Coffee Machine' => 'tassimo-coffee-machine',
+ 'tastecard' => 'tastecard',
+ 'Taxi' => 'taxi',
+ 'Tea' => 'tea',
+ 'Team Sonic Racing' => 'team-sonic-racing',
+ 'Team Sports' => 'team-sports',
+ 'Teapot' => 'teapot',
+ 'Technika' => 'technika',
+ 'Techwood' => 'techwood',
+ 'Ted Baker' => 'ted-baker',
+ 'Teddy Bear' => 'teddy-bear',
+ 'Teenage Mutant Ninja Turtles' => 'turtle',
+ 'Teeth Care' => 'teeth-care',
+ 'Teeth Whitening' => 'teeth-whitening',
+ 'Tefal' => 'tefal',
+ 'Tefal Actifry' => 'actifry',
+ 'Tefal Pan' => 'tefal-pan',
+ 'Tekken' => 'tekken',
+ 'Tekken 7' => 'tekken-7',
+ 'Telegraph' => 'telegraph',
+ 'Telescope' => 'telescope',
+ 'Telltale' => 'telltale',
+ 'Tennis' => 'tennis',
+ 'Tent' => 'tent',
+ 'Tequila' => 'tequila',
+ 'Tesco Clothing' => 'tesco-clothing',
+ 'Tesla' => 'tesla',
+ 'Tetris' => 'tetris',
+ 'Tetris 99' => 'tetris-99',
+ 'Theatre &amp; Musical' => 'theatre',
+ 'The Beatles' => 'beatles',
+ 'The Big Bang Theory' => 'big-bang-theory',
+ 'The Crew' => 'the-crew',
+ 'The Dark Pictures: Anthology Man of Medan' => 'the-dark-pictures-anthology-man-of-medan',
+ 'The Elder Scrolls' => 'elder-scrolls',
+ 'The Elder Scrolls V: Skyrim' => 'skyrim',
+ 'The Evil Within' => 'the-evil-within',
+ 'The Evil Within 2' => 'the-evil-within-2',
+ 'The Last Guardian' => 'the-last-guardian',
+ 'The Last of Us' => 'the-last-of-us',
+ 'The Last of Us Part II' => 'the-last-of-us-part-2',
+ 'The Legend of Zelda' => 'zelda',
+ 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild',
+ 'The Legend of Zelda: Link&#039;s Awakening' => 'the-legend-of-zelda-links-awakening',
+ 'The Legend of Zelda: Skyward Sword HD' => 'the-legend-of-zelda-skyward-sword-hd',
+ 'Theme Park' => 'theme-park',
+ 'The North Face' => 'north-face',
+ 'The Outer Worlds' => 'the-outer-worlds',
+ 'Thermos Storage' => 'thermos',
+ 'The Sims' => 'sims',
+ 'The Sims 4' => 'the-sims-4',
+ 'The Sinking City' => 'the-sinking-city',
+ 'The Sun' => 'the-sun',
+ 'The Sunday Times' => 'sunday-times',
+ 'The Walking Dead' => 'walking-dead',
+ 'The Witcher' => 'witcher',
+ 'The Witcher 3' => 'the-witcher-3',
+ 'Thierry Mugler' => 'thierry-mugler',
+ 'Thomas Sabo' => 'thomas-sabo',
+ 'Thomas The Tank Engine' => 'thomas-the-tank',
+ 'Thornton&#039;s' => 'thorntons',
+ 'Thorpe Park' => 'thorpe-park',
+ 'Throw' => 'throw',
+ 'Thrustmaster' => 'thrustmaster',
+ 'Thule' => 'thule',
+ 'Tickets &amp; Shows' => 'tickets-shows',
+ 'Tie' => 'tie',
+ 'Tights' => 'tights',
+ 'TIGI' => 'tigi',
+ 'Tilda' => 'tilda',
+ 'Tile' => 'tile',
+ 'Timberland' => 'timberland',
+ 'Timex' => 'timex',
+ 'Tissot' => 'tissot',
+ 'Tissues' => 'tissues',
+ 'Titanfall' => 'titanfall',
+ 'Titanfall 2' => 'titanfall-2',
+ 'Toaster' => 'toaster',
+ 'Toblerone' => 'toblerone',
+ 'Toddler Bed' => 'toddler-bed',
+ 'Toilet Brush' => 'brush',
+ 'Toilet Cleaner' => 'toilet',
+ 'Toilet Roll' => 'toilet-roll',
+ 'Toilet Seat' => 'toilet-seat',
+ 'Tokyo Laundry' => 'tokyo-laundry',
+ 'Tomb Raider' => 'tomb-raider',
+ 'Tom Clancy&#039;s' => 'tom-clancy',
+ 'Tom Clancy&#039;s: Ghost Recon' => 'ghost-recon',
+ 'Tom Clancy&#039;s Ghost Recon: Wildlands' => 'ghost-recon-wildlands',
+ 'Tom Clancy&#039;s Ghost Recon Breakpoint' => 'tom-clancys-ghost-recon-breakpoint',
+ 'Tom Clancy&#039;s The Division' => 'tom-clancy-the-division',
+ 'Tom Clancy&#039;s The Division 2' => 'tom-clancy-the-division-2',
+ 'Tom Ford' => 'tom-ford',
+ 'Tommee Tippee' => 'tommee-tippee',
+ 'Tommy Hilfiger' => 'tommy-hilfiger',
+ 'Toms' => 'toms',
+ 'TomTom' => 'tomtom',
+ 'Tonic Water' => 'tonic-water',
+ 'Tony Hawk&#039;s Pro Skater 1 + 2' => 'tony-hawks-pro-skater-1-2',
+ 'Tools' => 'tool',
+ 'Toothbrush' => 'toothbrush',
+ 'Toothpaste' => 'toothpaste',
+ 'Torch' => 'torch',
+ 'Torque Wrench' => 'torque-wrench',
+ 'Toshiba' => 'toshiba',
+ 'Toshiba Laptop' => 'toshiba-laptop',
+ 'Toshiba TV' => 'toshiba-tv',
+ 'Total War' => 'total-war',
+ 'Tottenham Hotspur F. C.' => 'tottenham',
+ 'Towel' => 'towel',
+ 'Toy Box' => 'toy-box',
+ 'Toy Cars' => 'toy-cars',
+ 'Toy Castle' => 'castle',
+ 'Toy Digger' => 'digger',
+ 'Toy Helicopter' => 'helicopter',
+ 'Toy Kitchen' => 'toy-kitchen',
+ 'Toy Mask' => 'mask',
+ 'Toyota' => 'toyota',
+ 'Toys' => 'toy',
+ 'Toy Story' => 'toy-story',
+ 'Toy Tractor' => 'tractor',
+ 'Toy Train' => 'train',
+ 'TP-Link' => 'tp-link',
+ 'TP-Link Archer' => 'archer',
+ 'TP-Link Router' => 'tp-link-router',
+ 'Tracksuit' => 'tracksuit',
+ 'Trainers' => 'trainers',
+ 'Trains &amp; Buses' => 'train-and-bus-ticket',
+ 'Train Ticket' => 'train-ticket',
+ 'Trampoline' => 'trampoline',
+ 'Transcend' => 'transcend',
+ 'Transformers' => 'transformers',
+ 'Travel' => 'travel',
+ 'Travel App' => 'travel-app',
+ 'Travel Insurance' => 'travel-insurance',
+ 'Travelodge' => 'travelodge',
+ 'Travel System' => 'travel-system',
+ 'Treadmill' => 'treadmill',
+ 'TRESemmé' => 'tresemme',
+ 'Trespass' => 'trespass',
+ 'Triathlon' => 'triathlon',
+ 'Trike' => 'trike',
+ 'Trine 4' => 'trine-4',
+ 'Tripod' => 'tripod',
+ 'Tripp' => 'tripp',
+ 'Triton Shower' => 'triton',
+ 'Trolley Bag' => 'trolley',
+ 'Tropico 5' => 'tropico-5',
+ 'Tropico 6' => 'tropico-6',
+ 'Tropico Series' => 'tropico-deals',
+ 'Trousers' => 'trousers',
+ 'True Wireless Earbuds' => 'wireless-earphones',
+ 'Trunki' => 'trunki',
+ 'Tumble Dryer' => 'tumble-dryer',
+ 'Tuna' => 'tuna',
+ 'Turbo Trainer' => 'turbo-trainer',
+ 'Turntable' => 'turntable',
+ 'Turtle Beach' => 'turtle-beach',
+ 'TV' => 'tv',
+ 'TV &amp; Video' => 'tv-video',
+ 'TV Accessories' => 'tv-accessories',
+ 'TV Mount' => 'tv-mount',
+ 'TV Series' => 'tv-series',
+ 'TV Stand' => 'tv-stand',
+ 'Twinings' => 'twinings',
+ 'Twin Peaks' => 'twin-peaks',
+ 'Twix' => 'twix',
+ 'Typhoo' => 'typhoo',
+ 'Tyres' => 'tyres',
+ 'Ubisoft' => 'ubisoft',
+ 'UE BOOM' => 'ue-boom',
+ 'UE Boom 2' => 'ue-boom-2',
+ 'UEFA' => 'uefa',
+ 'UE Megablast' => 'ue-megablast',
+ 'UE Megaboom' => 'ue-megaboom',
+ 'UGG' => 'ugg',
+ 'Ulefone' => 'ulefone',
+ 'Ultrabook' => 'ultrabook',
+ 'Ultrawide Monitor' => 'ultrawide',
+ 'Umbrella' => 'umbrella',
+ 'UMI' => 'umidigi',
+ 'Uncharted' => 'uncharted',
+ 'Uncharted 4: A Thief&#039;s End' => 'uncharted-4',
+ 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy',
+ 'Under Armour' => 'under-armour',
+ 'Underwear' => 'underwear',
+ 'Unicorn' => 'unicorn',
+ 'UNiDAYS' => 'unidays',
+ 'Universal Remote' => 'universal-remote',
+ 'Uno' => 'uno',
+ 'Uplay' => 'uplay',
+ 'Urban Decay' => 'urban-decay',
+ 'Urban Sports' => 'urban-sports',
+ 'USB Cable' => 'usb-cable',
+ 'USB Hub' => 'usb-hub',
+ 'USB Memory Stick' => 'flash-drive',
+ 'USB Type C' => 'usb-type-c',
+ 'USN' => 'usn',
+ 'Vacuum Cleaner' => 'vacuum-cleaners',
+ 'Vacuum Flask' => 'flask',
+ 'Valkyria Chronicles' => 'valkyria-chronicles',
+ 'Valkyria Chronicles 4' => 'valkyria-chronicles-4',
+ 'Vango' => 'vango',
+ 'Vanish' => 'vanish',
+ 'Vans' => 'vans',
+ 'Vans Old Skool' => 'vans-old-skool',
+ 'Vans Shoes' => 'vans-shoes',
+ 'Vase' => 'vase',
+ 'Vaseline' => 'vaseline',
+ 'Vauxhall' => 'vauxhall',
+ 'VAX' => 'vax',
+ 'Vax Blade' => 'vax-blade',
+ 'Vax Vacuum Cleaner' => 'vax-vacuum',
+ 'Veet' => 'veet',
+ 'Vega 7' => 'vega-7',
+ 'Vegetables' => 'vegetables',
+ 'Vegetarian' => 'vegetarian',
+ 'Vehicles' => 'vehicles',
+ 'Velvet Comfort' => 'velvet',
+ 'Vera Wang' => 'vera-wang',
+ 'Verbatim' => 'verbatim',
+ 'Versace' => 'versace',
+ 'Vibrator' => 'vibrator',
+ 'Victorinox' => 'victorinox',
+ 'Video Games' => 'videogame',
+ 'Video Streaming' => 'video-streaming',
+ 'Viktor &amp; Rolf Spicebomb' => 'spicebomb',
+ 'Vileda' => 'vileda',
+ 'Villeroy &amp; Boch' => 'villeroy-boch',
+ 'Viners' => 'viners',
+ 'Vinyl' => 'vinyl',
+ 'Virgin' => 'virgin',
+ 'Vitamins &amp; Supplements' => 'vitamins',
+ 'Vitamix' => 'vitamix',
+ 'Vodafone' => 'vodafone',
+ 'Vodka' => 'vodka',
+ 'Volvo' => 'volvo',
+ 'VPN' => 'vpn',
+ 'VR Headset' => 'vr-headset',
+ 'VTech' => 'vtech',
+ 'VTech Toot Toot' => 'toot-toot',
+ 'Vue' => 'vue',
+ 'VW' => 'vw',
+ 'Wacom' => 'wacom',
+ 'Waffle Maker' => 'waffle-maker',
+ 'Wahl' => 'wahl',
+ 'Walkers' => 'walkers',
+ 'Walking Boots' => 'walking-boots',
+ 'Wall Art' => 'wall-art',
+ 'Wallet' => 'wallet',
+ 'Wallpaper' => 'wallpaper',
+ 'Wardrobe' => 'wardrobe',
+ 'Warhammer' => 'warhammer',
+ 'Washbag' => 'washbag',
+ 'Washer Dryer' => 'washer-dryer',
+ 'Washing Machine' => 'washing-machine',
+ 'Washing Powder' => 'washing-powder',
+ 'Watch' => 'watch',
+ 'Watch Dogs' => 'watch-dogs',
+ 'Watch Dogs 2' => 'watch-dogs-2',
+ 'Watch Dogs: Legion' => 'watch-dogs-legion',
+ 'Water Bottle' => 'water-bottle',
+ 'Water Butt' => 'water-butt',
+ 'Water Dispenser' => 'water-dispenser',
+ 'Water Filter' => 'water-filter',
+ 'Water Gun' => 'water-gun',
+ 'Waterproof Camera' => 'waterproof-camera',
+ 'Waterproof Jacket' => 'waterproof-jacket',
+ 'Watersports' => 'watersport',
+ 'Water Toys' => 'water-toys',
+ 'Wayfarer' => 'wayfarer',
+ 'WD40' => 'wd40',
+ 'Wearable' => 'wearable',
+ 'Weather Station' => 'weather-station',
+ 'Webcam' => 'webcam',
+ 'Weber' => 'weber',
+ 'Web Hosting' => 'web-hosting',
+ 'Wedding' => 'wedding',
+ 'Weed Killer' => 'weed',
+ 'Weekend Break' => 'weekend-break',
+ 'Weetabix' => 'weetabix',
+ 'Weightlifting' => 'weightlifting',
+ 'Weight Watchers' => 'weight-watchers',
+ 'Wellies' => 'wellies',
+ 'Wellness and Health' => 'wellness-and-health',
+ 'Wenger' => 'wenger',
+ 'Western Digital' => 'western-digital',
+ 'Wetsuit' => 'wetsuit',
+ 'Wheelbarrow' => 'wheelbarrow',
+ 'Wheelchair' => 'wheelchair',
+ 'Whey' => 'whey',
+ 'Whiskas' => 'whiskas',
+ 'Whisky' => 'whisky',
+ 'Whole Home Mesh Wi-Fi System' => 'whole-home-mesh-wifi-system',
+ 'Wi-Fi Camera' => 'wifi-camera',
+ 'Wi-Fi Dongle' => 'dongle',
+ 'Wi-Fi Extender' => 'wifi-extender',
+ 'Wii' => 'wii',
+ 'Wii Game' => 'wii-games',
+ 'Wii U Game' => 'wii-u-game',
+ 'Wii U Pro Controller' => 'wii-u-pro-controller',
+ 'Wild Turkey' => 'wild-turkey',
+ 'Wileyfox' => 'wileyfox',
+ 'Wilkinson Sword Hydro 5' => 'hydro-5',
+ 'Wilkinson Sword Razor' => 'wilkinson-sword',
+ 'Wimbledon Tennis' => 'wimbledon',
+ 'Window Cleaner' => 'window-cleaner',
+ 'Windows' => 'windows',
+ 'Windows 8' => 'windows-8',
+ 'Windows 10' => 'windows-10',
+ 'Wine' => 'wine',
+ 'Wine Advent Calendar' => 'wine-advent-calendar',
+ 'Wine Glasses' => 'wine-glasses',
+ 'Winter Jacket' => 'winter-jacket',
+ 'Wiper Blades' => 'wiper-blades',
+ 'Wireless Adapter' => 'wireless-adapter',
+ 'Wireless Charger' => 'wireless-charger',
+ 'Wireless Controller' => 'wireless-controller',
+ 'Wireless Headphones' => 'wireless-headphones',
+ 'Wireless Headset' => 'wireless-headset',
+ 'Wireless Keyboard' => 'wireless-keyboard',
+ 'Wireless Mouse' => 'wireless-mouse',
+ 'Wok' => 'wok',
+ 'Wolfenstein' => 'wolfenstein',
+ 'Wolfenstein 2: The New Colossus' => 'wolfenstein-2',
+ 'Women&#039;s Boots' => 'womens-boots',
+ 'Women&#039;s Fragrance' => 'womens-fragrance',
+ 'Women&#039;s Shoes' => 'womens-shoes',
+ 'Workbench' => 'workbench',
+ 'World of Warcraft' => 'world-of-warcraft',
+ 'World War Z' => 'world-war-z',
+ 'WORX' => 'worx',
+ 'Wreckfest' => 'wreckfest',
+ 'Wuaki' => 'wuaki',
+ 'WWE 2K' => 'wwe',
+ 'Xbox' => 'xbox',
+ 'Xbox 360 Game' => 'xbox-360-game',
+ 'Xbox Accessories' => 'xbox-accessories',
+ 'Xbox Controller' => 'xbox-controller',
+ 'Xbox Game Pass' => 'xbox-game-pass',
+ 'Xbox Gift Card' => 'xbox-gift-card',
+ 'Xbox Headset' => 'xbox-headset',
+ 'Xbox Kinect' => 'kinect',
+ 'Xbox Live' => 'xbox-live',
+ 'Xbox One Controller' => 'xbox-one-controller',
+ 'Xbox One Elite Controller' => 'xbox-one-elite-controller',
+ 'Xbox One Games' => 'xbox-one-games',
+ 'Xbox One S' => 'xbox-one-s',
+ 'Xbox One X' => 'xbox-one-x',
+ 'Xbox Series S' => 'xbox-series-s',
+ 'Xbox Series X' => 'xbox-series-x',
+ 'Xbox Series X Controller' => 'xbox-series-x-controller',
+ 'Xbox Series X Games' => 'xbox-series-x-game',
+ 'Xbox Wireless Adapter' => 'xbox-wireless-adapter',
+ 'Xbox Wireless Headset' => 'xbox-wireless-headset',
+ 'XCOM' => 'xcom',
+ 'XCOM 2' => 'xcom-2',
+ 'Xenoblade Chronicles' => 'xenoblade-chronicles',
+ 'XFX' => 'xfx',
+ 'Xiaomi' => 'xiaomi',
+ 'Xiaomi AirDots' => 'xiaomi-airdots',
+ 'Xiaomi Black Shark' => 'xiaomi-black-shark',
+ 'Xiaomi Black Shark 2' => 'xiaomi-black-shark-2',
+ 'Xiaomi Headphones' => 'xiaomi-headphones',
+ 'Xiaomi Laptop' => 'xiaomi-laptop',
+ 'Xiaomi Mi 5' => 'xiaomi-mi-5',
+ 'Xiaomi Mi 6' => 'xiaomi-mi-6',
+ 'Xiaomi Mi 8' => 'xiaomi-mi-8',
+ 'Xiaomi Mi 8 Lite' => 'xiaomi-mi-8-lite',
+ 'Xiaomi Mi 8 Pro' => 'xiaomi-mi-8-pro',
+ 'Xiaomi Mi 9' => 'xiaomi-mi-9',
+ 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite',
+ 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se',
+ 'Xiaomi Mi 9T' => 'xiaomi-mi-9t',
+ 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro',
+ 'Xiaomi Mi 10' => 'xiaomi-mi-10',
+ 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite',
+ 'Xiaomi Mi 10T' => 'xiaomi-mi-10t',
+ 'Xiaomi Mi 10T Lite' => 'xiaomi-mi-10t-lite',
+ 'Xiaomi Mi 10T Pro' => 'xiaomi-mi-10t-pro',
+ 'Xiaomi Mi 11' => 'xiaomi-mi-11',
+ 'Xiaomi Mi 11 Lite 4G' => 'xiaomi-mi-11-lite-4g',
+ 'Xiaomi Mi 11 Lite 5G' => 'xiaomi-mi-11-lite-5g',
+ 'Xiaomi Mi 11 Pro' => 'xiaomi-mi-11-pro',
+ 'Xiaomi Mi 11 Ultra' => 'xiaomi-mi-11-ultra',
+ 'Xiaomi Mi 11i' => 'xiaomi-mi-11i',
+ 'Xiaomi Mi A1' => 'xiaomi-mi-a1',
+ 'Xiaomi Mi A2' => 'mi-a2',
+ 'Xiaomi Mi A3' => 'xiaomi-mi-a3',
+ 'Xiaomi Mi Band' => 'xiaomi-mi-band',
+ 'Xiaomi Mi Band 3' => 'xiaomi-mi-band-3',
+ 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4',
+ 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5',
+ 'Xiaomi Mi Box' => 'xiaomi-mi-box',
+ 'Xiaomi Mi Max 3' => 'xiaomi-mi-max3',
+ 'Xiaomi Mi Mix' => 'xiaomi-mi-mix',
+ 'Xiaomi Mi Mix 2' => 'xiaomi-mi-mix-2',
+ 'Xiaomi Mi Mix 2S' => 'xiaomi-mi-mix-2s',
+ 'Xiaomi Mi Mix 3' => 'xiaomi-mi-mix-3',
+ 'Xiaomi Mi Note' => 'xiaomi-mi-note',
+ 'Xiaomi Mi Note 10' => 'mi-note-10',
+ 'Xiaomi Mi Pad 4' => 'xiaomi-mi-pad-4',
+ 'Xiaomi Pocophone F1' => 'pocophone-f1',
+ 'Xiaomi Redmi' => 'redmi',
+ 'Xiaomi Redmi 4' => 'xiaomi-redmi-4',
+ 'Xiaomi Redmi 5' => 'redmi-5',
+ 'Xiaomi Redmi 6' => 'redmi-6',
+ 'Xiaomi Redmi 8' => 'redmi-8',
+ 'Xiaomi Redmi Note 4' => 'note-4',
+ 'Xiaomi Redmi Note 5' => 'redmi-note-5',
+ 'Xiaomi Redmi Note 6' => 'redmi-note-6',
+ 'Xiaomi Redmi Note 6 Pro' => 'xiaomi-redmi-note-6-pro',
+ 'Xiaomi Redmi Note 7' => 'redmi-note-7',
+ 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8',
+ 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro',
+ 'Xiaomi Redmi Note 8T' => 'redmi-note-8t',
+ 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9',
+ 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro',
+ 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s',
+ 'Xiaomi Roborock' => 'xiaomi-roborock',
+ 'Xiaomi Roborock S5' => 'xiaomi-roborock-s5',
+ 'Xiaomi Scooter' => 'xiaomi-scooter',
+ 'Xiaomi Smartphones' => 'xiaomi-smartphone',
+ 'Xiaomi Tablets' => 'xiaomi-tablet',
+ 'Yakuza' => 'yakuza',
+ 'Yale' => 'yale',
+ 'Yale Smart Lock' => 'yale-smart-lock',
+ 'Yamaha' => 'yamaha',
+ 'Yankee Candle' => 'yankee-candle',
+ 'Yeelight' => 'xiaomi-yeelight',
+ 'Yoga' => 'yoga',
+ 'Yoghurt' => 'yoghurt',
+ 'Yoshi' => 'yoshi',
+ 'Yoshi&#039;s Crafted World' => 'yoshis-crafted-world',
+ 'YouView' => 'youview',
+ 'Yves Saint Laurent' => 'yves-saint-laurent',
+ 'Zanussi' => 'zanussi',
+ 'Zippo' => 'zippo',
+ 'Zizzi' => 'zizzi',
+ 'Zoo' => 'zoo',
+ 'Zoostorm' => 'zoostorm',
+ 'ZOTAC' => 'zotac',
+ 'ZTE' => 'zte',
+ 'ZTE Smartphone' => 'zte-smartphone',
+ 'ZyXEL' => 'zyxel',
+ ]
+ ],
+ 'order' => [
+ 'name' => 'Order by',
+ 'type' => 'list',
+ 'title' => 'Sort order of deals',
+ 'values' => [
+ 'From the most to the least hot deal' => '-hot',
+ 'From the most recent deal to the oldest' => '-new',
+ ]
+ ]
+ ],
+ 'Discussion Monitoring' => [
+ 'url' => [
+ 'name' => 'Discussion URL',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Discussion URL to monitor. Ex: https://www.hotukdeals.com/discussions/title-123',
+ 'exampleValue' => 'https://www.hotukdeals.com/discussions/the-hukd-lego-thread-3599357',
+ ],
+ 'only_with_url' => [
+ 'name' => 'Exclude comments without URL',
+ 'type' => 'checkbox',
+ 'title' => 'Exclude comments that does not contains URL in the feed',
+ 'defaultValue' => false,
+ ]
+ ]
- 'Deals per group' => array(
- 'group' => array(
- 'name' => 'Group',
- 'type' => 'list',
- 'title' => 'Group whose deals must be displayed',
- 'values' => array(
- '3D Blu-ray' => '3d-bluray',
- '3D Printer' => '3d-printer',
- '3D TV' => '3d-tv',
- '4K Blu-ray' => '4k-bluray',
- '4K Monitor' => '4k-monitor',
- '4K TV' => '4k-tv',
- '5G Phones' => '5g-phones',
- '7 Up' => '7up',
- '8K TV' => '8k-tv',
- '32 inch TV' => '32-inch-tv',
- '40 inch TV' => '40-inch-tv',
- '55 inch TV' => '55-inch-tv',
- '65 inch TV' => '65-inch-tv',
- '75 inch TV' => '75-inch-tv',
- '144Hz Monitor' => '144hz',
- 'A4 Paper' => 'a4-paper',
- 'AAA Battery' => 'aaa',
- 'AA Battery' => 'aa',
- 'Abercrombie' => 'abercrombie',
- 'Aberlour' => 'aberlour',
- 'Accommodation' => 'accomodation',
- 'Accurist' => 'accurist',
- 'Ace Combat 7: Skies Unknown' => 'ace-combat-7',
- 'Acer' => 'acer',
- 'Acer Aspire' => 'acer-aspire',
- 'Acer Laptop' => 'acer-laptop',
- 'Acer PC Monitor' => 'acer-pc-monitor',
- 'Acer Predator' => 'acer-predator',
- 'Action Camera' => 'action-camera',
- 'Action Figure &amp; Playsets' => 'playsets',
- 'Activewear' => 'sports-clothes',
- 'Activia' => 'activia',
- 'adidas' => 'adidas',
- 'adidas Continental' => 'continental',
- 'Adidas Gazelle' => 'gazelle',
- 'Adidas Originals' => 'adidas-originals',
- 'Adidas Samba' => 'samba',
- 'Adidas Stan Smith' => 'stan-smith',
- 'Adidas Superstar' => 'adidas-superstar',
- 'Adidas Trainers' => 'adidas-shoes',
- 'Adidas Ultraboost' => 'adidas-ultraboost',
- 'Adidas ZX Flux' => 'adidas-zx-flux',
- 'Adobe' => 'adobe',
- 'Adobe Lightroom' => 'lightroom',
- 'Adobe Photoshop' => 'photoshop',
- 'Adult Products' => 'adult',
- 'Advent Calendar' => 'advent-calendar',
- 'Adventure Time' => 'adventure-time',
- 'AEG' => 'aeg',
- 'Aftershave' => 'aftershave',
- 'Age Of Empires' => 'age-of-empires',
- 'Air Bed' => 'air-bed',
- 'Air Conditioner' => 'air-con',
- 'Airer' => 'airer',
- 'Airfix' => 'airfix',
- 'Air Fryer' => 'air-fryer',
- 'Airline' => 'airline',
- 'Airport' => 'airport',
- 'Airport Parking' => 'airport-parking',
- 'Air Purifier' => 'air-purifier',
- 'AirTag' => 'airtag',
- 'Air Treatment' => 'air-treatment',
- 'AKG' => 'akg',
- 'Alarm Clock' => 'alarm-clock',
- 'Alarm System' => 'alarm-system',
- 'Alcatel' => 'alcatel',
- 'Alcohol' => 'alcohol',
- 'Alesis' => 'alesis',
- 'Alien: Isolation' => 'alien-isolation',
- 'Alienware' => 'alienware',
- 'All-in-One PC' => 'all-in-one-pc',
- 'All-in-One Printer' => 'all-in-one-printer',
- 'Alloy Wheel' => 'alloy-wheels',
- 'All Saints' => 'all-saints',
- 'Almonds' => 'almonds',
- 'Alpro' => 'alpro',
- 'Alton Towers' => 'alton-towers',
- 'Amazfit' => 'xiaomi-amazfit',
- 'Amazfit Bip' => 'xiaomi-amazfit-bip',
- 'Amazfit GTS' => 'amazfit-gts',
- 'Amazfit Verge' => 'amazfit-verge',
- 'Amazfit Verge Lite' => 'amazfit-verge-lite',
- 'Amazfit Watch' => 'amazfit-watch',
- 'Amazon Add On Item' => 'add-on-item',
- 'Amazon Business' => 'amazon-business',
- 'Amazon Echo' => 'amazon-echo',
- 'Amazon Echo Dot' => 'amazon-echo-dot',
- 'Amazon Echo Plus' => 'amazon-echo-plus',
- 'Amazon Echo Show' => 'amazon-echo-show',
- 'Amazon Echo Show 5' => 'echo-show-5',
- 'Amazon Echo Show 8' => 'amazon-echo-show-8',
- 'Amazon Echo Spot' => 'amazon-echo-spot',
- 'Amazon Fire 7' => 'amazon-fire-7',
- 'Amazon Fire HD 8' => 'amazon-fire-hd-7',
- 'Amazon Fire HD 10 Tablet' => 'amazon-fire-hd-10',
- 'Amazon Fire Tablet' => 'amazon-tablet',
- 'Amazon Fire TV Cube' => 'fire-tv-cube',
- 'Amazon Fire TV Stick' => 'amazon-fire-stick',
- 'Amazon Pantry' => 'amazon-pantry',
- 'Amazon Prime' => 'amazon-prime',
- 'Amazon Prime Video' => 'amazon-video',
- 'Amazon Warehouse' => 'amazon-warehouse',
- 'AMD' => 'amd',
- 'AMD Radeon' => 'radeon',
- 'AMD Ryzen' => 'amd-ryzen',
- 'AMD Ryzen 5 5600X' => 'amd-ryzen-5-5600x',
- 'AMD Ryzen 7 5800X' => 'amd-ryzen-7-5800x',
- 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x',
- 'AMD Ryzen 9 5950X' => 'amd-ryzen-9-5950x',
- 'Amex' => 'amex',
- 'Amiibo' => 'amiibo',
- 'Amplifier' => 'amplifier',
- 'Anchor Butter' => 'anchor-butter',
- 'Andrex' => 'andrex',
- 'Android Apps' => 'android-app',
- 'Android Smartphone' => 'android-smartphone',
- 'Android Tablet' => 'android-tablet',
- 'Angelcare' => 'angelcare',
- 'Angle Grinder' => 'grinder',
- 'Anglepoise' => 'anglepoise',
- 'Angry Birds' => 'angry-birds',
- 'Animal Crossing' => 'animal-crossing',
- 'Anime' => 'anime',
- 'Anker' => 'anker',
- 'Ankle Boots' => 'ankle-boots',
- 'Anno 1800' => 'anno-1800',
- 'Anthem' => 'anthem',
- 'Antibacterial Hand Gel' => 'hand-gel',
- 'Antibacterial Wipes' => 'cleaning-wipes',
- 'Antivirus' => 'antivirus',
- 'Antler' => 'antler',
- 'AOC' => 'aoc',
- 'Apex Legends' => 'apex-legends',
- 'A Plague Tale: Innocence' => 'a-plague-tale-innocence',
- 'App' => 'app',
- 'Apple' => 'apple',
- 'Apple AirPods' => 'apple-airpods',
- 'Apple Airpods 2' => 'airpods-2',
- 'Apple Airpods Max' => 'airpods-max',
- 'Apple Airpods Pro' => 'airpods-pro',
- 'Apple EarPods' => 'earpods',
- 'Apple Headphones' => 'apple-headphones',
- 'Apple HomePod' => 'apple-homepod',
- 'Apple HomePod mini' => 'apple-homepod-mini',
- 'Apple Keyboard' => 'apple-keyboard',
- 'Apple Pencil' => 'apple-pencil',
- 'Apple TV' => 'apple-tv',
- 'Apple TV 4K' => 'apple-tv-4k',
- 'Apple Watch' => 'apple-watch',
- 'Apple Watch 3' => 'apple-watch-3',
- 'Apple Watch 4' => 'apple-watch-4',
- 'Apple Watch 5' => 'apple-watch-5',
- 'Apple Watch 6' => 'apple-watch-6',
- 'Apple Watch SE' => 'apple-watch-se',
- 'Apron' => 'apron',
- 'Aquadoodle' => 'aquadoodle',
- 'Aqua Optima' => 'aqua-optima',
- 'Aquarium' => 'aquarium',
- 'Aramis' => 'aramis',
- 'Argan Oil' => 'argan-oil',
- 'Ariel' => 'ariel',
- 'Ark' => 'ark',
- 'Armani' => 'armani',
- 'Armchair' => 'armchair',
- 'Armed Forces Discount' => 'armed-forces',
- 'Arsenal F. C.' => 'arsenal',
- 'Arts and Crafts' => 'craft',
- 'Asics' => 'asics',
- 'Ask' => 'ask',
- 'ASRock' => 'asrock',
- 'Assassin&#039;s Creed' => 'assassins-creed',
- 'Assassin&#039;s Creed: Odyssey' => 'assassins-creed-odyssey',
- 'Assassin&#039;s Creed: Origins' => 'assassins-creed-origins',
- 'Assassin&#039;s Creed: Unity' => 'assassins-creed-unity',
- 'Assassin&#039;s Creed: Valhalla' => 'assasins-creed-valhalla',
- 'Astral Chain' => 'astral-chain',
- 'ASTRO Gaming' => 'astro-gaming',
- 'Astro Gaming A40' => 'astro-gaming-a40',
- 'Astro Gaming A50' => 'astro-gaming-a50',
- 'Asus' => 'asus',
- 'ASUS Laptop' => 'asus-laptop',
- 'ASUS Monitor' => 'asus-monitor',
- 'ASUS ROG' => 'asus-rog',
- 'Asus ROG Phone' => 'asus-rog-phone',
- 'Asus ROG Phone 2' => 'asus-rog-phone-2',
- 'ASUS Router' => 'asus-router',
- 'Asus Smartphone' => 'asus-smartphone',
- 'ASUS Vivobook' => 'asus-vivobook',
- 'ASUS Zenbook' => 'zenbook',
- 'Asus ZenFone 6' => 'asus-zenfone-6',
- 'Atari' => 'atari',
- 'Audi' => 'audi',
- 'Audio &amp; Hi-Fi' => 'audio',
- 'Audio Accessories' => 'audio-accessories',
- 'Audiobook' => 'audiobook',
- 'Audio Technica' => 'audio-technica',
- 'Aukey' => 'aukey',
- 'Aussie' => 'aussie',
- 'Autoglym' => 'autoglym',
- 'Aveeno' => 'aveeno',
- 'Avengers' => 'avengers',
- 'AVG' => 'avg',
- 'Aviva' => 'aviva',
- 'Avon' => 'avon',
- 'AV Receiver' => 'av-receiver',
- 'Axe' => 'axe',
- 'Baby Annabell' => 'baby-annabell',
- 'Baby Bath' => 'baby-bath',
- 'Baby Born' => 'baby-born',
- 'Baby Bottle' => 'baby-bottles',
- 'Baby Bouncer' => 'bouncer',
- 'Baby Carrier' => 'baby-carrier',
- 'Baby Clothes' => 'baby-clothes',
- 'Baby Food' => 'baby-food',
- 'Baby Gym' => 'baby-gym',
- 'Baby Jogger' => 'baby-jogger',
- 'Babyliss' => 'babyliss',
- 'Baby Monitor' => 'baby-monitor',
- 'Baby Shoes' => 'baby-shoes',
- 'Baby Swing' => 'baby-swing',
- 'Baby Walker' => 'baby-walker',
- 'Baby Wipes' => 'wipes',
- 'Bacardi' => 'bacardi',
- 'Backpack' => 'backpack',
- 'Back to the Future' => 'back-to-the-future',
- 'Bacon' => 'bacon',
- 'Badminton' => 'badminton',
- 'Bag' => 'bag',
- 'Bagless Vacuum Cleaner' => 'bagless-vacuum-cleaner',
- 'Bahco' => 'bahco',
- 'Baileys' => 'baileys',
- 'Baked Beans' => 'baked-beans',
- 'Bakery Products' => 'bakery-products',
- 'Baking' => 'baking',
- 'Ball Pit' => 'ball-pit',
- 'Ballpoint Pen' => 'pen',
- 'Band of Brothers' => 'band-of-brothers',
- 'Bang &amp; Olufsen' => 'bang-olufsen',
- 'Bank' => 'bank',
- 'Bank Account' => 'bank-account',
- 'Banks &amp; Credit Cards' => 'bank-credit-card',
- 'Barbell' => 'barbell',
- 'Barbie' => 'barbie',
- 'Barbour' => 'barbour',
- 'Barclaycard' => 'barclaycard',
- 'Barclays' => 'barclays',
- 'Barebones PC' => 'barebones',
- 'bareMinerals' => 'bareminerals',
- 'Barry M' => 'barry-m',
- 'Bar Stools' => 'bar-stools',
- 'Base Layer' => 'base-layer',
- 'Basket' => 'basket',
- 'Basketball' => 'basketball',
- 'Basmati Rice' => 'basmati-rice',
- 'Bath Mat' => 'bath-mat',
- 'Bathroom Accessories' => 'bathroom',
- 'Bathroom Cabinet' => 'bathroom-cabinet',
- 'Bathroom Scale' => 'bathroom-scales',
- 'Bathroom Tap' => 'tap',
- 'Batman' => 'batman',
- 'Battery' => 'battery',
- 'Battleborn' => 'battleborn',
- 'Battlefield' => 'battlefield',
- 'Battlefield 1' => 'battlefield-1',
- 'Battlefield 4' => 'battlefield-4',
- 'Battlefield 5' => 'battlefield-5',
- 'Battlestar Galactica' => 'battlestar-galactica',
- 'Baylis &amp; Harding' => 'baylis-and-harding',
- 'Bayonetta' => 'bayonetta',
- 'Bayonetta 2' => 'bayonetta-2',
- 'Baywatch' => 'baywatch',
- 'BB-8' => 'bb-8',
- 'BBC' => 'bbc',
- 'BBQ Food' => 'bbq',
- 'BBQs and Grills' => 'grill',
- 'Bean Bag' => 'bean-bag',
- 'Beanie Hat' => 'beanie-hat',
- 'Bean to Cup Machine' => 'bean-to-cup',
- 'Beard Trimmer' => 'beard-trimmer',
- 'Beats by Dre' => 'beats-by-dre',
- 'Beats Solo 3' => 'beats-solo-3',
- 'Beats Studio 3' => 'beats-studio-3',
- 'Beauty' => 'beauty-care',
- 'Beauty and the Beast' => 'beauty-and-the-beast',
- 'Becks' => 'becks',
- 'Bed' => 'bed',
- 'Bedding' => 'bedding',
- 'Bedding &amp; Linens' => 'bedding-linens',
- 'Bed Frame' => 'bed-frame',
- 'Bedroom' => 'bedroom-furniture',
- 'Beef' => 'beef',
- 'Beer' => 'beer',
- 'Beer Advent Calendar' => 'beer-advent-calendar',
- 'Beko' => 'beko',
- 'Belkin' => 'belkin',
- 'Belstaff' => 'belstaff',
- 'Belt' => 'belt',
- 'BelVita' => 'belvita',
- 'Ben &amp; Jerry&#039;s' => 'ben-jerrys',
- 'Benefit Cosmetics' => 'benefit-cosmetics',
- 'BenQ' => 'benq',
- 'BenQ Monitor' => 'benq-monitor',
- 'Ben Sherman' => 'ben-sherman',
- 'BeoPlay Headphones' => 'beoplay-headphones',
- 'Beoplay Speakers' => 'beoplay',
- 'Berghaus' => 'berghaus',
- 'Bestway' => 'bestway',
- 'Betting' => 'betting',
- 'Beyerdynamic' => 'beyerdynamic',
- 'Bic' => 'bic',
- 'Bike' => 'bike',
- 'Bike Accessories' => 'bike-accessories',
- 'Bike Brake' => 'brakes',
- 'Bike Computer' => 'bike-computer',
- 'Bike Helmet' => 'bicycle-helmet',
- 'Bike Inner Tube' => 'inner-tube',
- 'Bike Lights' => 'bike-lights',
- 'Bike Lock' => 'bike-lock',
- 'Bike Parts' => 'bike-parts',
- 'Bike Pump' => 'bike-pump',
- 'Biker Equipment' => 'biker-equipment',
- 'Bike Saddle' => 'saddle',
- 'Biking &amp; Urban Sports' => 'biking-urban-sports',
- 'Bikini' => 'bikini',
- 'Billabong' => 'billabong',
- 'Bin' => 'bin',
- 'Binatone' => 'binatone',
- 'Bingo' => 'bingo',
- 'Binoculars' => 'binoculars',
- 'Bio Oil' => 'bio-oil',
- 'Bioshock' => 'bioshock',
- 'Birds Eye' => 'birds-eye',
- 'Birkenstock' => 'birkenstock',
- 'Biscuits' => 'biscuits',
- 'Bissell' => 'bissell',
- 'Bistro Set' => 'bistro-set',
- 'Bitdefender' => 'bitdefender',
- 'Black &amp; Decker' => 'black-decker',
- 'Blackberry Smartphone' => 'blackberry',
- 'Blanket' => 'blanket',
- 'Blaupunkt' => 'blaupunkt',
- 'Blazer' => 'blazer',
- 'Bleach' => 'bleach',
- 'Blended Malt' => 'malt',
- 'Blender' => 'blender',
- 'Blinds' => 'blinds',
- 'Blink XT2 Smart Security Camera' => 'blink-xt2',
- 'Blizzard' => 'blizzard',
- 'Blood &amp; Truth' => 'blood-and-truth',
- 'Bloodborne' => 'bloodborne',
- 'Blood Pressure Monitor' => 'blood-pressure',
- 'Blu-ray' => 'blu-ray',
- 'Blu-ray Player' => 'blu-ray-player',
- 'Bluetooth Headphones' => 'bluetooth-headphones',
- 'Bluetooth Speaker' => 'bluetooth-speaker',
- 'BMW' => 'bmw',
- 'BMW Mini Cooper' => 'mini-cooper',
- 'BMX' => 'bmx',
- 'Board Game' => 'board-game',
- 'Boardman' => 'boardman',
- 'Boat Shoes' => 'boat-shoes',
- 'Bodum' => 'bodum',
- 'Bogof' => 'bogof',
- 'Boiler' => 'boiler',
- 'Bold' => 'bold',
- 'Bombay Sapphire' => 'bombay-sapphire',
- 'Bomber Jacket' => 'bomber-jacket',
- 'Bonne Maman' => 'bonne-maman',
- 'Bonsai' => 'bonsai',
- 'Book' => 'book',
- 'Bookcase' => 'bookcase',
- 'Books &amp; Magazines' => 'books-magazines',
- 'Booster Seat' => 'booster-seat',
- 'Boots' => 'boots',
- 'Borderlands' => 'borderlands',
- 'Borderlands 3' => 'borderlands-3',
- 'Bosch' => 'bosch',
- 'Bosch Dishwasher' => 'bosch-dishwasher',
- 'Bosch Drill' => 'bosch-drill',
- 'Bosch Fridge' => 'bosch-fridge',
- 'Bosch Rotak' => 'rotak',
- 'Bosch Washing Machine' => 'bosch-washing-machine',
- 'Bose' => 'bose',
- 'Bose Headphones' => 'bose-headphones',
- 'Bose Noise Cancelling Headphones 700' => 'bose-headphones-700',
- 'Bose QuietComfort' => 'bose-quietcomfort',
- 'Bose QuietComfort 35 II' => 'bose-quietcomfort-35-ii',
- 'Bose SoundLink' => 'bose-soundlink',
- 'Bose SoundLink Around-Ear II' => 'bose-soundlink-2',
- 'Bose SoundTouch' => 'bose-soundtouch',
- 'BOSS' => 'hugo-boss',
- 'Boss Bottled' => 'boss-bottled',
- 'Bouncy Castle' => 'bouncy-castle',
- 'Bourbon' => 'bourbon',
- 'Bourjois' => 'bourjois',
- 'Bowers &amp; Wilkins' => 'bowers-wilkins',
- 'Bowling' => 'bowling',
- 'Bowmore' => 'bowmore',
- 'Boxers' => 'boxers',
- 'Boxing' => 'boxing',
- 'Boxing Gloves' => 'boxing-gloves',
- 'Boy&#039;s Clothes' => 'clothes-for-boys',
- 'Bra' => 'bra',
- 'Brabantia' => 'brabantia',
- 'Bracelet' => 'bracelet',
- 'Brands' => 'brand',
- 'Brandy' => 'brandy',
- 'Branston' => 'branston',
- 'Branston Beans' => 'branston-beans',
- 'Braun' => 'braun',
- 'Braun Series 3' => 'braun-series-3',
- 'Braun Series 5' => 'braun-series-5',
- 'Braun Series 7' => 'braun-series-7',
- 'Braun Series 9' => 'braun-series-9',
- 'Braun Shaver' => 'braun-shaver',
- 'Bread' => 'bread',
- 'Breadmaker' => 'breadmaker',
- 'Breakdown Cover' => 'breakdown',
- 'Breaking Bad' => 'breaking-bad',
- 'Breast Pump' => 'breast-pump',
- 'Breville' => 'breville',
- 'Breville Blend Active' => 'blendactive',
- 'Brewdog' => 'brewdog',
- 'Bridge Camera' => 'bridge-camera',
- 'Briefcase' => 'briefcase',
- 'Brita' => 'brita',
- 'Britax' => 'britax',
- 'British Airways' => 'british-airways',
- 'Broadband' => 'broadband',
- 'Broadband &amp; Phone Contracts' => 'broadband-phone-service',
- 'Brogues' => 'brogues',
- 'Brother' => 'brother',
- 'Brother Printer' => 'brother-printer',
- 'Brownie' => 'brownie',
- 'BT' => 'bt',
- 'BT Sport' => 'bt-sport',
- 'Budweiser' => 'budweiser',
- 'Buffalo' => 'buffalo',
- 'Bugaboo' => 'bugaboo',
- 'Buggy' => 'buggy',
- 'Build-A-Bear' => 'build-a-bear',
- 'Bulb' => 'bulbs',
- 'Bulletstorm' => 'bulletstorm',
- 'Bulmers' => 'bulmers',
- 'Bulova' => 'bulova',
- 'Burberry' => 'burberry',
- 'Burger' => 'burger',
- 'Burnout Paradise' => 'burnout-paradise',
- 'Burt&#039;s Bees' => 'burts-bees',
- 'Bus and Coach Ticket' => 'bus',
- 'Bush' => 'bush',
- 'Bushmills' => 'bushmills',
- 'Butter' => 'butter',
- 'Buying From Abroad' => 'buying-from-abroad',
- 'Bvlgari' => 'bvlgari',
- 'Cabin Case' => 'cabin-case',
- 'Cabinet' => 'cabinet',
- 'Cable Reel' => 'cable-reel',
- 'Cables' => 'cables',
- 'Cadbury&#039;s' => 'cadbury',
- 'Café Rouge' => 'cafe-rouge',
- 'Cafetière' => 'cafetiere',
- 'Caffè Nero' => 'cafe-nero',
- 'Cake' => 'cake',
- 'Calculator' => 'calculator',
- 'Calendar' => 'calendar',
- 'Call of Duty' => 'call-of-duty',
- 'Call of Duty: Black Ops' => 'black-ops',
- 'Call of Duty: Black Ops 3' => 'black-ops-3',
- 'Call of Duty: Black Ops 4' => 'black-ops-4',
- 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war',
- 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare',
- 'Call of Duty: Modern Warfare' => 'modern-warfare',
- 'Call of Duty: WW2' => 'call-of-duty-ww2',
- 'Calpol' => 'calpol',
- 'Calvin Klein' => 'calvin-klein',
- 'Camcorder' => 'camcorder',
- 'Camelbak' => 'camelbak',
- 'Camera' => 'camera',
- 'Camera Accessories' => 'camera-accessories',
- 'Camera Bag' => 'camera-bag',
- 'Camera Lens' => 'lens',
- 'Camping' => 'camping',
- 'Campingaz' => 'campingaz',
- 'Candle' => 'candle',
- 'Cannondale' => 'cannondale',
- 'Canon' => 'canon',
- 'Canon Camera' => 'canon-camera',
- 'Canon EOS' => 'canon-eos',
- 'Canon Lens' => 'canon-lens',
- 'Canon Pixma' => 'canon-pixma',
- 'Canon PowerShot' => 'canon-powershot',
- 'Canon PowerShot SX430 IS' => 'canon-powershot-sx430-is',
- 'Canon Printer' => 'canon-printer',
- 'Canterbury' => 'canterbury',
- 'Canton' => 'canton',
- 'Canvas Print' => 'canvas-print',
- 'Cap' => 'cap',
- 'Capsule Machine' => 'capsule-machine',
- 'Captain America' => 'captain-america',
- 'Captain Morgan' => 'captain-morgan',
- 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker',
- 'Car' => 'car',
- 'Car &amp; Motorcycle' => 'car-motorcycle',
- 'Car Accessories' => 'car-accessories',
- 'Caravan' => 'caravan',
- 'Car Battery' => 'car-battery',
- 'Carbon Monoxide Detector' => 'carbon-monoxide',
- 'Car Care' => 'car-care',
- 'Car Charger' => 'car-charger',
- 'Cardhu' => 'cardhu',
- 'Cardigan' => 'cardigan',
- 'Card Reader' => 'card-reader',
- 'Carex' => 'carex',
- 'Carhartt' => 'carhartt',
- 'Car Hire' => 'car-hire',
- 'Car Insurance' => 'car-insurance',
- 'Car Leasing' => 'car-lease',
- 'Carling' => 'carling',
- 'Car Lock' => 'lock',
- 'Carlsberg' => 'carlsberg',
- 'Car Mats' => 'car-mats',
- 'Carolina Herrera' => 'carolina-herrera',
- 'Car Parts' => 'car-parts',
- 'Carpet' => 'carpet',
- 'Carpet Cleaner' => 'carpet-cleaner',
- 'CarPlan' => 'carplan',
- 'Car Polish' => 'car-polish',
- 'Carrera Bikes' => 'carrera',
- 'Car Seat' => 'car-seat',
- 'Car Service' => 'car-service',
- 'Car Stereo' => 'car-stereo',
- 'Car Wash' => 'car-wash',
- 'Car Wax' => 'car-wax',
- 'Casio' => 'casio',
- 'Casio Eco-Drive' => 'eco-drive',
- 'Casio Edifice' => 'edifice',
- 'Casio G-Shock' => 'g-shock',
- 'Casserole' => 'casserole',
- 'Cast Iron Pots and Pans' => 'cast-iron',
- 'Castrol' => 'castrol',
- 'Caterpillar' => 'caterpillar',
- 'Cat Flap' => 'cat-flap',
- 'Cat Food' => 'cat-food',
- 'Cath Kidston' => 'cath-kidston',
- 'Cat Supplies' => 'cat-supplies',
- 'CCTV' => 'cctv',
- 'CD' => 'cd',
- 'CD Player' => 'cd-player',
- 'Ceiling Light' => 'ceiling-light',
- 'Celebrations' => 'celebrations',
- 'Cereal' => 'cereal',
- 'Cetirizine' => 'cetirizine',
- 'Chad Valley' => 'chad-valley',
- 'Chainsaw' => 'chainsaw',
- 'Champagne' => 'champagne',
- 'Champneys' => 'champneys',
- 'Chanel' => 'chanel',
- 'Chanel Coco Mademoiselle' => 'coco-mademoiselle',
- 'Changing Bag' => 'changing-bag',
- 'Channel 4' => 'channel-4',
- 'Charger' => 'charger',
- 'Cheese' => 'cheese',
- 'Chelsea Boots' => 'chelsea-boots',
- 'Chelsea F. C.' => 'chelsea',
- 'Chess' => 'chess',
- 'Chessington' => 'chessington',
- 'Chest Freezer' => 'chest-freezer',
- 'Chest of Drawers' => 'chest-of-drawers',
- 'Chicco' => 'chicco',
- 'Chicken' => 'chicken',
- 'Childcare' => 'baby',
- 'Children&#039;s Books' => 'childrens-books',
- 'Chino' => 'chino',
- 'Chisel' => 'chisel',
- 'Chloe' => 'chloe',
- 'Chocolate' => 'chocolate',
- 'Chocolate Advent Calendar' => 'chocolate-advent-calendar',
- 'Chopper' => 'chopper',
- 'Chopping Board' => 'chopping-board',
- 'Christmas Card' => 'christmas-card',
- 'Christmas Decoration' => 'christmas-decorations',
- 'Christmas Gift' => 'christmas-gifts',
- 'Christmas Jumper' => 'christmas-jumper',
- 'Christmas Lights' => 'christmas-lights',
- 'Christmas Stocking Fillers' => 'christmas-stocking-fillers',
- 'Christmas Toys' => 'christmas-toys',
- 'Christmas Tree' => 'christmas-tree',
- 'Chromebook' => 'chromebook',
- 'Chromecast' => 'chromecast',
- 'Chromecast Ultra' => 'chromecast-ultra',
- 'Chromecast with Google TV' => 'chromecast-google-tv',
- 'Chronograph' => 'chronograph',
- 'Chupa Chups' => 'chupa-chups',
- 'Chuwi' => 'chuwi',
- 'Cider' => 'cider',
- 'Cinema' => 'cinema',
- 'Cineworld' => 'cineworld',
- 'Circular Saw' => 'circular-saw',
- 'Circulon' => 'circulon',
- 'Ciroc' => 'ciroc',
- 'Cities Skylines' => 'cities-skylines',
- 'Citizen' => 'citizen',
- 'Citroen' => 'citroen',
- 'City Break' => 'city-breaks',
- 'Civilization' => 'civilization',
- 'Clarins' => 'clarins',
- 'Clarks' => 'clarks',
- 'Clearance' => 'clearance',
- 'Climbing' => 'climbing',
- 'Climbing Frame' => 'climbing-frame',
- 'Clinique' => 'clinique',
- 'Clothes' => 'clothes',
- 'Cloud Service' => 'cloud',
- 'Clutch Bag' => 'clutch',
- 'Coat' => 'coat',
- 'Coca Cola' => 'coke',
- 'Cocktail' => 'cocktail',
- 'Coconut Oil' => 'coconut',
- 'Coffee' => 'coffee',
- 'Coffee Beans' => 'coffee-beans',
- 'Coffee Machine' => 'coffee-machine',
- 'Coffee Pods' => 'coffee-pods',
- 'Coffee Table' => 'coffee-table',
- 'Cognac' => 'cognac',
- 'Cola' => 'cola',
- 'Coleman' => 'coleman',
- 'Colgate' => 'colgate',
- 'Combi Drill' => 'combi',
- 'Comfort' => 'comfort',
- 'Comic' => 'comic',
- 'Command &amp; Conquer' => 'command-and-conquer',
- 'Compact Camera' => 'compact-camera',
- 'Compact Flash' => 'compact-flash',
- 'Competitions' => 'competitions',
- 'Compost' => 'compost',
- 'Compressor' => 'compressor',
- 'Computer Accessories' => 'computer-accessories',
- 'Computers &amp; Tablets' => 'computers',
- 'Concert' => 'concert',
- 'Condé Nast' => 'conde-nast',
- 'Conditioner' => 'conditioner',
- 'Condom' => 'condom',
- 'Connectors' => 'connectors',
- 'Contact Lenses' => 'contact-lenses',
- 'Contents Insurance' => 'contents-insurance',
- 'Controller' => 'controller',
- 'Converse' => 'converse',
- 'Converse Chuck Taylor' => 'chuck-taylor',
- 'Cooker' => 'cooker',
- 'Cooking Oil' => 'cooking-oil',
- 'Cookware' => 'cooking',
- 'Cookware Set' => 'cookware-set',
- 'Cookworks' => 'cookworks',
- 'Cool Box' => 'cool-box',
- 'Coors Light' => 'coors-light',
- 'Cordless Drill' => 'cordless-drill',
- 'Cordless Phone' => 'cordless-phone',
- 'Cornetto' => 'cornetto',
- 'Corona Beer' => 'corona',
- 'Corsair' => 'corsair',
- 'Cosatto' => 'cosatto',
- 'Costa Coffee' => 'costa-coffee',
- 'Costume' => 'costume',
- 'Cot' => 'cot',
- 'Counter Strike' => 'counter-strike',
- 'Courses and Training' => 'education',
- 'Cow &amp; Gate' => 'cow-and-gate',
- 'Cozy Coupe' => 'cozy-coupe',
- 'CPU' => 'cpu',
- 'CPU Cooler' => 'cpu-cooler',
- 'Craghoppers' => 'craghoppers',
- 'Crash Bandicoot' => 'crash-bandicoot',
- 'Crash Team Racing Nitro-Fueled' => 'crash-team-racing-nitro-fueled',
- 'Crayola' => 'crayola',
- 'Creatine' => 'creatine',
- 'Credit Card' => 'credit-card',
- 'Creme Egg' => 'creme-egg',
- 'Cricket' => 'cricket',
- 'Crisps' => 'crisps',
- 'Crocs' => 'crocs',
- 'Cross Trainer' => 'cross-trainer',
- 'Crown Paint' => 'crown',
- 'Crucial' => 'crucial',
- 'Cruelty Free Makeup' => 'cruelty-free-makeup',
- 'Cruises' => 'cruise',
- 'Cube Bikes' => 'cube',
- 'Cubot' => 'cubot',
- 'Cufflinks' => 'cufflinks',
- 'Culture &amp; Leisure' => 'entertainment',
- 'Cuphead' => 'cuphead',
- 'Cuprinol' => 'cuprinol',
- 'Curling Wand' => 'curling-wand',
- 'Curtain' => 'curtain',
- 'Cushelle' => 'cushelle',
- 'Cushion' => 'cushion',
- 'Cutlery' => 'cutlery',
- 'CyberLink' => 'cyberlink',
- 'Cyberpunk 2077' => 'cyberpunk-2077',
- 'Cybex' => 'cybex',
- 'Cycling' => 'cycling',
- 'Cycling Jacket' => 'cycling-jacket',
- 'D-Link' => 'd-link',
- 'DAB Radio' => 'dab-radio',
- 'Dacia' => 'dacia',
- 'Daily Mail' => 'daily-mail',
- 'Dairy Milk' => 'dairy-milk',
- 'Darksiders' => 'darksiders',
- 'Dark Souls' => 'dark-souls',
- 'Dark Souls 3' => 'dark-souls-3',
- 'Dartboard' => 'dartboard',
- 'Darts' => 'darts',
- 'Dash Cam' => 'dash-cam',
- 'Data Storage' => 'storage',
- 'Davidoff' => 'davidoff',
- 'Days Gone' => 'days-gone',
- 'Days Out' => 'days-out',
- 'Daz' => 'daz',
- 'DC Comic' => 'dc',
- 'DDR3' => 'ddr3',
- 'DDR4' => 'ddr4',
- 'Dead Island' => 'dead-island',
- 'Dead or Alive 6' => 'dead-or-alive-6',
- 'Deadpool' => 'deadpool',
- 'Dead Rising' => 'dead-rising',
- 'Death Stranding' => 'death-stranding',
- 'Deezer' => 'deezer',
- 'Dehumidifier' => 'dehumidifier',
- 'Dell' => 'dell',
- 'Dell Laptop' => 'dell-laptop',
- 'Dell Monitor' => 'dell-monitor',
- 'Dell XPS' => 'xps',
- 'Delonghi' => 'delonghi',
- 'Demon&#039;s Souls' => 'demon-souls',
- 'Denby' => 'denby',
- 'Denon' => 'denon',
- 'Deodorant' => 'deodorant',
- 'Desk' => 'desk',
- 'Desperados Beer' => 'desperados',
- 'Despicable Me' => 'despicable-me',
- 'Destiny' => 'destiny',
- 'Destiny 2' => 'destiny-2',
- 'Detergent' => 'detergent',
- 'Detroit: Become Human' => 'detroit-become-human',
- 'Dettol' => 'dettol',
- 'Deus Ex' => 'deus-ex',
- 'Deus Ex: Mankind Divided' => 'deus-ex-mankind-divided',
- 'Development Boards' => 'development-boards',
- 'Devil May Cry 5' => 'devil-may-cry-5',
- 'DeWalt' => 'dewalt',
- 'DFDS' => 'dfds',
- 'Diablo 3' => 'diablo-3',
- 'Diary' => 'diary',
- 'Dickies' => 'dickies',
- 'Diesel' => 'diesel',
- 'Diet' => 'diet',
- 'Diggerland' => 'diggerland',
- 'Digihome' => 'digihome',
- 'Digimon' => 'digimon',
- 'Digital Camera' => 'digital-camera',
- 'Digital Watch' => 'digital-watch',
- 'Dildo' => 'dildo',
- 'Dimplex' => 'dimplex',
- 'Dining Room' => 'dining-room',
- 'Dining Room Chair' => 'chair',
- 'Dining Set' => 'dining-set',
- 'Dining Table' => 'dining-table',
- 'Dinner Plate' => 'plates',
- 'Dinner Set' => 'dinner-set',
- 'Dinosaur' => 'dinosaur',
- 'Dior' => 'dior',
- 'Dior Sauvage' => 'dior-sauvage',
- 'Dirt' => 'dirt',
- 'Dirt 4' => 'dirt-4',
- 'DIRT 5' => 'dirt-5',
- 'Dirt Rally 2.0' => 'dirt-rally-2',
- 'Disaronno' => 'disaronno',
- 'Discord Nitro' => 'discord-nitro',
- 'Disgaea' => 'disgaea',
- 'Dishonored' => 'dishonored',
- 'Dishonored 2' => 'dishonored-2',
- 'Dishwasher' => 'dishwasher',
- 'Dishwasher Tablets' => 'dishwasher-tablets',
- 'Disinfectants' => 'disinfectants',
- 'Disney' => 'disney',
- 'Disney&#039;s Cars' => 'disney-cars',
- 'Disney&#039;s Frozen' => 'disney-frozen',
- 'Disney+' => 'disney-plus',
- 'Disney Infinity' => 'disney-infinity',
- 'Disneyland' => 'disneyland',
- 'Disney Princess' => 'disney-princess',
- 'Disney Tsum Tsum' => 'tsum-tsum',
- 'Disney World' => 'disney-world',
- 'Divan' => 'divan',
- 'DIY' => 'diy',
- 'DJ Equipment' => 'dj',
- 'DJI Phantom' => 'dji-phantom',
- 'DKNY' => 'dkny',
- 'Doctor Who' => 'doctor-who',
- 'Dog Bed' => 'dog-bed',
- 'Dog Food' => 'dog-food',
- 'Dog Supplies' => 'dog',
- 'Dolce &amp; Gabbana' => 'dolce',
- 'Dolce Gusto' => 'dolce-gusto',
- 'Dolce Gusto Coffee Machine' => 'dolce-gusto-coffee-machine',
- 'Doll' => 'doll',
- 'Dolls House' => 'dolls-house',
- 'Domain Service' => 'domain',
- 'Doogee' => 'doogee',
- 'Doom' => 'doom',
- 'Door' => 'door',
- 'Doorbell' => 'doorbell',
- 'Door Handles' => 'door-handles',
- 'Doormat' => 'doormat',
- 'Doritos' => 'doritos',
- 'Dove' => 'dove',
- 'Down Jacket' => 'down-jacket',
- 'Downton Abbey' => 'downton-abbey',
- 'Dr. Martens' => 'dr-martens',
- 'Dragon Age' => 'dragon-age',
- 'Dragon Ball' => 'dragon-ball',
- 'Dragon Ball: FighterZ' => 'dragon-ball-fighterz',
- 'Dragon Quest' => 'dragon-quest',
- 'Dragon Quest Builders' => 'dragon-quest-builders',
- 'Dragon Quest Builders 2' => 'dragon-quest-builders-2',
- 'Dragon Quest XI: Echoes of an Elusive Age' => 'dragon-quest-xi',
- 'Draper' => 'draper',
- 'Drayton Manor' => 'drayton-manor',
- 'Dreame T20' => 'dreame-t20',
- 'Dreame V9' => 'dreame-v9',
- 'Dreame V9P' => 'dreame-v9p',
- 'Dreame V10' => 'dreame-v10',
- 'Dreame V11' => 'dreame-v11',
- 'Dreame Vacuum Cleaner' => 'xiaomi-vacuum-cleaner',
- 'Dremel' => 'dremel',
- 'Dress' => 'dress',
- 'Dressing Gown' => 'dressing-gown',
- 'Drill' => 'drill',
- 'Drill Driver' => 'driver',
- 'Drinks' => 'drinks',
- 'Driveclub' => 'driveclub',
- 'Driving Lessons' => 'driving-lessons',
- 'Drone' => 'drone',
- 'Dryer' => 'dryer',
- 'DSLR Camera' => 'dslr',
- 'Dual Fuel Cooker' => 'dual-fuel',
- 'Dualit' => 'dualit',
- 'Dual Sim' => 'sim',
- 'Dulux' => 'dulux',
- 'Duracell' => 'duracell',
- 'Durex' => 'durex',
- 'Duvet' => 'duvet',
- 'DVD' => 'dvd',
- 'DVD Player' => 'dvd-player',
- 'Dying Light' => 'dying-light',
- 'Dymo' => 'dymo',
- 'Dyson' => 'dyson',
- 'Dyson Supersonic' => 'dyson-supersonic',
- 'Dyson V6' => 'dyson-v6',
- 'Dyson V7' => 'dyson-v7',
- 'Dyson V8' => 'dyson-v8',
- 'Dyson V10' => 'dyson-v10',
- 'Dyson V11' => 'dyson-v11',
- 'Dyson Vacuum Cleaner' => 'dyson-vacuum-cleaner',
- 'e-Reader' => 'ereader',
- 'EA' => 'ea',
- 'EA Access' => 'ea-access',
- 'Earphones' => 'earphones',
- 'Earrings' => 'earrings',
- 'EA Sports' => 'ea-sports',
- 'EA Sports UFC' => 'ufc',
- 'Easter Eggs' => 'egg',
- 'Eastpak' => 'eastpak',
- 'eBook' => 'ebook',
- 'Ecovacs' => 'ecovacs',
- 'Ecover' => 'ecover',
- 'Educational Toys' => 'educational-toys',
- 'EE' => 'ee',
- 'eFootball PES 2021' => 'pes-2021',
- 'ELC Happyland' => 'happyland',
- 'Electrical Accessories' => 'electrical-accessories',
- 'Electric Bike' => 'electric-bike',
- 'Electric Blanket' => 'electric-blanket',
- 'Electric Cooker' => 'electric-cooker',
- 'Electric Fires' => 'electric-fire',
- 'Electric Scooter' => 'electric-scooter',
- 'Electric Shower' => 'electric-shower',
- 'Electric Toothbrush' => 'electric-toothbrush',
- 'Electronic Accessories' => 'electronics-accessories',
- 'Electronics' => 'electronics',
- 'Elemis' => 'elemis',
- 'Elephone' => 'elephone',
- 'Elgato' => 'elgato',
- 'Elite Dangerous' => 'elite-dangerous',
- 'Elizabeth Arden' => 'elizabeth-arden',
- 'Emirates' => 'emirates',
- 'Endura' => 'endura',
- 'Eneloop' => 'eneloop',
- 'Energizer' => 'energizer',
- 'Energy' => 'energy',
- 'Energy, Heating &amp; Gas' => 'energy-heating-gas',
- 'Energy Drinks' => 'energy-drinks',
- 'Engine Oil' => 'engine-oil',
- 'Epilator' => 'epilator',
- 'Epson' => 'epson',
- 'Epson Printer' => 'epson-printer',
- 'Espresso' => 'espresso',
- 'Espresso Machine' => 'espresso-machine',
- 'Esprit' => 'esprit',
- 'Estée Lauder' => 'estee-lauder',
- 'Ethernet' => 'ethernet',
- 'Etnies' => 'etnies',
- 'Eurostar Ticket' => 'eurostar',
- 'Eurotunnel' => 'eurotunnel',
- 'Everton F. C.' => 'everton',
- 'EVGA' => 'evga',
- 'Evian' => 'evian',
- 'Exercise Equipment' => 'exercise-equipment',
- 'Exercise Weights' => 'weight',
- 'Extension Lead' => 'extension-lead',
- 'External Hard Drive' => 'external-hard-drive',
- 'F1' => 'formula-one',
- 'F1 2017' => 'f1-2017',
- 'F1 2018' => 'f1-2018',
- 'F1 2019' => 'f1-2019',
- 'F1 2020' => 'f1-2020',
- 'Fabric Conditioner' => 'fabric-conditioner',
- 'Face Cream' => 'face-cream',
- 'Face Mask' => 'face-mask',
- 'Fairy' => 'fairy',
- 'Fairy Light' => 'fairy-light',
- 'Fallout' => 'fallout',
- 'Fallout 4' => 'fallout-4',
- 'Fallout 76' => 'fallout-76',
- 'Family &amp; Kids' => 'kids',
- 'Family Break' => 'family-break',
- 'Family Guy' => 'family-guy',
- 'Famous Grouse' => 'famous-grouse',
- 'Fancy Dress' => 'fancy-dress',
- 'Fans' => 'fan',
- 'Fanta' => 'fanta',
- 'Far Cry' => 'far-cry',
- 'Far Cry 4' => 'far-cry-4',
- 'Far Cry 5' => 'far-cry-5',
- 'Far Cry New Dawn' => 'far-cry-new-dawn',
- 'Far Cry Primal' => 'far-cry-primal',
- 'Farming Simulator' => 'farming-simulator',
- 'Fashion &amp; Accessories' => 'fashion',
- 'Fashion Accessories' => 'fashion-accessories',
- 'Fashion for Men' => 'mens-clothing',
- 'Fashion for Women' => 'womens-clothes',
- 'Fast and Furious' => 'fast-and-furious',
- 'Father&#039;s Day' => 'fathers-day',
- 'FatMax' => 'fatmax',
- 'FC Barcelona' => 'fc-barcelona',
- 'Felix' => 'felix',
- 'Fence' => 'fence',
- 'Fender Guitar' => 'fender',
- 'Ferrero Rocher' => 'ferrero-rocher',
- 'Ferry' => 'ferry',
- 'Festival' => 'festival',
- 'Fever Thermometer' => 'thermometer',
- 'Fiat' => 'fiat',
- 'Fidget Spinner' => 'spinner',
- 'FIFA' => 'fifa',
- 'FIFA 17' => 'fifa-17',
- 'FIFA 18' => 'fifa-18',
- 'FIFA 19' => 'fifa-19',
- 'FIFA 20' => 'fifa-20',
- 'FIFA 21' => 'fifa-21',
- 'FightStick' => 'fightstick',
- 'Figures' => 'figures',
- 'Fila Trainers' => 'fila-trainers',
- 'Filing Cabinet' => 'filing-cabinet',
- 'Final Fantasy' => 'final-fantasy',
- 'Final Fantasy 15' => 'final-fantasy-15',
- 'Finance &amp; Insurance' => 'personal-finance',
- 'Finish' => 'finish',
- 'Finlux' => 'finlux',
- 'Fiorelli' => 'fiorelli',
- 'Fire Emblem' => 'fire-emblem',
- 'Fire Pit' => 'fire-pit',
- 'Fireplace' => 'fireplace',
- 'Firewall: Zero Hour' => 'firewall-zero-hour',
- 'First Aid' => 'first-aid',
- 'Fish &amp; Seafood' => 'fish-and-seafood',
- 'Fish and Aquatic Pet Supplies' => 'fish',
- 'Fisher Price' => 'fisher-price',
- 'Fisher Price Imaginext' => 'imaginext',
- 'Fisher Price Jumperoo' => 'jumperoo',
- 'Fisher Price Little People' => 'little-people',
- 'Fishing' => 'fishing',
- 'Fiskars' => 'fiskars',
- 'Fitbit' => 'fitbit',
- 'Fitbit Alta' => 'fitbit-alta',
- 'Fitbit Blaze' => 'fitbit-blaze',
- 'Fitbit Charge 2' => 'fitbit-charge-2',
- 'Fitbit Inspire' => 'fitbit-inspire',
- 'Fitbit Versa' => 'fitbit-versa',
- 'Fitness &amp; Running' => 'fitness',
- 'Fitness App' => 'fitness-app',
- 'Fitness Tracker' => 'fitness-tracker',
- 'Flamingo Land' => 'flamingo-land',
- 'Flea Treatment' => 'flea',
- 'Fleece Clothing' => 'fleece',
- 'Flights' => 'flight',
- 'Flip Flops' => 'flip-flops',
- 'Floodlight' => 'floodlight',
- 'Flooring' => 'flooring',
- 'Flowers' => 'flowers',
- 'Flymo' => 'flymo',
- 'FM Transmitter' => 'fm-transmitter',
- 'Food' => 'food',
- 'Food Containers' => 'food-containers',
- 'Food Processor' => 'food-processor',
- 'Food Server' => 'food-server',
- 'Football' => 'football',
- 'Football Boots' => 'football-boots',
- 'Football Manager' => 'football-manager',
- 'Football Matches' => 'football-matches',
- 'Football Shirt' => 'football-shirt',
- 'Foot Pump' => 'foot-pump',
- 'Ford' => 'ford',
- 'For Honor' => 'for-honor',
- 'Fortnite' => 'fortnite',
- 'Fortnite: Darkfire' => 'fortnite-darkfire',
- 'Forza' => 'forza',
- 'Forza 7' => 'forza-7',
- 'Forza Horizon' => 'forza-horizon',
- 'Forza Horizon 3' => 'forza-horizon-3',
- 'Forza Horizon 4' => 'forza-horizon-4',
- 'Forza Motorsport' => 'forza-motorsport',
- 'Foscam' => 'foscam',
- 'Fossil' => 'fossil',
- 'Foster&#039;s' => 'fosters',
- 'Foundation' => 'foundation',
- 'Fountain Pen' => 'fountain-pen',
- 'Fred Perry' => 'fred-perry',
- 'Freesat' => 'freesat',
- 'Freeview' => 'freeview',
- 'Freezer' => 'freezer',
- 'Fridge' => 'fridge',
- 'Fridge Freezer' => 'fridge-freezer',
- 'Frontline' => 'frontline',
- 'Frozen Food' => 'frozen',
- 'Fruit' => 'fruit',
- 'Fruit and Vegetables' => 'fruit-and-vegetable',
- 'Fruit of the Loom' => 'fruit-of-the-loom',
- 'Fryer' => 'fryer',
- 'Frying Pan' => 'frying-pan',
- 'Fujifilm' => 'fuji',
- 'Fujitsu' => 'fujitsu',
- 'Funko Pop' => 'funko-pop',
- 'Furby' => 'furby',
- 'Furniture' => 'furniture',
- 'G-Star' => 'g-star',
- 'G-Sync Monitor' => 'g-sync',
- 'Gaggia' => 'gaggia',
- 'Gambling' => 'gambling',
- 'Game App' => 'game-app',
- 'Game of Thrones' => 'game-of-thrones',
- 'Games &amp; Board Games' => 'board-games',
- 'Games Consoles' => 'console',
- 'Gaming' => 'gaming',
- 'Gaming Accessories' => 'gaming-accessories',
- 'Gaming Chair' => 'gaming-chair',
- 'Gaming Headset' => 'gaming-headset',
- 'Gaming Keyboard' => 'gaming-keyboard',
- 'Gaming Laptop' => 'gaming-laptop',
- 'Gaming Monitor' => 'gaming-monitor',
- 'Gaming Mouse' => 'gaming-mouse',
- 'Gaming PC' => 'gaming-pc',
- 'Gant' => 'gant',
- 'Garage' => 'garage',
- 'Garage &amp; Service' => 'garage-service',
- 'Garden' => 'garden',
- 'Garden &amp; Do It Yourself' => 'garden-diy',
- 'Garden Furniture' => 'garden-furniture',
- 'Gardening' => 'gardening',
- 'Garden Storage' => 'garden-storage',
- 'Garden Table' => 'table',
- 'Garmin' => 'garmin',
- 'Garmin Fenix' => 'garmin-fenix',
- 'Garmin Fenix 6' => 'garmin-fenix-6',
- 'Garmin Fenix 6 Pro' => 'garmin-fenix-6-pro',
- 'Garmin Forerunner' => 'garmin-forerunner',
- 'Garmin Vivoactive' => 'garmin-vivoactive',
- 'Garmin Watch' => 'garmin-watch',
- 'Garnier' => 'garnier',
- 'Gas' => 'gas',
- 'Gas Canister' => 'butane',
- 'Gas Cooker' => 'gas-cooker',
- 'Gatwick' => 'gatwick',
- 'Gazebo' => 'gazebo',
- 'GBK' => 'gbk',
- 'Gears 5' => 'gears-5',
- 'Gears of War' => 'gears-of-war',
- 'Gears of War 4' => 'gears-of-war-4',
- 'George Foreman' => 'george-foreman',
- 'Geox' => 'geox',
- 'GHD' => 'ghd',
- 'Ghostbusters' => 'ghostbusters',
- 'Ghostbusters: The Video Game Remastered' => 'ghostbusters-the-video-game',
- 'Ghost of Tsushima' => 'ghost-of-tsushima',
- 'Gibson Guitar' => 'gibson',
- 'giffgaff' => 'giffgaff',
- 'Gift Card' => 'gift-card',
- 'Gift Hamper' => 'hamper',
- 'Gifts' => 'gifts',
- 'Gift Set' => 'gift-set',
- 'GIGABYTE' => 'gigabyte',
- 'Gigaset' => 'gigaset',
- 'Gilet' => 'gilet',
- 'Gillette Fusion' => 'fusion',
- 'Gillette Mach3' => 'mach-3',
- 'Gillette Razor' => 'gillette',
- 'Gimbal' => 'gimbal',
- 'Gin' => 'gin',
- 'Girl&#039;s Clothes' => 'girls-clothes',
- 'Glasses' => 'glasses',
- 'Glassware' => 'glassware',
- 'Glenfiddich' => 'glenfiddich',
- 'Glenlivet' => 'glenlivet',
- 'Glenmorangie' => 'glenmorangie',
- 'Gloves' => 'gloves',
- 'Glue' => 'glue',
- 'Glue Gun' => 'glue-gun',
- 'Gluten-Free' => 'gluten-free',
- 'God of War' => 'god-of-war',
- 'Go Kart' => 'go-kart',
- 'Golf' => 'golf',
- 'Golf Balls' => 'golf-balls',
- 'Golf Clubs' => 'golf-clubs',
- 'Goodfellas' => 'goodfellas',
- 'Goodmans' => 'goodmans',
- 'Goodyear' => 'goodyear',
- 'Google' => 'google',
- 'Google Home' => 'google-home',
- 'Google Home Max' => 'google-home-max',
- 'Google Home Mini' => 'google-home-mini',
- 'Google Nest' => 'nest',
- 'Google Nest Audio' => 'google-nest-audio',
- 'Google Nest Hub' => 'google-home-hub',
- 'Google Nest Mini' => 'nest-mini',
- 'Google Nest Protect' => 'google-nest-protect',
- 'Google Nexus' => 'nexus',
- 'Google Pixel' => 'google-pixel',
- 'Google Pixel 2' => 'google-pixel-2',
- 'Google Pixel 2 XL' => 'google-pixel-2-xl',
- 'Google Pixel 3' => 'google-pixel-3',
- 'Google Pixel 3 XL' => 'google-pixel-3-xl',
- 'Google Pixel 3a' => 'google-pixel-3a',
- 'Google Pixel 3a XL' => 'google-pixel-3a-xl',
- 'Google Pixel 4' => 'google-pixel-4',
- 'Google Pixel 4 XL' => 'google-pixel-4-xl',
- 'Google Pixel 4a' => 'google-pixel-4a',
- 'Google Pixel 4a 5G' => 'google-pixel-4a-5g',
- 'Google Pixel 5' => 'google-pixel-5',
- 'Google Pixelbook' => 'google-pixelbook',
- 'Google Pixel XL' => 'google-pixel-xl',
- 'Google Smartphone' => 'google-smartphone',
- 'Google Stadia' => 'google-stadia',
- 'GoPro' => 'gopro',
- 'GoPro HERO 6' => 'gopro-hero-6',
- 'GoPro HERO 7' => 'gopro-hero-7',
- 'GoPro HERO 8' => 'gopro-hero-8',
- 'GoPro HERO 9' => 'gopro-hero-9',
- 'Gore-Tex Clothing and Shoes' => 'gore-tex',
- 'Graco' => 'graco',
- 'Grand National' => 'grand-national',
- 'Gran Turismo' => 'gran-turismo',
- 'Gran Turismo Sport' => 'gran-turismo-sport',
- 'Graphics Card' => 'graphics-card',
- 'Gravity Rush' => 'gravity-rush',
- 'Graze' => 'graze',
- 'GreedFall' => 'greedfall',
- 'Greenhouse' => 'greenhouse',
- 'Greeting Cards and Wrapping Paper' => 'wrapping-paper-and-cards',
- 'Greggs' => 'greggs',
- 'Grey Goose' => 'grey-goose',
- 'Griffin Technology' => 'griffin',
- 'GroBag' => 'grobag',
- 'Groceries' => 'groceries',
- 'Gruffalo' => 'gruffalo',
- 'Grundig' => 'grundig',
- 'GTA' => 'gta',
- 'GTA V' => 'gta-v',
- 'GTX 970' => 'gtx-970',
- 'GTX 980' => 'gtx-980',
- 'GTX 1060' => 'gtx-1060',
- 'GTX 1070' => 'gtx-1070',
- 'GTX 1080' => 'gtx-1080',
- 'GTX 1080 Ti' => 'gtx-1080-ti',
- 'GTX 1660' => 'gtx-1660',
- 'GTX 1660 Ti' => 'gtx-1660-ti',
- 'Guardians of the Galaxy' => 'guardians-of-the-galaxy',
- 'Gucci' => 'gucci',
- 'Guinness' => 'guinness',
- 'Guitar' => 'guitar',
- 'Guitar Amp' => 'guitar-amp',
- 'Guitar Hero' => 'guitar-hero',
- 'Gulliver&#039;s' => 'gullivers',
- 'Gym' => 'gym',
- 'Gym Membership' => 'gym-membership',
- 'H1Z1' => 'h1z1',
- 'Häagen Dazs' => 'haagen-dazs',
- 'Habitat' => 'habitat',
- 'Hacksaw' => 'hacksaw',
- 'Hair Brush' => 'hair-brush',
- 'Hair Care' => 'hair',
- 'Hair Clipper' => 'hair-clipper',
- 'Hair Colour' => 'hair-colour',
- 'Haircut' => 'haircut',
- 'Hair Dryer' => 'hair-dryer',
- 'Hair Dye' => 'hair-dye',
- 'Hair Removal Devices' => 'hair-removal-devices',
- 'Halifax' => 'halifax',
- 'Hall' => 'hall',
- 'Halloween' => 'halloween',
- 'Halo' => 'halo',
- 'Halo 5' => 'halo-5',
- 'Ham' => 'ham',
- 'Hammer' => 'hammer',
- 'Hammer Drill' => 'hammer-drill',
- 'Hammock' => 'hammock',
- 'Handbag' => 'handbag',
- 'Hand Blender' => 'hand-blender',
- 'Hand Cream' => 'hand-cream',
- 'Hand Mixer' => 'hand-mixer',
- 'Hand Tools' => 'hand-tools',
- 'Handwash' => 'handwash',
- 'Hard Drive' => 'hard-drive',
- 'Haribo' => 'haribo',
- 'Harman Kardon' => 'harman-kardon',
- 'Harry Potter' => 'harry-potter',
- 'Hasbro' => 'hasbro',
- 'Hat' => 'hat',
- 'Hatchimals' => 'hatchimals',
- 'Hats &amp; Caps' => 'hats-caps',
- 'Hauck' => 'hauck',
- 'Hayfever Remedies' => 'hayfever',
- 'Headboard' => 'headboard',
- 'Headphones' => 'headphones',
- 'Headset' => 'headset',
- 'Health &amp; Beauty' => 'beauty',
- 'Healthcare' => 'health-care',
- 'Heart Rate Monitor' => 'heart-rate-monitor',
- 'Heater' => 'heater',
- 'Heating' => 'heating',
- 'Heating Appliances' => 'heating-appliances',
- 'Hedge Trimmer' => 'hedge-trimmer',
- 'Heineken' => 'heineken',
- 'Heinz' => 'heinz',
- 'Heinz Beanz' => 'heinz-baked-beans',
- 'Hello Kitty' => 'hello-kitty',
- 'Hello Neighbour' => 'hello-neighbour',
- 'Helly Hansen' => 'helly-hansen',
- 'Henry Hoover' => 'henry-hoover',
- 'Hermes' => 'hermes',
- 'High5' => 'high-5',
- 'Highchair' => 'highchair',
- 'Hiking' => 'hiking',
- 'Hilton' => 'hilton',
- 'Hisense' => 'hisense',
- 'Hisense TVs' => 'hisense-tv',
- 'Hitachi' => 'hitachi',
- 'Hitman' => 'hitman',
- 'Hive' => 'hive',
- 'Hive Active Heating' => 'hive-active-heating',
- 'Hob' => 'hob',
- 'Hobbit' => 'hobbit',
- 'Hockey' => 'hockey',
- 'Holiday Inn' => 'holiday-inn',
- 'Holiday Park' => 'holiday-parks',
- 'Holidays and Trips' => 'holidays-and-trips',
- 'Hollow Knight' => 'hollow-knight',
- 'Home &amp; Living' => 'home',
- 'Home Accessories' => 'home-accessories',
- 'Home Appliances' => 'home-appliances',
- 'Home Care' => 'home-care',
- 'Home Cinema' => 'home-cinema',
- 'HoMedics' => 'homedics',
- 'Homefront' => 'homefront',
- 'Home Networking' => 'network',
- 'Homeplug' => 'homeplug',
- 'Home Security' => 'home-security',
- 'Homeware' => 'homeware',
- 'Honda' => 'honda',
- 'Honey' => 'honey',
- 'Honeywell' => 'honeywell',
- 'Honor 6X' => 'honor-6x',
- 'Honor 7' => 'honor-7',
- 'Honor 8S' => 'honor-8s',
- 'Honor 8X' => 'honor-8x',
- 'Honor 8X Max' => 'honor-8x-max',
- 'Honor 9' => 'honor-9',
- 'Honor 9X' => 'honor-9x',
- 'Honor 10' => 'honor-10',
- 'Honor Band 5' => 'honor-band-5',
- 'Honor Play' => 'honor-play',
- 'Honor Smartphone' => 'honor',
- 'Honor View 20' => 'honor-view-20',
- 'Hoodie' => 'hoodie',
- 'Hoover' => 'hoover',
- 'Hori' => 'hori',
- 'Horizon: Zero Dawn' => 'horizon-zero-dawn',
- 'Hornby' => 'hornby',
- 'Horse Races' => 'horse-races',
- 'Hose' => 'hose',
- 'HOTAS' => 'hotas',
- 'Hotel' => 'hotel',
- 'Hotpoint' => 'hotpoint',
- 'Hotspot' => 'hotspot',
- 'Hot Tub' => 'hot-tub',
- 'Hot Water Bottle' => 'hot-water-bottle',
- 'Hot Wheels' => 'hot-wheels',
- 'Hozelock' => 'hozelock',
- 'HP' => 'hp',
- 'HP Envy' => 'hp-envy',
- 'HP Laptop' => 'hp-laptop',
- 'HP Omen' => 'hp-omen',
- 'HP Printer' => 'hp-printer',
- 'HTC' => 'htc',
- 'HTC 10' => 'htc-10',
- 'HTC Desire' => 'htc-desire',
- 'HTC One' => 'htc-one',
- 'HTC Smartphone' => 'htc-smartphone',
- 'HTC U11' => 'htc-u11',
- 'HTC Vive' => 'htc-vive',
- 'Huawei' => 'huawei',
- 'Huawei Freebuds 3' => 'huawei-freebuds-3',
- 'Huawei Headphones' => 'huawei-headphones',
- 'Huawei Mate 20' => 'huawei-mate-20',
- 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro',
- 'Huawei Mate 30' => 'huawei-mate-30',
- 'Huawei Mate 30 Lite' => 'huawei-mate-30-lite',
- 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro',
- 'Huawei Matebook' => 'huawei-matebook',
- 'Huawei MediaPad M3' => 'huawei-mediapad-m3',
- 'Huawei MediaPad M5' => 'huawei-mediapad-m5',
- 'Huawei MediaPad T3' => 'huawei-mediapad-t3',
- 'Huawei MediaPad T5' => 'huawei-mediapad-t5',
- 'Huawei P9' => 'huawei-p9',
- 'Huawei P10' => 'huawei-p10',
- 'Huawei P20' => 'huawei-p20',
- 'Huawei P20 Lite' => 'huawei-p20-lite',
- 'Huawei P20 Pro' => 'huawei-p20-pro',
- 'Huawei P30' => 'huawei-p30',
- 'Huawei P30 Lite' => 'huawei-p30-lite',
- 'Huawei P30 Pro' => 'huawei-p30-pro',
- 'Huawei P40' => 'huawei-p40',
- 'Huawei P40 Lite' => 'huawei-p40-lite',
- 'Huawei P40 Pro' => 'huawei-p40-pro',
- 'Huawei P Smart' => 'huawei-p-smart',
- 'Huawei Smartphone' => 'huawei-smartphone',
- 'Huawei Smartwatch' => 'huawei-smartwatch',
- 'Huawei Tablet' => 'huawei-tablet',
- 'Huawei Watch 2' => 'huawei-watch-2',
- 'Huawei Watch GT' => 'huawei-watch-gt',
- 'Huawei Watch GT2' => 'huawei-watch-gt2',
- 'Huawei Watch GT 2 Pro' => 'huawei-watch-gt-2-pro',
- 'Huawei Y7' => 'huawei-y7',
- 'Huggies' => 'huggies',
- 'Hulk' => 'hulk',
- 'Humax' => 'humax',
- 'Humidifier' => 'humidifier',
- 'Hunter' => 'hunter',
- 'HyperX' => 'hyperx',
- 'Hyrule Warriors' => 'hyrule-warriors',
- 'Hyundai' => 'hyundai',
- 'IAMS' => 'iams',
- 'iCandy' => 'icandy',
- 'Ice-Watch' => 'ice-watch',
- 'Ice Cream' => 'ice-cream',
- 'Ice Cream Maker' => 'ice-cream-maker',
- 'iMac' => 'apple-imac',
- 'iMac 2021' => 'imac-2021',
- 'Impact Driver' => 'impact-driver',
- 'Indesit' => 'indesit',
- 'Inflatable Boats' => 'boat',
- 'Inflatable Toys' => 'inflatable',
- 'Injustice' => 'injustice',
- 'Injustice 2' => 'injustice-2',
- 'Ink Cartridge' => 'ink',
- 'Inkjet Printer' => 'inkjet-printer',
- 'Innocent' => 'innocent',
- 'Instant Cameras' => 'instant-cameras',
- 'Instant Ink' => 'instant-ink',
- 'Instax Mini 9' => 'instax-mini-9',
- 'Insulation' => 'insulation',
- 'Insurance' => 'insurance',
- 'Intel' => 'intel',
- 'Intel Atom' => 'atom',
- 'Intel i3' => 'i3',
- 'Intel i5' => 'i5',
- 'Intel i7' => 'i7',
- 'Intel i9' => 'intel-i9',
- 'Internet' => 'internet',
- 'Internet Security' => 'internet-security',
- 'In the Night Garden' => 'in-the-night-garden',
- 'Intimate Care' => 'intimate-care',
- 'Introduce Yourself' => 'introduce-yourself',
- 'iOS Apps' => 'ios-apps',
- 'iPad' => 'ipad',
- 'iPad 2019' => 'ipad-2019',
- 'iPad 2020' => 'ipad-2020',
- 'iPad Air' => 'ipad-air',
- 'iPad Air 2019' => 'ipad-air-2019',
- 'iPad Air 2020' => 'ipad-air-2020',
- 'iPad Case' => 'ipad-case',
- 'iPad mini' => 'ipad-mini',
- 'iPad Pro' => 'ipad-pro',
- 'iPad Pro 11' => 'ipad-pro-11',
- 'iPad Pro 12.9' => 'ipad-pro-12-9',
- 'iPad Pro 2020' => 'ipad-pro-2020',
- 'iPad Pro 2021' => 'ipad-pro-2021',
- 'IP Camera' => 'ip-camera',
- 'iPhone' => 'iphone',
- 'iPhone 5s' => 'iphone-5s',
- 'iPhone 6' => 'iphone-6',
- 'iPhone 6 Plus' => 'iphone-6-plus',
- 'iPhone 6s' => 'iphone-6s',
- 'iPhone 6s Plus' => 'iphone-6s-plus',
- 'iPhone 7' => 'iphone-7',
- 'iPhone 7 Plus' => 'iphone-7-plus',
- 'iPhone 8' => 'iphone-8',
- 'iPhone 8 Plus' => 'iphone-8-plus',
- 'iPhone 11' => 'iphone-11',
- 'iPhone 11 Pro' => 'iphone-11-pro',
- 'iPhone 11 Pro Max' => 'iphone-11-pro-max',
- 'iPhone 12' => 'iphone-12',
- 'iPhone 12 mini' => 'iphone-12-mini',
- 'iPhone 12 Pro' => 'iphone-12-pro',
- 'iPhone 12 Pro Max' => 'iphone-12-pro-max',
- 'iPhone Accessories' => 'iphone-accessories',
- 'iPhone Case' => 'iphone-case',
- 'iPhone SE' => 'iphone-se',
- 'iPhone X' => 'iphone-x',
- 'iPhone Xr' => 'iphone-xr',
- 'iPhone Xs' => 'iphone-xs',
- 'iPhone Xs Max' => 'iphone-xs-max',
- 'iPod' => 'ipod',
- 'iPod Nano' => 'ipod-nano',
- 'iPod Shuffle' => 'ipod-shuffle',
- 'iPod Touch' => 'ipod-touch',
- 'Irish Whiskey' => 'irish-whisky',
- 'Irn Bru' => 'irn-bru',
- 'iRobot' => 'irobot',
- 'Iron' => 'iron',
- 'Ironing' => 'ironing',
- 'Ironing Board' => 'ironing-board',
- 'Iron Man' => 'iron-man',
- 'Issey Miyake' => 'issey-miyake',
- 'ITV' => 'itv',
- 'Jabra' => 'jabra',
- 'Jabra Elite 85h' => 'jabra-elite-85h',
- 'Jabra Elite Active 65t' => 'jabra-elite-active-65t',
- 'Jabra Elite Active 75t' => 'jabra-elite-active-75t',
- 'Jabra Headphones' => 'jabra-headphones',
- 'Jack &amp; Jones' => 'jack-and-jones',
- 'Jack Daniel&#039;s' => 'jack-daniels',
- 'Jacket' => 'jacket',
- 'Jack Wills' => 'jack-wills',
- 'Jack Wolfskin' => 'jack-wolfskin',
- 'Jaffa Cakes' => 'jaffa-cakes',
- 'Jägermeister' => 'jagermeister',
- 'Jameson' => 'jameson',
- 'Jamie Oliver' => 'jamie-oliver',
- 'Jaybird' => 'jaybird',
- 'JBL' => 'jbl',
- 'JBL Flip' => 'jbl-flip',
- 'JBL GO' => 'jbl-go',
- 'JBL Headphones' => 'jbl-headphones',
- 'JBL Link' => 'jbl-link',
- 'JBL Live' => 'jbl-live',
- 'JBL Tune' => 'jbl-tune',
- 'JCB' => 'jcb',
- 'Jean Paul Gaultier' => 'jean-paul-gautier',
- 'Jean Paul Gaultier Le Male' => 'le-male',
- 'Jeans' => 'jeans',
- 'Jelly Belly' => 'jelly-belly',
- 'Jewellery' => 'jewellery',
- 'Jigsaw' => 'jigsaw',
- 'Jim Beam' => 'jim-beam',
- 'Jimmy Choo' => 'jimmy-choo',
- 'JML' => 'jml',
- 'Jogging Bottoms' => 'jogging-bottoms',
- 'Johnnie Walker' => 'johnnie-walker',
- 'Johnson&#039;s' => 'johnsons',
- 'John West' => 'john-west',
- 'John Wick' => 'john-wick',
- 'JoJo Siwa' => 'jojo',
- 'Joop' => 'joop',
- 'Joseph Joseph' => 'joseph-joseph',
- 'Joules' => 'joules',
- 'Juice' => 'juice',
- 'Juicer' => 'juicer',
- 'Jumper' => 'jumper',
- 'Jurassic World' => 'jurassic-world',
- 'Jura Whisky' => 'jura',
- 'Just Cause' => 'just-cause',
- 'Just Cause 3' => 'just-cause-3',
- 'Just Cause 4' => 'just-cause-4',
- 'Just Dance' => 'just-dance',
- 'JVC' => 'jvc',
- 'K-Swiss' => 'k-swiss',
- 'Karcher' => 'karcher',
- 'Karcher Window Vacuum' => 'karcher-window-cleaner',
- 'Karen Millen' => 'karen-millen',
- 'Karrimor' => 'karrimor',
- 'Kaspersky' => 'kaspersky',
- 'Kayak' => 'kayak',
- 'Keg' => 'keg',
- 'Kellogg&#039;s' => 'kelloggs',
- 'Kellogg&#039;s Cornflakes' => 'cornflakes',
- 'Kellogg&#039;s Crunchy Nut' => 'crunchy-nut',
- 'Kenco' => 'kenco',
- 'Kenwood' => 'kenwood',
- 'Kenwood kMix' => 'kmix',
- 'Kenzo' => 'kenzo',
- 'Ketchup' => 'ketchup',
- 'Keter' => 'keter',
- 'Kettle' => 'kettle',
- 'Kettlebell' => 'kettlebell',
- 'Keyboard' => 'keyboard',
- 'KIA' => 'kia',
- 'Kickers' => 'kickers',
- 'Kid&#039;s Bike' => 'kids-bike',
- 'Kid&#039;s Clothes' => 'kids-clothes',
- 'Kid&#039;s Room' => 'kids-rooms',
- 'Kid&#039;s Shoes' => 'kids-shoes',
- 'Kidizoom' => 'kidizoom',
- 'Killzone' => 'killzone',
- 'Kilner' => 'kilner',
- 'Kinder' => 'kinder',
- 'Kindle' => 'kindle',
- 'Kindle Book' => 'kindle-book',
- 'Kindle Fire' => 'kindle-fire',
- 'Kindle Oasis' => 'kindle-oasis',
- 'Kindle Paperwhite' => 'kindle-paperwhite',
- 'Kingdom Come: Deliverance' => 'kingdom-come-deliverance',
- 'Kingdom Hearts' => 'kingdom-hearts',
- 'Kingdom Hearts 3' => 'kingdom-hearts-3',
- 'Kingdom Hearts: The Story So Far' => 'kingdom-hearts-the-story-so-far',
- 'King Kong' => 'king-kong',
- 'King Size Bed' => 'king-size',
- 'Kingsmill' => 'kingsmill',
- 'Kingston' => 'kingston',
- 'Kitchen' => 'kitchen',
- 'KitchenAid' => 'kitchenaid',
- 'Kitchen Appliances' => 'kitchen-appliances',
- 'Kitchen Knife' => 'knife',
- 'Kitchen Roll' => 'kitchen-roll',
- 'Kitchen Scale' => 'kitchen-scales',
- 'Kitchen Tap' => 'kitchen-tap',
- 'Kitchen Utensils' => 'kitchen-utensils',
- 'Kite' => 'kite',
- 'KitSound' => 'kitsound',
- 'Knickers' => 'knickers',
- 'Kobo' => 'kobo',
- 'Kodak' => 'kodak',
- 'Kodi' => 'kodi',
- 'Kohinoor' => 'kohinoor',
- 'Kopparberg' => 'kopparberg',
- 'Kraken' => 'kraken',
- 'Krispy Kreme' => 'krispy-kreme',
- 'Krups' => 'krups',
- 'KTC' => 'ktc',
- 'Kurt Geiger' => 'kurt-geiger',
- 'L&#039;Occitane' => 'loccitane',
- 'L.O.L. Surprise!' => 'lol-surprise',
- 'Lacoste' => 'lacoste',
- 'Ladder' => 'ladder',
- 'Lamaze' => 'lamaze',
- 'Lamb' => 'lamb',
- 'Laminate' => 'laminate',
- 'Laminator' => 'laminator',
- 'Lamp' => 'lamp',
- 'Lancôme' => 'lancome',
- 'Landmann' => 'landmann',
- 'Lantern' => 'lantern',
- 'Laphroaig' => 'laphroaig',
- 'Laptop' => 'laptop',
- 'Laptop Accessories' => 'laptop-accessories',
- 'Laptop Case' => 'laptop-case',
- 'Laptop Sleeve' => 'laptop-sleeve',
- 'Laser Printer' => 'laser-printer',
- 'Last Minute' => 'last-minute',
- 'Laundry Basket' => 'laundry-basket',
- 'Laura Ashley' => 'laura-ashley',
- 'Lavazza' => 'lavazza',
- 'Lavender' => 'lavender',
- 'Lawnmower' => 'lawnmower',
- 'Lay-Z-Spa' => 'lay-z-spa',
- 'LeapFrog' => 'leapfrog',
- 'Le Creuset' => 'le-creuset',
- 'LED Bulb' => 'led-bulbs',
- 'LED Light' => 'led-light',
- 'LED Strip Lights' => 'led-strip-lights',
- 'LED TV' => 'led-tv',
- 'Lee Stafford' => 'lee-stafford',
- 'Leffe' => 'leffe',
- 'Leggings' => 'leggings',
- 'Lego' => 'lego',
- 'Lego Advent Calendar' => 'lego-advent-calendar',
- 'Lego Architecture' => 'lego-architecture',
- 'Lego Art' => 'lego-art',
- 'Lego Batman' => 'lego-batman',
- 'Lego BrickHeadz' => 'lego-brickheadz',
- 'Lego City' => 'lego-city',
- 'Lego Classic' => 'lego-classic',
- 'Lego Creator' => 'lego-creator',
- 'Lego Dimensions' => 'lego-dimensions',
- 'Lego Disney' => 'lego-disney',
- 'Lego Dots' => 'lego-dots',
- 'Lego Duplo' => 'lego-duplo',
- 'Lego Friends' => 'lego-friends',
- 'LEGO Harry Potter' => 'lego-harry-potter',
- 'Lego Hidden Side' => 'lego-hidden-side',
- 'Legoland' => 'legoland',
- 'Lego Marvel' => 'lego-marvel',
- 'Lego Mindstorms' => 'lego-mindstorms',
- 'Lego Nexo Knights' => 'lego-nexo-knights',
- 'Lego Ninjago' => 'lego-ninjago',
- 'Lego Porsche' => 'lego-porsche',
- 'Lego Simpsons' => 'lego-simpsons',
- 'Lego Speed Champions' => 'lego-speed-champions',
- 'Lego Star Wars' => 'lego-star-wars',
- 'Lego Star Wars Millennium Falcon' => 'lego-star-wars-millennium-falcon',
- 'Lego Super Mario' => 'lego-mario',
- 'Lego Technic' => 'lego-technic',
- 'Lego VIDIYO' => 'lego-vidiyo',
- 'Lemonade' => 'lemonade',
- 'Lenor' => 'lenor',
- 'Lenovo' => 'lenovo',
- 'Lenovo IdeaPad' => 'lenovo-ideapad',
- 'Lenovo Laptop' => 'lenovo-laptop',
- 'Lenovo Tablet' => 'lenovo-tablet',
- 'Lenovo Thinkpad' => 'thinkpad',
- 'Lenovo Yoga Laptop' => 'lenovo-yoga-laptop',
- 'Lenovo Yoga Tablet' => 'lenovo-yoga',
- 'Les Paul' => 'les-paul',
- 'Levi&#039;s' => 'levi',
- 'Lexar' => 'lexar',
- 'LG' => 'lg',
- 'LG G3' => 'lg-g3',
- 'LG G5' => 'lg-g5',
- 'LG G6' => 'lg-g6',
- 'LG G7' => 'lg-g7',
- 'LG G8S ThinQ' => 'lg-g8s-thinq',
- 'LG OLED TV' => 'lg-oled-tv',
- 'LG Smartphone' => 'lg-smartphone',
- 'LG TV' => 'lg-tv',
- 'LG V30' => 'lg-v30',
- 'LG V40 ThinQ' => 'lg-v40-thinq',
- 'Life Insurance' => 'life-insurance',
- 'Life is Strange' => 'life-is-strange',
- 'Light Box' => 'light-box',
- 'Lighting' => 'lighting',
- 'Lightning Cable' => 'lightning-cable',
- 'Lightsaber' => 'lightsaber',
- 'Lindor' => 'lindor',
- 'Lindt' => 'lindt',
- 'Lingerie' => 'lingerie',
- 'Linksys' => 'linksys',
- 'Linx' => 'linx',
- 'Lion King' => 'lion-king',
- 'Lipstick' => 'lipstick',
- 'Lipsy' => 'lipsy',
- 'Little Tikes' => 'little-tikes',
- 'Liverpool F. C.' => 'liverpool-fc',
- 'Living Room' => 'living-room',
- 'Local Traffic' => 'local-traffic',
- 'Lodge' => 'lodge',
- 'Loft' => 'loft',
- 'Logitech' => 'logitech',
- 'Logitech G430' => 'logitech-g430',
- 'Logitech G703' => 'logitech-g703',
- 'Logitech G903' => 'logitech-g903',
- 'Logitech Harmony' => 'harmony',
- 'Logitech Keyboard' => 'logitech-keyboard',
- 'Logitech Mouse' => 'logitech-mouse',
- 'Logitech MX Master' => 'logitech-mx-master',
- 'Logitech MX Master 2S' => 'logitech-mx-master-2s',
- 'London Eye' => 'london-eye',
- 'London Zoo' => 'london-zoo',
- 'Longleat' => 'longleat',
- 'Long Sleeve' => 'long-sleeve',
- 'Lord of the Rings' => 'lord-of-the-rings',
- 'Lottery' => 'lottery',
- 'Lounger' => 'lounger',
- 'Lowepro' => 'lowepro',
- 'Lucozade' => 'lucozade',
- 'Luigi' => 'luigi',
- 'Luigi&#039;s Mansion' => 'luigis-manison',
- 'Luigi&#039;s Mansion 3' => 'luigis-mansion-3',
- 'Lunch Bag' => 'lunch-bag',
- 'Lunch Box' => 'lunch-box',
- 'Lurpak' => 'lurpak',
- 'Luton' => 'luton',
- 'Lyle &amp; Scott' => 'lyle-and-scott',
- 'Lynx' => 'lynx',
- 'M.2 SSD' => 'm2-ssd',
- 'MacBook' => 'macbook',
- 'MacBook Air' => 'macbook-air',
- 'MacBook Pro' => 'macbook-pro',
- 'MacBook Pro 13' => 'macbook-pro-13',
- 'MacBook Pro 15' => 'macbook-pro-15',
- 'MacBook Pro 16' => 'macbook-pro-16',
- 'Maclaren' => 'maclaren',
- 'Mac mini' => 'mac-mini',
- 'Madame Tussauds' => 'madame-tussauds',
- 'Mad Catz' => 'madcatz',
- 'Madden NFL' => 'madden',
- 'Madden NFL 20' => 'madden-nfl-20',
- 'Mad Max' => 'mad-max',
- 'Mafia 3' => 'mafia-3',
- 'Magazine' => 'magazine',
- 'Magimix' => 'magimix',
- 'Magners' => 'magners',
- 'Magnum' => 'magnum',
- 'Make Up' => 'make-up',
- 'Makeup Advent Calendar' => 'makeup-advent-calendar',
- 'Make Up Brush' => 'make-up-brush',
- 'Makita' => 'makita',
- 'Makita Drill' => 'makita-drill',
- 'Malibu' => 'malibu',
- 'Maltesers' => 'maltesers',
- 'MAM' => 'mam',
- 'Mamas &amp; Papas' => 'mamas-and-papas',
- 'Manchester United' => 'manchester-united',
- 'Manfrotto' => 'manfrotto',
- 'Manga' => 'manga',
- 'Manuka Honey' => 'manuka-honey',
- 'Marantz' => 'marantz',
- 'Marc Jacobs' => 'marc-jacobs',
- 'Marc Jacobs Daisy' => 'daisy',
- 'Mario &amp; Sonic at the Olympic Games: Tokyo 2020' => 'mario-and-sonic-tokyo-2020',
- 'Mario + Rabbids Kingdom Battle' => 'mario-rabbids-kingdom-battle',
- 'Mario Kart' => 'mario-kart',
- 'Mario Kart 8' => 'mario-kart-8',
- 'Mario Kart 8 Deluxe' => 'mario-kart-8-deluxe',
- 'Marmite' => 'marmite',
- 'Mars' => 'mars',
- 'Marshall' => 'marshall',
- 'Marshall Headphones' => 'marshall-headphones',
- 'Marvel' => 'marvel',
- 'Marvel&#039;s Spider-Man (PS4)' => 'spider-man-2018',
- 'Marvel&#039;s Spider-Man: Miles Morales' => 'spiderman-miles-morales',
- 'Mascara' => 'mascara',
- 'Massage' => 'massage',
- 'Mass Effect' => 'mass-effect',
- 'Mass Effect: Andromeda' => 'mass-effect-andromeda',
- 'Mastercard' => 'mastercard',
- 'Masterplug' => 'masterplug',
- 'Maternity &amp; Pregnancy' => 'maternity',
- 'Mattress' => 'mattress',
- 'Mattress Protector' => 'mattress-protector',
- 'Mattress Topper' => 'mattress-topper',
- 'Mavic' => 'mavic',
- 'Max Factor' => 'max-factor',
- 'Maxi Cosi' => 'maxi-cosi',
- 'Maximuscle' => 'maximuscle',
- 'Maxtor' => 'maxtor',
- 'Maybelline' => 'maybelline',
- 'Mayo' => 'mayo',
- 'Mazda' => 'mazda',
- 'McAfee' => 'mcafee',
- 'Meat &amp; Sausages' => 'meat',
- 'Meccano' => 'meccano',
- 'Mechanical Keyboard' => 'mechanical-keyboard',
- 'Medal of Honor' => 'medal-of-honor',
- 'Medela' => 'medela',
- 'Media Player' => 'media-player',
- 'Medievil' => 'medievil',
- 'Medion' => 'medion',
- 'Mega Bloks' => 'mega-bloks',
- 'Megathread' => 'megathread',
- 'Melissa &amp; Doug' => 'melissa',
- 'Memory Cards' => 'memory-cards',
- 'Memory Foam Mattress' => 'memory-foam',
- 'Men&#039;s Boots' => 'mens-boots',
- 'Men&#039;s Fragrance' => 'mens-fragrance',
- 'Men&#039;s Shoes' => 'mens-shoes',
- 'Men&#039;s Suit' => 'suit',
- 'Mercedes' => 'mercedes',
- 'Meridian' => 'meridian',
- 'Merlin' => 'merlin',
- 'Merrell' => 'merrell',
- 'Messenger Bag' => 'messenger-bag',
- 'Metal Gear Solid' => 'metal-gear-solid',
- 'Metro Exodus' => 'metro-exodus',
- 'Metroid' => 'metroid',
- 'Metro Series' => 'metro-series',
- 'Michael Kors' => 'michael-kors',
- 'Michelin' => 'michelin',
- 'Microphone' => 'microphone',
- 'Micro SD Card' => 'micro-sd',
- 'Micro SDHC' => 'micro-sdhc',
- 'Micro SDXC' => 'micro-sdxc',
- 'Microserver' => 'microserver',
- 'Microsoft' => 'microsoft',
- 'Microsoft Flight Simulator' => 'microsoft-flight-simulator',
- 'Microsoft Office' => 'microsoft-office',
- 'Microsoft Points' => 'microsoft-points',
- 'Microsoft Software' => 'microsoft-software',
- 'Microsoft Surface Book' => 'surface-book',
- 'Microsoft Surface Laptop' => 'surface',
- 'Microsoft Surface Pro 6' => 'surface-pro-6',
- 'Microsoft Surface Pro 7' => 'surface-pro-7',
- 'Microsoft Surface Tablet' => 'microsoft-surface-tablet',
- 'Microwave' => 'microwave',
- 'Middle Earth' => 'middle-earth',
- 'Middle Earth: Shadow of Mordor' => 'shadow-of-mordor',
- 'Middle Earth: Shadow of War' => 'middle-earth-shadow-of-war',
- 'Miele' => 'miele',
- 'Miele Vacuum Cleaner' => 'miele-vacuum-cleaner',
- 'Milk' => 'milk',
- 'Milk Frother' => 'milk-frother',
- 'Milk Tray' => 'milk-tray',
- 'Milwaukee' => 'milwaukee',
- 'Mince' => 'mince',
- 'Minecraft Game' => 'minecraft',
- 'Mineral Water' => 'mineral-water',
- 'Mini Fridge' => 'mini-fridge',
- 'Minions' => 'minions',
- 'Mini PC' => 'mini-pc',
- 'Minky' => 'minky',
- 'Mira' => 'mira',
- 'Mirror' => 'mirror',
- 'Mirror&#039;s Edge' => 'mirrors-edge',
- 'Misc' => 'misc',
- 'Misfit' => 'misfit',
- 'Mitre Saw' => 'mitre-saw',
- 'Mitsubishi' => 'mitsubishi',
- 'Mixer &amp; Blender' => 'mixer-and-blender',
- 'Mobile Contracts' => 'mobile-contract',
- 'Mobile Phone' => 'mobile-phone',
- 'Model Building' => 'model-building',
- 'Moët' => 'moet',
- 'Molton Brown' => 'molton-brown',
- 'Money Saving Tips and Tricks' => 'money-saving-tips',
- 'Monitor' => 'monitor',
- 'Monopoly' => 'monopoly',
- 'Monsoon' => 'monsoon',
- 'Monster Energy' => 'monster-energy',
- 'Monster High' => 'monster-high',
- 'Monster Hunter' => 'monster-hunter',
- 'Monster Hunter World' => 'monster-hunter-world',
- 'Mont Blanc' => 'mont-blanc',
- 'Mop' => 'mop',
- 'Morphy Richards' => 'morphy-richards',
- 'Mortal Kombat' => 'mortal-kombat',
- 'Mortal Kombat 11' => 'mortal-kombat-11',
- 'Mortgage' => 'mortgage',
- 'Moschino' => 'moschino',
- 'Moses Basket' => 'moses-basket',
- 'MOT' => 'mot',
- 'Motherboard' => 'motherboard',
- 'Moto 360' => 'moto-360',
- 'Moto E' => 'moto-e',
- 'Moto G' => 'moto-g',
- 'Moto G4' => 'moto-g4',
- 'Moto G5' => 'moto-g5',
- 'Moto G6' => 'moto-g6',
- 'Moto G7' => 'moto-g7',
- 'Motorcycle' => 'motorcycle',
- 'Motorcycle Accessories' => 'motorcycle-accessories',
- 'Motorcycle Helmet' => 'motorcycle-helmet',
- 'Motorola' => 'motorola',
- 'Motorola Smartphone' => 'motorola-smartphone',
- 'Moto X' => 'moto-x',
- 'Moto Z' => 'moto-z',
- 'Mountain Bike' => 'mountain-bike',
- 'Mouse &amp; Keyboard Bundles' => 'mouse-and-keyboard-bundle',
- 'Mouse Mat' => 'mouse-mat',
- 'Mouthwash' => 'mouthwash',
- 'Movie and TV Box Set' => 'box-set',
- 'Movies &amp; Series' => 'movie',
- 'MP3 Player' => 'mp3-player',
- 'Mr Kipling' => 'mr-kipling',
- 'Mr Men' => 'mr-men',
- 'MSI' => 'msi',
- 'MSI Laptop' => 'msi-laptop',
- 'Muc-Off' => 'muc-off',
- 'Mug' => 'mug',
- 'Muller' => 'muller',
- 'Multi-Room Audio System' => 'multi-room-audio-system',
- 'Multitool' => 'multitool',
- 'Museums' => 'museums',
- 'Music' => 'music',
- 'Musical Instruments' => 'musical-instrument',
- 'Music App' => 'music-app',
- 'Music Streaming' => 'music-streaming',
- 'My Little Pony' => 'my-little-pony',
- 'Nail Gun' => 'nail-gun',
- 'Nail Polish' => 'nail-polish',
- 'Nails' => 'nails',
- 'Nails Inc.' => 'nails-inc',
- 'Nakd' => 'nakd',
- 'Nando&#039;s' => 'nandos',
- 'Nappy' => 'nappy',
- 'NAS' => 'nas',
- 'National Express Ticket' => 'national-express',
- 'National Trust' => 'national-trust',
- 'Nature Observation' => 'nature-observation',
- 'NatWest' => 'natwest',
- 'NBA 2K' => 'nba-2k',
- 'NBA Live' => 'nba',
- 'Necklace' => 'necklace',
- 'Need for Speed' => 'need-for-speed',
- 'Need for Speed: Payback' => 'need-for-speed-payback',
- 'Need for Speed Heat' => 'need-for-speed-heat',
- 'Neff' => 'neff',
- 'Nerf Guns' => 'nerf',
- 'Nescafé Azera' => 'azera',
- 'Nescafé Coffee' => 'nescafe',
- 'Nespresso' => 'nespresso',
- 'Nespresso Coffee Machine' => 'nespresso-coffee-machine',
- 'Nest Hello' => 'nest-hello',
- 'Nestlé' => 'nestle',
- 'Nest Learning Thermostat' => 'nest-learning-thermostat',
- 'Nestlé Cheerios' => 'cheerios',
- 'Nestlé Shreddies' => 'shreddies',
- 'Netatmo' => 'netatmo',
- 'Netflix' => 'netflix',
- 'Netgear' => 'netgear',
- 'Netgear Arlo' => 'arlo',
- 'New Balance' => 'new-balance',
- 'New Balance Trainers' => 'new-balance-trainers',
- 'New Look' => 'new-look',
- 'Newspapers' => 'newspapers',
- 'Nextbase' => 'nextbase',
- 'NFL' => 'nfl',
- 'NHL' => 'nhl',
- 'NHL 20' => 'nhl-20',
- 'NHS' => 'nhs',
- 'NieR: Automata' => 'nier',
- 'Night Light' => 'night-light',
- 'Nike' => 'nike',
- 'Nike Air Max' => 'nike-air-max',
- 'Nike Air Max 200' => 'nike-air-max-200',
- 'Nike Air Max 270' => 'nike-air-max-270',
- 'Nike Air Max 720' => 'nike-air-max-720',
- 'Nike Free' => 'nike-free',
- 'Nike Huarache' => 'nike-huarache',
- 'Nike Jordan' => 'jordan',
- 'Nike Presto' => 'nike-presto',
- 'Nike Roshe' => 'nike-roshe',
- 'Nike Trainers' => 'nike-shoes',
- 'Nikon' => 'nikon',
- 'Nikon Camera' => 'nikon-camera',
- 'Nikon Coolpix' => 'nikon-coolpix',
- 'Nikon D3400' => 'nikon-d3400',
- 'Nikon Lens' => 'nikon-lens',
- 'Nilfisk' => 'nilfisk',
- 'Ni No Kuni' => 'ni-no-kuni',
- 'Ni No Kuni: Wrath of the White Witch' => 'ni-no-kuni-white-witch',
- 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-2',
- 'Nintendo' => 'nintendo',
- 'Nintendo 2DS' => '2ds',
- 'Nintendo 3DS' => '3ds',
- 'Nintendo 3DS Game' => '3ds-games',
- 'Nintendo 3DS XL' => 'nintendo-3ds-xl',
- 'Nintendo Accessories' => 'nintendo-accessories',
- 'Nintendo Classic Mini' => 'nintendo-classic-mini',
- 'Nintendo DS Game' => 'ds-games',
- 'Nintendo Labo' => 'switch-labo',
- 'Nintendo Switch' => 'nintendo-switch',
- 'Nintendo Switch Accessories' => 'switch-accessories',
- 'Nintendo Switch Case' => 'switch-case',
- 'Nintendo Switch Controller' => 'switch-controller',
- 'Nintendo Switch Game' => 'switch-game',
- 'Nintendo Switch Joy-Con' => 'switch-joy-con',
- 'Nintendo Switch Lite' => 'nintendo-switch-lite',
- 'Nintendo Switch Pro Controller' => 'switch-pro-controller',
- 'Nioh' => 'nioh',
- 'Nissan' => 'nissan',
- 'Nivea' => 'nivea',
- 'No7' => 'no7',
- 'Noise Cancelling Headphones' => 'noise-cancelling-headphones',
- 'Nokia' => 'nokia',
- 'Nokia Smartphones' => 'nokia-mobile',
- 'No Man&#039;s Sky' => 'no-man-s-sky',
- 'Noodles' => 'noodles',
- 'Norton' => 'norton',
- 'Now' => 'now-tv',
- 'Numatic' => 'numatic',
- 'Nursery' => 'nursery',
- 'Nutella' => 'nutella',
- 'NutriBullet' => 'nutribullet',
- 'Nutri Ninja' => 'nutri-ninja',
- 'Nuts' => 'nuts',
- 'Nvidia' => 'nvidia',
- 'Nvidia GeForce' => 'geforce',
- 'Nvidia Shield' => 'nvidia-shield',
- 'NYX' => 'nyx',
- 'NZXT' => 'nzxt',
- 'O2' => 'o2',
- 'O2 Refresh' => 'o2-refresh',
- 'Oakley' => 'oakley',
- 'Octonauts' => 'octonauts',
- 'Oculus Game' => 'oculus-game',
- 'Oculus Go' => 'oculus-go',
- 'Oculus Quest' => 'oculus-quest',
- 'Oculus Rift' => 'oculus',
- 'Oculus Rift S' => 'oculus-rift-s',
- 'Odeon' => 'odeon',
- 'Office' => 'office',
- 'Office Chair' => 'office-chair',
- 'Official Announcements' => 'official-announcements',
- 'Olay' => 'olay',
- 'OLED TV' => 'oled',
- 'Olive Oil' => 'olive-oil',
- 'Olympus' => 'olympus',
- 'Omega Seamaster' => 'omega-seamaster',
- 'Omega Speedmaster' => 'omega-speedmaster',
- 'Omega Watches' => 'omega-watch',
- 'OnePlus 3' => 'oneplus-3',
- 'OnePlus 5' => 'oneplus-5',
- 'OnePlus 6' => 'oneplus-6',
- 'OnePlus 6T' => 'oneplus-6t',
- 'OnePlus 7' => 'oneplus-7',
- 'OnePlus 7 Pro' => 'oneplus-7-pro',
- 'OnePlus 7T' => 'oneplus-7t',
- 'OnePlus 7T Pro' => 'one-plus-7t-pro',
- 'OnePlus 8' => 'oneplus-8',
- 'OnePlus 8 Pro' => 'oneplus-8-pro',
- 'OnePlus 8T' => 'oneplus-8t',
- 'OnePlus 9' => 'oneplus-9',
- 'OnePlus 9 Pro' => 'oneplus-9-pro',
- 'OnePlus Nord' => 'oneplus-nord',
- 'OnePlus Nord N10 5G' => 'oneplus-n10',
- 'OnePlus Nord N100' => 'oneplus-n100',
- 'OnePlus Smartphone' => 'oneplus',
- 'Onesie' => 'onesie',
- 'Onkyo' => 'onkyo',
- 'Online Courses' => 'online-courses',
- 'Operating System' => 'operating-system',
- 'Oppo Find X2 Lite' => 'oppo-find-x2-lite',
- 'Oppo Find X2 Neo' => 'oppo-find-x2-neo',
- 'Oppo Find X2 Pro' => 'oppo-find-x2-pro',
- 'Oppo Reno' => 'oppo-reno',
- 'Oppo Reno4 5G' => 'oppo-reno4',
- 'Oppo Reno4 Z 5G' => 'oppo-reno4-z',
- 'Oppo Smartphone' => 'oppo-smartphone',
- 'Opticians' => 'opticians',
- 'Optoma' => 'optoma',
- 'Oral-B' => 'oral-b',
- 'Oral-B Toothbrush' => 'oral-b-toothbrush',
- 'Oreo' => 'oreo',
- 'Origin' => 'origin',
- 'Original Penguin' => 'penguin',
- 'Orla Kiely' => 'orla-kiely',
- 'Osprey' => 'osprey',
- 'Osram' => 'osram',
- 'Other' => 'other-deals',
- 'Ottoman' => 'ottoman',
- 'Oukitel' => 'oukitel',
- 'Outdoor Clothing' => 'outdoor-clothing',
- 'Outdoor Lighting' => 'outdoor-lighting',
- 'Outdoor Sports &amp; Camping' => 'outdoor',
- 'Outdoor Toys' => 'outdoor-toys',
- 'Outlast' => 'outlast',
- 'Outlet' => 'outlet',
- 'Outwell' => 'outwell',
- 'Oven' => 'oven',
- 'Overcooked' => 'overcooked',
- 'Overcooked 2' => 'overcooked-2',
- 'Overwatch' => 'overwatch',
- 'Oyster Card' => 'oyster',
- 'Package Holidays' => 'holiday',
- 'Paco Rabanne' => 'paco-rabanne',
- 'Paco Rabanne 1 Million' => 'paco-rabanne-1-million',
- 'Paco Rabanne Lady Million' => 'lady-million',
- 'Paddling Pool' => 'paddling-pool',
- 'Padlock' => 'padlock',
- 'Paint' => 'paint',
- 'Paint Brush' => 'paint-brush',
- 'Pampers' => 'pampers',
- 'Panasonic' => 'panasonic',
- 'Panasonic Camera' => 'panasonic-camera',
- 'Panasonic Lumix' => 'lumix',
- 'Panasonic TV' => 'panasonic-tv',
- 'Pandora' => 'pandora',
- 'Panini' => 'panini',
- 'Panini Stickers' => 'panini-stickers',
- 'Papa Johns' => 'papa-johns',
- 'Paper Mario' => 'paper-mario',
- 'Parasol' => 'parasol',
- 'Parcel and Delivery Services' => 'parcel',
- 'Parka' => 'parka',
- 'Parking' => 'parking',
- 'Parrot' => 'parrot',
- 'Paul Smith' => 'paul-smith',
- 'PAW Patrol' => 'paw-patrol',
- 'Payday' => 'payday',
- 'Payday 2' => 'payday-2',
- 'PAYG' => 'payg',
- 'Pay Monthly' => 'pay-monthly',
- 'PC' => 'pc',
- 'PC Case' => 'pc-case',
- 'PC Game' => 'pc-game',
- 'PC Gaming Accessories' => 'pc-gaming-accessories',
- 'PC Gaming Systems' => 'pc-gaming-systems',
- 'PC Mouse' => 'mouse',
- 'PC Parts' => 'pc-parts',
- 'Peanut Butter' => 'peanut-butter',
- 'Peanuts' => 'peanuts',
- 'Pedometer' => 'pedometer',
- 'Pentax' => 'pentax',
- 'Peppa Pig' => 'peppa-pig',
- 'PepperBonus' => 'pepperbonus',
- 'Pepsi' => 'pepsi',
- 'Perfume' => 'perfume',
- 'Persil' => 'persil',
- 'Persona' => 'persona',
- 'Persona 5' => 'persona-5',
- 'Personal Care &amp; Hygiene' => 'personal-care-hygiene',
- 'Petrol and Diesel' => 'petrol',
- 'Pet Supplies' => 'pets',
- 'Peugeot' => 'peugeot',
- 'PG Tips' => 'pg-tips',
- 'Philips' => 'philips',
- 'Philips Alarm Clock' => 'philips-alarm-clock',
- 'Philips Avent' => 'avent',
- 'Philips Hue' => 'philips-hue',
- 'Philips Lumea' => 'lumea',
- 'Philips OneBlade' => 'philips-one-blade',
- 'Philips Senseo' => 'philips-senseo',
- 'Philips Senseo Coffee Machine' => 'philips-senseo-coffee-machine',
- 'Philips Shaver' => 'philips-shaver',
- 'Philips Sonicare' => 'sonicare',
- 'Philips TV' => 'philips-tv',
- 'Phone Holder' => 'phone-holder',
- 'Phones &amp; Accessories' => 'phone',
- 'Photo &amp; Cameras' => 'photo-video',
- 'Photo &amp; Video App' => 'photo-video-app',
- 'Photo Editing' => 'photo-editing',
- 'Photo Frame' => 'photo-frame',
- 'Photo Paper' => 'photo-paper',
- 'Piano' => 'piano',
- 'Picnic &amp; Outdoor Cooking' => 'picnic',
- 'Pikmin 3 Deluxe' => 'pikmin-3-deluxe',
- 'Pillow' => 'pillow',
- 'Pimm&#039;s' => 'pimms',
- 'Pioneer' => 'pioneer',
- 'Pirate Toys' => 'pirates',
- 'PIR Lights' => 'pir',
- 'Pixel C' => 'pixel-c',
- 'Piz Buin' => 'piz-buin',
- 'Pizza' => 'pizza',
- 'Pizza Stone' => 'pizza-stone',
- 'Planer' => 'planer',
- 'Planet Earth' => 'planet-earth',
- 'Plant' => 'plant',
- 'Plant Pot' => 'plant-pots',
- 'Plants vs. Zombies: Battle for Neighborville' => 'battle-for-neighborville',
- 'Plants vs Zombies' => 'plants-vs-zombies',
- 'Play-Doh' => 'play-doh',
- 'PlayerUnknown&#039;s Battlegrounds' => 'playerunknown-s-battlegrounds',
- 'Playhouse' => 'playhouse',
- 'Playing Cards' => 'playing-cards',
- 'Playmat' => 'playmat',
- 'Playmobil' => 'playmobil',
- 'Playmobil Advent Calendar' => 'playmobil-advent-calendar',
- 'PlayStation' => 'playstation',
- 'PlayStation 5 DualSense Controller' => 'ps5-controller',
- 'PlayStation Accessories' => 'playstation-accessories',
- 'PlayStation Classic' => 'playstation-classic',
- 'PlayStation Move' => 'playstation-move',
- 'PlayStation Now' => 'playstation-now',
- 'PlayStation Plus' => 'playstation-plus',
- 'PlayStation VR' => 'playstation-vr',
- 'PlayStation VR Aim Controller' => 'aim-controller-ps4',
- 'Pliers' => 'pliers',
- 'Plumbing &amp; Fittings' => 'plumbing-and-fitting',
- 'Plus Size' => 'plus-size',
- 'PNY' => 'pny',
- 'POCO F2 Pro' => 'poco-f2-pro',
- 'POCO F3' => 'poco-f3',
- 'Poco M3' => 'poco-m3',
- 'POCO X3' => 'poco-x3',
- 'POCO X3 Pro' => 'poco-x3-pro',
- 'Pokémon' => 'pokemon',
- 'Pokémon: Let&#039;s Go' => 'pokemon-lets-go',
- 'Pokémon Go' => 'pokemon-go',
- 'Pokemon Sword and Shield' => 'pokemon-sword-and-shield',
- 'Pokémon Ultra Sun and Ultra Moon' => 'pokemon-ultra-sun-ultra-moon',
- 'Poker' => 'poker',
- 'Pokken Tournament' => 'pokken-tournament',
- 'Polaroid' => 'polaroid',
- 'Police Toys' => 'police',
- 'Polo Shirt' => 'polo-shirt',
- 'Pool' => 'pool',
- 'Pool &amp; Snooker' => 'pool-table',
- 'Popcorn' => 'popcorn',
- 'Pork' => 'pork',
- 'Porridge &amp; Oats' => 'porridge-and-oats',
- 'Portable Wireless Speaker' => 'wireless-speaker',
- 'Poster' => 'poster',
- 'Pots and Pans' => 'pan',
- 'Potty' => 'potty',
- 'Power Bank' => 'power-bank',
- 'Powerbeats Pro' => 'powerbeats-pro',
- 'Power Dental Flosser' => 'floss',
- 'Powerline' => 'powerline',
- 'Power Rangers' => 'power-rangers',
- 'Power Tool' => 'power-tool',
- 'Prada' => 'prada',
- 'Pram' => 'pram',
- 'Pregnancy' => 'pregnancy',
- 'Prescription Glasses' => 'prescription-glasses',
- 'Pressure Cooker' => 'pressure-cooker',
- 'Pressure Washer' => 'pressure-washer',
- 'Price Glitch' => 'price-glitch',
- 'Prime Gaming' => 'twitch',
- 'Pringles' => 'pringles',
- 'Printer &amp; Printer Supplies' => 'printer',
- 'Printer Supplies' => 'printer-supplies',
- 'Productivity App' => 'productivity-app',
- 'Pro Evolution Soccer' => 'pro-evolution-soccer',
- 'Pro Evolution Soccer 2018' => 'pro-evolution-soccer-2018',
- 'Pro Evolution Soccer 2019' => 'pro-evolution-soccer-2019',
- 'Pro Evolution Soccer 2020' => 'pes-2020',
- 'Project Cars' => 'project-cars',
- 'Project Cars 2' => 'project-cars-2',
- 'Projector' => 'projector',
- 'Protein' => 'protein',
- 'Protein Bars' => 'protein-bars',
- 'Protein Shaker' => 'shaker',
- 'PS4' => 'ps4-slim',
- 'PS4 Camera' => 'ps4-camera',
- 'PS4 Controller' => 'ps4-controller',
- 'PS4 Games' => 'ps4-games',
- 'PS4 Headset' => 'ps4-headset',
- 'PS4 Pro' => 'ps4-pro',
- 'PS5' => 'ps5',
- 'PS5 Games' => 'ps5-game',
- 'PSU' => 'psu',
- 'Public Transport' => 'public-transport',
- 'Pukka' => 'pukka',
- 'Pulse Light Epilator' => 'pulse-light-epilator',
- 'Puma' => 'puma',
- 'Puma Trainers' => 'puma-trainers',
- 'Puppy Supplies' => 'puppy',
- 'Purse' => 'purse',
- 'Pushchair' => 'pushchair',
- 'Pushchairs and Strollers' => 'baby-transport',
- 'Puzzle' => 'puzzle',
- 'PVR' => 'pvr',
- 'Pyjamas' => 'pyjamas',
- 'Pyrex' => 'pyrex',
- 'Q Acoustics' => 'q-acoustics',
- 'QNAP' => 'qnap',
- 'Qualcast' => 'qualcast',
- 'Quality Street' => 'quality-street',
- 'Quantum Break' => 'quantum-break',
- 'Quechua' => 'quechua',
- 'Quick Charge' => 'quick-charge',
- 'Quiksilver' => 'quiksilver',
- 'Quinny' => 'quinny',
- 'Quorn' => 'quorn',
- 'Rab' => 'rab',
- 'Radeon RX 480' => 'rx-480',
- 'Radeon RX 5700' => 'radeon-rx-5700',
- 'Radeon RX 5700 XT' => 'radeon-rx-5700-xt',
- 'Radeon RX 6800' => 'radeon-rx-6800',
- 'Radeon RX 6800 XT' => 'radeon-rx-6800-xt',
- 'Radeon RX 6900 XT' => 'radeon-rx-6900-xt',
- 'Radiator' => 'radiator',
- 'Radio' => 'radio',
- 'Radley' => 'radley',
- 'Rage 2' => 'rage-2',
- 'Railcard' => 'railcard',
- 'Rainbow Six' => 'rainbow-six',
- 'Rake' => 'rake',
- 'Ralph Lauren' => 'ralph-lauren',
- 'RAM' => 'ram',
- 'Raspberry Pi' => 'raspberry-pi',
- 'Ratchet' => 'ratchet',
- 'Ratchet and Clank' => 'ratchet-and-clank',
- 'Rattan Garden Furniture' => 'rattan',
- 'RAVPower' => 'ravpower',
- 'Ray Ban' => 'ray-ban',
- 'Razer' => 'razer',
- 'Razor' => 'razor',
- 'Razor Blade' => 'razor-blade',
- 'Real Madrid' => 'real-madrid',
- 'Realme Smartphones' => 'realme-smartphone',
- 'Real Techniques' => 'real-techniques',
- 'Recliner' => 'recliner',
- 'ReCore' => 'recore',
- 'Recreational Sports' => 'recreational-sports',
- 'Red Bull' => 'red-bull',
- 'Red Dead Redemption' => 'red-dead-redemption',
- 'Red Dead Redemption 2' => 'red-dead-redemption-2',
- 'Redex' => 'redex',
- 'Red Kite' => 'red-kite',
- 'Reebok' => 'reebok',
- 'Reese&#039;s' => 'reeses',
- 'Regatta' => 'regatta',
- 'Regina' => 'regina',
- 'Remington' => 'remington',
- 'Remote Control Car' => 'remote-control-car',
- 'Renault' => 'renault',
- 'Resident Evil' => 'resident-evil',
- 'Resident Evil 2' => 'resident-evil-2',
- 'Resident Evil 7' => 'resident-evil-7',
- 'Restaurant, Café &amp; Pub' => 'restaurant',
- 'Retailer Offers and Issues' => 'retailer-offers-and-issues',
- 'Ribena' => 'ribena',
- 'Rice' => 'rice',
- 'Rice Cooker' => 'rice-cooker',
- 'Rick and Morty' => 'rick-and-morty',
- 'Ricoh' => 'ricoh',
- 'Ride On' => 'ride-on',
- 'Ring' => 'ring',
- 'Ring Door View Cam' => 'ring-door-view-cam',
- 'Ring Fit Adventures' => 'ring-fit-adventures',
- 'Ring Stick Up Cam' => 'ring-stick-up-cam',
- 'Ring Video Doorbell' => 'ring-video-doorbell',
- 'Ring Video Doorbell 2' => 'ring-video-doorbell-2',
- 'Ring Video Doorbell 3' => 'ring-video-doorbell-3',
- 'Ring Video Doorbell Pro' => 'ring-video-doorbell-pro',
- 'Road Bike' => 'road-bike',
- 'Roaming' => 'roaming',
- 'Robinsons' => 'robinsons',
- 'Robotic Lawnmower' => 'robotic-lawnmower',
- 'Robot Vacuum Cleaner' => 'robot-vacuum-cleaner',
- 'Rock Band' => 'rock-band',
- 'Rocket League' => 'rocket-league',
- 'Rocking Horse' => 'rocking-horse',
- 'Rogue One: A Star Wars Story' => 'rogue-one',
- 'Roku' => 'roku',
- 'Rolex' => 'rolex',
- 'Rollerskates' => 'skate',
- 'Ronseal' => 'ronseal',
- 'Roof Box' => 'roof-box',
- 'Roses' => 'roses',
- 'Rotary' => 'rotary',
- 'Router' => 'router',
- 'Rowenta' => 'rowenta',
- 'RTX 2060' => 'rtx-2060',
- 'RTX 2070' => 'rtx-2070',
- 'RTX 2080' => 'rtx-2080',
- 'RTX 2080 Ti' => 'rtx-2080-ti',
- 'RTX 3070' => 'rtx-3070',
- 'RTX 3080' => 'rtx-3080',
- 'RTX 3090' => 'rtx-3090',
- 'Rug' => 'rug',
- 'Rugby' => 'rugby',
- 'Rum' => 'rum',
- 'Running' => 'running',
- 'Running Shoes' => 'running-shoes',
- 'Russell Hobbs' => 'russell-hobbs',
- 'RX 570' => 'rx-570',
- 'RX 580' => 'rx-580',
- 'RX 590' => 'rx-590',
- 'RX Vega 56' => 'rx-vega-56',
- 'RX Vega 64' => 'rx-vega-64',
- 'Ryanair' => 'ryanair',
- 'Ryobi' => 'ryobi',
- 'Safari' => 'safari',
- 'Safety Boots' => 'safety-boots',
- 'Sage by Heston Blumenthal' => 'sage',
- 'Saints Row' => 'saints-row',
- 'Saitek' => 'saitek',
- 'Sale' => 'sale',
- 'Salmon' => 'salmon',
- 'Salomon' => 'salomon',
- 'Salter' => 'salter',
- 'Samsonite' => 'samsonite',
- 'Samsung' => 'samsung',
- 'Samsung Ecobubble' => 'ecobubble',
- 'Samsung Fridge' => 'samsung-fridge',
- 'Samsung Galaxy' => 'samsung-galaxy',
- 'Samsung Galaxy A10' => 'samsung-galaxy-a10',
- 'Samsung Galaxy A20e' => 'samsung-galaxy-a20e',
- 'Samsung Galaxy A40' => 'samsung-galaxy-a40',
- 'Samsung Galaxy A42 5G' => 'samsung-galaxy-a42-5g',
- 'Samsung Galaxy A50' => 'samsung-galaxy-a50',
- 'Samsung Galaxy A51' => 'samsung-galaxy-a51',
- 'Samsung Galaxy A52 5G' => 'samsung-galaxy-a52',
- 'Samsung Galaxy A60' => 'samsung-galaxy-a60',
- 'Samsung Galaxy A70' => 'samsung-galaxy-a70',
- 'Samsung Galaxy A71' => 'samsung-galaxy-a71',
- 'Samsung Galaxy A72' => 'samsung-galaxy-a72',
- 'Samsung Galaxy A80' => 'samsung-galaxy-a80',
- 'Samsung Galaxy A90' => 'samsung-galaxy-a90',
- 'Samsung Galaxy Buds' => 'samsung-galaxy-buds',
- 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus',
- 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live',
- 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro',
- 'Samsung Galaxy Fold' => 'samsung-galaxy-fold',
- 'Samsung Galaxy J5' => 'galaxy-j5',
- 'Samsung Galaxy Note' => 'samsung-galaxy-note',
- 'Samsung Galaxy Note 8' => 'samsung-galaxy-note-8',
- 'Samsung Galaxy Note 9' => 'samsung-galaxy-note-9',
- 'Samsung Galaxy Note 10' => 'samsung-galaxy-note-10',
- 'Samsung Galaxy Note 10+' => 'samsung-galaxy-note-10-plus',
- 'Samsung Galaxy Note20' => 'samsung-galaxy-note20',
- 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note20-ultra',
- 'Samsung Galaxy S6' => 'samsung-galaxy-s6',
- 'Samsung Galaxy S7' => 'samsung-galaxy-s7',
- 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge',
- 'Samsung Galaxy S8' => 'samsung-galaxy-s8',
- 'Samsung Galaxy S8+' => 'samsung-s8-plus',
- 'Samsung Galaxy S9' => 'samsung-galaxy-s9',
- 'Samsung Galaxy S9 Plus' => 'samsung-s9-plus',
- 'Samsung Galaxy S10' => 'samsung-galaxy-s10',
- 'Samsung Galaxy S10 Lite' => 'samsung-galaxy-s10-lite',
- 'Samsung Galaxy S10 Plus' => 'samsung-galaxy-s10-plus',
- 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e',
- 'Samsung Galaxy S20' => 'samsung-galaxy-s20',
- 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe',
- 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra',
- 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus',
- 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g',
- 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g',
- 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g',
- 'Samsung Galaxy Tab' => 'samsung-galaxy-tab',
- 'Samsung Galaxy Tab A' => 'samsung-galaxy-tab-a',
- 'Samsung Galaxy Tab A7' => 'samsung-galaxy-tab-a7',
- 'Samsung Galaxy Tab S' => 'samsung-galaxy-tab-s',
- 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4',
- 'Samsung Galaxy Tab S5e' => 'samsung-galaxy-tab-s5e',
- 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6',
- 'Samsung Galaxy Watch' => 'samsung-galaxy-watch',
- 'Samsung Galaxy Watch3' => 'samsung-galaxy-watch3',
- 'Samsung Galaxy Watch Active2' => 'samsung-galaxy-watch-active-2',
- 'Samsung Gear' => 'samsung-gear',
- 'Samsung Gear S3' => 'gear-s3',
- 'Samsung Gear VR' => 'samsung-gear-vr',
- 'Samsung Headphones' => 'samsung-headphones',
- 'Samsung Monitor' => 'samsung-monitor',
- 'Samsung QLED TVs' => 'samsung-qled-tv',
- 'Samsung Smartphone' => 'samsung-smartphone',
- 'Samsung SSD' => 'samsung-ssd',
- 'Samsung The Frame TV' => 'samsung-the-frame',
- 'Samsung TV' => 'samsung-tv',
- 'Samsung Washing Machine' => 'samsung-washing-machine',
- 'Samsung Watch' => 'samsung-watch',
- 'Sandals' => 'sandals',
- 'Sander' => 'sander',
- 'SanDisk' => 'sandisk',
- 'SanDisk SSD' => 'sandisk-ssd',
- 'Sand Pit' => 'sand-pit',
- 'Sandwich Maker' => 'sandwich',
- 'San Miguel' => 'san-miguel',
- 'Santander' => 'santander',
- 'Satchel' => 'satchel',
- 'Sat Nav' => 'sat-nav',
- 'Sauce' => 'sauce',
- 'Saw' => 'saw',
- 'Scalextric' => 'scalextric',
- 'Scanner' => 'scanner',
- 'School Bag' => 'school-bag',
- 'School Supplies' => 'school',
- 'School Uniform' => 'school-uniform',
- 'Schwalbe' => 'schwalbe',
- 'Scooby Doo' => 'scooby-doo',
- 'Scooter' => 'scooter',
- 'Scotch Whisky' => 'scotch',
- 'Scrabble' => 'scrabble',
- 'Screen Protector' => 'screen-protector',
- 'Screenwash' => 'screenwash',
- 'Screwdriver' => 'screwdriver',
- 'Screws' => 'screws',
- 'SD Cards' => 'sd-card',
- 'SDHC' => 'sdhc',
- 'SDXC' => 'sdxc',
- 'Seagate' => 'seagate',
- 'Sea Life' => 'sea-life',
- 'Sea of Thieves' => 'sea-of-thieves',
- 'Season Pass' => 'season-pass',
- 'Seaworld' => 'seaworld',
- 'Security Camera' => 'security-camera',
- 'Seeds &amp; Bulbs' => 'seeds-and-bulbs',
- 'Sega' => 'sega',
- 'SEGA Mega Drive Mini' => 'sega-mega-drive-mini',
- 'Segway' => 'segway',
- 'Seiko' => 'seiko',
- 'Sekiro: Shadows Die Twice' => 'sekiro',
- 'Sekonda' => 'sekonda',
- 'Selfie Stick' => 'selfie-stick',
- 'Sennheiser' => 'sennheiser',
- 'Sennheiser Headphones' => 'sennheiser-headphones',
- 'Sensodyne' => 'sensodyne',
- 'Server' => 'server',
- 'Services &amp; Contracts' => 'services-contracts',
- 'Services and Subscriptions' => 'service-contract',
- 'Sewing' => 'sewing',
- 'Sewing Machine' => 'sewing-machine',
- 'Sex Toys' => 'sex-toys',
- 'Shadow of the Tomb Raider' => 'shadow-of-the-tomb-raider',
- 'Shampoo' => 'shampoo',
- 'Shark' => 'shark',
- 'Shark DuoClean' => 'shark-duoclean',
- 'Shark Vacuum Cleaner' => 'shark-vacuum-cleaner',
- 'Sharp' => 'sharp',
- 'Sharpener' => 'sharpener',
- 'Sharpie' => 'sharpie',
- 'Shaver' => 'shaver',
- 'Shaving &amp; Beard Care' => 'shaving',
- 'Shaving, Trimming, &amp; Hair Removal' => 'hair-removal',
- 'Shaving Foam' => 'shaving-foam',
- 'Shears' => 'shears',
- 'Sheba' => 'sheba',
- 'Shed' => 'shed',
- 'Shelter' => 'shelter',
- 'Shelves' => 'shelves',
- 'Shenmue I &amp; II' => 'shenmue-one-and-two',
- 'Shenmue III' => 'shenmue-3',
- 'Shenmue Series' => 'shenmue-series',
- 'Shimano' => 'shimano',
- 'Shirt' => 'shirt',
- 'Shoe Rack' => 'shoe-rack',
- 'Shoes' => 'shoe',
- 'Shopkins' => 'shopkins',
- 'Shortbread' => 'shortbread',
- 'Shorts' => 'shorts',
- 'Short Trip' => 'break',
- 'Shoulder Bag' => 'shoulder-bag',
- 'Shovel' => 'shovel',
- 'Shower Curtain' => 'shower-curtain',
- 'Shower Enclosure' => 'shower-enclosure',
- 'Shower Fittings' => 'shower',
- 'Shower Gel' => 'shower-gel',
- 'Shower Head' => 'shower-head',
- 'Shredder' => 'shredder',
- 'Side-by-Side-Fridge' => 'side-by-side-fridge',
- 'Sideboard' => 'sideboard',
- 'Sid Meier&#039;s Civilization VI' => 'civilization-vi',
- 'Siemens' => 'siemens',
- 'Siemens Washing Machine' => 'siemens-washing-machine',
- 'Sigma' => 'sigma',
- 'Silentnight' => 'silentnight',
- 'Silvercrest' => 'silvercrest',
- 'Silver Cross' => 'silver-cross',
- 'Sim Free' => 'sim-free',
- 'Sim Only' => 'sim-only',
- 'Simplehuman' => 'simplehuman',
- 'Simpsons' => 'simpsons',
- 'Single Malt' => 'single-malt',
- 'Sink' => 'sink',
- 'Sistema' => 'sistema',
- 'Skateboard' => 'skateboard',
- 'Skating' => 'skating',
- 'Skechers' => 'skechers',
- 'Skiing' => 'ski',
- 'Skin Care' => 'skincare',
- 'Skittles' => 'skittles',
- 'Skoda' => 'skoda',
- 'Skullcandy' => 'skullcandy',
- 'Sky' => 'sky',
- 'Sky Cinema' => 'sky-cinema',
- 'Skylanders' => 'skylanders',
- 'Skylanders Battlecast' => 'skylanders-battlecast',
- 'Skylanders Imaginators' => 'skylanders-imaginators',
- 'Sleeping Bag' => 'sleeping-bag',
- 'Sleeping Dogs' => 'sleeping-dogs',
- 'Sleepwear' => 'sleepwear',
- 'Slide' => 'slide',
- 'Slimming World' => 'slimming-world',
- 'Slippers' => 'slippers',
- 'Slow Cooker' => 'slow-cooker',
- 'Smart Clock' => 'clock',
- 'Smart Doorbells' => 'smart-doorbell',
- 'Smart Home' => 'smart-home',
- 'Smart Light' => 'smart-light',
- 'Smart Lock' => 'smart-lock',
- 'Smartphone Accessories' => 'smartphone-accessories',
- 'Smartphone Case' => 'smartphone-case',
- 'Smartphone under £200' => 'smartphone-under-200-pounds',
- 'Smartphone under £400' => 'smartphone-under-400-pounds',
- 'Smart Plugs' => 'smart-plugs',
- 'Smart Speaker' => 'smart-speaker',
- 'Smart Tech &amp; Gadgets' => 'smart-tech',
- 'Smart Thermostat' => 'thermostat',
- 'SmartThings' => 'smartthings',
- 'Smart TV' => 'smart-tv',
- 'Smart Watch' => 'smartwatch',
- 'Smeg' => 'smeg',
- 'Smirnoff' => 'smirnoff',
- 'Smoke Alarm' => 'smoke-alarm',
- 'Smoothie' => 'smoothie',
- 'Smoothie Maker' => 'smoothie-maker',
- 'Snacks' => 'snacks',
- 'Sneakers' => 'sneakers',
- 'SNES Nintendo Classic Mini' => 'snes-nintendo-classic',
- 'Snickers' => 'snickers',
- 'Sniper Elite' => 'sniper-elite',
- 'Snowboard' => 'snowboard',
- 'Snow Boots' => 'snow-boots',
- 'Soap' => 'soap',
- 'Soap and Glory' => 'soap-and-glory',
- 'Socket Set' => 'socket-set',
- 'Socks' => 'socks',
- 'SodaStream' => 'soda-stream',
- 'Sofa' => 'sofa',
- 'Soft Drinks' => 'soft-drinks',
- 'Soft Toy' => 'soft-toy',
- 'Software' => 'software',
- 'Software &amp; Apps' => 'software-apps',
- 'Solar Lights' => 'solar-lights',
- 'Soldering Iron' => 'soldering',
- 'Sonic' => 'sonic',
- 'Sonos' => 'sonos',
- 'Sonos Beam' => 'sonos-beam',
- 'Sonos Move' => 'sonos-move',
- 'Sonos One' => 'sonos-one',
- 'Sonos PLAY:1' => 'sonos-play-1',
- 'Sonos PLAY:3' => 'sonos-play-3',
- 'Sonos PLAY:5' => 'sonos-play-5',
- 'Sonos PLAYBAR' => 'sonos-playbar',
- 'Sonos PLAYBASE' => 'sonos-playbase',
- 'Sony' => 'sony',
- 'Sony Camera' => 'sony-camera',
- 'Sony Headphones' => 'sony-headphones',
- 'Sony Pulse 3D Wireless Headset' => 'pulse-3d-wireless-headsets',
- 'Sony TV' => 'sony-tv',
- 'Sony WF-1000XM3' => 'sony-wf1000xm3',
- 'Sony WH-1000XM3' => 'sony-wh-1000xm3',
- 'Sony WH-1000XM4' => 'sony-wh1000xm4',
- 'Sony Xperia' => 'xperia',
- 'Sony Xperia 5' => 'sony-xperia-5',
- 'Sony Xperia 10' => 'sony-xperia-10',
- 'Sony Xperia Xa' => 'sony-xperia-xa',
- 'Sony Xperia Z3' => 'xperia-z3',
- 'Sony Xperia Z5' => 'xperia-z5',
- 'Soulcalibur' => 'soulcalibur',
- 'Soundbar' => 'soundbar',
- 'Soundbase' => 'soundbase',
- 'Sound Card' => 'sound-card',
- 'Soundmagic' => 'soundmagic',
- 'Soup' => 'soup',
- 'Soup Maker' => 'soup-maker',
- 'Sous-Vide' => 'sousvide',
- 'Southern Comfort' => 'southern-comfort',
- 'South Park' => 'south-park',
- 'Spa' => 'spa',
- 'Spade' => 'spade',
- 'Spanner' => 'spanner',
- 'Speaker' => 'speakers',
- 'Specialized' => 'specialized',
- 'Speedo' => 'speedo',
- 'Sphero' => 'sphero',
- 'Spice Rack' => 'spice-rack',
- 'Spiderman' => 'spiderman',
- 'Spiralizer' => 'spiralizer',
- 'Spirit &amp; Liqueur' => 'spirits',
- 'Spirit Level' => 'spirit-level',
- 'Splatoon' => 'splatoon',
- 'Sports &amp; Outdoors' => 'sports-fitness',
- 'Sports Events' => 'sports-events',
- 'Sports Nutrition' => 'nutrition',
- 'Spreads' => 'spreads',
- 'Spyro Reignited Trilogy' => 'spyro-reignited-trilogy',
- 'SSD' => 'ssd',
- 'SSHD' => 'sshd',
- 'Staedtler' => 'staedtler',
- 'Stair Gate' => 'stair-gate',
- 'Stanley' => 'stanley',
- 'Stapler' => 'stapler',
- 'Starbucks' => 'starbucks',
- 'Starlink: Battle for Atlas' => 'starlink-battle-for-atlas',
- 'Star Ocean' => 'star-ocean',
- 'Star Trek' => 'star-trek',
- 'Star Wars' => 'star-wars',
- 'Star Wars: Battlefront' => 'star-wars-battlefront',
- 'Star Wars: Battlefront II' => 'star-wars-battlefront-2',
- 'Star Wars: Squadrons' => 'star-wars-squadrons',
- 'Star Wars Jedi: Fallen Order' => 'star-wars-jedi-fallen-order',
- 'Stationery' => 'stationery',
- 'Stationery &amp; Office Supplies' => 'stationery-office-supplies',
- 'Staycation' => 'staycation',
- 'Steak' => 'steak',
- 'Steam Cleaner' => 'steam-cleaner',
- 'Steam Controller' => 'steam-controller',
- 'Steamer' => 'steamer',
- 'Steam Gaming' => 'steam',
- 'Steam Iron' => 'steam-iron',
- 'Steam Link' => 'steam-link',
- 'Steam Mop' => 'steam-mop',
- 'SteelSeries' => 'steelseries',
- 'Steering Wheel' => 'steering-wheel',
- 'Stella' => 'stella',
- 'Stool' => 'stool',
- 'Storage Box' => 'storage-box',
- 'Stormtrooper' => 'stormtrooper',
- 'Straightener' => 'straightener',
- 'Streaming' => 'streaming',
- 'Street Fighter' => 'street-fighter',
- 'Street Fighter V' => 'street-fighter-v',
- 'Streetwear' => 'streetwear',
- 'Strimmer' => 'strimmer',
- 'Strongbow' => 'strongbow',
- 'Student Discount' => 'student-discount',
- 'Subwoofer' => 'subwoofer',
- 'Suitcase' => 'suitcase',
- 'Suncare' => 'suncare',
- 'Sun Cream' => 'sun-cream',
- 'Sunglasses' => 'sunglasses',
- 'Superdry' => 'superdry',
- 'Superfast Broadband' => 'superfast-broadband',
- 'Superking' => 'superking',
- 'Super Mario' => 'mario',
- 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars',
- 'Super Mario 3D World' => 'super-mario-3d-world',
- 'Super Mario Maker 2' => 'super-mario-maker-2',
- 'Super Mario Odyssey' => 'super-mario-odyssey',
- 'Super Mario Party' => 'mario-party',
- 'Supermarket' => 'supermarket',
- 'Super Smash Bros.' => 'super-smash-bros',
- 'Surf' => 'surf',
- 'Swarovski' => 'swarovski',
- 'Sweets' => 'sweets',
- 'Swimming' => 'swimming',
- 'Swimming Goggles' => 'goggles',
- 'Swimwear' => 'swimwear',
- 'Swing' => 'swing',
- 'Swingball' => 'swingball',
- 'Syberia' => 'syberia',
- 'Sylvanian' => 'sylvanian',
- 'Synology' => 'synology',
- 'T-Mobile' => 't-mobile',
- 'T-Shirt' => 't-shirt',
- 'Table Lamp' => 'table-lamp',
- 'Tablet' => 'tablet',
- 'Tablet Accessories' => 'tablet-accessories',
- 'Table Tennis' => 'table-tennis',
- 'Tableware' => 'tableware',
- 'Tacx' => 'tacx',
- 'Tado' => 'tado',
- 'Tag Heuer' => 'tag-heuer',
- 'Takeaway and Food Delivery' => 'takeaway',
- 'Tales of Vesperia: Definitive Edition' => 'tales-of-vesperia-definitive-edition',
- 'Talisker' => 'talisker',
- 'Talkmobile' => 'talkmobile',
- 'Tamron' => 'tamron',
- 'Tangle Teezer' => 'tangle-teezer',
- 'Tank Top' => 'tank-top',
- 'Tannoy' => 'tannoy',
- 'Tanqueray' => 'tanqueray',
- 'Tape' => 'tape',
- 'Tassimo' => 'tassimo',
- 'Tassimo Coffee Machine' => 'tassimo-coffee-machine',
- 'tastecard' => 'tastecard',
- 'Taxi' => 'taxi',
- 'Tea' => 'tea',
- 'Team Sonic Racing' => 'team-sonic-racing',
- 'Team Sports' => 'team-sports',
- 'Teapot' => 'teapot',
- 'Technika' => 'technika',
- 'Techwood' => 'techwood',
- 'Ted Baker' => 'ted-baker',
- 'Teddy Bear' => 'teddy-bear',
- 'Teenage Mutant Ninja Turtles' => 'turtle',
- 'Teeth Care' => 'teeth-care',
- 'Teeth Whitening' => 'teeth-whitening',
- 'Tefal' => 'tefal',
- 'Tefal Actifry' => 'actifry',
- 'Tefal Pan' => 'tefal-pan',
- 'Tekken' => 'tekken',
- 'Tekken 7' => 'tekken-7',
- 'Telegraph' => 'telegraph',
- 'Telescope' => 'telescope',
- 'Telltale' => 'telltale',
- 'Tennis' => 'tennis',
- 'Tent' => 'tent',
- 'Tequila' => 'tequila',
- 'Tesco Clothing' => 'tesco-clothing',
- 'Tesla' => 'tesla',
- 'Tetris' => 'tetris',
- 'Tetris 99' => 'tetris-99',
- 'Theatre &amp; Musical' => 'theatre',
- 'The Beatles' => 'beatles',
- 'The Big Bang Theory' => 'big-bang-theory',
- 'The Crew' => 'the-crew',
- 'The Dark Pictures: Anthology Man of Medan' => 'the-dark-pictures-anthology-man-of-medan',
- 'The Elder Scrolls' => 'elder-scrolls',
- 'The Elder Scrolls V: Skyrim' => 'skyrim',
- 'The Evil Within' => 'the-evil-within',
- 'The Evil Within 2' => 'the-evil-within-2',
- 'The Last Guardian' => 'the-last-guardian',
- 'The Last of Us' => 'the-last-of-us',
- 'The Last of Us Part II' => 'the-last-of-us-part-2',
- 'The Legend of Zelda' => 'zelda',
- 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild',
- 'The Legend of Zelda: Link&#039;s Awakening' => 'the-legend-of-zelda-links-awakening',
- 'The Legend of Zelda: Skyward Sword HD' => 'the-legend-of-zelda-skyward-sword-hd',
- 'Theme Park' => 'theme-park',
- 'The North Face' => 'north-face',
- 'The Outer Worlds' => 'the-outer-worlds',
- 'Thermos Storage' => 'thermos',
- 'The Sims' => 'sims',
- 'The Sims 4' => 'the-sims-4',
- 'The Sinking City' => 'the-sinking-city',
- 'The Sun' => 'the-sun',
- 'The Sunday Times' => 'sunday-times',
- 'The Walking Dead' => 'walking-dead',
- 'The Witcher' => 'witcher',
- 'The Witcher 3' => 'the-witcher-3',
- 'Thierry Mugler' => 'thierry-mugler',
- 'Thomas Sabo' => 'thomas-sabo',
- 'Thomas The Tank Engine' => 'thomas-the-tank',
- 'Thornton&#039;s' => 'thorntons',
- 'Thorpe Park' => 'thorpe-park',
- 'Throw' => 'throw',
- 'Thrustmaster' => 'thrustmaster',
- 'Thule' => 'thule',
- 'Tickets &amp; Shows' => 'tickets-shows',
- 'Tie' => 'tie',
- 'Tights' => 'tights',
- 'TIGI' => 'tigi',
- 'Tilda' => 'tilda',
- 'Tile' => 'tile',
- 'Timberland' => 'timberland',
- 'Timex' => 'timex',
- 'Tissot' => 'tissot',
- 'Tissues' => 'tissues',
- 'Titanfall' => 'titanfall',
- 'Titanfall 2' => 'titanfall-2',
- 'Toaster' => 'toaster',
- 'Toblerone' => 'toblerone',
- 'Toddler Bed' => 'toddler-bed',
- 'Toilet Brush' => 'brush',
- 'Toilet Cleaner' => 'toilet',
- 'Toilet Roll' => 'toilet-roll',
- 'Toilet Seat' => 'toilet-seat',
- 'Tokyo Laundry' => 'tokyo-laundry',
- 'Tomb Raider' => 'tomb-raider',
- 'Tom Clancy&#039;s' => 'tom-clancy',
- 'Tom Clancy&#039;s: Ghost Recon' => 'ghost-recon',
- 'Tom Clancy&#039;s Ghost Recon: Wildlands' => 'ghost-recon-wildlands',
- 'Tom Clancy&#039;s Ghost Recon Breakpoint' => 'tom-clancys-ghost-recon-breakpoint',
- 'Tom Clancy&#039;s The Division' => 'tom-clancy-the-division',
- 'Tom Clancy&#039;s The Division 2' => 'tom-clancy-the-division-2',
- 'Tom Ford' => 'tom-ford',
- 'Tommee Tippee' => 'tommee-tippee',
- 'Tommy Hilfiger' => 'tommy-hilfiger',
- 'Toms' => 'toms',
- 'TomTom' => 'tomtom',
- 'Tonic Water' => 'tonic-water',
- 'Tony Hawk&#039;s Pro Skater 1 + 2' => 'tony-hawks-pro-skater-1-2',
- 'Tools' => 'tool',
- 'Toothbrush' => 'toothbrush',
- 'Toothpaste' => 'toothpaste',
- 'Torch' => 'torch',
- 'Torque Wrench' => 'torque-wrench',
- 'Toshiba' => 'toshiba',
- 'Toshiba Laptop' => 'toshiba-laptop',
- 'Toshiba TV' => 'toshiba-tv',
- 'Total War' => 'total-war',
- 'Tottenham Hotspur F. C.' => 'tottenham',
- 'Towel' => 'towel',
- 'Toy Box' => 'toy-box',
- 'Toy Cars' => 'toy-cars',
- 'Toy Castle' => 'castle',
- 'Toy Digger' => 'digger',
- 'Toy Helicopter' => 'helicopter',
- 'Toy Kitchen' => 'toy-kitchen',
- 'Toy Mask' => 'mask',
- 'Toyota' => 'toyota',
- 'Toys' => 'toy',
- 'Toy Story' => 'toy-story',
- 'Toy Tractor' => 'tractor',
- 'Toy Train' => 'train',
- 'TP-Link' => 'tp-link',
- 'TP-Link Archer' => 'archer',
- 'TP-Link Router' => 'tp-link-router',
- 'Tracksuit' => 'tracksuit',
- 'Trainers' => 'trainers',
- 'Trains &amp; Buses' => 'train-and-bus-ticket',
- 'Train Ticket' => 'train-ticket',
- 'Trampoline' => 'trampoline',
- 'Transcend' => 'transcend',
- 'Transformers' => 'transformers',
- 'Travel' => 'travel',
- 'Travel App' => 'travel-app',
- 'Travel Insurance' => 'travel-insurance',
- 'Travelodge' => 'travelodge',
- 'Travel System' => 'travel-system',
- 'Treadmill' => 'treadmill',
- 'TRESemmé' => 'tresemme',
- 'Trespass' => 'trespass',
- 'Triathlon' => 'triathlon',
- 'Trike' => 'trike',
- 'Trine 4' => 'trine-4',
- 'Tripod' => 'tripod',
- 'Tripp' => 'tripp',
- 'Triton Shower' => 'triton',
- 'Trolley Bag' => 'trolley',
- 'Tropico 5' => 'tropico-5',
- 'Tropico 6' => 'tropico-6',
- 'Tropico Series' => 'tropico-deals',
- 'Trousers' => 'trousers',
- 'True Wireless Earbuds' => 'wireless-earphones',
- 'Trunki' => 'trunki',
- 'Tumble Dryer' => 'tumble-dryer',
- 'Tuna' => 'tuna',
- 'Turbo Trainer' => 'turbo-trainer',
- 'Turntable' => 'turntable',
- 'Turtle Beach' => 'turtle-beach',
- 'TV' => 'tv',
- 'TV &amp; Video' => 'tv-video',
- 'TV Accessories' => 'tv-accessories',
- 'TV Mount' => 'tv-mount',
- 'TV Series' => 'tv-series',
- 'TV Stand' => 'tv-stand',
- 'Twinings' => 'twinings',
- 'Twin Peaks' => 'twin-peaks',
- 'Twix' => 'twix',
- 'Typhoo' => 'typhoo',
- 'Tyres' => 'tyres',
- 'Ubisoft' => 'ubisoft',
- 'UE BOOM' => 'ue-boom',
- 'UE Boom 2' => 'ue-boom-2',
- 'UEFA' => 'uefa',
- 'UE Megablast' => 'ue-megablast',
- 'UE Megaboom' => 'ue-megaboom',
- 'UGG' => 'ugg',
- 'Ulefone' => 'ulefone',
- 'Ultrabook' => 'ultrabook',
- 'Ultrawide Monitor' => 'ultrawide',
- 'Umbrella' => 'umbrella',
- 'UMI' => 'umidigi',
- 'Uncharted' => 'uncharted',
- 'Uncharted 4: A Thief&#039;s End' => 'uncharted-4',
- 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy',
- 'Under Armour' => 'under-armour',
- 'Underwear' => 'underwear',
- 'Unicorn' => 'unicorn',
- 'UNiDAYS' => 'unidays',
- 'Universal Remote' => 'universal-remote',
- 'Uno' => 'uno',
- 'Uplay' => 'uplay',
- 'Urban Decay' => 'urban-decay',
- 'Urban Sports' => 'urban-sports',
- 'USB Cable' => 'usb-cable',
- 'USB Hub' => 'usb-hub',
- 'USB Memory Stick' => 'flash-drive',
- 'USB Type C' => 'usb-type-c',
- 'USN' => 'usn',
- 'Vacuum Cleaner' => 'vacuum-cleaners',
- 'Vacuum Flask' => 'flask',
- 'Valkyria Chronicles' => 'valkyria-chronicles',
- 'Valkyria Chronicles 4' => 'valkyria-chronicles-4',
- 'Vango' => 'vango',
- 'Vanish' => 'vanish',
- 'Vans' => 'vans',
- 'Vans Old Skool' => 'vans-old-skool',
- 'Vans Shoes' => 'vans-shoes',
- 'Vase' => 'vase',
- 'Vaseline' => 'vaseline',
- 'Vauxhall' => 'vauxhall',
- 'VAX' => 'vax',
- 'Vax Blade' => 'vax-blade',
- 'Vax Vacuum Cleaner' => 'vax-vacuum',
- 'Veet' => 'veet',
- 'Vega 7' => 'vega-7',
- 'Vegetables' => 'vegetables',
- 'Vegetarian' => 'vegetarian',
- 'Vehicles' => 'vehicles',
- 'Velvet Comfort' => 'velvet',
- 'Vera Wang' => 'vera-wang',
- 'Verbatim' => 'verbatim',
- 'Versace' => 'versace',
- 'Vibrator' => 'vibrator',
- 'Victorinox' => 'victorinox',
- 'Video Games' => 'videogame',
- 'Video Streaming' => 'video-streaming',
- 'Viktor &amp; Rolf Spicebomb' => 'spicebomb',
- 'Vileda' => 'vileda',
- 'Villeroy &amp; Boch' => 'villeroy-boch',
- 'Viners' => 'viners',
- 'Vinyl' => 'vinyl',
- 'Virgin' => 'virgin',
- 'Vitamins &amp; Supplements' => 'vitamins',
- 'Vitamix' => 'vitamix',
- 'Vodafone' => 'vodafone',
- 'Vodka' => 'vodka',
- 'Volvo' => 'volvo',
- 'VPN' => 'vpn',
- 'VR Headset' => 'vr-headset',
- 'VTech' => 'vtech',
- 'VTech Toot Toot' => 'toot-toot',
- 'Vue' => 'vue',
- 'VW' => 'vw',
- 'Wacom' => 'wacom',
- 'Waffle Maker' => 'waffle-maker',
- 'Wahl' => 'wahl',
- 'Walkers' => 'walkers',
- 'Walking Boots' => 'walking-boots',
- 'Wall Art' => 'wall-art',
- 'Wallet' => 'wallet',
- 'Wallpaper' => 'wallpaper',
- 'Wardrobe' => 'wardrobe',
- 'Warhammer' => 'warhammer',
- 'Washbag' => 'washbag',
- 'Washer Dryer' => 'washer-dryer',
- 'Washing Machine' => 'washing-machine',
- 'Washing Powder' => 'washing-powder',
- 'Watch' => 'watch',
- 'Watch Dogs' => 'watch-dogs',
- 'Watch Dogs 2' => 'watch-dogs-2',
- 'Watch Dogs: Legion' => 'watch-dogs-legion',
- 'Water Bottle' => 'water-bottle',
- 'Water Butt' => 'water-butt',
- 'Water Dispenser' => 'water-dispenser',
- 'Water Filter' => 'water-filter',
- 'Water Gun' => 'water-gun',
- 'Waterproof Camera' => 'waterproof-camera',
- 'Waterproof Jacket' => 'waterproof-jacket',
- 'Watersports' => 'watersport',
- 'Water Toys' => 'water-toys',
- 'Wayfarer' => 'wayfarer',
- 'WD40' => 'wd40',
- 'Wearable' => 'wearable',
- 'Weather Station' => 'weather-station',
- 'Webcam' => 'webcam',
- 'Weber' => 'weber',
- 'Web Hosting' => 'web-hosting',
- 'Wedding' => 'wedding',
- 'Weed Killer' => 'weed',
- 'Weekend Break' => 'weekend-break',
- 'Weetabix' => 'weetabix',
- 'Weightlifting' => 'weightlifting',
- 'Weight Watchers' => 'weight-watchers',
- 'Wellies' => 'wellies',
- 'Wellness and Health' => 'wellness-and-health',
- 'Wenger' => 'wenger',
- 'Western Digital' => 'western-digital',
- 'Wetsuit' => 'wetsuit',
- 'Wheelbarrow' => 'wheelbarrow',
- 'Wheelchair' => 'wheelchair',
- 'Whey' => 'whey',
- 'Whiskas' => 'whiskas',
- 'Whisky' => 'whisky',
- 'Whole Home Mesh Wi-Fi System' => 'whole-home-mesh-wifi-system',
- 'Wi-Fi Camera' => 'wifi-camera',
- 'Wi-Fi Dongle' => 'dongle',
- 'Wi-Fi Extender' => 'wifi-extender',
- 'Wii' => 'wii',
- 'Wii Game' => 'wii-games',
- 'Wii U Game' => 'wii-u-game',
- 'Wii U Pro Controller' => 'wii-u-pro-controller',
- 'Wild Turkey' => 'wild-turkey',
- 'Wileyfox' => 'wileyfox',
- 'Wilkinson Sword Hydro 5' => 'hydro-5',
- 'Wilkinson Sword Razor' => 'wilkinson-sword',
- 'Wimbledon Tennis' => 'wimbledon',
- 'Window Cleaner' => 'window-cleaner',
- 'Windows' => 'windows',
- 'Windows 8' => 'windows-8',
- 'Windows 10' => 'windows-10',
- 'Wine' => 'wine',
- 'Wine Advent Calendar' => 'wine-advent-calendar',
- 'Wine Glasses' => 'wine-glasses',
- 'Winter Jacket' => 'winter-jacket',
- 'Wiper Blades' => 'wiper-blades',
- 'Wireless Adapter' => 'wireless-adapter',
- 'Wireless Charger' => 'wireless-charger',
- 'Wireless Controller' => 'wireless-controller',
- 'Wireless Headphones' => 'wireless-headphones',
- 'Wireless Headset' => 'wireless-headset',
- 'Wireless Keyboard' => 'wireless-keyboard',
- 'Wireless Mouse' => 'wireless-mouse',
- 'Wok' => 'wok',
- 'Wolfenstein' => 'wolfenstein',
- 'Wolfenstein 2: The New Colossus' => 'wolfenstein-2',
- 'Women&#039;s Boots' => 'womens-boots',
- 'Women&#039;s Fragrance' => 'womens-fragrance',
- 'Women&#039;s Shoes' => 'womens-shoes',
- 'Workbench' => 'workbench',
- 'World of Warcraft' => 'world-of-warcraft',
- 'World War Z' => 'world-war-z',
- 'WORX' => 'worx',
- 'Wreckfest' => 'wreckfest',
- 'Wuaki' => 'wuaki',
- 'WWE 2K' => 'wwe',
- 'Xbox' => 'xbox',
- 'Xbox 360 Game' => 'xbox-360-game',
- 'Xbox Accessories' => 'xbox-accessories',
- 'Xbox Controller' => 'xbox-controller',
- 'Xbox Game Pass' => 'xbox-game-pass',
- 'Xbox Gift Card' => 'xbox-gift-card',
- 'Xbox Headset' => 'xbox-headset',
- 'Xbox Kinect' => 'kinect',
- 'Xbox Live' => 'xbox-live',
- 'Xbox One Controller' => 'xbox-one-controller',
- 'Xbox One Elite Controller' => 'xbox-one-elite-controller',
- 'Xbox One Games' => 'xbox-one-games',
- 'Xbox One S' => 'xbox-one-s',
- 'Xbox One X' => 'xbox-one-x',
- 'Xbox Series S' => 'xbox-series-s',
- 'Xbox Series X' => 'xbox-series-x',
- 'Xbox Series X Controller' => 'xbox-series-x-controller',
- 'Xbox Series X Games' => 'xbox-series-x-game',
- 'Xbox Wireless Adapter' => 'xbox-wireless-adapter',
- 'Xbox Wireless Headset' => 'xbox-wireless-headset',
- 'XCOM' => 'xcom',
- 'XCOM 2' => 'xcom-2',
- 'Xenoblade Chronicles' => 'xenoblade-chronicles',
- 'XFX' => 'xfx',
- 'Xiaomi' => 'xiaomi',
- 'Xiaomi AirDots' => 'xiaomi-airdots',
- 'Xiaomi Black Shark' => 'xiaomi-black-shark',
- 'Xiaomi Black Shark 2' => 'xiaomi-black-shark-2',
- 'Xiaomi Headphones' => 'xiaomi-headphones',
- 'Xiaomi Laptop' => 'xiaomi-laptop',
- 'Xiaomi Mi 5' => 'xiaomi-mi-5',
- 'Xiaomi Mi 6' => 'xiaomi-mi-6',
- 'Xiaomi Mi 8' => 'xiaomi-mi-8',
- 'Xiaomi Mi 8 Lite' => 'xiaomi-mi-8-lite',
- 'Xiaomi Mi 8 Pro' => 'xiaomi-mi-8-pro',
- 'Xiaomi Mi 9' => 'xiaomi-mi-9',
- 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite',
- 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se',
- 'Xiaomi Mi 9T' => 'xiaomi-mi-9t',
- 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro',
- 'Xiaomi Mi 10' => 'xiaomi-mi-10',
- 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite',
- 'Xiaomi Mi 10T' => 'xiaomi-mi-10t',
- 'Xiaomi Mi 10T Lite' => 'xiaomi-mi-10t-lite',
- 'Xiaomi Mi 10T Pro' => 'xiaomi-mi-10t-pro',
- 'Xiaomi Mi 11' => 'xiaomi-mi-11',
- 'Xiaomi Mi 11 Lite 4G' => 'xiaomi-mi-11-lite-4g',
- 'Xiaomi Mi 11 Lite 5G' => 'xiaomi-mi-11-lite-5g',
- 'Xiaomi Mi 11 Pro' => 'xiaomi-mi-11-pro',
- 'Xiaomi Mi 11 Ultra' => 'xiaomi-mi-11-ultra',
- 'Xiaomi Mi 11i' => 'xiaomi-mi-11i',
- 'Xiaomi Mi A1' => 'xiaomi-mi-a1',
- 'Xiaomi Mi A2' => 'mi-a2',
- 'Xiaomi Mi A3' => 'xiaomi-mi-a3',
- 'Xiaomi Mi Band' => 'xiaomi-mi-band',
- 'Xiaomi Mi Band 3' => 'xiaomi-mi-band-3',
- 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4',
- 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5',
- 'Xiaomi Mi Box' => 'xiaomi-mi-box',
- 'Xiaomi Mi Max 3' => 'xiaomi-mi-max3',
- 'Xiaomi Mi Mix' => 'xiaomi-mi-mix',
- 'Xiaomi Mi Mix 2' => 'xiaomi-mi-mix-2',
- 'Xiaomi Mi Mix 2S' => 'xiaomi-mi-mix-2s',
- 'Xiaomi Mi Mix 3' => 'xiaomi-mi-mix-3',
- 'Xiaomi Mi Note' => 'xiaomi-mi-note',
- 'Xiaomi Mi Note 10' => 'mi-note-10',
- 'Xiaomi Mi Pad 4' => 'xiaomi-mi-pad-4',
- 'Xiaomi Pocophone F1' => 'pocophone-f1',
- 'Xiaomi Redmi' => 'redmi',
- 'Xiaomi Redmi 4' => 'xiaomi-redmi-4',
- 'Xiaomi Redmi 5' => 'redmi-5',
- 'Xiaomi Redmi 6' => 'redmi-6',
- 'Xiaomi Redmi 8' => 'redmi-8',
- 'Xiaomi Redmi Note 4' => 'note-4',
- 'Xiaomi Redmi Note 5' => 'redmi-note-5',
- 'Xiaomi Redmi Note 6' => 'redmi-note-6',
- 'Xiaomi Redmi Note 6 Pro' => 'xiaomi-redmi-note-6-pro',
- 'Xiaomi Redmi Note 7' => 'redmi-note-7',
- 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8',
- 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro',
- 'Xiaomi Redmi Note 8T' => 'redmi-note-8t',
- 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9',
- 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro',
- 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s',
- 'Xiaomi Roborock' => 'xiaomi-roborock',
- 'Xiaomi Roborock S5' => 'xiaomi-roborock-s5',
- 'Xiaomi Scooter' => 'xiaomi-scooter',
- 'Xiaomi Smartphones' => 'xiaomi-smartphone',
- 'Xiaomi Tablets' => 'xiaomi-tablet',
- 'Yakuza' => 'yakuza',
- 'Yale' => 'yale',
- 'Yale Smart Lock' => 'yale-smart-lock',
- 'Yamaha' => 'yamaha',
- 'Yankee Candle' => 'yankee-candle',
- 'Yeelight' => 'xiaomi-yeelight',
- 'Yoga' => 'yoga',
- 'Yoghurt' => 'yoghurt',
- 'Yoshi' => 'yoshi',
- 'Yoshi&#039;s Crafted World' => 'yoshis-crafted-world',
- 'YouView' => 'youview',
- 'Yves Saint Laurent' => 'yves-saint-laurent',
- 'Zanussi' => 'zanussi',
- 'Zippo' => 'zippo',
- 'Zizzi' => 'zizzi',
- 'Zoo' => 'zoo',
- 'Zoostorm' => 'zoostorm',
- 'ZOTAC' => 'zotac',
- 'ZTE' => 'zte',
- 'ZTE Smartphone' => 'zte-smartphone',
- 'ZyXEL' => 'zyxel',
- )
- ),
- 'order' => array(
- 'name' => 'Order by',
- 'type' => 'list',
- 'title' => 'Sort order of deals',
- 'values' => array(
- 'From the most to the least hot deal' => '-hot',
- 'From the most recent deal to the oldest' => '-new',
- )
- )
- ),
- 'Discussion Monitoring' => array(
- 'url' => array(
- 'name' => 'Discussion URL',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Discussion URL to monitor. Ex: https://www.hotukdeals.com/discussions/title-123',
- 'exampleValue' => 'https://www.hotukdeals.com/discussions/the-hukd-lego-thread-3599357',
- ),
- 'only_with_url' => array(
- 'name' => 'Exclude comments without URL',
- 'type' => 'checkbox',
- 'title' => 'Exclude comments that does not contains URL in the feed',
- 'defaultValue' => false,
- )
- )
+ ];
- );
-
- public $lang = array(
- 'bridge-uri' => SELF::URI,
- 'bridge-name' => SELF::NAME,
- 'context-keyword' => 'Search by keyword(s))',
- 'context-group' => 'Deals per group',
- 'context-talk' => 'Discussion Monitoring',
- 'uri-group' => 'tag/',
- 'request-error' => 'Could not request HotUKDeals',
- 'thread-error' => 'Unable to determine the thread ID. Check the URL you entered',
- 'no-results' => 'Ooops, looks like we could',
- 'relative-date-indicator' => array(
- 'ago',
- ),
- 'price' => 'Price',
- 'shipping' => 'Shipping',
- 'origin' => 'Origin',
- 'discount' => 'Discount',
- 'title-keyword' => 'Search',
- 'title-group' => 'Group',
- 'title-talk' => 'Discussion Monitoring',
- 'local-months' => array(
- 'Jan',
- 'Feb',
- 'Mar',
- 'Apr',
- 'May',
- 'Jun',
- 'Jul',
- 'Aug',
- 'Sep',
- 'Occ',
- 'Nov',
- 'Dec',
- 'st',
- 'nd',
- 'rd',
- 'th'
- ),
- 'local-time-relative' => array(
- 'Posted ',
- 'm',
- 'h,',
- 'day',
- 'days',
- 'month',
- 'year',
- 'and '
- ),
- 'date-prefixes' => array(
- 'Found ',
- 'Refreshed ',
- 'Made hot '
- ),
- 'relative-date-alt-prefixes' => array(
- 'Made hot ',
- 'Refreshed ',
- 'Last updated '
- ),
- 'relative-date-ignore-suffix' => array(
- '/by.*$/'
- ),
- 'localdeal' => array(
- 'Local',
- 'Expires'
- )
- );
-
+ public $lang = [
+ 'bridge-uri' => self::URI,
+ 'bridge-name' => self::NAME,
+ 'context-keyword' => 'Search by keyword(s))',
+ 'context-group' => 'Deals per group',
+ 'context-talk' => 'Discussion Monitoring',
+ 'uri-group' => 'tag/',
+ 'request-error' => 'Could not request HotUKDeals',
+ 'thread-error' => 'Unable to determine the thread ID. Check the URL you entered',
+ 'no-results' => 'Ooops, looks like we could',
+ 'relative-date-indicator' => [
+ 'ago',
+ ],
+ 'price' => 'Price',
+ 'shipping' => 'Shipping',
+ 'origin' => 'Origin',
+ 'discount' => 'Discount',
+ 'title-keyword' => 'Search',
+ 'title-group' => 'Group',
+ 'title-talk' => 'Discussion Monitoring',
+ 'local-months' => [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Occ',
+ 'Nov',
+ 'Dec',
+ 'st',
+ 'nd',
+ 'rd',
+ 'th'
+ ],
+ 'local-time-relative' => [
+ 'Posted ',
+ 'm',
+ 'h,',
+ 'day',
+ 'days',
+ 'month',
+ 'year',
+ 'and '
+ ],
+ 'date-prefixes' => [
+ 'Found ',
+ 'Refreshed ',
+ 'Made hot '
+ ],
+ 'relative-date-alt-prefixes' => [
+ 'Made hot ',
+ 'Refreshed ',
+ 'Last updated '
+ ],
+ 'relative-date-ignore-suffix' => [
+ '/by.*$/'
+ ],
+ 'localdeal' => [
+ 'Local',
+ 'Expires'
+ ]
+ ];
}
diff --git a/bridges/IGNBridge.php b/bridges/IGNBridge.php
index ef5088f2..d00b6a18 100644
--- a/bridges/IGNBridge.php
+++ b/bridges/IGNBridge.php
@@ -1,65 +1,68 @@
<?php
-class IGNBridge extends FeedExpander {
- const MAINTAINER = 'IceWreck';
- const NAME = 'IGN Bridge';
- const URI = 'https://www.ign.com/';
- const CACHE_TIMEOUT = 3600;
- const DESCRIPTION = 'RSS Feed For IGN';
+class IGNBridge extends FeedExpander
+{
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'IGN Bridge';
+ const URI = 'https://www.ign.com/';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'RSS Feed For IGN';
- public function collectData(){
- $this->collectExpandableDatas('http://feeds.ign.com/ign/all', 15);
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas('http://feeds.ign.com/ign/all', 15);
+ }
- // IGNs feed is both hidden and incomplete. This bridge tries to fix this.
+ // IGNs feed is both hidden and incomplete. This bridge tries to fix this.
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
- // $articlePage gets the entire page's contents
- $articlePage = getSimpleHTMLDOM($newsItem->link);
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
- // List of BS elements
- $uselessElements = array(
- '.wiki-page-tools',
- '.feedback-container',
- '.paging-container',
- '.dropdown-wrapper',
- '.mw-editsection',
- '.jsx-4115608983',
- '.jsx-4213937408',
- '.commerce-container',
- '.widget-container',
- '.newsletter-signup-button'
- );
+ // List of BS elements
+ $uselessElements = [
+ '.wiki-page-tools',
+ '.feedback-container',
+ '.paging-container',
+ '.dropdown-wrapper',
+ '.mw-editsection',
+ '.jsx-4115608983',
+ '.jsx-4213937408',
+ '.commerce-container',
+ '.widget-container',
+ '.newsletter-signup-button'
+ ];
- // Remove useless elements
- foreach($uselessElements as $uslElement) {
- foreach($articlePage->find($uslElement) as $jsWidget) {
- $jsWidget->remove();
- }
- }
+ // Remove useless elements
+ foreach ($uselessElements as $uslElement) {
+ foreach ($articlePage->find($uslElement) as $jsWidget) {
+ $jsWidget->remove();
+ }
+ }
- /*
- * NOTE: Though articles and wiki/howtos have seperate styles of pages, there is no mechanism
- * for handling them seperately as it just ignores the DOM querys which it does not find.
- * (and their scraping)
- */
+ /*
+ * NOTE: Though articles and wiki/howtos have seperate styles of pages, there is no mechanism
+ * for handling them seperately as it just ignores the DOM querys which it does not find.
+ * (and their scraping)
+ */
- // For Articles
- $article = $articlePage->find('section.article-page', 0);
- // add in verdicts in articles, reviews etc
- foreach($articlePage->find('div.article-section') as $element) {
- $article = $article . $element;
- }
+ // For Articles
+ $article = $articlePage->find('section.article-page', 0);
+ // add in verdicts in articles, reviews etc
+ foreach ($articlePage->find('div.article-section') as $element) {
+ $article = $article . $element;
+ }
- // For Wikis and HowTos
- foreach($articlePage->find('.wiki-page') as $wikiContents) {
- $article = $article . $wikiContents;
- }
+ // For Wikis and HowTos
+ foreach ($articlePage->find('.wiki-page') as $wikiContents) {
+ $article = $article . $wikiContents;
+ }
- // Add content to feed
- $item['content'] = $article;
- return $item;
- }
+ // Add content to feed
+ $item['content'] = $article;
+ return $item;
+ }
}
diff --git a/bridges/IKWYDBridge.php b/bridges/IKWYDBridge.php
index eed7dc38..344efffe 100644
--- a/bridges/IKWYDBridge.php
+++ b/bridges/IKWYDBridge.php
@@ -1,114 +1,128 @@
<?php
-class IKWYDBridge extends BridgeAbstract {
- const MAINTAINER = 'DevonHess';
- const NAME = 'I Know What You Download';
- const URI = 'https://iknowwhatyoudownload.com/';
- const CACHE_TIMEOUT = 3600; // 1h
- const DESCRIPTION = 'Returns torrent downloads and distributions for an IP address';
- const PARAMETERS = array(
- array(
- 'ip' => array(
- 'name' => 'IP Address',
- 'exampleValue' => '8.8.8.8',
- 'required' => true
- ),
- 'update' => array(
- 'name' => 'Update last seen',
- 'type' => 'checkbox',
- 'title' => 'Update timestamp every time "last seen" changes'
- )
- )
- );
- private $name;
- private $uri;
- public function detectParameters($url) {
- $params = array();
+class IKWYDBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'DevonHess';
+ const NAME = 'I Know What You Download';
+ const URI = 'https://iknowwhatyoudownload.com/';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Returns torrent downloads and distributions for an IP address';
+ const PARAMETERS = [
+ [
+ 'ip' => [
+ 'name' => 'IP Address',
+ 'exampleValue' => '8.8.8.8',
+ 'required' => true
+ ],
+ 'update' => [
+ 'name' => 'Update last seen',
+ 'type' => 'checkbox',
+ 'title' => 'Update timestamp every time "last seen" changes'
+ ]
+ ]
+ ];
+ private $name;
+ private $uri;
- $regex = '/^(https?:\/\/)?iknowwhatyoudownload\.com\/';
- $regex .= '(?:en|ru)\/peer\/\?ip=(\d+\.\d+\.\d+\.\d+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['ip'] = urldecode($matches[2]);
- return $params;
- }
+ public function detectParameters($url)
+ {
+ $params = [];
- $regex = '/^(https?:\/\/)?iknowwhatyoudownload\.com\/';
- $regex .= '(?:(?:en|ru)\/peer\/)?/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['ip'] = $_SERVER['REMOTE_ADDR'];
- return $params;
- }
+ $regex = '/^(https?:\/\/)?iknowwhatyoudownload\.com\/';
+ $regex .= '(?:en|ru)\/peer\/\?ip=(\d+\.\d+\.\d+\.\d+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['ip'] = urldecode($matches[2]);
+ return $params;
+ }
- return null;
- }
+ $regex = '/^(https?:\/\/)?iknowwhatyoudownload\.com\/';
+ $regex .= '(?:(?:en|ru)\/peer\/)?/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['ip'] = $_SERVER['REMOTE_ADDR'];
+ return $params;
+ }
- public function getName() {
- if($this->name) {
- return $this->name;
- } else {
- return self::NAME;
- }
- }
+ return null;
+ }
- public function getURI() {
- if($this->uri) {
- return $this->uri;
- } else {
- return self::URI;
- }
- }
+ public function getName()
+ {
+ if ($this->name) {
+ return $this->name;
+ } else {
+ return self::NAME;
+ }
+ }
- public function collectData() {
- $ip = $this->getInput('ip');
- $root = self::URI . 'en/peer/?ip=' . $ip;
- $html = getSimpleHTMLDOM($root);
+ public function getURI()
+ {
+ if ($this->uri) {
+ return $this->uri;
+ } else {
+ return self::URI;
+ }
+ }
- $this->name = 'IKWYD: ' . $ip;
- $this->uri = $root;
+ public function collectData()
+ {
+ $ip = $this->getInput('ip');
+ $root = self::URI . 'en/peer/?ip=' . $ip;
+ $html = getSimpleHTMLDOM($root);
- foreach($html->find('.table > tbody > tr') as $download) {
- $download = defaultLinkTo($download, self::URI);
- $firstSeen = $download->find('.date-column',
- 0)->innertext;
- $lastSeen = $download->find('.date-column',
- 1)->innertext;
- $category = $download->find('.category-column',
- 0)->innertext;
- $torlink = $download->find('.name-column > div > a',
- 0);
- $tortitle = strip_tags($torlink);
- $size = $download->find('td', 4)->innertext;
- $title = $tortitle;
- $author = $ip;
+ $this->name = 'IKWYD: ' . $ip;
+ $this->uri = $root;
- if($this->getInput('update')) {
- $timestamp = strtotime($lastSeen);
- } else {
- $timestamp = strtotime($firstSeen);
- }
+ foreach ($html->find('.table > tbody > tr') as $download) {
+ $download = defaultLinkTo($download, self::URI);
+ $firstSeen = $download->find(
+ '.date-column',
+ 0
+ )->innertext;
+ $lastSeen = $download->find(
+ '.date-column',
+ 1
+ )->innertext;
+ $category = $download->find(
+ '.category-column',
+ 0
+ )->innertext;
+ $torlink = $download->find(
+ '.name-column > div > a',
+ 0
+ );
+ $tortitle = strip_tags($torlink);
+ $size = $download->find('td', 4)->innertext;
+ $title = $tortitle;
+ $author = $ip;
- $uri = $torlink->href;
+ if ($this->getInput('update')) {
+ $timestamp = strtotime($lastSeen);
+ } else {
+ $timestamp = strtotime($firstSeen);
+ }
- $content = 'IP address: <a href="' . $root . '">';
- $content .= $ip . '</a><br>';
- $content .= 'First seen: ' . $firstSeen . '<br>';
- $content .= ($this->getInput('update') ? 'Last seen: ' .
- $lastSeen . '<br>' : '');
- $content .= ($category ? 'Category: ' .
- $category . '<br>' : '');
- $content .= 'Title: ' . $torlink . '<br>';
- $content .= 'Size: ' . $size;
+ $uri = $torlink->href;
- $item = array();
- $item['uri'] = $uri;
- $item['title'] = $title;
- $item['author'] = $author;
- $item['timestamp'] = $timestamp;
- $item['content'] = $content;
- if($category) {
- $item['categories'] = array($category);
- }
- $this->items[] = $item;
- }
- }
+ $content = 'IP address: <a href="' . $root . '">';
+ $content .= $ip . '</a><br>';
+ $content .= 'First seen: ' . $firstSeen . '<br>';
+ $content .= ($this->getInput('update') ? 'Last seen: ' .
+ $lastSeen . '<br>' : '');
+ $content .= ($category ? 'Category: ' .
+ $category . '<br>' : '');
+ $content .= 'Title: ' . $torlink . '<br>';
+ $content .= 'Size: ' . $size;
+
+ $item = [];
+ $item['uri'] = $uri;
+ $item['title'] = $title;
+ $item['author'] = $author;
+ $item['timestamp'] = $timestamp;
+ $item['content'] = $content;
+ if ($category) {
+ $item['categories'] = [$category];
+ }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/IPBBridge.php b/bridges/IPBBridge.php
index af2ed390..d5db0111 100644
--- a/bridges/IPBBridge.php
+++ b/bridges/IPBBridge.php
@@ -1,309 +1,325 @@
<?php
-class IPBBridge extends FeedExpander {
-
- const NAME = 'IPB Bridge';
- const URI = 'https://www.invisionpower.com';
- const DESCRIPTION = 'Returns feeds for forums powered by IPB';
- const MAINTAINER = 'logmanoriginal';
- const PARAMETERS = array(
- array(
- 'uri' => array(
- 'name' => 'URI',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Insert forum, subforum or topic URI',
- 'exampleValue' => 'https://invisioncommunity.com/forums/forum/499-feedback-and-ideas/'
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Specifies the number of items to return on each request (-1: all)',
- 'defaultValue' => 10
- )
- )
- );
- const CACHE_TIMEOUT = 3600;
-
- // Constants for internal use
- const FORUM_TYPE_LIST_FILTER = '.cForumTopicTable';
- const FORUM_TYPE_TABLE_FILTER = '#forum_table';
-
- const TOPIC_TYPE_ARTICLE = 'article';
- const TOPIC_TYPE_DIV = 'div.post_block';
-
- public function getURI(){
- return $this->getInput('uri') ?: parent::getURI();
- }
-
- public function collectData(){
- // The URI cannot be the mainpage (or anything related)
- switch(parse_url($this->getInput('uri'), PHP_URL_PATH)) {
- case null:
- case '/index.php':
- returnClientError('Provided URI is invalid!');
- break;
- default:
- break;
- }
-
- // Sanitize the URI (because else it won't work)
- $uri = rtrim($this->getInput('uri'), '/'); // No trailing slashes!
-
- // Forums might provide feeds, though that's optional *facepalm*
- // Let's check if there is a valid feed available
- $headers = get_headers($uri . '.xml');
-
- if($headers[0] === 'HTTP/1.1 200 OK') { // Heureka! It's a valid feed!
- return $this->collectExpandableDatas($uri . '.xml');
- }
-
- // No valid feed, so do it the hard way
- $html = getSimpleHTMLDOM($uri);
-
- $limit = $this->getInput('limit');
-
- // Determine if this is a topic or a forum
- switch(true) {
- case $this->isTopic($html):
- $this->collectTopic($html, $limit);
- break;
- case $this->isForum($html):
- $this->collectForum($html);
- break;
- default:
- returnClientError('Unknown type!');
- break;
- }
- }
-
- private function isForum($html){
- return !is_null($html->find('div[data-controller*=forums.front.forum.forumPage]', 0))
- || !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0));
- }
-
- private function isTopic($html){
- return !is_null($html->find('div[data-controller*=core.front.core.commentFeed]', 0))
- || !is_null($html->find(static::TOPIC_TYPE_DIV, 0));
- }
-
- private function collectForum($html){
- // There are multiple forum designs in use (depends on version?)
- // 1 - Uses an ordered list (based on https://invisioncommunity.com/forums)
- // 2 - Uses a table (based on https://onehallyu.com)
-
- switch(true) {
- case !is_null($html->find(static::FORUM_TYPE_LIST_FILTER, 0)):
- $this->collectForumList($html);
- break;
- case !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)):
- $this->collectForumTable($html);
- break;
- default:
- returnClientError('Unknown forum format!');
- break;
- }
- }
-
- private function collectForumList($html){
- foreach($html->find(static::FORUM_TYPE_LIST_FILTER, 0)->children() as $row) {
- // Columns: Title, Statistics, Last modified
- $item = array();
-
- $item['uri'] = $row->find('a', 0)->href;
- $item['title'] = $row->find('a', 0)->title;
- $item['author'] = $row->find('a', 1)->innertext;
- $item['timestamp'] = strtotime($row->find('time', 0)->getAttribute('datetime'));
-
- $this->items[] = $item;
- }
- }
-
- private function collectForumTable($html){
- foreach($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)->children() as $row) {
- // Columns: Icon, Content, Preview, Statistics, Last modified
- $item = array();
-
- // Skip header row
- if(!is_null($row->find('th', 0))) continue;
-
- $item['uri'] = $row->find('a', 0)->href;
- $item['title'] = $row->find('.title', 0)->plaintext;
- $item['timestamp'] = strtotime($row->find('[itemprop=dateCreated]', 0)->plaintext);
-
- $this->items[] = $item;
- }
- }
-
- private function collectTopic($html, $limit){
- // There are multiple topic designs in use (depends on version?)
- // 1 - Uses articles (based on https://invisioncommunity.com/forums)
- // 2 - Uses divs (based on https://onehallyu.com)
-
- switch(true) {
- case !is_null($html->find(static::TOPIC_TYPE_ARTICLE, 0)):
- $this->collectTopicHistory($html, $limit, 'collectTopicArticle');
- break;
- case !is_null($html->find(static::TOPIC_TYPE_DIV, 0)):
- $this->collectTopicHistory($html, $limit, 'collectTopicDiv');
- break;
- default:
- returnClientError('Unknown topic format!');
- break;
- }
- }
-
- private function collectTopicHistory($html, $limit, $callback){
- // Make sure the callback is valid!
- if(!method_exists($this, $callback))
- returnServerError('Unknown function (\'' . $callback . '\')!');
-
- $next = null; // Holds the URI of the next page
-
- while(true) {
- $next = $this->$callback($html, is_null($next));
-
- if(is_null($next) || ($limit > 0 && count($this->items) >= $limit)) {
- break;
- }
-
- $html = getSimpleHTMLDOMCached($next);
- }
-
- // We might have more items than specified, remove excess
- $this->items = array_slice($this->items, 0, $limit);
- }
-
- private function collectTopicArticle($html, $firstrun = true){
- $title = $html->find('h1.ipsType_pageTitle', 0)->plaintext;
-
- // Are we on last page?
- if($firstrun && !is_null($html->find('.ipsPagination', 0))) {
- $last = $html->find('.ipsPagination_last a', 0)->{'data-page'};
- $active = $html->find('.ipsPagination_active a', 0)->{'data-page'};
-
- if($active !== $last) {
- // Load last page into memory (cached)
- $html = getSimpleHTMLDOMCached($html->find('.ipsPagination_last a', 0)->href);
- }
- }
-
- foreach(array_reverse($html->find(static::TOPIC_TYPE_ARTICLE)) as $article) {
- $item = array();
-
- $item['uri'] = $article->find('time', 0)->parent()->href;
- $item['author'] = $article->find('aside a', 0)->plaintext;
- $item['title'] = $item['author'] . ' - ' . $title;
- $item['timestamp'] = strtotime($article->find('time', 0)->getAttribute('datetime'));
-
- $content = $article->find('[data-role=commentContent]', 0);
- $content = $this->scaleImages($content);
- $item['content'] = $this->fixContent($content);
- $item['enclosures'] = $this->findImages($article->find('[data-role=commentContent]', 0)) ?: null;
-
- $this->items[] = $item;
- }
-
- // Return whatever page comes next (previous, as we add in inverse order)
- // Do we have a previous page? (inactive means no)
- if(!is_null($html->find('li[class=ipsPagination_prev ipsPagination_inactive]', 0))) {
- return null; // No, or no more
- } elseif(!is_null($html->find('li[class=ipsPagination_prev]', 0))) {
- return $html->find('.ipsPagination_prev a', 0)->href;
- }
-
- return null;
- }
-
- private function collectTopicDiv($html, $firstrun = true){
- $title = $html->find('h1.ipsType_pagetitle', 0)->plaintext;
-
- // Are we on last page?
- if($firstrun && !is_null($html->find('.pagination', 0))) {
-
- $active = $html->find('li[class=page active]', 0)->plaintext;
-
- // There are two ways the 'last' page is displayed:
- // - With a distict 'last' button (only if there are enough pages)
- // - With a button for each page (use last button)
- if(!is_null($html->find('li.last', 0))) {
- $last = $html->find('li.last a', 0);
- } else {
- $last = $html->find('li[class=page] a', -1);
- }
-
- if($active !== $last->plaintext) {
- // Load last page into memory (cached)
- $html = getSimpleHTMLDOMCached($last->href);
- }
- }
-
- foreach(array_reverse($html->find(static::TOPIC_TYPE_DIV)) as $article) {
- $item = array();
-
- $item['uri'] = $article->find('a[rel=bookmark]', 0)->href;
- $item['author'] = $article->find('.author', 0)->plaintext;
- $item['title'] = $item['author'] . ' - ' . $title;
- $item['timestamp'] = strtotime($article->find('.published', 0)->getAttribute('title'));
-
- $content = $article->find('[itemprop=commentText]', 0);
- $content = $this->scaleImages($content);
- $item['content'] = $this->fixContent($content);
-
- $item['enclosures'] = $this->findImages($article->find('.post_body', 0)) ?: null;
- $this->items[] = $item;
- }
-
- // Return whatever page comes next (previous, as we add in inverse order)
- // Do we have a previous page?
- if(!is_null($html->find('li.prev', 0))) {
- return $html->find('li.prev a', 0)->href;
- }
-
- return null;
- }
-
- /** Returns all images from the provide HTML DOM */
- private function findImages($html){
- $images = array();
-
- foreach($html->find('img') as $img) {
- $images[] = $img->src;
- }
-
- return $images;
- }
-
- /** Sets the maximum width and height for all images */
- private function scaleImages($html, $width = 400, $height = 400){
- foreach($html->find('img') as $img) {
- $img->style = "max-width: {$width}px; max-height: {$height}px;";
- }
-
- return $html;
- }
-
- /** Removes all unnecessary tags and adds formatting */
- private function fixContent($html){
-
- // Restore quote highlighting
- foreach($html->find('blockquote') as $quote) {
- $quote->style = <<<EOD
+class IPBBridge extends FeedExpander
+{
+ const NAME = 'IPB Bridge';
+ const URI = 'https://www.invisionpower.com';
+ const DESCRIPTION = 'Returns feeds for forums powered by IPB';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = [
+ [
+ 'uri' => [
+ 'name' => 'URI',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert forum, subforum or topic URI',
+ 'exampleValue' => 'https://invisioncommunity.com/forums/forum/499-feedback-and-ideas/'
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Specifies the number of items to return on each request (-1: all)',
+ 'defaultValue' => 10
+ ]
+ ]
+ ];
+ const CACHE_TIMEOUT = 3600;
+
+ // Constants for internal use
+ const FORUM_TYPE_LIST_FILTER = '.cForumTopicTable';
+ const FORUM_TYPE_TABLE_FILTER = '#forum_table';
+
+ const TOPIC_TYPE_ARTICLE = 'article';
+ const TOPIC_TYPE_DIV = 'div.post_block';
+
+ public function getURI()
+ {
+ return $this->getInput('uri') ?: parent::getURI();
+ }
+
+ public function collectData()
+ {
+ // The URI cannot be the mainpage (or anything related)
+ switch (parse_url($this->getInput('uri'), PHP_URL_PATH)) {
+ case null:
+ case '/index.php':
+ returnClientError('Provided URI is invalid!');
+ break;
+ default:
+ break;
+ }
+
+ // Sanitize the URI (because else it won't work)
+ $uri = rtrim($this->getInput('uri'), '/'); // No trailing slashes!
+
+ // Forums might provide feeds, though that's optional *facepalm*
+ // Let's check if there is a valid feed available
+ $headers = get_headers($uri . '.xml');
+
+ if ($headers[0] === 'HTTP/1.1 200 OK') { // Heureka! It's a valid feed!
+ return $this->collectExpandableDatas($uri . '.xml');
+ }
+
+ // No valid feed, so do it the hard way
+ $html = getSimpleHTMLDOM($uri);
+
+ $limit = $this->getInput('limit');
+
+ // Determine if this is a topic or a forum
+ switch (true) {
+ case $this->isTopic($html):
+ $this->collectTopic($html, $limit);
+ break;
+ case $this->isForum($html):
+ $this->collectForum($html);
+ break;
+ default:
+ returnClientError('Unknown type!');
+ break;
+ }
+ }
+
+ private function isForum($html)
+ {
+ return !is_null($html->find('div[data-controller*=forums.front.forum.forumPage]', 0))
+ || !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0));
+ }
+
+ private function isTopic($html)
+ {
+ return !is_null($html->find('div[data-controller*=core.front.core.commentFeed]', 0))
+ || !is_null($html->find(static::TOPIC_TYPE_DIV, 0));
+ }
+
+ private function collectForum($html)
+ {
+ // There are multiple forum designs in use (depends on version?)
+ // 1 - Uses an ordered list (based on https://invisioncommunity.com/forums)
+ // 2 - Uses a table (based on https://onehallyu.com)
+
+ switch (true) {
+ case !is_null($html->find(static::FORUM_TYPE_LIST_FILTER, 0)):
+ $this->collectForumList($html);
+ break;
+ case !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)):
+ $this->collectForumTable($html);
+ break;
+ default:
+ returnClientError('Unknown forum format!');
+ break;
+ }
+ }
+
+ private function collectForumList($html)
+ {
+ foreach ($html->find(static::FORUM_TYPE_LIST_FILTER, 0)->children() as $row) {
+ // Columns: Title, Statistics, Last modified
+ $item = [];
+
+ $item['uri'] = $row->find('a', 0)->href;
+ $item['title'] = $row->find('a', 0)->title;
+ $item['author'] = $row->find('a', 1)->innertext;
+ $item['timestamp'] = strtotime($row->find('time', 0)->getAttribute('datetime'));
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function collectForumTable($html)
+ {
+ foreach ($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)->children() as $row) {
+ // Columns: Icon, Content, Preview, Statistics, Last modified
+ $item = [];
+
+ // Skip header row
+ if (!is_null($row->find('th', 0))) {
+ continue;
+ }
+
+ $item['uri'] = $row->find('a', 0)->href;
+ $item['title'] = $row->find('.title', 0)->plaintext;
+ $item['timestamp'] = strtotime($row->find('[itemprop=dateCreated]', 0)->plaintext);
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function collectTopic($html, $limit)
+ {
+ // There are multiple topic designs in use (depends on version?)
+ // 1 - Uses articles (based on https://invisioncommunity.com/forums)
+ // 2 - Uses divs (based on https://onehallyu.com)
+
+ switch (true) {
+ case !is_null($html->find(static::TOPIC_TYPE_ARTICLE, 0)):
+ $this->collectTopicHistory($html, $limit, 'collectTopicArticle');
+ break;
+ case !is_null($html->find(static::TOPIC_TYPE_DIV, 0)):
+ $this->collectTopicHistory($html, $limit, 'collectTopicDiv');
+ break;
+ default:
+ returnClientError('Unknown topic format!');
+ break;
+ }
+ }
+
+ private function collectTopicHistory($html, $limit, $callback)
+ {
+ // Make sure the callback is valid!
+ if (!method_exists($this, $callback)) {
+ returnServerError('Unknown function (\'' . $callback . '\')!');
+ }
+
+ $next = null; // Holds the URI of the next page
+
+ while (true) {
+ $next = $this->$callback($html, is_null($next));
+
+ if (is_null($next) || ($limit > 0 && count($this->items) >= $limit)) {
+ break;
+ }
+
+ $html = getSimpleHTMLDOMCached($next);
+ }
+
+ // We might have more items than specified, remove excess
+ $this->items = array_slice($this->items, 0, $limit);
+ }
+
+ private function collectTopicArticle($html, $firstrun = true)
+ {
+ $title = $html->find('h1.ipsType_pageTitle', 0)->plaintext;
+
+ // Are we on last page?
+ if ($firstrun && !is_null($html->find('.ipsPagination', 0))) {
+ $last = $html->find('.ipsPagination_last a', 0)->{'data-page'};
+ $active = $html->find('.ipsPagination_active a', 0)->{'data-page'};
+
+ if ($active !== $last) {
+ // Load last page into memory (cached)
+ $html = getSimpleHTMLDOMCached($html->find('.ipsPagination_last a', 0)->href);
+ }
+ }
+
+ foreach (array_reverse($html->find(static::TOPIC_TYPE_ARTICLE)) as $article) {
+ $item = [];
+
+ $item['uri'] = $article->find('time', 0)->parent()->href;
+ $item['author'] = $article->find('aside a', 0)->plaintext;
+ $item['title'] = $item['author'] . ' - ' . $title;
+ $item['timestamp'] = strtotime($article->find('time', 0)->getAttribute('datetime'));
+
+ $content = $article->find('[data-role=commentContent]', 0);
+ $content = $this->scaleImages($content);
+ $item['content'] = $this->fixContent($content);
+ $item['enclosures'] = $this->findImages($article->find('[data-role=commentContent]', 0)) ?: null;
+
+ $this->items[] = $item;
+ }
+
+ // Return whatever page comes next (previous, as we add in inverse order)
+ // Do we have a previous page? (inactive means no)
+ if (!is_null($html->find('li[class=ipsPagination_prev ipsPagination_inactive]', 0))) {
+ return null; // No, or no more
+ } elseif (!is_null($html->find('li[class=ipsPagination_prev]', 0))) {
+ return $html->find('.ipsPagination_prev a', 0)->href;
+ }
+
+ return null;
+ }
+
+ private function collectTopicDiv($html, $firstrun = true)
+ {
+ $title = $html->find('h1.ipsType_pagetitle', 0)->plaintext;
+
+ // Are we on last page?
+ if ($firstrun && !is_null($html->find('.pagination', 0))) {
+ $active = $html->find('li[class=page active]', 0)->plaintext;
+
+ // There are two ways the 'last' page is displayed:
+ // - With a distict 'last' button (only if there are enough pages)
+ // - With a button for each page (use last button)
+ if (!is_null($html->find('li.last', 0))) {
+ $last = $html->find('li.last a', 0);
+ } else {
+ $last = $html->find('li[class=page] a', -1);
+ }
+
+ if ($active !== $last->plaintext) {
+ // Load last page into memory (cached)
+ $html = getSimpleHTMLDOMCached($last->href);
+ }
+ }
+
+ foreach (array_reverse($html->find(static::TOPIC_TYPE_DIV)) as $article) {
+ $item = [];
+
+ $item['uri'] = $article->find('a[rel=bookmark]', 0)->href;
+ $item['author'] = $article->find('.author', 0)->plaintext;
+ $item['title'] = $item['author'] . ' - ' . $title;
+ $item['timestamp'] = strtotime($article->find('.published', 0)->getAttribute('title'));
+
+ $content = $article->find('[itemprop=commentText]', 0);
+ $content = $this->scaleImages($content);
+ $item['content'] = $this->fixContent($content);
+
+ $item['enclosures'] = $this->findImages($article->find('.post_body', 0)) ?: null;
+
+ $this->items[] = $item;
+ }
+
+ // Return whatever page comes next (previous, as we add in inverse order)
+ // Do we have a previous page?
+ if (!is_null($html->find('li.prev', 0))) {
+ return $html->find('li.prev a', 0)->href;
+ }
+
+ return null;
+ }
+
+ /** Returns all images from the provide HTML DOM */
+ private function findImages($html)
+ {
+ $images = [];
+
+ foreach ($html->find('img') as $img) {
+ $images[] = $img->src;
+ }
+
+ return $images;
+ }
+
+ /** Sets the maximum width and height for all images */
+ private function scaleImages($html, $width = 400, $height = 400)
+ {
+ foreach ($html->find('img') as $img) {
+ $img->style = "max-width: {$width}px; max-height: {$height}px;";
+ }
+
+ return $html;
+ }
+
+ /** Removes all unnecessary tags and adds formatting */
+ private function fixContent($html)
+ {
+ // Restore quote highlighting
+ foreach ($html->find('blockquote') as $quote) {
+ $quote->style = <<<EOD
padding: 0px 15px;
border-width: 1px 1px 1px 2px;
border-style: solid;
border-color: #ededed #e8e8e8 #dbdbdb #666666;
background: #fbfbfb;
EOD;
- }
+ }
- // Remove unnecessary tags
- $content = strip_tags(
- $html->innertext,
- '<p><a><img><ol><ul><li><table><tr><th><td><strong><blockquote><br><hr><h>'
- );
+ // Remove unnecessary tags
+ $content = strip_tags(
+ $html->innertext,
+ '<p><a><img><ol><ul><li><table><tr><th><td><strong><blockquote><br><hr><h>'
+ );
- return $content;
- }
+ return $content;
+ }
}
diff --git a/bridges/IdenticaBridge.php b/bridges/IdenticaBridge.php
index a9f47d10..6029eea2 100644
--- a/bridges/IdenticaBridge.php
+++ b/bridges/IdenticaBridge.php
@@ -1,52 +1,56 @@
<?php
-class IdenticaBridge extends BridgeAbstract {
-
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Identica Bridge';
- const URI = 'https://identi.ca/';
- const CACHE_TIMEOUT = 300; // 5min
- const DESCRIPTION = 'Returns user timelines';
-
- const PARAMETERS = array( array(
- 'u' => array(
- 'name' => 'username',
- 'exampleValue' => 'jxself',
- 'required' => true
- )
- ));
-
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI());
-
- foreach($html->find('li.major') as $dent) {
- $item = array();
-
- // get dent link
- $item['uri'] = html_entity_decode($dent->find('a', 0)->href);
-
- // extract dent timestamp
- $item['timestamp'] = strtotime($dent->find('abbr.easydate', 0)->plaintext);
-
- // extract dent text
- $item['content'] = trim($dent->find('div.activity-content', 0)->innertext);
- $item['title'] = $this->getInput('u') . ' | ' . $item['content'];
- $this->items[] = $item;
- }
- }
-
- public function getName(){
- if(!is_null($this->getInput('u'))) {
- return $this->getInput('u') . ' - Identica Bridge';
- }
-
- return parent::getName();
- }
-
- public function getURI(){
- if(!is_null($this->getInput('u'))) {
- return self::URI . urlencode($this->getInput('u'));
- }
-
- return parent::getURI();
- }
+
+class IdenticaBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Identica Bridge';
+ const URI = 'https://identi.ca/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns user timelines';
+
+ const PARAMETERS = [ [
+ 'u' => [
+ 'name' => 'username',
+ 'exampleValue' => 'jxself',
+ 'required' => true
+ ]
+ ]];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ foreach ($html->find('li.major') as $dent) {
+ $item = [];
+
+ // get dent link
+ $item['uri'] = html_entity_decode($dent->find('a', 0)->href);
+
+ // extract dent timestamp
+ $item['timestamp'] = strtotime($dent->find('abbr.easydate', 0)->plaintext);
+
+ // extract dent text
+ $item['content'] = trim($dent->find('div.activity-content', 0)->innertext);
+ $item['title'] = $this->getInput('u') . ' | ' . $item['content'];
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('u'))) {
+ return $this->getInput('u') . ' - Identica Bridge';
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('u'))) {
+ return self::URI . urlencode($this->getInput('u'));
+ }
+
+ return parent::getURI();
+ }
}
diff --git a/bridges/IndeedBridge.php b/bridges/IndeedBridge.php
index d86430a0..080d232d 100644
--- a/bridges/IndeedBridge.php
+++ b/bridges/IndeedBridge.php
@@ -1,220 +1,227 @@
<?php
-class IndeedBridge extends BridgeAbstract {
-
- const NAME = 'Indeed';
- const URI = 'https://www.indeed.com/';
- const DESCRIPTION = 'Returns reviews and comments for a company of your choice';
- const MAINTAINER = 'logmanoriginal';
- const CACHE_TIMEOUT = 14400; // 4 hours
-
- const PARAMETERS = array(
- array(
- 'c' => array(
- 'name' => 'Company',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Company name',
- 'exampleValue' => 'GitHub',
- )
- ),
- 'global' => array(
- 'language' => array(
- 'name' => 'Language Code',
- 'type' => 'list',
- 'title' => 'Choose your language code',
- 'defaultValue' => 'en-US',
- 'values' => array(
- 'es-AR' => 'es-AR',
- 'de-AT' => 'de-AT',
- 'en-AU' => 'en-AU',
- 'nl-BE' => 'nl-BE',
- 'fr-BE' => 'fr-BE',
- 'pt-BR' => 'pt-BR',
- 'en-CA' => 'en-CA',
- 'fr-CA' => 'fr-CA',
- 'de-CH' => 'de-CH',
- 'fr-CH' => 'fr-CH',
- 'es-CL' => 'es-CL',
- 'zh-CN' => 'zh-CN',
- 'es-CO' => 'es-CO',
- 'de-DE' => 'de-DE',
- 'es-ES' => 'es-ES',
- 'fr-FR' => 'fr-FR',
- 'en-GB' => 'en-GB',
- 'en-HK' => 'en-HK',
- 'en-IE' => 'en-IE',
- 'en-IN' => 'en-IN',
- 'it-IT' => 'it-IT',
- 'ja-JP' => 'ja-JP',
- 'ko-KR' => 'ko-KR',
- 'es-MX' => 'es-MX',
- 'nl-NL' => 'nl-NL',
- 'pl-PL' => 'pl-PL',
- 'en-SG' => 'en-SG',
- 'en-US' => 'en-US',
- 'en-ZA' => 'en-ZA',
- 'en-AE' => 'en-AE',
- 'da-DK' => 'da-DK',
- 'in-ID' => 'in-ID',
- 'en-MY' => 'en-MY',
- 'es-PE' => 'es-PE',
- 'en-PH' => 'en-PH',
- 'en-PK' => 'en-PK',
- 'ro-RO' => 'ro-RO',
- 'ru-RU' => 'ru-RU',
- 'tr-TR' => 'tr-TR',
- 'zh-TW' => 'zh-TW',
- 'vi-VN' => 'vi-VN',
- 'en-VN' => 'en-VN',
- 'ar-EG' => 'ar-EG',
- 'fr-MA' => 'fr-MA',
- 'en-NG' => 'en-NG',
- )
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => true,
- 'title' => 'Maximum number of items to return',
- 'exampleValue' => 20,
- )
- )
- );
-
- const SITES = array(
- 'es-AR' => 'https://ar.indeed.com/',
- 'de-AT' => 'https://at.indeed.com/',
- 'en-AU' => 'https://au.indeed.com/',
- 'nl-BE' => 'https://be.indeed.com/',
- 'fr-BE' => 'https://emplois.be.indeed.com/',
- 'pt-BR' => 'https://www.indeed.com.br/',
- 'en-CA' => 'https://ca.indeed.com/',
- 'fr-CA' => 'https://emplois.ca.indeed.com/',
- 'de-CH' => 'https://www.indeed.ch/',
- 'fr-CH' => 'https://emplois.indeed.ch/',
- 'es-CL' => 'https://www.indeed.cl/',
- 'zh-CN' => 'https://cn.indeed.com/',
- 'es-CO' => 'https://co.indeed.com/',
- 'de-DE' => 'https://de.indeed.com/',
- 'es-ES' => 'https://www.indeed.es/',
- 'fr-FR' => 'https://www.indeed.fr/',
- 'en-GB' => 'https://www.indeed.co.uk/',
- 'en-HK' => 'https://www.indeed.hk/',
- 'en-IE' => 'https://ie.indeed.com/',
- 'en-IN' => 'https://www.indeed.co.in/',
- 'it-IT' => 'https://it.indeed.com/',
- 'ja-JP' => 'https://jp.indeed.com/',
- 'ko-KR' => 'https://kr.indeed.com/',
- 'es-MX' => 'https://www.indeed.com.mx/',
- 'nl-NL' => 'https://www.indeed.nl/',
- 'pl-PL' => 'https://pl.indeed.com/',
- 'en-SG' => 'https://www.indeed.com.sg/',
- 'en-US' => 'https://www.indeed.com/',
- 'en-ZA' => 'https://www.indeed.co.za/',
- 'en-AE' => 'https://www.indeed.ae/',
- 'da-DK' => 'https://dk.indeed.com/',
- 'in-ID' => 'https://id.indeed.com/',
- 'en-MY' => 'https://www.indeed.com.my/',
- 'es-PE' => 'https://www.indeed.com.pe/',
- 'en-PH' => 'https://www.indeed.com.ph/',
- 'en-PK' => 'https://www.indeed.com.pk/',
- 'ro-RO' => 'https://ro.indeed.com/',
- 'ru-RU' => 'https://ru.indeed.com/',
- 'tr-TR' => 'https://tr.indeed.com/',
- 'zh-TW' => 'https://tw.indeed.com/',
- 'vi-VN' => 'https://vn.indeed.com/',
- 'en-VN' => 'https://jobs.vn.indeed.com/',
- 'ar-EG' => 'https://eg.indeed.com/',
- 'fr-MA' => 'https://ma.indeed.com/',
- 'en-NG' => 'https://ng.indeed.com/',
- );
-
- private $title;
-
- public function collectData() {
-
- $url = $this->getURI();
- $limit = $this->getInput('limit') ?: 20;
-
- do {
-
- $html = getSimpleHTMLDOM($url);
-
- $html = defaultLinkTo($html, $url);
-
- $this->title = $html->find('h1', 0)->innertext;
-
- foreach($html->find('.cmp-ReviewsList div[itemprop="review"]') as $review) {
- $item = array();
-
- $title = $review->find('h2[data-testid="title"]', 0)->innertext;
- $rating = $review->find('meta[itemprop="ratingValue"]', 0)->getAttribute('content');
- $comment = $review->find('span[itemprop="reviewBody"]', 0)->innertext;
-
- $item['uri'] = $review->find('a[data-tn-element="individualReviewLink"]', 0)->href;
- $item['title'] = "$title | ($rating)";
- $item['author'] = $review->find('span > meta[itemprop="name"]', 0)->getAttribute('content');
- $item['content'] = $comment;
-
- $this->items[] = $item;
-
- if(count($this->items) >= $limit) {
- break;
- }
- }
- } while(count($this->items) < $limit);
- }
-
- public function getURI() {
- if($this->getInput('language')
- && $this->getInput('c')) {
- return self::SITES[$this->getInput('language')]
- . 'cmp/'
- . urlencode($this->getInput('c'))
- . '/reviews';
- }
-
- return parent::getURI();
- }
-
- public function getName() {
- return $this->title ?: parent::getName();
- }
-
- public function detectParameters($url) {
- /**
- * Expected: https://<...>.indeed.<...>/cmp/<company>[/reviews][/...]
- *
- * Note that most users will be redirected to their localized version
- * of the page, which adds the language code to the host. For example,
- * "en.indeed.com" or "www.indeed.fr" (see link[rel="alternate"]). At
- * least each of the sites have ".indeed." in the name.
- */
-
- if(filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false
- || stristr($url, '.indeed.') === false) {
- return null;
- }
-
- $url_components = parse_url($url);
- $path_segments = array_values(array_filter(explode('/', $url_components['path'])));
-
- if(count($path_segments) < 2 || $path_segments[0] !== 'cmp') {
- return null;
- }
-
- $language = array_search('https://' . $url_components['host'] . '/', self::SITES);
- if($language === false) {
- return null;
- }
-
- $limit = self::PARAMETERS['global']['limit']['defaultValue'] ?: 20;
- $company = $path_segments[1];
-
- return array(
- 'c' => $company,
- 'language' => $language,
- 'limit' => $limit,
- );
- }
+
+class IndeedBridge extends BridgeAbstract
+{
+ const NAME = 'Indeed';
+ const URI = 'https://www.indeed.com/';
+ const DESCRIPTION = 'Returns reviews and comments for a company of your choice';
+ const MAINTAINER = 'logmanoriginal';
+ const CACHE_TIMEOUT = 14400; // 4 hours
+
+ const PARAMETERS = [
+ [
+ 'c' => [
+ 'name' => 'Company',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Company name',
+ 'exampleValue' => 'GitHub',
+ ]
+ ],
+ 'global' => [
+ 'language' => [
+ 'name' => 'Language Code',
+ 'type' => 'list',
+ 'title' => 'Choose your language code',
+ 'defaultValue' => 'en-US',
+ 'values' => [
+ 'es-AR' => 'es-AR',
+ 'de-AT' => 'de-AT',
+ 'en-AU' => 'en-AU',
+ 'nl-BE' => 'nl-BE',
+ 'fr-BE' => 'fr-BE',
+ 'pt-BR' => 'pt-BR',
+ 'en-CA' => 'en-CA',
+ 'fr-CA' => 'fr-CA',
+ 'de-CH' => 'de-CH',
+ 'fr-CH' => 'fr-CH',
+ 'es-CL' => 'es-CL',
+ 'zh-CN' => 'zh-CN',
+ 'es-CO' => 'es-CO',
+ 'de-DE' => 'de-DE',
+ 'es-ES' => 'es-ES',
+ 'fr-FR' => 'fr-FR',
+ 'en-GB' => 'en-GB',
+ 'en-HK' => 'en-HK',
+ 'en-IE' => 'en-IE',
+ 'en-IN' => 'en-IN',
+ 'it-IT' => 'it-IT',
+ 'ja-JP' => 'ja-JP',
+ 'ko-KR' => 'ko-KR',
+ 'es-MX' => 'es-MX',
+ 'nl-NL' => 'nl-NL',
+ 'pl-PL' => 'pl-PL',
+ 'en-SG' => 'en-SG',
+ 'en-US' => 'en-US',
+ 'en-ZA' => 'en-ZA',
+ 'en-AE' => 'en-AE',
+ 'da-DK' => 'da-DK',
+ 'in-ID' => 'in-ID',
+ 'en-MY' => 'en-MY',
+ 'es-PE' => 'es-PE',
+ 'en-PH' => 'en-PH',
+ 'en-PK' => 'en-PK',
+ 'ro-RO' => 'ro-RO',
+ 'ru-RU' => 'ru-RU',
+ 'tr-TR' => 'tr-TR',
+ 'zh-TW' => 'zh-TW',
+ 'vi-VN' => 'vi-VN',
+ 'en-VN' => 'en-VN',
+ 'ar-EG' => 'ar-EG',
+ 'fr-MA' => 'fr-MA',
+ 'en-NG' => 'en-NG',
+ ]
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => true,
+ 'title' => 'Maximum number of items to return',
+ 'exampleValue' => 20,
+ ]
+ ]
+ ];
+
+ const SITES = [
+ 'es-AR' => 'https://ar.indeed.com/',
+ 'de-AT' => 'https://at.indeed.com/',
+ 'en-AU' => 'https://au.indeed.com/',
+ 'nl-BE' => 'https://be.indeed.com/',
+ 'fr-BE' => 'https://emplois.be.indeed.com/',
+ 'pt-BR' => 'https://www.indeed.com.br/',
+ 'en-CA' => 'https://ca.indeed.com/',
+ 'fr-CA' => 'https://emplois.ca.indeed.com/',
+ 'de-CH' => 'https://www.indeed.ch/',
+ 'fr-CH' => 'https://emplois.indeed.ch/',
+ 'es-CL' => 'https://www.indeed.cl/',
+ 'zh-CN' => 'https://cn.indeed.com/',
+ 'es-CO' => 'https://co.indeed.com/',
+ 'de-DE' => 'https://de.indeed.com/',
+ 'es-ES' => 'https://www.indeed.es/',
+ 'fr-FR' => 'https://www.indeed.fr/',
+ 'en-GB' => 'https://www.indeed.co.uk/',
+ 'en-HK' => 'https://www.indeed.hk/',
+ 'en-IE' => 'https://ie.indeed.com/',
+ 'en-IN' => 'https://www.indeed.co.in/',
+ 'it-IT' => 'https://it.indeed.com/',
+ 'ja-JP' => 'https://jp.indeed.com/',
+ 'ko-KR' => 'https://kr.indeed.com/',
+ 'es-MX' => 'https://www.indeed.com.mx/',
+ 'nl-NL' => 'https://www.indeed.nl/',
+ 'pl-PL' => 'https://pl.indeed.com/',
+ 'en-SG' => 'https://www.indeed.com.sg/',
+ 'en-US' => 'https://www.indeed.com/',
+ 'en-ZA' => 'https://www.indeed.co.za/',
+ 'en-AE' => 'https://www.indeed.ae/',
+ 'da-DK' => 'https://dk.indeed.com/',
+ 'in-ID' => 'https://id.indeed.com/',
+ 'en-MY' => 'https://www.indeed.com.my/',
+ 'es-PE' => 'https://www.indeed.com.pe/',
+ 'en-PH' => 'https://www.indeed.com.ph/',
+ 'en-PK' => 'https://www.indeed.com.pk/',
+ 'ro-RO' => 'https://ro.indeed.com/',
+ 'ru-RU' => 'https://ru.indeed.com/',
+ 'tr-TR' => 'https://tr.indeed.com/',
+ 'zh-TW' => 'https://tw.indeed.com/',
+ 'vi-VN' => 'https://vn.indeed.com/',
+ 'en-VN' => 'https://jobs.vn.indeed.com/',
+ 'ar-EG' => 'https://eg.indeed.com/',
+ 'fr-MA' => 'https://ma.indeed.com/',
+ 'en-NG' => 'https://ng.indeed.com/',
+ ];
+
+ private $title;
+
+ public function collectData()
+ {
+ $url = $this->getURI();
+ $limit = $this->getInput('limit') ?: 20;
+
+ do {
+ $html = getSimpleHTMLDOM($url);
+
+ $html = defaultLinkTo($html, $url);
+
+ $this->title = $html->find('h1', 0)->innertext;
+
+ foreach ($html->find('.cmp-ReviewsList div[itemprop="review"]') as $review) {
+ $item = [];
+
+ $title = $review->find('h2[data-testid="title"]', 0)->innertext;
+ $rating = $review->find('meta[itemprop="ratingValue"]', 0)->getAttribute('content');
+ $comment = $review->find('span[itemprop="reviewBody"]', 0)->innertext;
+
+ $item['uri'] = $review->find('a[data-tn-element="individualReviewLink"]', 0)->href;
+ $item['title'] = "$title | ($rating)";
+ $item['author'] = $review->find('span > meta[itemprop="name"]', 0)->getAttribute('content');
+ $item['content'] = $comment;
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= $limit) {
+ break;
+ }
+ }
+ } while (count($this->items) < $limit);
+ }
+
+ public function getURI()
+ {
+ if (
+ $this->getInput('language')
+ && $this->getInput('c')
+ ) {
+ return self::SITES[$this->getInput('language')]
+ . 'cmp/'
+ . urlencode($this->getInput('c'))
+ . '/reviews';
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ return $this->title ?: parent::getName();
+ }
+
+ public function detectParameters($url)
+ {
+ /**
+ * Expected: https://<...>.indeed.<...>/cmp/<company>[/reviews][/...]
+ *
+ * Note that most users will be redirected to their localized version
+ * of the page, which adds the language code to the host. For example,
+ * "en.indeed.com" or "www.indeed.fr" (see link[rel="alternate"]). At
+ * least each of the sites have ".indeed." in the name.
+ */
+
+ if (
+ filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false
+ || stristr($url, '.indeed.') === false
+ ) {
+ return null;
+ }
+
+ $url_components = parse_url($url);
+ $path_segments = array_values(array_filter(explode('/', $url_components['path'])));
+
+ if (count($path_segments) < 2 || $path_segments[0] !== 'cmp') {
+ return null;
+ }
+
+ $language = array_search('https://' . $url_components['host'] . '/', self::SITES);
+ if ($language === false) {
+ return null;
+ }
+
+ $limit = self::PARAMETERS['global']['limit']['defaultValue'] ?: 20;
+ $company = $path_segments[1];
+
+ return [
+ 'c' => $company,
+ 'language' => $language,
+ 'limit' => $limit,
+ ];
+ }
}
diff --git a/bridges/IndiegogoBridge.php b/bridges/IndiegogoBridge.php
index 8e725209..c5e5033f 100644
--- a/bridges/IndiegogoBridge.php
+++ b/bridges/IndiegogoBridge.php
@@ -1,141 +1,142 @@
<?php
-class IndiegogoBridge extends BridgeAbstract {
- const NAME = 'Indiegogo';
- const URI = 'https://www.indiegogo.com';
- const DESCRIPTION = 'Fetch projects by category';
- const MAINTAINER = 'bockiii';
- const PARAMETERS = array(
- 'global' => array(
- 'timing' => array(
- 'name' => 'Project Timing',
- 'type' => 'list',
- 'values' => array(
- 'All' => 'all',
- 'Launching Soon' => 'launching_soon',
- 'Just Launched' => 'just_launched',
- 'Ending Soon' => 'ending_soon',
- ),
- 'defaultValue' => 'Just Launched'
- ),
- ),
- 'All Categories' => array(),
- 'Tech & Innovation' => array(
- 'tech' => array(
- 'name' => 'Tech & Innovation',
- 'type' => 'list',
- 'values' => array(
- 'All' => 'all',
- 'Audio' => 'Audio',
- 'Camera Gear' => 'Camera Gear',
- 'Education' => 'Education',
- 'Energy & Green Tech' => 'Energy & Green Tech',
- 'Fashion & Wearables' => 'Fashion & Wearables',
- 'Food & Beverages' => 'Food & Beverages',
- 'Health & Fitness' => 'Health & Fitness',
- 'Home' => 'Home',
- 'Phones & Accessories' => 'Phones & Accessories',
- 'Productivity' => 'Productivity',
- 'Transportation' => 'Transportation',
- 'Travel & Outdoors' => 'Travel & Outdoors',
- ),
- ),
- ),
- 'Creative Works' => array(
- 'creative' => array(
- 'name' => 'Creative Works',
- 'type' => 'list',
- 'values' => array(
- 'All' => 'all',
- 'Comics' => 'Comics',
- 'Dance & Theater' => 'Dance & Theater',
- 'Film' => 'Film',
- 'Music' => 'Music',
- 'Photography' => 'Photography',
- 'Podcasts, Blogs & Vlogs' => 'Podcasts, Blogs & Vlogs',
- 'Tabletop Games' => 'Tabletop Games',
- 'Video Games' => 'Video Games',
- 'Web Series & TV Shows' => 'Web Series & TV Shows',
- 'Writing & Publishing' => 'Writing & Publishing',
- ),
- ),
- ),
- 'Community Projects' => array(
- 'community' => array(
- 'name' => 'Community Projects',
- 'type' => 'list',
- 'values' => array(
- 'All' => 'all',
- 'Culture' => 'Culture',
- 'Environment' => 'Environment',
- 'Human Rights' => 'Human Rights',
- 'Local Businesses' => 'Local Businesses',
- 'Wellness' => 'Wellness',
- ),
- ),
- ),
- );
+class IndiegogoBridge extends BridgeAbstract
+{
+ const NAME = 'Indiegogo';
+ const URI = 'https://www.indiegogo.com';
+ const DESCRIPTION = 'Fetch projects by category';
+ const MAINTAINER = 'bockiii';
+ const PARAMETERS = [
+ 'global' => [
+ 'timing' => [
+ 'name' => 'Project Timing',
+ 'type' => 'list',
+ 'values' => [
+ 'All' => 'all',
+ 'Launching Soon' => 'launching_soon',
+ 'Just Launched' => 'just_launched',
+ 'Ending Soon' => 'ending_soon',
+ ],
+ 'defaultValue' => 'Just Launched'
+ ],
+ ],
+ 'All Categories' => [],
+ 'Tech & Innovation' => [
+ 'tech' => [
+ 'name' => 'Tech & Innovation',
+ 'type' => 'list',
+ 'values' => [
+ 'All' => 'all',
+ 'Audio' => 'Audio',
+ 'Camera Gear' => 'Camera Gear',
+ 'Education' => 'Education',
+ 'Energy & Green Tech' => 'Energy & Green Tech',
+ 'Fashion & Wearables' => 'Fashion & Wearables',
+ 'Food & Beverages' => 'Food & Beverages',
+ 'Health & Fitness' => 'Health & Fitness',
+ 'Home' => 'Home',
+ 'Phones & Accessories' => 'Phones & Accessories',
+ 'Productivity' => 'Productivity',
+ 'Transportation' => 'Transportation',
+ 'Travel & Outdoors' => 'Travel & Outdoors',
+ ],
+ ],
+ ],
+ 'Creative Works' => [
+ 'creative' => [
+ 'name' => 'Creative Works',
+ 'type' => 'list',
+ 'values' => [
+ 'All' => 'all',
+ 'Comics' => 'Comics',
+ 'Dance & Theater' => 'Dance & Theater',
+ 'Film' => 'Film',
+ 'Music' => 'Music',
+ 'Photography' => 'Photography',
+ 'Podcasts, Blogs & Vlogs' => 'Podcasts, Blogs & Vlogs',
+ 'Tabletop Games' => 'Tabletop Games',
+ 'Video Games' => 'Video Games',
+ 'Web Series & TV Shows' => 'Web Series & TV Shows',
+ 'Writing & Publishing' => 'Writing & Publishing',
+ ],
+ ],
+ ],
+ 'Community Projects' => [
+ 'community' => [
+ 'name' => 'Community Projects',
+ 'type' => 'list',
+ 'values' => [
+ 'All' => 'all',
+ 'Culture' => 'Culture',
+ 'Environment' => 'Environment',
+ 'Human Rights' => 'Human Rights',
+ 'Local Businesses' => 'Local Businesses',
+ 'Wellness' => 'Wellness',
+ ],
+ ],
+ ],
+ ];
- const CACHE_TIMEOUT = 21600; // 6 hours
+ const CACHE_TIMEOUT = 21600; // 6 hours
- public function collectData() {
+ public function collectData()
+ {
+ $url = 'https://www.indiegogo.com/private_api/discover';
+ $data_array = self::getCategories();
- $url = 'https://www.indiegogo.com/private_api/discover';
- $data_array = self::getCategories();
+ $header = ['Content-type: application/json'];
+ $opts = [CURLOPT_POSTFIELDS => json_encode($data_array)];
+ $html = getContents($url, $header, $opts);
+ $html_response = json_decode($html, true);
- $header = array('Content-type: application/json');
- $opts = array(CURLOPT_POSTFIELDS => json_encode($data_array));
- $html = getContents($url, $header, $opts);
- $html_response = json_decode($html, true);
+ foreach ($html_response['response']['discoverables'] as $obj) {
+ $this->items[] = [
+ 'title' => $obj['title'],
+ 'uri' => $this->getURI() . $obj['clickthrough_url'],
+ 'timestamp' => $obj['open_date'],
+ 'enclosures' => $obj['image_url'],
+ 'content' => '<a href=' . $this->getURI() . $obj['clickthrough_url']
+ . '><img src="' . $obj['image_url'] . '" /></a><br><br><b>'
+ . $obj['title'] . '</b><br><br><small>'
+ . $obj['tagline'] . '</small><br>',
+ ];
+ }
+ }
- foreach ($html_response['response']['discoverables'] as $obj) {
- $this->items[] = array(
- 'title' => $obj['title'],
- 'uri' => $this->getURI() . $obj['clickthrough_url'],
- 'timestamp' => $obj['open_date'],
- 'enclosures' => $obj['image_url'],
- 'content' => '<a href=' . $this->getURI() . $obj['clickthrough_url']
- . '><img src="' . $obj['image_url'] . '" /></a><br><br><b>'
- . $obj['title'] . '</b><br><br><small>'
- . $obj['tagline'] . '</small><br>',
- );
- }
- }
+ protected function getCategories()
+ {
+ $selection = [
+ 'sort' => 'trending',
+ 'project_type' => 'campaign',
+ 'project_timing' => $this->getInput('timing'),
+ 'category_main' => null,
+ 'category_top_level' => null,
+ 'page_num' => 1,
+ 'per_page' => 12,
+ 'q' => '',
+ 'tags' => []
+ ];
- protected function getCategories() {
-
- $selection = array(
- 'sort' => 'trending',
- 'project_type' => 'campaign',
- 'project_timing' => $this->getInput('timing'),
- 'category_main' => null,
- 'category_top_level' => null,
- 'page_num' => 1,
- 'per_page' => 12,
- 'q' => '',
- 'tags' => array()
- );
-
- switch($this->queriedContext) {
- case 'Tech & Innovation':
- $selection['category_top_level'] = $this->queriedContext;
- if ($this->getInput('tech') != 'all') {
- $selection['category_main'] = $this->getInput('tech');
- }
- break;
- case 'Creative Works':
- $selection['category_top_level'] = $this->queriedContext;
- if ($this->getInput('creative') != 'all') {
- $selection['category_main'] = $this->getInput('creative');
- }
- break;
- case 'Community Projects':
- $selection['category_top_level'] = $this->queriedContext;
- if ($this->getInput('community') != 'all') {
- $selection['category_main'] = $this->getInput('community');
- }
- break;
- }
- return $selection;
- }
+ switch ($this->queriedContext) {
+ case 'Tech & Innovation':
+ $selection['category_top_level'] = $this->queriedContext;
+ if ($this->getInput('tech') != 'all') {
+ $selection['category_main'] = $this->getInput('tech');
+ }
+ break;
+ case 'Creative Works':
+ $selection['category_top_level'] = $this->queriedContext;
+ if ($this->getInput('creative') != 'all') {
+ $selection['category_main'] = $this->getInput('creative');
+ }
+ break;
+ case 'Community Projects':
+ $selection['category_top_level'] = $this->queriedContext;
+ if ($this->getInput('community') != 'all') {
+ $selection['category_main'] = $this->getInput('community');
+ }
+ break;
+ }
+ return $selection;
+ }
}
diff --git a/bridges/InstagramBridge.php b/bridges/InstagramBridge.php
index 4d05e3d4..ce7ff2bf 100644
--- a/bridges/InstagramBridge.php
+++ b/bridges/InstagramBridge.php
@@ -1,312 +1,330 @@
<?php
-class InstagramBridge extends BridgeAbstract {
-
- // const MAINTAINER = 'pauder';
- const NAME = 'Instagram Bridge';
- const URI = 'https://www.instagram.com/';
- const DESCRIPTION = 'Returns the newest images';
-
- const CONFIGURATION = array(
- 'session_id' => array(
- 'required' => false,
- ),
- 'cache_timeout' => array(
- 'required' => false,
- ),
- );
-
- const PARAMETERS = array(
- 'Username' => array(
- 'u' => array(
- 'name' => 'username',
- 'exampleValue' => 'aesoprockwins',
- 'required' => true
- )
- ),
- 'Hashtag' => array(
- 'h' => array(
- 'name' => 'hashtag',
- 'exampleValue' => 'beautifulday',
- 'required' => true
- )
- ),
- 'Location' => array(
- 'l' => array(
- 'name' => 'location',
- 'exampleValue' => 'london',
- 'required' => true
- )
- ),
- 'global' => array(
- 'media_type' => array(
- 'name' => 'Media type',
- 'type' => 'list',
- 'required' => false,
- 'values' => array(
- 'All' => 'all',
- 'Video' => 'video',
- 'Picture' => 'picture',
- 'Multiple' => 'multiple',
- ),
- 'defaultValue' => 'all'
- ),
- 'direct_links' => array(
- 'name' => 'Use direct media links',
- 'type' => 'checkbox',
- )
- )
-
- );
-
- const TEST_DETECT_PARAMETERS = array(
- 'https://www.instagram.com/metaverse' => array('u' => 'metaverse'),
- 'https://instagram.com/metaverse' => array('u' => 'metaverse'),
- 'http://www.instagram.com/metaverse' => array('u' => 'metaverse'),
- );
-
- const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951';
- const TAG_QUERY_HASH = '9b498c08113f1e09617a1703c22b2f32';
- const SHORTCODE_QUERY_HASH = '865589822932d1b43dfe312121dd353a';
-
- public function getCacheTimeout() {
- $customTimeout = $this->getOption('cache_timeout');
- if ($customTimeout) {
- return $customTimeout;
- }
- return parent::getCacheTimeout();
- }
-
- protected function getContents($uri) {
- $headers = array();
- $sessionId = $this->getOption('session_id');
- if ($sessionId) {
- $headers[] = 'cookie: sessionid=' . $sessionId;
- }
- return getContents($uri, $headers);
- }
-
- protected function getInstagramUserId($username) {
-
- if(is_numeric($username)) return $username;
-
- $cacheFac = new CacheFactory();
-
- $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $cache->setScope(get_called_class());
- $cache->setKey(array($username));
- $key = $cache->loadData();
-
- if($key == null) {
- $data = $this->getContents(self::URI . 'web/search/topsearch/?query=' . $username);
- foreach(json_decode($data)->users as $user) {
- if(strtolower($user->user->username) === strtolower($username)) {
- $key = $user->user->pk;
- }
- }
- if($key == null) {
- returnServerError('Unable to find username in search result.');
- }
- $cache->saveData($key);
- }
- return $key;
-
- }
-
- public function collectData(){
- $directLink = !is_null($this->getInput('direct_links')) && $this->getInput('direct_links');
-
- $data = $this->getInstagramJSON($this->getURI());
-
- if(!is_null($this->getInput('u'))) {
- $userMedia = $data->data->user->edge_owner_to_timeline_media->edges;
- } elseif(!is_null($this->getInput('h'))) {
- $userMedia = $data->data->hashtag->edge_hashtag_to_media->edges;
- } elseif(!is_null($this->getInput('l'))) {
- $userMedia = $data->entry_data->LocationsPage[0]->graphql->location->edge_location_to_media->edges;
- }
-
- foreach($userMedia as $media) {
- $media = $media->node;
-
- switch($this->getInput('media_type')) {
- case 'all': break;
- case 'video':
- if($media->__typename != 'GraphVideo' || !$media->is_video) continue 2;
- break;
- case 'picture':
- if($media->__typename != 'GraphImage') continue 2;
- break;
- case 'multiple':
- if($media->__typename != 'GraphSidecar') continue 2;
- break;
- default: break;
- }
-
- $item = array();
- $item['uri'] = self::URI . 'p/' . $media->shortcode . '/';
-
- if (isset($media->owner->username)) {
- $item['author'] = $media->owner->username;
- }
-
- $textContent = $this->getTextContent($media);
-
- $item['title'] = ($media->is_video ? '▶ ' : '') . $textContent;
- $titleLinePos = strpos(wordwrap($item['title'], 120), "\n");
- if ($titleLinePos != false) {
- $item['title'] = substr($item['title'], 0, $titleLinePos) . '...';
- }
-
- if($directLink) {
- $mediaURI = $media->display_url;
- } else {
- $mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l';
- }
-
- $pattern = array('/\@([\w\.]+)/', '/#([\w\.]+)/');
- $replace = array(
- '<a href="https://www.instagram.com/$1">@$1</a>',
- '<a href="https://www.instagram.com/explore/tags/$1">#$1</a>');
-
- switch($media->__typename) {
- case 'GraphSidecar':
- $data = $this->getInstagramSidecarData($item['uri'], $item['title'], $media, $textContent);
- $item['content'] = $data[0];
- $item['enclosures'] = $data[1];
- break;
- case 'GraphImage':
- $item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
- $item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />';
- $item['content'] .= '</a><br><br>' . nl2br(preg_replace($pattern, $replace, htmlentities($textContent)));
- $item['enclosures'] = array($mediaURI);
- break;
- case 'GraphVideo':
- $data = $this->getInstagramVideoData($item['uri'], $mediaURI, $media, $textContent);
- $item['content'] = $data[0];
- if($directLink) {
- $item['enclosures'] = $data[1];
- } else {
- $item['enclosures'] = array($mediaURI);
- }
- $item['thumbnail'] = $mediaURI;
- break;
- default: break;
- }
- $item['timestamp'] = $media->taken_at_timestamp;
-
- $this->items[] = $item;
- }
- }
-
- // returns Sidecar(a post which has multiple media)'s contents and enclosures
- protected function getInstagramSidecarData($uri, $postTitle, $mediaInfo, $textContent) {
- $enclosures = array();
- $content = '';
- foreach($mediaInfo->edge_sidecar_to_children->edges as $singleMedia) {
- $singleMedia = $singleMedia->node;
- if($singleMedia->is_video) {
- if(in_array($singleMedia->video_url, $enclosures)) continue; // check if not added yet
- $content .= '<video controls><source src="' . $singleMedia->video_url . '" type="video/mp4"></video><br>';
- array_push($enclosures, $singleMedia->video_url);
- } else {
- if(in_array($singleMedia->display_url, $enclosures)) continue; // check if not added yet
- $content .= '<a href="' . $singleMedia->display_url . '" target="_blank">';
- $content .= '<img src="' . $singleMedia->display_url . '" alt="' . $postTitle . '" />';
- $content .= '</a><br>';
- array_push($enclosures, $singleMedia->display_url);
- }
- }
- $content .= '<br>' . nl2br(htmlentities($textContent));
-
- return array($content, $enclosures);
- }
-
- // returns Video post's contents and enclosures
- protected function getInstagramVideoData($uri, $mediaURI, $mediaInfo, $textContent) {
- $content = '<video controls>';
- $content .= '<source src="' . $mediaInfo->video_url . '" poster="' . $mediaURI . '" type="video/mp4">';
- $content .= '<img src="' . $mediaURI . '" alt="">';
- $content .= '</video><br>';
- $content .= '<br>' . nl2br(htmlentities($textContent));
-
- return array($content, array($mediaInfo->video_url));
- }
-
- protected function getTextContent($media) {
- $textContent = '(no text)';
- //Process the first element, that isn't in the node graph
- if (count($media->edge_media_to_caption->edges) > 0) {
- $textContent = trim($media->edge_media_to_caption->edges[0]->node->text);
- }
- return $textContent;
- }
-
- protected function getInstagramJSON($uri) {
-
- if(!is_null($this->getInput('u'))) {
-
- $userId = $this->getInstagramUserId($this->getInput('u'));
- $data = $this->getContents(self::URI .
- 'graphql/query/?query_hash=' .
- self::USER_QUERY_HASH .
- '&variables={"id"%3A"' .
- $userId .
- '"%2C"first"%3A10}');
- return json_decode($data);
-
- } elseif(!is_null($this->getInput('h'))) {
- $data = $this->getContents(self::URI .
- 'graphql/query/?query_hash=' .
- self::TAG_QUERY_HASH .
- '&variables={"tag_name"%3A"' .
- $this->getInput('h') .
- '"%2C"first"%3A10}');
-
- return json_decode($data);
-
- } else {
-
- $html = getContents($uri);
- $scriptRegex = '/window\._sharedData = (.*);<\/script>/';
-
- preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0);
-
- return json_decode($matches[1][0]);
-
- }
-
- }
-
- public function getName(){
- if(!is_null($this->getInput('u'))) {
- return $this->getInput('u') . ' - Instagram Bridge';
- }
-
- return parent::getName();
- }
-
- public function getURI(){
- if(!is_null($this->getInput('u'))) {
- return self::URI . urlencode($this->getInput('u')) . '/';
- } elseif(!is_null($this->getInput('h'))) {
- return self::URI . 'explore/tags/' . urlencode($this->getInput('h'));
- } elseif(!is_null($this->getInput('l'))) {
- return self::URI . 'explore/locations/' . urlencode($this->getInput('l'));
- }
- return parent::getURI();
- }
-
- public function detectParameters($url){
- $params = array();
-
- // By username
- $regex = '/^(https?:\/\/)?(www\.)?instagram\.com\/([^\/?\n]+)/';
-
- if(preg_match($regex, $url, $matches) > 0) {
- $params['u'] = urldecode($matches[3]);
- return $params;
- }
-
- return null;
- }
+
+class InstagramBridge extends BridgeAbstract
+{
+ // const MAINTAINER = 'pauder';
+ const NAME = 'Instagram Bridge';
+ const URI = 'https://www.instagram.com/';
+ const DESCRIPTION = 'Returns the newest images';
+
+ const CONFIGURATION = [
+ 'session_id' => [
+ 'required' => false,
+ ],
+ 'cache_timeout' => [
+ 'required' => false,
+ ],
+ ];
+
+ const PARAMETERS = [
+ 'Username' => [
+ 'u' => [
+ 'name' => 'username',
+ 'exampleValue' => 'aesoprockwins',
+ 'required' => true
+ ]
+ ],
+ 'Hashtag' => [
+ 'h' => [
+ 'name' => 'hashtag',
+ 'exampleValue' => 'beautifulday',
+ 'required' => true
+ ]
+ ],
+ 'Location' => [
+ 'l' => [
+ 'name' => 'location',
+ 'exampleValue' => 'london',
+ 'required' => true
+ ]
+ ],
+ 'global' => [
+ 'media_type' => [
+ 'name' => 'Media type',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => [
+ 'All' => 'all',
+ 'Video' => 'video',
+ 'Picture' => 'picture',
+ 'Multiple' => 'multiple',
+ ],
+ 'defaultValue' => 'all'
+ ],
+ 'direct_links' => [
+ 'name' => 'Use direct media links',
+ 'type' => 'checkbox',
+ ]
+ ]
+
+ ];
+
+ const TEST_DETECT_PARAMETERS = [
+ 'https://www.instagram.com/metaverse' => ['u' => 'metaverse'],
+ 'https://instagram.com/metaverse' => ['u' => 'metaverse'],
+ 'http://www.instagram.com/metaverse' => ['u' => 'metaverse'],
+ ];
+
+ const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951';
+ const TAG_QUERY_HASH = '9b498c08113f1e09617a1703c22b2f32';
+ const SHORTCODE_QUERY_HASH = '865589822932d1b43dfe312121dd353a';
+
+ public function getCacheTimeout()
+ {
+ $customTimeout = $this->getOption('cache_timeout');
+ if ($customTimeout) {
+ return $customTimeout;
+ }
+ return parent::getCacheTimeout();
+ }
+
+ protected function getContents($uri)
+ {
+ $headers = [];
+ $sessionId = $this->getOption('session_id');
+ if ($sessionId) {
+ $headers[] = 'cookie: sessionid=' . $sessionId;
+ }
+ return getContents($uri, $headers);
+ }
+
+ protected function getInstagramUserId($username)
+ {
+ if (is_numeric($username)) {
+ return $username;
+ }
+
+ $cacheFac = new CacheFactory();
+
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope(get_called_class());
+ $cache->setKey([$username]);
+ $key = $cache->loadData();
+
+ if ($key == null) {
+ $data = $this->getContents(self::URI . 'web/search/topsearch/?query=' . $username);
+ foreach (json_decode($data)->users as $user) {
+ if (strtolower($user->user->username) === strtolower($username)) {
+ $key = $user->user->pk;
+ }
+ }
+ if ($key == null) {
+ returnServerError('Unable to find username in search result.');
+ }
+ $cache->saveData($key);
+ }
+ return $key;
+ }
+
+ public function collectData()
+ {
+ $directLink = !is_null($this->getInput('direct_links')) && $this->getInput('direct_links');
+
+ $data = $this->getInstagramJSON($this->getURI());
+
+ if (!is_null($this->getInput('u'))) {
+ $userMedia = $data->data->user->edge_owner_to_timeline_media->edges;
+ } elseif (!is_null($this->getInput('h'))) {
+ $userMedia = $data->data->hashtag->edge_hashtag_to_media->edges;
+ } elseif (!is_null($this->getInput('l'))) {
+ $userMedia = $data->entry_data->LocationsPage[0]->graphql->location->edge_location_to_media->edges;
+ }
+
+ foreach ($userMedia as $media) {
+ $media = $media->node;
+
+ switch ($this->getInput('media_type')) {
+ case 'all':
+ break;
+ case 'video':
+ if ($media->__typename != 'GraphVideo' || !$media->is_video) {
+ continue 2;
+ }
+ break;
+ case 'picture':
+ if ($media->__typename != 'GraphImage') {
+ continue 2;
+ }
+ break;
+ case 'multiple':
+ if ($media->__typename != 'GraphSidecar') {
+ continue 2;
+ }
+ break;
+ default:
+ break;
+ }
+
+ $item = [];
+ $item['uri'] = self::URI . 'p/' . $media->shortcode . '/';
+
+ if (isset($media->owner->username)) {
+ $item['author'] = $media->owner->username;
+ }
+
+ $textContent = $this->getTextContent($media);
+
+ $item['title'] = ($media->is_video ? '▶ ' : '') . $textContent;
+ $titleLinePos = strpos(wordwrap($item['title'], 120), "\n");
+ if ($titleLinePos != false) {
+ $item['title'] = substr($item['title'], 0, $titleLinePos) . '...';
+ }
+
+ if ($directLink) {
+ $mediaURI = $media->display_url;
+ } else {
+ $mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l';
+ }
+
+ $pattern = ['/\@([\w\.]+)/', '/#([\w\.]+)/'];
+ $replace = [
+ '<a href="https://www.instagram.com/$1">@$1</a>',
+ '<a href="https://www.instagram.com/explore/tags/$1">#$1</a>'];
+
+ switch ($media->__typename) {
+ case 'GraphSidecar':
+ $data = $this->getInstagramSidecarData($item['uri'], $item['title'], $media, $textContent);
+ $item['content'] = $data[0];
+ $item['enclosures'] = $data[1];
+ break;
+ case 'GraphImage':
+ $item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
+ $item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />';
+ $item['content'] .= '</a><br><br>' . nl2br(preg_replace($pattern, $replace, htmlentities($textContent)));
+ $item['enclosures'] = [$mediaURI];
+ break;
+ case 'GraphVideo':
+ $data = $this->getInstagramVideoData($item['uri'], $mediaURI, $media, $textContent);
+ $item['content'] = $data[0];
+ if ($directLink) {
+ $item['enclosures'] = $data[1];
+ } else {
+ $item['enclosures'] = [$mediaURI];
+ }
+ $item['thumbnail'] = $mediaURI;
+ break;
+ default:
+ break;
+ }
+ $item['timestamp'] = $media->taken_at_timestamp;
+
+ $this->items[] = $item;
+ }
+ }
+
+ // returns Sidecar(a post which has multiple media)'s contents and enclosures
+ protected function getInstagramSidecarData($uri, $postTitle, $mediaInfo, $textContent)
+ {
+ $enclosures = [];
+ $content = '';
+ foreach ($mediaInfo->edge_sidecar_to_children->edges as $singleMedia) {
+ $singleMedia = $singleMedia->node;
+ if ($singleMedia->is_video) {
+ if (in_array($singleMedia->video_url, $enclosures)) {
+ continue; // check if not added yet
+ }
+ $content .= '<video controls><source src="' . $singleMedia->video_url . '" type="video/mp4"></video><br>';
+ array_push($enclosures, $singleMedia->video_url);
+ } else {
+ if (in_array($singleMedia->display_url, $enclosures)) {
+ continue; // check if not added yet
+ }
+ $content .= '<a href="' . $singleMedia->display_url . '" target="_blank">';
+ $content .= '<img src="' . $singleMedia->display_url . '" alt="' . $postTitle . '" />';
+ $content .= '</a><br>';
+ array_push($enclosures, $singleMedia->display_url);
+ }
+ }
+ $content .= '<br>' . nl2br(htmlentities($textContent));
+
+ return [$content, $enclosures];
+ }
+
+ // returns Video post's contents and enclosures
+ protected function getInstagramVideoData($uri, $mediaURI, $mediaInfo, $textContent)
+ {
+ $content = '<video controls>';
+ $content .= '<source src="' . $mediaInfo->video_url . '" poster="' . $mediaURI . '" type="video/mp4">';
+ $content .= '<img src="' . $mediaURI . '" alt="">';
+ $content .= '</video><br>';
+ $content .= '<br>' . nl2br(htmlentities($textContent));
+
+ return [$content, [$mediaInfo->video_url]];
+ }
+
+ protected function getTextContent($media)
+ {
+ $textContent = '(no text)';
+ //Process the first element, that isn't in the node graph
+ if (count($media->edge_media_to_caption->edges) > 0) {
+ $textContent = trim($media->edge_media_to_caption->edges[0]->node->text);
+ }
+ return $textContent;
+ }
+
+ protected function getInstagramJSON($uri)
+ {
+ if (!is_null($this->getInput('u'))) {
+ $userId = $this->getInstagramUserId($this->getInput('u'));
+ $data = $this->getContents(self::URI .
+ 'graphql/query/?query_hash=' .
+ self::USER_QUERY_HASH .
+ '&variables={"id"%3A"' .
+ $userId .
+ '"%2C"first"%3A10}');
+ return json_decode($data);
+ } elseif (!is_null($this->getInput('h'))) {
+ $data = $this->getContents(self::URI .
+ 'graphql/query/?query_hash=' .
+ self::TAG_QUERY_HASH .
+ '&variables={"tag_name"%3A"' .
+ $this->getInput('h') .
+ '"%2C"first"%3A10}');
+
+ return json_decode($data);
+ } else {
+ $html = getContents($uri);
+ $scriptRegex = '/window\._sharedData = (.*);<\/script>/';
+
+ preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0);
+
+ return json_decode($matches[1][0]);
+ }
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('u'))) {
+ return $this->getInput('u') . ' - Instagram Bridge';
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('u'))) {
+ return self::URI . urlencode($this->getInput('u')) . '/';
+ } elseif (!is_null($this->getInput('h'))) {
+ return self::URI . 'explore/tags/' . urlencode($this->getInput('h'));
+ } elseif (!is_null($this->getInput('l'))) {
+ return self::URI . 'explore/locations/' . urlencode($this->getInput('l'));
+ }
+ return parent::getURI();
+ }
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ // By username
+ $regex = '/^(https?:\/\/)?(www\.)?instagram\.com\/([^\/?\n]+)/';
+
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['u'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ return null;
+ }
}
diff --git a/bridges/InstructablesBridge.php b/bridges/InstructablesBridge.php
index 98987944..e1d2ef0b 100644
--- a/bridges/InstructablesBridge.php
+++ b/bridges/InstructablesBridge.php
@@ -1,359 +1,368 @@
<?php
+
/**
* This class implements a bridge for http://www.instructables.com, supporting
* general feeds and feeds by category.
*
* Remarks:
* - For some reason it is very important to have the category URI end with a
-* slash, otherwise the site defaults to the main category (i.e. Technology)!
-* If you need to update the categories list, enable the 'listCategories'
-* function (see comments below) and run the bridge with format=Html (see page
-* source)
+* slash, otherwise the site defaults to the main category (i.e. Technology)!
+* If you need to update the categories list, enable the 'listCategories'
+* function (see comments below) and run the bridge with format=Html (see page
+* source)
*/
-class InstructablesBridge extends BridgeAbstract {
- const NAME = 'Instructables Bridge';
- const URI = 'https://www.instructables.com';
- const DESCRIPTION = 'Returns general feeds and feeds by category';
- const MAINTAINER = 'logmanoriginal';
- const PARAMETERS = array(
- 'Category' => array(
- 'category' => array(
- 'name' => 'Category',
- 'type' => 'list',
- 'values' => array(
- 'Circuits' => array(
- 'All' => '/circuits/',
- 'Apple' => '/circuits/apple/projects/',
- 'Arduino' => '/circuits/arduino/projects/',
- 'Art' => '/circuits/art/projects/',
- 'Assistive Tech' => '/circuits/assistive-tech/projects/',
- 'Audio' => '/circuits/audio/projects/',
- 'Cameras' => '/circuits/cameras/projects/',
- 'Clocks' => '/circuits/clocks/projects/',
- 'Computers' => '/circuits/computers/projects/',
- 'Electronics' => '/circuits/electronics/projects/',
- 'Gadgets' => '/circuits/gadgets/projects/',
- 'Lasers' => '/circuits/lasers/projects/',
- 'LEDs' => '/circuits/leds/projects/',
- 'Linux' => '/circuits/linux/projects/',
- 'Microcontrollers' => '/circuits/microcontrollers/projects/',
- 'Microsoft' => '/circuits/microsoft/projects/',
- 'Mobile' => '/circuits/mobile/projects/',
- 'Raspberry Pi' => '/circuits/raspberry-pi/projects/',
- 'Remote Control' => '/circuits/remote-control/projects/',
- 'Reuse' => '/circuits/reuse/projects/',
- 'Robots' => '/circuits/robots/projects/',
- 'Sensors' => '/circuits/sensors/projects/',
- 'Software' => '/circuits/software/projects/',
- 'Soldering' => '/circuits/soldering/projects/',
- 'Speakers' => '/circuits/speakers/projects/',
- 'Tools' => '/circuits/tools/projects/',
- 'USB' => '/circuits/usb/projects/',
- 'Wearables' => '/circuits/wearables/projects/',
- 'Websites' => '/circuits/websites/projects/',
- 'Wireless' => '/circuits/wireless/projects/',
- ),
- 'Workshop' => array(
- 'All' => '/workshop/',
- '3D Printing' => '/workshop/3d-printing/projects/',
- 'Cars' => '/workshop/cars/projects/',
- 'CNC' => '/workshop/cnc/projects/',
- 'Electric Vehicles' => '/workshop/electric-vehicles/projects/',
- 'Energy' => '/workshop/energy/projects/',
- 'Furniture' => '/workshop/furniture/projects/',
- 'Home Improvement' => '/workshop/home-improvement/projects/',
- 'Home Theater' => '/workshop/home-theater/projects/',
- 'Hydroponics' => '/workshop/hydroponics/projects/',
- 'Knives' => '/workshop/knives/projects/',
- 'Laser Cutting' => '/workshop/laser-cutting/projects/',
- 'Lighting' => '/workshop/lighting/projects/',
- 'Metalworking' => '/workshop/metalworking/projects/',
- 'Molds & Casting' => '/workshop/molds-and-casting/projects/',
- 'Motorcycles' => '/workshop/motorcycles/projects/',
- 'Organizing' => '/workshop/organizing/projects/',
- 'Pallets' => '/workshop/pallets/projects/',
- 'Repair' => '/workshop/repair/projects/',
- 'Science' => '/workshop/science/projects/',
- 'Shelves' => '/workshop/shelves/projects/',
- 'Solar' => '/workshop/solar/projects/',
- 'Tools' => '/workshop/tools/projects/',
- 'Woodworking' => '/workshop/woodworking/projects/',
- 'Workbenches' => '/workshop/workbenches/projects/',
- ),
- 'Craft' => array(
- 'All' => '/craft/',
- 'Art' => '/craft/art/projects/',
- 'Books & Journals' => '/craft/books-and-journals/projects/',
- 'Cardboard' => '/craft/cardboard/projects/',
- 'Cards' => '/craft/cards/projects/',
- 'Clay' => '/craft/clay/projects/',
- 'Costumes & Cosplay' => '/craft/costumes-and-cosplay/projects/',
- 'Digital Graphics' => '/craft/digital-graphics/projects/',
- 'Duct Tape' => '/craft/duct-tape/projects/',
- 'Embroidery' => '/craft/embroidery/projects/',
- 'Fashion' => '/craft/fashion/projects/',
- 'Felt' => '/craft/felt/projects/',
- 'Fiber Arts' => '/craft/fiber-arts/projects/',
- 'Gift Wrapping' => '/craft/gift-wrapping/projects/',
- 'Jewelry' => '/craft/jewelry/projects/',
- 'Knitting & Crochet' => '/craft/knitting-and-crochet/projects/',
- 'Leather' => '/craft/leather/projects/',
- 'Mason Jars' => '/craft/mason-jars/projects/',
- 'No-Sew' => '/craft/no-sew/projects/',
- 'Paper' => '/craft/paper/projects/',
- 'Parties & Weddings' => '/craft/parties-and-weddings/projects/',
- 'Photography' => '/craft/photography/projects/',
- 'Printmaking' => '/craft/printmaking/projects/',
- 'Reuse' => '/craft/reuse/projects/',
- 'Sewing' => '/craft/sewing/projects/',
- 'Soapmaking' => '/craft/soapmaking/projects/',
- 'Wallets' => '/craft/wallets/projects/',
- ),
- 'Cooking' => array(
- 'All' => '/cooking/',
- 'Bacon' => '/cooking/bacon/projects/',
- 'BBQ & Grilling' => '/cooking/bbq-and-grilling/projects/',
- 'Beverages' => '/cooking/beverages/projects/',
- 'Bread' => '/cooking/bread/projects/',
- 'Breakfast' => '/cooking/breakfast/projects/',
- 'Cake' => '/cooking/cake/projects/',
- 'Candy' => '/cooking/candy/projects/',
- 'Canning & Preserving' => '/cooking/canning-and-preserving/projects/',
- 'Cocktails & Mocktails' => '/cooking/cocktails-and-mocktails/projects/',
- 'Coffee' => '/cooking/coffee/projects/',
- 'Cookies' => '/cooking/cookies/projects/',
- 'Cupcakes' => '/cooking/cupcakes/projects/',
- 'Dessert' => '/cooking/dessert/projects/',
- 'Homebrew' => '/cooking/homebrew/projects/',
- 'Main Course' => '/cooking/main-course/projects/',
- 'Pasta' => '/cooking/pasta/projects/',
- 'Pie' => '/cooking/pie/projects/',
- 'Pizza' => '/cooking/pizza/projects/',
- 'Salad' => '/cooking/salad/projects/',
- 'Sandwiches' => '/cooking/sandwiches/projects/',
- 'Snacks & Appetizers' => '/cooking/snacks-and-appetizers/projects/',
- 'Soups & Stews' => '/cooking/soups-and-stews/projects/',
- 'Vegetarian & Vegan' => '/cooking/vegetarian-and-vegan/projects/',
- ),
- 'Living' => array(
- 'All' => '/living/',
- 'Beauty' => '/living/beauty/projects/',
- 'Christmas' => '/living/christmas/projects/',
- 'Cleaning' => '/living/cleaning/projects/',
- 'Decorating' => '/living/decorating/projects/',
- 'Education' => '/living/education/projects/',
- 'Gardening' => '/living/gardening/projects/',
- 'Halloween' => '/living/halloween/projects/',
- 'Health' => '/living/health/projects/',
- 'Hiding Places' => '/living/hiding-places/projects/',
- 'Holidays' => '/living/holidays/projects/',
- 'Homesteading' => '/living/homesteading/projects/',
- 'Kids' => '/living/kids/projects/',
- 'Kitchen' => '/living/kitchen/projects/',
- 'LEGO & KNEX' => '/living/lego-and-knex/projects/',
- 'Life Hacks' => '/living/life-hacks/projects/',
- 'Music' => '/living/music/projects/',
- 'Office Supply Hacks' => '/living/office-supply-hacks/projects/',
- 'Organizing' => '/living/organizing/projects/',
- 'Pest Control' => '/living/pest-control/projects/',
- 'Pets' => '/living/pets/projects/',
- 'Pranks, Tricks, & Humor' => '/living/pranks-tricks-and-humor/projects/',
- 'Relationships' => '/living/relationships/projects/',
- 'Toys & Games' => '/living/toys-and-games/projects/',
- 'Travel' => '/living/travel/projects/',
- 'Video Games' => '/living/video-games/projects/',
- ),
- 'Outside' => array(
- 'All' => '/outside/',
- 'Backyard' => '/outside/backyard/projects/',
- 'Beach' => '/outside/beach/projects/',
- 'Bikes' => '/outside/bikes/projects/',
- 'Birding' => '/outside/birding/projects/',
- 'Boats' => '/outside/boats/projects/',
- 'Camping' => '/outside/camping/projects/',
- 'Climbing' => '/outside/climbing/projects/',
- 'Fire' => '/outside/fire/projects/',
- 'Fishing' => '/outside/fishing/projects/',
- 'Hunting' => '/outside/hunting/projects/',
- 'Kites' => '/outside/kites/projects/',
- 'Knots' => '/outside/knots/projects/',
- 'Launchers' => '/outside/launchers/projects/',
- 'Paracord' => '/outside/paracord/projects/',
- 'Rockets' => '/outside/rockets/projects/',
- 'Siege Engines' => '/outside/siege-engines/projects/',
- 'Skateboarding' => '/outside/skateboarding/projects/',
- 'Snow' => '/outside/snow/projects/',
- 'Sports' => '/outside/sports/projects/',
- 'Survival' => '/outside/survival/projects/',
- 'Water' => '/outside/water/projects/',
- ),
- 'Makeymakey' => array(
- 'All' => '/makeymakey/',
- 'Makey Makey on Instructables' => '/makeymakey/',
- ),
- 'Teachers' => array(
- 'All' => '/teachers/',
- 'ELA' => '/teachers/ela/projects/',
- 'Math' => '/teachers/math/projects/',
- 'Science' => '/teachers/science/projects/',
- 'Social Studies' => '/teachers/social-studies/projects/',
- 'Engineering' => '/teachers/engineering/projects/',
- 'Coding' => '/teachers/coding/projects/',
- 'Electronics' => '/teachers/electronics/projects/',
- 'Robotics' => '/teachers/robotics/projects/',
- 'Arduino' => '/teachers/arduino/projects/',
- 'CNC' => '/teachers/cnc/projects/',
- 'Laser Cutting' => '/teachers/laser-cutting/projects/',
- '3D Printing' => '/teachers/3d-printing/projects/',
- '3D Design' => '/teachers/3d-design/projects/',
- 'Art' => '/teachers/art/projects/',
- 'Music' => '/teachers/music/projects/',
- 'Theatre' => '/teachers/theatre/projects/',
- 'Wood Shop' => '/teachers/wood-shop/projects/',
- 'Metal Shop' => '/teachers/metal-shop/projects/',
- 'Resources' => '/teachers/resources/projects/',
- ),
- ),
- 'title' => 'Select your category (required)',
- 'defaultValue' => 'Circuits'
- ),
- 'filter' => array(
- 'name' => 'Filter',
- 'type' => 'list',
- 'values' => array(
- 'Featured' => ' ',
- 'Recent' => 'recent/',
- 'Popular' => 'popular/',
- 'Views' => 'views/',
- 'Contest Winners' => 'winners/'
- ),
- 'title' => 'Select a filter',
- 'defaultValue' => 'Featured'
- )
- )
- );
+class InstructablesBridge extends BridgeAbstract
+{
+ const NAME = 'Instructables Bridge';
+ const URI = 'https://www.instructables.com';
+ const DESCRIPTION = 'Returns general feeds and feeds by category';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = [
+ 'Category' => [
+ 'category' => [
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => [
+ 'Circuits' => [
+ 'All' => '/circuits/',
+ 'Apple' => '/circuits/apple/projects/',
+ 'Arduino' => '/circuits/arduino/projects/',
+ 'Art' => '/circuits/art/projects/',
+ 'Assistive Tech' => '/circuits/assistive-tech/projects/',
+ 'Audio' => '/circuits/audio/projects/',
+ 'Cameras' => '/circuits/cameras/projects/',
+ 'Clocks' => '/circuits/clocks/projects/',
+ 'Computers' => '/circuits/computers/projects/',
+ 'Electronics' => '/circuits/electronics/projects/',
+ 'Gadgets' => '/circuits/gadgets/projects/',
+ 'Lasers' => '/circuits/lasers/projects/',
+ 'LEDs' => '/circuits/leds/projects/',
+ 'Linux' => '/circuits/linux/projects/',
+ 'Microcontrollers' => '/circuits/microcontrollers/projects/',
+ 'Microsoft' => '/circuits/microsoft/projects/',
+ 'Mobile' => '/circuits/mobile/projects/',
+ 'Raspberry Pi' => '/circuits/raspberry-pi/projects/',
+ 'Remote Control' => '/circuits/remote-control/projects/',
+ 'Reuse' => '/circuits/reuse/projects/',
+ 'Robots' => '/circuits/robots/projects/',
+ 'Sensors' => '/circuits/sensors/projects/',
+ 'Software' => '/circuits/software/projects/',
+ 'Soldering' => '/circuits/soldering/projects/',
+ 'Speakers' => '/circuits/speakers/projects/',
+ 'Tools' => '/circuits/tools/projects/',
+ 'USB' => '/circuits/usb/projects/',
+ 'Wearables' => '/circuits/wearables/projects/',
+ 'Websites' => '/circuits/websites/projects/',
+ 'Wireless' => '/circuits/wireless/projects/',
+ ],
+ 'Workshop' => [
+ 'All' => '/workshop/',
+ '3D Printing' => '/workshop/3d-printing/projects/',
+ 'Cars' => '/workshop/cars/projects/',
+ 'CNC' => '/workshop/cnc/projects/',
+ 'Electric Vehicles' => '/workshop/electric-vehicles/projects/',
+ 'Energy' => '/workshop/energy/projects/',
+ 'Furniture' => '/workshop/furniture/projects/',
+ 'Home Improvement' => '/workshop/home-improvement/projects/',
+ 'Home Theater' => '/workshop/home-theater/projects/',
+ 'Hydroponics' => '/workshop/hydroponics/projects/',
+ 'Knives' => '/workshop/knives/projects/',
+ 'Laser Cutting' => '/workshop/laser-cutting/projects/',
+ 'Lighting' => '/workshop/lighting/projects/',
+ 'Metalworking' => '/workshop/metalworking/projects/',
+ 'Molds & Casting' => '/workshop/molds-and-casting/projects/',
+ 'Motorcycles' => '/workshop/motorcycles/projects/',
+ 'Organizing' => '/workshop/organizing/projects/',
+ 'Pallets' => '/workshop/pallets/projects/',
+ 'Repair' => '/workshop/repair/projects/',
+ 'Science' => '/workshop/science/projects/',
+ 'Shelves' => '/workshop/shelves/projects/',
+ 'Solar' => '/workshop/solar/projects/',
+ 'Tools' => '/workshop/tools/projects/',
+ 'Woodworking' => '/workshop/woodworking/projects/',
+ 'Workbenches' => '/workshop/workbenches/projects/',
+ ],
+ 'Craft' => [
+ 'All' => '/craft/',
+ 'Art' => '/craft/art/projects/',
+ 'Books & Journals' => '/craft/books-and-journals/projects/',
+ 'Cardboard' => '/craft/cardboard/projects/',
+ 'Cards' => '/craft/cards/projects/',
+ 'Clay' => '/craft/clay/projects/',
+ 'Costumes & Cosplay' => '/craft/costumes-and-cosplay/projects/',
+ 'Digital Graphics' => '/craft/digital-graphics/projects/',
+ 'Duct Tape' => '/craft/duct-tape/projects/',
+ 'Embroidery' => '/craft/embroidery/projects/',
+ 'Fashion' => '/craft/fashion/projects/',
+ 'Felt' => '/craft/felt/projects/',
+ 'Fiber Arts' => '/craft/fiber-arts/projects/',
+ 'Gift Wrapping' => '/craft/gift-wrapping/projects/',
+ 'Jewelry' => '/craft/jewelry/projects/',
+ 'Knitting & Crochet' => '/craft/knitting-and-crochet/projects/',
+ 'Leather' => '/craft/leather/projects/',
+ 'Mason Jars' => '/craft/mason-jars/projects/',
+ 'No-Sew' => '/craft/no-sew/projects/',
+ 'Paper' => '/craft/paper/projects/',
+ 'Parties & Weddings' => '/craft/parties-and-weddings/projects/',
+ 'Photography' => '/craft/photography/projects/',
+ 'Printmaking' => '/craft/printmaking/projects/',
+ 'Reuse' => '/craft/reuse/projects/',
+ 'Sewing' => '/craft/sewing/projects/',
+ 'Soapmaking' => '/craft/soapmaking/projects/',
+ 'Wallets' => '/craft/wallets/projects/',
+ ],
+ 'Cooking' => [
+ 'All' => '/cooking/',
+ 'Bacon' => '/cooking/bacon/projects/',
+ 'BBQ & Grilling' => '/cooking/bbq-and-grilling/projects/',
+ 'Beverages' => '/cooking/beverages/projects/',
+ 'Bread' => '/cooking/bread/projects/',
+ 'Breakfast' => '/cooking/breakfast/projects/',
+ 'Cake' => '/cooking/cake/projects/',
+ 'Candy' => '/cooking/candy/projects/',
+ 'Canning & Preserving' => '/cooking/canning-and-preserving/projects/',
+ 'Cocktails & Mocktails' => '/cooking/cocktails-and-mocktails/projects/',
+ 'Coffee' => '/cooking/coffee/projects/',
+ 'Cookies' => '/cooking/cookies/projects/',
+ 'Cupcakes' => '/cooking/cupcakes/projects/',
+ 'Dessert' => '/cooking/dessert/projects/',
+ 'Homebrew' => '/cooking/homebrew/projects/',
+ 'Main Course' => '/cooking/main-course/projects/',
+ 'Pasta' => '/cooking/pasta/projects/',
+ 'Pie' => '/cooking/pie/projects/',
+ 'Pizza' => '/cooking/pizza/projects/',
+ 'Salad' => '/cooking/salad/projects/',
+ 'Sandwiches' => '/cooking/sandwiches/projects/',
+ 'Snacks & Appetizers' => '/cooking/snacks-and-appetizers/projects/',
+ 'Soups & Stews' => '/cooking/soups-and-stews/projects/',
+ 'Vegetarian & Vegan' => '/cooking/vegetarian-and-vegan/projects/',
+ ],
+ 'Living' => [
+ 'All' => '/living/',
+ 'Beauty' => '/living/beauty/projects/',
+ 'Christmas' => '/living/christmas/projects/',
+ 'Cleaning' => '/living/cleaning/projects/',
+ 'Decorating' => '/living/decorating/projects/',
+ 'Education' => '/living/education/projects/',
+ 'Gardening' => '/living/gardening/projects/',
+ 'Halloween' => '/living/halloween/projects/',
+ 'Health' => '/living/health/projects/',
+ 'Hiding Places' => '/living/hiding-places/projects/',
+ 'Holidays' => '/living/holidays/projects/',
+ 'Homesteading' => '/living/homesteading/projects/',
+ 'Kids' => '/living/kids/projects/',
+ 'Kitchen' => '/living/kitchen/projects/',
+ 'LEGO & KNEX' => '/living/lego-and-knex/projects/',
+ 'Life Hacks' => '/living/life-hacks/projects/',
+ 'Music' => '/living/music/projects/',
+ 'Office Supply Hacks' => '/living/office-supply-hacks/projects/',
+ 'Organizing' => '/living/organizing/projects/',
+ 'Pest Control' => '/living/pest-control/projects/',
+ 'Pets' => '/living/pets/projects/',
+ 'Pranks, Tricks, & Humor' => '/living/pranks-tricks-and-humor/projects/',
+ 'Relationships' => '/living/relationships/projects/',
+ 'Toys & Games' => '/living/toys-and-games/projects/',
+ 'Travel' => '/living/travel/projects/',
+ 'Video Games' => '/living/video-games/projects/',
+ ],
+ 'Outside' => [
+ 'All' => '/outside/',
+ 'Backyard' => '/outside/backyard/projects/',
+ 'Beach' => '/outside/beach/projects/',
+ 'Bikes' => '/outside/bikes/projects/',
+ 'Birding' => '/outside/birding/projects/',
+ 'Boats' => '/outside/boats/projects/',
+ 'Camping' => '/outside/camping/projects/',
+ 'Climbing' => '/outside/climbing/projects/',
+ 'Fire' => '/outside/fire/projects/',
+ 'Fishing' => '/outside/fishing/projects/',
+ 'Hunting' => '/outside/hunting/projects/',
+ 'Kites' => '/outside/kites/projects/',
+ 'Knots' => '/outside/knots/projects/',
+ 'Launchers' => '/outside/launchers/projects/',
+ 'Paracord' => '/outside/paracord/projects/',
+ 'Rockets' => '/outside/rockets/projects/',
+ 'Siege Engines' => '/outside/siege-engines/projects/',
+ 'Skateboarding' => '/outside/skateboarding/projects/',
+ 'Snow' => '/outside/snow/projects/',
+ 'Sports' => '/outside/sports/projects/',
+ 'Survival' => '/outside/survival/projects/',
+ 'Water' => '/outside/water/projects/',
+ ],
+ 'Makeymakey' => [
+ 'All' => '/makeymakey/',
+ 'Makey Makey on Instructables' => '/makeymakey/',
+ ],
+ 'Teachers' => [
+ 'All' => '/teachers/',
+ 'ELA' => '/teachers/ela/projects/',
+ 'Math' => '/teachers/math/projects/',
+ 'Science' => '/teachers/science/projects/',
+ 'Social Studies' => '/teachers/social-studies/projects/',
+ 'Engineering' => '/teachers/engineering/projects/',
+ 'Coding' => '/teachers/coding/projects/',
+ 'Electronics' => '/teachers/electronics/projects/',
+ 'Robotics' => '/teachers/robotics/projects/',
+ 'Arduino' => '/teachers/arduino/projects/',
+ 'CNC' => '/teachers/cnc/projects/',
+ 'Laser Cutting' => '/teachers/laser-cutting/projects/',
+ '3D Printing' => '/teachers/3d-printing/projects/',
+ '3D Design' => '/teachers/3d-design/projects/',
+ 'Art' => '/teachers/art/projects/',
+ 'Music' => '/teachers/music/projects/',
+ 'Theatre' => '/teachers/theatre/projects/',
+ 'Wood Shop' => '/teachers/wood-shop/projects/',
+ 'Metal Shop' => '/teachers/metal-shop/projects/',
+ 'Resources' => '/teachers/resources/projects/',
+ ],
+ ],
+ 'title' => 'Select your category (required)',
+ 'defaultValue' => 'Circuits'
+ ],
+ 'filter' => [
+ 'name' => 'Filter',
+ 'type' => 'list',
+ 'values' => [
+ 'Featured' => ' ',
+ 'Recent' => 'recent/',
+ 'Popular' => 'popular/',
+ 'Views' => 'views/',
+ 'Contest Winners' => 'winners/'
+ ],
+ 'title' => 'Select a filter',
+ 'defaultValue' => 'Featured'
+ ]
+ ]
+ ];
- public function collectData() {
- // Enable the following line to get the category list (dev mode)
- // $this->listCategories();
+ public function collectData()
+ {
+ // Enable the following line to get the category list (dev mode)
+ // $this->listCategories();
- $html = getSimpleHTMLDOM($this->getURI());
- $html = defaultLinkTo($html, $this->getURI());
+ $html = getSimpleHTMLDOM($this->getURI());
+ $html = defaultLinkTo($html, $this->getURI());
- $covers = $html->find('
+ $covers = $html->find('
.category-projects-list > div,
.category-landing-projects-list > div,
');
- foreach($covers as $cover) {
- $item = array();
-
- $item['uri'] = $cover->find('a.ible-title', 0)->href;
- $item['title'] = $cover->find('a.ible-title', 0)->innertext;
- $item['author'] = $this->getCategoryAuthor($cover);
- $item['content'] = '<a href='
- . $item['uri']
- . '><img src='
- . $cover->find('img', 0)->getAttribute('data-src')
- . '></a>';
-
- $item['enclosures'][] = str_replace(
- '.RECTANGLE1',
- '.LARGE',
- $cover->find('img', 0)->getAttribute('data-src')
- );
+ foreach ($covers as $cover) {
+ $item = [];
- $this->items[] = $item;
- }
- }
+ $item['uri'] = $cover->find('a.ible-title', 0)->href;
+ $item['title'] = $cover->find('a.ible-title', 0)->innertext;
+ $item['author'] = $this->getCategoryAuthor($cover);
+ $item['content'] = '<a href='
+ . $item['uri']
+ . '><img src='
+ . $cover->find('img', 0)->getAttribute('data-src')
+ . '></a>';
- public function getName() {
- switch($this->queriedContext) {
- case 'Category':
- foreach(self::PARAMETERS[$this->queriedContext]['category']['values'] as $key => $value) {
- $subcategory = array_search($this->getInput('category'), $value);
+ $item['enclosures'][] = str_replace(
+ '.RECTANGLE1',
+ '.LARGE',
+ $cover->find('img', 0)->getAttribute('data-src')
+ );
- if($subcategory !== false)
- break;
- }
+ $this->items[] = $item;
+ }
+ }
- $filter = array_search(
- $this->getInput('filter'),
- self::PARAMETERS[$this->queriedContext]['filter']['values']
- );
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Category':
+ foreach (self::PARAMETERS[$this->queriedContext]['category']['values'] as $key => $value) {
+ $subcategory = array_search($this->getInput('category'), $value);
- return $subcategory . ' (' . $filter . ') - ' . static::NAME;
- }
+ if ($subcategory !== false) {
+ break;
+ }
+ }
- return parent::getName();
- }
+ $filter = array_search(
+ $this->getInput('filter'),
+ self::PARAMETERS[$this->queriedContext]['filter']['values']
+ );
- public function getURI() {
- switch($this->queriedContext) {
- case 'Category':
- return self::URI
- . $this->getInput('category')
- . $this->getInput('filter');
- }
+ return $subcategory . ' (' . $filter . ') - ' . static::NAME;
+ }
- return parent::getURI();
- }
+ return parent::getName();
+ }
- /**
- * Returns a list of categories for development purposes (used to build the
- * parameters list)
- */
- private function listCategories(){
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Category':
+ return self::URI
+ . $this->getInput('category')
+ . $this->getInput('filter');
+ }
- // Use home page to acquire main categories
- $html = getSimpleHTMLDOM(self::URI);
- $html = defaultLinkTo($html, self::URI);
+ return parent::getURI();
+ }
- foreach($html->find('.home-content-explore-link') as $category) {
+ /**
+ * Returns a list of categories for development purposes (used to build the
+ * parameters list)
+ */
+ private function listCategories()
+ {
+ // Use home page to acquire main categories
+ $html = getSimpleHTMLDOM(self::URI);
+ $html = defaultLinkTo($html, self::URI);
- // Use arbitrary category to receive full list
- $html = getSimpleHTMLDOM($category->href);
+ foreach ($html->find('.home-content-explore-link') as $category) {
+ // Use arbitrary category to receive full list
+ $html = getSimpleHTMLDOM($category->href);
- foreach($html->find('.channel-thumbnail a') as $channel) {
- $name = html_entity_decode(trim($channel->title));
+ foreach ($html->find('.channel-thumbnail a') as $channel) {
+ $name = html_entity_decode(trim($channel->title));
- // Remove unwanted entities
- $name = str_replace("'", '', $name);
- $name = str_replace('&#39;', '', $name);
+ // Remove unwanted entities
+ $name = str_replace("'", '', $name);
+ $name = str_replace('&#39;', '', $name);
- $uri = $channel->href;
+ $uri = $channel->href;
- $category_name = explode('/', $uri)[1];
+ $category_name = explode('/', $uri)[1];
- if(!isset($categories)
- || !array_key_exists($category_name, $categories)
- || !in_array($uri, $categories[$category_name]))
- $categories[$category_name][$name] = $uri;
- }
- }
+ if (
+ !isset($categories)
+ || !array_key_exists($category_name, $categories)
+ || !in_array($uri, $categories[$category_name])
+ ) {
+ $categories[$category_name][$name] = $uri;
+ }
+ }
+ }
- // Build PHP array manually
- foreach($categories as $key => $value) {
- $name = ucfirst($key);
- echo "'{$name}' => array(\n";
- echo "\t'All' => '/{$key}/',\n";
- foreach($value as $name => $uri) {
- echo "\t'{$name}' => '{$uri}',\n";
- }
- echo "),\n";
- }
+ // Build PHP array manually
+ foreach ($categories as $key => $value) {
+ $name = ucfirst($key);
+ echo "'{$name}' => array(\n";
+ echo "\t'All' => '/{$key}/',\n";
+ foreach ($value as $name => $uri) {
+ echo "\t'{$name}' => '{$uri}',\n";
+ }
+ echo "),\n";
+ }
- die;
- }
+ die;
+ }
- /**
- * Returns the author as anchor for a given cover.
- */
- private function getCategoryAuthor($cover) {
- return '<a href='
- . $cover->find('.ible-author a', 0)->href
- . '>'
- . $cover->find('.ible-author a', 0)->innertext
- . '</a>';
- }
+ /**
+ * Returns the author as anchor for a given cover.
+ */
+ private function getCategoryAuthor($cover)
+ {
+ return '<a href='
+ . $cover->find('.ible-author a', 0)->href
+ . '>'
+ . $cover->find('.ible-author a', 0)->innertext
+ . '</a>';
+ }
}
diff --git a/bridges/InternetArchiveBridge.php b/bridges/InternetArchiveBridge.php
index b9f9d274..7175cde8 100644
--- a/bridges/InternetArchiveBridge.php
+++ b/bridges/InternetArchiveBridge.php
@@ -1,319 +1,326 @@
<?php
-class InternetArchiveBridge extends BridgeAbstract {
- const NAME = 'Internet Archive Bridge';
- const URI = 'https://archive.org';
- const DESCRIPTION = 'Returns newest uploads, posts and more from an account';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(
- 'Account' => array(
- 'username' => array(
- 'name' => 'Username',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => '@verifiedjoseph',
- ),
- 'content' => array(
- 'name' => 'Content',
- 'type' => 'list',
- 'values' => array(
- 'Uploads' => 'uploads',
- 'Posts' => 'posts',
- 'Reviews' => 'reviews',
- 'Collections' => 'collections',
- 'Web Archives' => 'web-archive',
- ),
- 'defaultValue' => 'uploads',
- ),
- 'limit' => self::LIMIT,
- )
- );
-
- const CACHE_TIMEOUT = 900; // 15 mins
-
- const TEST_DETECT_PARAMETERS = array(
- 'https://archive.org/details/@verifiedjoseph' => array(
- 'context' => 'Account', 'username' => 'verifiedjoseph', 'content' => 'uploads'
- ),
- 'https://archive.org/details/@verifiedjoseph?tab=collections' => array(
- 'context' => 'Account', 'username' => 'verifiedjoseph', 'content' => 'collections'
- ),
- );
-
- private $skipClasses = array(
- 'item-ia mobile-header hidden-tiles',
- 'item-ia account-ia'
- );
-
- private $detectParamsRegex = '/https?:\/\/archive\.org\/details\/@([\w]+)(?:\?tab=([a-z-]+))?/';
-
- public function detectParameters($url) {
- $params = array();
-
- if(preg_match($this->detectParamsRegex, $url, $matches) > 0) {
- $params['context'] = 'Account';
- $params['username'] = $matches[1];
- $params['content'] = 'uploads';
-
- if (isset($matches[2])) {
- $params['content'] = $matches[2];
- }
-
- return $params;
- }
-
- return null;
- }
-
- public function collectData() {
-
- $html = getSimpleHTMLDOM($this->getURI());
-
- $html = defaultLinkTo($html, $this->getURI());
-
- if ($this->getInput('content') !== 'posts') {
- $detailsDivNumber = 0;
-
- $results = $html->find('div.results > div[data-id]');
- foreach ($results as $index => $result) {
- $item = array();
-
- if (in_array($result->class, $this->skipClasses)) {
- continue;
- }
-
- switch($result->class) {
- case 'item-ia':
- switch($this->getInput('content')) {
- case 'reviews':
- $item = $this->processReview($result);
- break;
- case 'uploads':
- $item = $this->processUpload($result);
- break;
- }
-
- break;
- case 'item-ia url-item':
- $item = $this->processWebArchives($result);
- break;
- case 'item-ia collection-ia':
- $item = $this->processCollection($result);
- break;
- }
-
- if ($this->getInput('content') !== 'reviews') {
- $hiddenDetails = $this->processHiddenDetails($html, $detailsDivNumber, $item);
-
- $this->items[] = array_merge($item, $hiddenDetails);
- } else {
-
- $this->items[] = $item;
-
- }
-
- $detailsDivNumber++;
- $limit = $this->getInput('limit') ?? 10;
- if (count($this->items) >= $limit) {
- break;
- }
- }
- }
-
- if ($this->getInput('content') === 'posts') {
- $this->items = $this->processPosts($html);
- }
- }
-
- public function getURI() {
-
- if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) {
- return self::URI . '/details/' . $this->processUsername() . '&tab=' . $this->getInput('content');
- }
-
- return parent::getURI();
- }
-
- public function getName() {
-
- if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) {
- $contentValues = array_flip(self::PARAMETERS['Account']['content']['values']);
-
- return $contentValues[$this->getInput('content')] . ' - '
- . $this->processUsername() . ' - Internet Archive';
- }
-
- return parent::getName();
- }
-
- private function processUsername() {
-
- if (substr($this->getInput('username'), 0, 1) !== '@') {
- return '@' . $this->getInput('username');
- }
-
- return $this->getInput('username');
- }
-
- private function processUpload($result) {
- $item = array();
-
- $collection = $result->find('a.stealth', 0);
- $collectionLink = $collection->href;
- $collectionTitle = $collection->find('div.item-parent-ttl', 0)->plaintext;
-
- $item['title'] = trim($result->find('div.ttl', 0)->innertext);
- $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
- $item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href;
-
- if ($result->find('div.by.C.C4', 0)->children(2)) {
- $item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext;
- }
-
- $item['content'] = <<<EOD
+class InternetArchiveBridge extends BridgeAbstract
+{
+ const NAME = 'Internet Archive Bridge';
+ const URI = 'https://archive.org';
+ const DESCRIPTION = 'Returns newest uploads, posts and more from an account';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [
+ 'Account' => [
+ 'username' => [
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => '@verifiedjoseph',
+ ],
+ 'content' => [
+ 'name' => 'Content',
+ 'type' => 'list',
+ 'values' => [
+ 'Uploads' => 'uploads',
+ 'Posts' => 'posts',
+ 'Reviews' => 'reviews',
+ 'Collections' => 'collections',
+ 'Web Archives' => 'web-archive',
+ ],
+ 'defaultValue' => 'uploads',
+ ],
+ 'limit' => self::LIMIT,
+ ]
+ ];
+
+ const CACHE_TIMEOUT = 900; // 15 mins
+
+ const TEST_DETECT_PARAMETERS = [
+ 'https://archive.org/details/@verifiedjoseph' => [
+ 'context' => 'Account', 'username' => 'verifiedjoseph', 'content' => 'uploads'
+ ],
+ 'https://archive.org/details/@verifiedjoseph?tab=collections' => [
+ 'context' => 'Account', 'username' => 'verifiedjoseph', 'content' => 'collections'
+ ],
+ ];
+
+ private $skipClasses = [
+ 'item-ia mobile-header hidden-tiles',
+ 'item-ia account-ia'
+ ];
+
+ private $detectParamsRegex = '/https?:\/\/archive\.org\/details\/@([\w]+)(?:\?tab=([a-z-]+))?/';
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ if (preg_match($this->detectParamsRegex, $url, $matches) > 0) {
+ $params['context'] = 'Account';
+ $params['username'] = $matches[1];
+ $params['content'] = 'uploads';
+
+ if (isset($matches[2])) {
+ $params['content'] = $matches[2];
+ }
+
+ return $params;
+ }
+
+ return null;
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $html = defaultLinkTo($html, $this->getURI());
+
+ if ($this->getInput('content') !== 'posts') {
+ $detailsDivNumber = 0;
+
+ $results = $html->find('div.results > div[data-id]');
+ foreach ($results as $index => $result) {
+ $item = [];
+
+ if (in_array($result->class, $this->skipClasses)) {
+ continue;
+ }
+
+ switch ($result->class) {
+ case 'item-ia':
+ switch ($this->getInput('content')) {
+ case 'reviews':
+ $item = $this->processReview($result);
+ break;
+ case 'uploads':
+ $item = $this->processUpload($result);
+ break;
+ }
+
+ break;
+ case 'item-ia url-item':
+ $item = $this->processWebArchives($result);
+ break;
+ case 'item-ia collection-ia':
+ $item = $this->processCollection($result);
+ break;
+ }
+
+ if ($this->getInput('content') !== 'reviews') {
+ $hiddenDetails = $this->processHiddenDetails($html, $detailsDivNumber, $item);
+
+ $this->items[] = array_merge($item, $hiddenDetails);
+ } else {
+ $this->items[] = $item;
+ }
+
+ $detailsDivNumber++;
+
+ $limit = $this->getInput('limit') ?? 10;
+ if (count($this->items) >= $limit) {
+ break;
+ }
+ }
+ }
+
+ if ($this->getInput('content') === 'posts') {
+ $this->items = $this->processPosts($html);
+ }
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) {
+ return self::URI . '/details/' . $this->processUsername() . '&tab=' . $this->getInput('content');
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) {
+ $contentValues = array_flip(self::PARAMETERS['Account']['content']['values']);
+
+ return $contentValues[$this->getInput('content')] . ' - '
+ . $this->processUsername() . ' - Internet Archive';
+ }
+
+ return parent::getName();
+ }
+
+ private function processUsername()
+ {
+ if (substr($this->getInput('username'), 0, 1) !== '@') {
+ return '@' . $this->getInput('username');
+ }
+
+ return $this->getInput('username');
+ }
+
+ private function processUpload($result)
+ {
+ $item = [];
+
+ $collection = $result->find('a.stealth', 0);
+ $collectionLink = $collection->href;
+ $collectionTitle = $collection->find('div.item-parent-ttl', 0)->plaintext;
+
+ $item['title'] = trim($result->find('div.ttl', 0)->innertext);
+ $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
+ $item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href;
+
+ if ($result->find('div.by.C.C4', 0)->children(2)) {
+ $item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext;
+ }
+
+ $item['content'] = <<<EOD
<p>Media Type: {$result->attr['data-mediatype']}<br>
Collection: <a href="{$collectionLink}">{$collectionTitle}</a></p>
EOD;
- $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
+ $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
- return $item;
- }
+ return $item;
+ }
- private function processReview($result) {
- $item = array();
+ private function processReview($result)
+ {
+ $item = [];
- $item['title'] = trim($result->find('div.ttl', 0)->innertext);
- $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
- $item['uri'] = $result->find('div.review-title', 0)->children(0)->href;
+ $item['title'] = trim($result->find('div.ttl', 0)->innertext);
+ $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
+ $item['uri'] = $result->find('div.review-title', 0)->children(0)->href;
- if ($result->find('div.by.C.C4', 0)->children(2)) {
- $item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext;
- }
+ if ($result->find('div.by.C.C4', 0)->children(2)) {
+ $item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext;
+ }
- $item['content'] = <<<EOD
+ $item['content'] = <<<EOD
<p><strong>Subject: {$result->find('div.review-title', 0)->plaintext}</strong></p>
<p>{$result->find('div.hidden-lists.review' , 0)->children(1)->plaintext}</p>
EOD;
- $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
+ $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
- return $item;
- }
+ return $item;
+ }
- private function processWebArchives($result) {
- $item = array();
+ private function processWebArchives($result)
+ {
+ $item = [];
- $item['title'] = trim($result->find('div.ttl', 0)->plaintext);
- $item['timestamp'] = strtotime($result->find('div.hidden-lists', 0)->children(0)->plaintext);
- $item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href;
+ $item['title'] = trim($result->find('div.ttl', 0)->plaintext);
+ $item['timestamp'] = strtotime($result->find('div.hidden-lists', 0)->children(0)->plaintext);
+ $item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href;
- $item['content'] = <<<EOD
+ $item['content'] = <<<EOD
{$this->processUsername()} archived <a href="{$item['uri']}">{$result->find('div.ttl', 0)->plaintext}</a>
EOD;
- $item['enclosures'][] = $result->find('img.item-img', 0)->source;
+ $item['enclosures'][] = $result->find('img.item-img', 0)->source;
- return $item;
- }
+ return $item;
+ }
- private function processCollection($result) {
- $item = array();
+ private function processCollection($result)
+ {
+ $item = [];
- $title = trim($result->find('div.collection-title.C.C2', 0)->children(0)->plaintext);
- $itemCount = strtolower(trim($result->find('div.num-items.topinblock', 0)->plaintext));
+ $title = trim($result->find('div.collection-title.C.C2', 0)->children(0)->plaintext);
+ $itemCount = strtolower(trim($result->find('div.num-items.topinblock', 0)->plaintext));
- $item['title'] = $title . ' (' . $itemCount . ')';
- $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
- $item['uri'] = $result->find('div.collection-title.C.C2 > a', 0)->href;
+ $item['title'] = $title . ' (' . $itemCount . ')';
+ $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
+ $item['uri'] = $result->find('div.collection-title.C.C2 > a', 0)->href;
- $item['content'] = '';
+ $item['content'] = '';
- if ($result->find('img.item-img', 0)) {
- $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
- }
+ if ($result->find('img.item-img', 0)) {
+ $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
+ }
- return $item;
- }
+ return $item;
+ }
- private function processHiddenDetails($html, $detailsDivNumber, $item) {
- $description = '';
+ private function processHiddenDetails($html, $detailsDivNumber, $item)
+ {
+ $description = '';
- if ($html->find('div.details-ia.hidden-tiles', $detailsDivNumber)) {
- $detailsDiv = $html->find('div.details-ia.hidden-tiles', $detailsDivNumber);
+ if ($html->find('div.details-ia.hidden-tiles', $detailsDivNumber)) {
+ $detailsDiv = $html->find('div.details-ia.hidden-tiles', $detailsDivNumber);
- if ($detailsDiv->find('div.C234', 0)->children(0)) {
- $description = $detailsDiv->find('div.C234', 0)->children(0)->plaintext;
+ if ($detailsDiv->find('div.C234', 0)->children(0)) {
+ $description = $detailsDiv->find('div.C234', 0)->children(0)->plaintext;
- $detailsDiv->find('div.C234', 0)->children(0)->innertext = '';
- }
+ $detailsDiv->find('div.C234', 0)->children(0)->innertext = '';
+ }
- $topics = trim($detailsDiv->find('div.C234', 0)->plaintext);
+ $topics = trim($detailsDiv->find('div.C234', 0)->plaintext);
- if (!empty($topics)) {
- $topics = trim($detailsDiv->find('div.C234', 0)->plaintext);
- $topics = trim(substr($topics, 7));
+ if (!empty($topics)) {
+ $topics = trim($detailsDiv->find('div.C234', 0)->plaintext);
+ $topics = trim(substr($topics, 7));
- $item['categories'] = explode(',', $topics);
- }
+ $item['categories'] = explode(',', $topics);
+ }
- $item['content'] = '<p>' . $description . '</p>' . $item['content'];
- }
+ $item['content'] = '<p>' . $description . '</p>' . $item['content'];
+ }
- return $item;
- }
+ return $item;
+ }
- private function processPosts($html) {
- $items = array();
+ private function processPosts($html)
+ {
+ $items = [];
- foreach ($html->find('table.forumTable > tr') as $index => $tr) {
- $item = array();
+ foreach ($html->find('table.forumTable > tr') as $index => $tr) {
+ $item = [];
- if ($index === 0) {
- continue;
- }
+ if ($index === 0) {
+ continue;
+ }
- $item['title'] = $tr->find('td', 0)->plaintext;
- $item['timestamp'] = strtotime($tr->find('td', 4)->children(0)->plaintext);
- $item['uri'] = $tr->find('td', 0)->children(0)->href;
+ $item['title'] = $tr->find('td', 0)->plaintext;
+ $item['timestamp'] = strtotime($tr->find('td', 4)->children(0)->plaintext);
+ $item['uri'] = $tr->find('td', 0)->children(0)->href;
- $formLink = <<<EOD
+ $formLink = <<<EOD
<a href="{$tr->find('td', 2)->children(0)->href}">{$tr->find('td', 2)->children(0)->plaintext}</a>
EOD;
- $postDate = $tr->find('td', 4)->children(0)->plaintext;
+ $postDate = $tr->find('td', 4)->children(0)->plaintext;
- $postPageHtml = getSimpleHTMLDOMCached($item['uri'], 3600);
+ $postPageHtml = getSimpleHTMLDOMCached($item['uri'], 3600);
- $postPageHtml = defaultLinkTo($postPageHtml, $this->getURI());
+ $postPageHtml = defaultLinkTo($postPageHtml, $this->getURI());
- $post = $postPageHtml->find('div.box.well.well-sm', 0);
+ $post = $postPageHtml->find('div.box.well.well-sm', 0);
- $parentLink = '';
- $replyLink = <<<EOD
+ $parentLink = '';
+ $replyLink = <<<EOD
<a href="{$post->find('a', 0)->href}">Reply</a>
EOD;
- if ($post->find('a', 1)->innertext = 'See parent post') {
- $parentLink = <<<EOD
+ if ($post->find('a', 1)->innertext = 'See parent post') {
+ $parentLink = <<<EOD
<a href="{$post->find('a', 1)->href}">View parent post</a>
EOD;
- }
+ }
- $post->find('h1', 0)->outertext = '';
- $post->find('h2', 0)->outertext = '';
+ $post->find('h1', 0)->outertext = '';
+ $post->find('h2', 0)->outertext = '';
- $item['content'] = <<<EOD
+ $item['content'] = <<<EOD
<p>{$post->innertext}</p>{$replyLink} - {$parentLink} - Posted in {$formLink} on {$postDate}
EOD;
- $items[] = $item;
+ $items[] = $item;
- if (count($items) >= $this->getInput('limit') ?? 10) {
- break;
- }
- }
+ if (count($items) >= $this->getInput('limit') ?? 10) {
+ break;
+ }
+ }
- return $items;
- }
+ return $items;
+ }
}
diff --git a/bridges/ItchioBridge.php b/bridges/ItchioBridge.php
index 3dcbd6bf..e7892306 100644
--- a/bridges/ItchioBridge.php
+++ b/bridges/ItchioBridge.php
@@ -1,46 +1,48 @@
<?php
-class ItchioBridge extends BridgeAbstract {
- const NAME = 'itch.io';
- const URI = 'https://itch.io';
- const DESCRIPTION = 'Fetches the file uploads for a product';
- const MAINTAINER = 'jacquesh';
- const PARAMETERS = array(array(
- 'url' => array(
- 'name' => 'Product URL',
- 'exampleValue' => 'https://remedybg.itch.io/remedybg',
- 'required' => true,
- )
- ));
- const CACHE_TIMEOUT = 21600; // 6 hours
+class ItchioBridge extends BridgeAbstract
+{
+ const NAME = 'itch.io';
+ const URI = 'https://itch.io';
+ const DESCRIPTION = 'Fetches the file uploads for a product';
+ const MAINTAINER = 'jacquesh';
+ const PARAMETERS = [[
+ 'url' => [
+ 'name' => 'Product URL',
+ 'exampleValue' => 'https://remedybg.itch.io/remedybg',
+ 'required' => true,
+ ]
+ ]];
+ const CACHE_TIMEOUT = 21600; // 6 hours
- public function collectData() {
- $url = $this->getInput('url');
- $html = getSimpleHTMLDOM($url);
+ public function collectData()
+ {
+ $url = $this->getInput('url');
+ $html = getSimpleHTMLDOM($url);
- $title = $html->find('.game_title', 0)->innertext;
+ $title = $html->find('.game_title', 0)->innertext;
- $content = 'The following files are available to download:<br/>';
- foreach ($html->find('div.upload') as $element) {
- $filename = $element->find('strong.name', 0)->innertext;
- $filesize = $element->find('span.file_size', 0)->first_child()->innertext;
- $content = $content . $filename . ' (' . $filesize . ')<br/>';
- }
+ $content = 'The following files are available to download:<br/>';
+ foreach ($html->find('div.upload') as $element) {
+ $filename = $element->find('strong.name', 0)->innertext;
+ $filesize = $element->find('span.file_size', 0)->first_child()->innertext;
+ $content = $content . $filename . ' (' . $filesize . ')<br/>';
+ }
- // On 2021-04-28/29, itch.io changed their project page format so that the
- // 'last updated' timestamp is only shown to logged-in users.
- // Since we can't use the last-updated date to identify a post, we include
- // the description text in the input for the UID hash so that if the
- // project posts an update that changes the description but does not add
- // or rename any files, we'll still flag it as an update.
- $project_description = $html->find('div.formatted_description', 0)->plaintext;
- $uidContent = $project_description . $content;
+ // On 2021-04-28/29, itch.io changed their project page format so that the
+ // 'last updated' timestamp is only shown to logged-in users.
+ // Since we can't use the last-updated date to identify a post, we include
+ // the description text in the input for the UID hash so that if the
+ // project posts an update that changes the description but does not add
+ // or rename any files, we'll still flag it as an update.
+ $project_description = $html->find('div.formatted_description', 0)->plaintext;
+ $uidContent = $project_description . $content;
- $item = array();
- $item['uri'] = $url;
- $item['uid'] = $uidContent;
- $item['title'] = 'Update for ' . $title;
- $item['content'] = $content;
- $this->items[] = $item;
- }
+ $item = [];
+ $item['uri'] = $url;
+ $item['uid'] = $uidContent;
+ $item['title'] = 'Update for ' . $title;
+ $item['content'] = $content;
+ $this->items[] = $item;
+ }
}
diff --git a/bridges/IvooxBridge.php b/bridges/IvooxBridge.php
index d48be598..971c4632 100644
--- a/bridges/IvooxBridge.php
+++ b/bridges/IvooxBridge.php
@@ -1,128 +1,132 @@
<?php
+
/**
* IvooxRssBridge
* Returns the latest search result
* TODO: support podcast episodes list
*/
-class IvooxBridge extends BridgeAbstract {
- const NAME = 'Ivoox Bridge';
- const URI = 'https://www.ivoox.com/';
- const CACHE_TIMEOUT = 10800; // 3h
- const DESCRIPTION = 'Returns the 10 newest episodes by keyword search';
- const MAINTAINER = 'xurxof'; // based on YoutubeBridge by mitsukarenai
- const PARAMETERS = array(
- 'Search result' => array(
- 's' => array(
- 'name' => 'keyword',
- 'required' => true,
- 'exampleValue' => 'car'
- )
- )
- );
-
- private function ivBridgeAddItem(
- $episode_link,
- $podcast_name,
- $episode_title,
- $author_name,
- $episode_description,
- $publication_date,
- $episode_duration) {
- $item = array();
- $item['title'] = htmlspecialchars_decode($podcast_name . ': ' . $episode_title);
- $item['author'] = $author_name;
- $item['timestamp'] = $publication_date;
- $item['uri'] = $episode_link;
- $item['content'] = '<a href="' . $episode_link . '">' . $podcast_name . ': ' . $episode_title
- . '</a><br />Duration: ' . $episode_duration
- . '<br />Description:<br />' . $episode_description;
- $this->items[] = $item;
- }
-
- private function ivBridgeParseHtmlListing($html) {
- $limit = 4;
- $count = 0;
-
- foreach($html->find('div.flip-container') as $flipper) {
- $linkcount = 0;
- if(!empty($flipper->find( 'div.modulo-type-banner' ))) {
- // ad
- continue;
- }
-
- if($count < $limit) {
- foreach($flipper->find('div.header-modulo') as $element) {
- foreach($element->find('a') as $link) {
- if ($linkcount == 0) {
- $episode_link = $link->href;
- $episode_title = $link->title;
- } elseif ($linkcount == 1) {
- $author_link = $link->href;
- $author_name = $link->title;
- } elseif ($linkcount == 2) {
- $podcast_link = $link->href;
- $podcast_name = $link->title;
- }
-
- $linkcount++;
- }
- }
-
- $episode_description = $flipper->find('button.btn-link', 0)->getAttribute('data-content');
- $episode_duration = $flipper->find('p.time', 0)->innertext;
- $publication_date = $flipper->find('li.date', 0)->getAttribute('title');
-
- // alternative date_parse_from_format
- // or DateTime::createFromFormat('G:i - d \d\e M \d\e Y', $publication);
- // TODO: month name translations, due function doesn't support locale
-
- $a = strptime($publication_date, '%H:%M - %d de %b. de %Y'); // obsolete function, uses c libraries
- $publication_date = mktime(0, 0, 0, $a['tm_mon'] + 1, $a['tm_mday'], $a['tm_year'] + 1900);
-
- $this->ivBridgeAddItem(
- $episode_link,
- $podcast_name,
- $episode_title,
- $author_name,
- $episode_description,
- $publication_date,
- $episode_duration
- );
- $count++;
- }
- }
- }
-
- public function collectData() {
-
- // store locale, change to spanish
- $originalLocales = explode(';', setlocale(LC_ALL, 0));
- setlocale(LC_ALL, 'es_ES.utf8');
-
- $xml = '';
- $html = '';
- $url_feed = '';
- if($this->getInput('s')) { /* Search modes */
- $this->request = str_replace(' ', '-', $this->getInput('s'));
- $url_feed = self::URI . urlencode($this->request) . '_sb_f_1.html?o=uploaddate';
- } else {
- returnClientError('Not valid mode at IvooxBridge');
- }
-
- $dom = getSimpleHTMLDOM($url_feed);
- $this->ivBridgeParseHtmlListing($dom);
-
- // restore locale
-
- foreach($originalLocales as $localeSetting) {
- if(strpos($localeSetting, '=') !== false) {
- list($category, $locale) = explode('=', $localeSetting);
- } else {
- $category = LC_ALL;
- $locale = $localeSetting;
- }
-
- setlocale($category, $locale);
- }
- }
+class IvooxBridge extends BridgeAbstract
+{
+ const NAME = 'Ivoox Bridge';
+ const URI = 'https://www.ivoox.com/';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the 10 newest episodes by keyword search';
+ const MAINTAINER = 'xurxof'; // based on YoutubeBridge by mitsukarenai
+ const PARAMETERS = [
+ 'Search result' => [
+ 's' => [
+ 'name' => 'keyword',
+ 'required' => true,
+ 'exampleValue' => 'car'
+ ]
+ ]
+ ];
+
+ private function ivBridgeAddItem(
+ $episode_link,
+ $podcast_name,
+ $episode_title,
+ $author_name,
+ $episode_description,
+ $publication_date,
+ $episode_duration
+ ) {
+ $item = [];
+ $item['title'] = htmlspecialchars_decode($podcast_name . ': ' . $episode_title);
+ $item['author'] = $author_name;
+ $item['timestamp'] = $publication_date;
+ $item['uri'] = $episode_link;
+ $item['content'] = '<a href="' . $episode_link . '">' . $podcast_name . ': ' . $episode_title
+ . '</a><br />Duration: ' . $episode_duration
+ . '<br />Description:<br />' . $episode_description;
+ $this->items[] = $item;
+ }
+
+ private function ivBridgeParseHtmlListing($html)
+ {
+ $limit = 4;
+ $count = 0;
+
+ foreach ($html->find('div.flip-container') as $flipper) {
+ $linkcount = 0;
+ if (!empty($flipper->find('div.modulo-type-banner'))) {
+ // ad
+ continue;
+ }
+
+ if ($count < $limit) {
+ foreach ($flipper->find('div.header-modulo') as $element) {
+ foreach ($element->find('a') as $link) {
+ if ($linkcount == 0) {
+ $episode_link = $link->href;
+ $episode_title = $link->title;
+ } elseif ($linkcount == 1) {
+ $author_link = $link->href;
+ $author_name = $link->title;
+ } elseif ($linkcount == 2) {
+ $podcast_link = $link->href;
+ $podcast_name = $link->title;
+ }
+
+ $linkcount++;
+ }
+ }
+
+ $episode_description = $flipper->find('button.btn-link', 0)->getAttribute('data-content');
+ $episode_duration = $flipper->find('p.time', 0)->innertext;
+ $publication_date = $flipper->find('li.date', 0)->getAttribute('title');
+
+ // alternative date_parse_from_format
+ // or DateTime::createFromFormat('G:i - d \d\e M \d\e Y', $publication);
+ // TODO: month name translations, due function doesn't support locale
+
+ $a = strptime($publication_date, '%H:%M - %d de %b. de %Y'); // obsolete function, uses c libraries
+ $publication_date = mktime(0, 0, 0, $a['tm_mon'] + 1, $a['tm_mday'], $a['tm_year'] + 1900);
+
+ $this->ivBridgeAddItem(
+ $episode_link,
+ $podcast_name,
+ $episode_title,
+ $author_name,
+ $episode_description,
+ $publication_date,
+ $episode_duration
+ );
+ $count++;
+ }
+ }
+ }
+
+ public function collectData()
+ {
+ // store locale, change to spanish
+ $originalLocales = explode(';', setlocale(LC_ALL, 0));
+ setlocale(LC_ALL, 'es_ES.utf8');
+
+ $xml = '';
+ $html = '';
+ $url_feed = '';
+ if ($this->getInput('s')) { /* Search modes */
+ $this->request = str_replace(' ', '-', $this->getInput('s'));
+ $url_feed = self::URI . urlencode($this->request) . '_sb_f_1.html?o=uploaddate';
+ } else {
+ returnClientError('Not valid mode at IvooxBridge');
+ }
+
+ $dom = getSimpleHTMLDOM($url_feed);
+ $this->ivBridgeParseHtmlListing($dom);
+
+ // restore locale
+
+ foreach ($originalLocales as $localeSetting) {
+ if (strpos($localeSetting, '=') !== false) {
+ list($category, $locale) = explode('=', $localeSetting);
+ } else {
+ $category = LC_ALL;
+ $locale = $localeSetting;
+ }
+
+ setlocale($category, $locale);
+ }
+ }
}
diff --git a/bridges/JapanExpoBridge.php b/bridges/JapanExpoBridge.php
index c56999fe..0d02e753 100644
--- a/bridges/JapanExpoBridge.php
+++ b/bridges/JapanExpoBridge.php
@@ -1,104 +1,108 @@
<?php
-class JapanExpoBridge extends BridgeAbstract {
- const MAINTAINER = 'Ginko';
- const NAME = 'Japan Expo Actualités';
- const URI = 'https://www.japan-expo-paris.com/fr/actualites';
- const CACHE_TIMEOUT = 14400; // 4h
- const DESCRIPTION = 'Returns most recent entries from Japan Expo actualités.';
- const PARAMETERS = array( array(
- 'mode' => array(
- 'name' => 'Show full contents',
- 'type' => 'checkbox',
- )
- ));
+class JapanExpoBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Ginko';
+ const NAME = 'Japan Expo Actualités';
+ const URI = 'https://www.japan-expo-paris.com/fr/actualites';
+ const CACHE_TIMEOUT = 14400; // 4h
+ const DESCRIPTION = 'Returns most recent entries from Japan Expo actualités.';
+ const PARAMETERS = [ [
+ 'mode' => [
+ 'name' => 'Show full contents',
+ 'type' => 'checkbox',
+ ]
+ ]];
- public function getIcon() {
- return 'https://s.japan-expo.com/katana/images/JES073/favicons/paris.png';
- }
+ public function getIcon()
+ {
+ return 'https://s.japan-expo.com/katana/images/JES073/favicons/paris.png';
+ }
- public function collectData(){
+ public function collectData()
+ {
+ $convert_article_images = function ($matches) {
+ if (is_array($matches) && count($matches) > 1) {
+ return '<img src="' . $matches[1] . '" />';
+ }
+ };
- $convert_article_images = function($matches){
- if(is_array($matches) && count($matches) > 1) {
- return '<img src="' . $matches[1] . '" />';
- }
- };
+ $html = getSimpleHTMLDOM(self::URI);
+ $fullcontent = $this->getInput('mode');
+ $count = 0;
- $html = getSimpleHTMLDOM(self::URI);
- $fullcontent = $this->getInput('mode');
- $count = 0;
+ foreach ($html->find('a._tile2') as $element) {
+ $url = $element->href;
+ $thumbnail = 'https://s.japan-expo.com/katana/images/JES049/paris.png';
+ preg_match('/url\(([^)]+)\)/', $element->find('img.rspvimgset', 0)->style, $img_search_result);
- foreach($html->find('a._tile2') as $element) {
+ if (count($img_search_result) >= 2) {
+ $thumbnail = trim($img_search_result[1], "'");
+ }
- $url = $element->href;
- $thumbnail = 'https://s.japan-expo.com/katana/images/JES049/paris.png';
- preg_match('/url\(([^)]+)\)/', $element->find('img.rspvimgset', 0)->style, $img_search_result);
+ if ($fullcontent) {
+ if ($count >= 5) {
+ break;
+ }
- if(count($img_search_result) >= 2)
- $thumbnail = trim($img_search_result[1], "'");
+ $article_html = getSimpleHTMLDOMCached($url);
+ $header = $article_html->find('header.pageHeadBox', 0);
+ $timestamp = strtotime($header->find('time', 0)->datetime);
+ $title_html = $header->find('div.section', 0)->next_sibling();
+ $title = $title_html->plaintext;
+ $headings = $title_html->next_sibling()->outertext;
+ $article = $article_html->find('div.content', 0)->innertext;
+ $article = preg_replace_callback(
+ '/<img [^>]+ style="[^\(]+\(\'([^\']+)\'[^>]+>/i',
+ $convert_article_images,
+ $article
+ );
- if($fullcontent) {
- if($count >= 5) {
- break;
- }
+ $content = $headings . $article;
+ } else {
+ $date_text = $element->find('span.date', 0)->plaintext;
+ $timestamp = $this->frenchPubDateToTimestamp($date_text);
+ $title = trim($element->find('span._title', 0)->plaintext);
+ $content = '<img src="'
+ . $thumbnail
+ . '"></img><br />'
+ . $date_text
+ . '<br /><a href="'
+ . $url
+ . '">Lire l\'article</a>';
+ }
- $article_html = getSimpleHTMLDOMCached($url);
- $header = $article_html->find('header.pageHeadBox', 0);
- $timestamp = strtotime($header->find('time', 0)->datetime);
- $title_html = $header->find('div.section', 0)->next_sibling();
- $title = $title_html->plaintext;
- $headings = $title_html->next_sibling()->outertext;
- $article = $article_html->find('div.content', 0)->innertext;
- $article = preg_replace_callback(
- '/<img [^>]+ style="[^\(]+\(\'([^\']+)\'[^>]+>/i',
- $convert_article_images,
- $article);
+ $item = [];
+ $item['uri'] = $url;
+ $item['title'] = $title;
+ $item['timestamp'] = $timestamp;
+ $item['enclosures'] = [$thumbnail];
+ $item['content'] = $content;
+ $this->items[] = $item;
+ $count++;
+ }
+ }
- $content = $headings . $article;
- } else {
- $date_text = $element->find('span.date', 0)->plaintext;
- $timestamp = $this->frenchPubDateToTimestamp($date_text);
- $title = trim($element->find('span._title', 0)->plaintext);
- $content = '<img src="'
- . $thumbnail
- . '"></img><br />'
- . $date_text
- . '<br /><a href="'
- . $url
- . '">Lire l\'article</a>';
- }
-
- $item = array();
- $item['uri'] = $url;
- $item['title'] = $title;
- $item['timestamp'] = $timestamp;
- $item['enclosures'] = array($thumbnail);
- $item['content'] = $content;
- $this->items[] = $item;
- $count++;
- }
- }
-
- private function frenchPubDateToTimestamp($date_to_parse) {
- return strtotime(
- strtr(
- strtolower(str_replace('Publié le ', '', $date_to_parse)),
- array(
- 'janvier' => 'jan',
- 'février' => 'feb',
- 'mars' => 'march',
- 'avril' => 'apr',
- 'mai' => 'may',
- 'juin' => 'jun',
- 'juillet' => 'jul',
- 'août' => 'aug',
- 'septembre' => 'sep',
- 'octobre' => 'oct',
- 'novembre' => 'nov',
- 'décembre' => 'dec'
- )
- )
- );
- }
+ private function frenchPubDateToTimestamp($date_to_parse)
+ {
+ return strtotime(
+ strtr(
+ strtolower(str_replace('Publié le ', '', $date_to_parse)),
+ [
+ 'janvier' => 'jan',
+ 'février' => 'feb',
+ 'mars' => 'march',
+ 'avril' => 'apr',
+ 'mai' => 'may',
+ 'juin' => 'jun',
+ 'juillet' => 'jul',
+ 'août' => 'aug',
+ 'septembre' => 'sep',
+ 'octobre' => 'oct',
+ 'novembre' => 'nov',
+ 'décembre' => 'dec'
+ ]
+ )
+ );
+ }
}
diff --git a/bridges/JornalDeNoticiasBridge.php b/bridges/JornalDeNoticiasBridge.php
index e61a7a42..a9c2031e 100644
--- a/bridges/JornalDeNoticiasBridge.php
+++ b/bridges/JornalDeNoticiasBridge.php
@@ -1,54 +1,59 @@
<?php
-class JornalDeNoticiasBridge extends BridgeAbstract {
- const NAME = 'Jornal de Notícias (PT)';
- const URI = 'https://jn.pt';
- const DESCRIPTION = 'Jornal de Notícias (JN.PT)';
- const MAINTAINER = 'somini';
- const PARAMETERS = array(
- 'URL' => array(
- 'url' => array(
- 'name' => 'URL (relative)',
- 'exampleValue' => 'opiniao/catia-domingues.html',
- )
- )
- );
-
- public function getIcon() {
- return 'https://static.globalnoticias.pt/jn/common/images/favicons/favicon-128.png';
- }
-
- public function getURI() {
- switch($this->queriedContext) {
- case 'URL':
- $url = self::URI . '/' . $this->getInput('url');
- break;
- default:
- $url = self::URI;
- }
- return $url;
- }
-
- public function collectData() {
- $archives = self::getURI();
- $html = getSimpleHTMLDOMCached($archives);
-
- foreach($html->find('article') as $element) {
- $item = array();
-
- $title = $element->find('h2 a', 0);
- $link = $element->find('h2 a', 0);
- $auth = $element->find('h3 a', 0);
-
- $item['title'] = $title->plaintext;
- $item['uri'] = self::URI . $link->href;
- $item['author'] = $auth->plaintext;
-
- $snippet = $element->find('h4 a', 0);
- if ($snippet) {
- $item['content'] = $snippet->plaintext;
- }
-
- $this->items[] = $item;
- }
- }
+
+class JornalDeNoticiasBridge extends BridgeAbstract
+{
+ const NAME = 'Jornal de Notícias (PT)';
+ const URI = 'https://jn.pt';
+ const DESCRIPTION = 'Jornal de Notícias (JN.PT)';
+ const MAINTAINER = 'somini';
+ const PARAMETERS = [
+ 'URL' => [
+ 'url' => [
+ 'name' => 'URL (relative)',
+ 'exampleValue' => 'opiniao/catia-domingues.html',
+ ]
+ ]
+ ];
+
+ public function getIcon()
+ {
+ return 'https://static.globalnoticias.pt/jn/common/images/favicons/favicon-128.png';
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'URL':
+ $url = self::URI . '/' . $this->getInput('url');
+ break;
+ default:
+ $url = self::URI;
+ }
+ return $url;
+ }
+
+ public function collectData()
+ {
+ $archives = self::getURI();
+ $html = getSimpleHTMLDOMCached($archives);
+
+ foreach ($html->find('article') as $element) {
+ $item = [];
+
+ $title = $element->find('h2 a', 0);
+ $link = $element->find('h2 a', 0);
+ $auth = $element->find('h3 a', 0);
+
+ $item['title'] = $title->plaintext;
+ $item['uri'] = self::URI . $link->href;
+ $item['author'] = $auth->plaintext;
+
+ $snippet = $element->find('h4 a', 0);
+ if ($snippet) {
+ $item['content'] = $snippet->plaintext;
+ }
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/JustETFBridge.php b/bridges/JustETFBridge.php
index 2f322789..88920133 100644
--- a/bridges/JustETFBridge.php
+++ b/bridges/JustETFBridge.php
@@ -1,352 +1,368 @@
<?php
-class JustETFBridge extends BridgeAbstract {
- const NAME = 'justETF Bridge';
- const URI = 'https://www.justetf.com';
- const DESCRIPTION = 'Currently only supports the news feed';
- const MAINTAINER = 'logmanoriginal';
- const PARAMETERS = array(
- 'News' => array(
- 'full' => array(
- 'name' => 'Full Article',
- 'type' => 'checkbox',
- 'title' => 'Enable to load full articles'
- )
- ),
- 'Profile' => array(
- 'isin' => array(
- 'name' => 'ISIN',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'IE00B4X9L533',
- 'pattern' => '[a-zA-Z]{2}[a-zA-Z0-9]{10}',
- 'title' => 'ISIN, consisting of 2-letter country code, 9-character identifier, check character'
- ),
- 'strategy' => array(
- 'name' => 'Include Strategy',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'description' => array(
- 'name' => 'Include Description',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- )
- ),
- 'global' => array(
- 'lang' => array(
- 'name' => 'Language',
- 'type' => 'list',
- 'values' => array(
- 'Englisch' => 'en',
- 'Deutsch' => 'de',
- 'Italiano' => 'it'
- ),
- 'defaultValue' => 'Englisch'
- )
- )
- );
-
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI());
-
- defaultLinkTo($html, static::URI);
-
- switch($this->queriedContext) {
- case 'News':
- $this->collectNews($html);
- break;
- case 'Profile':
- $this->collectProfile($html);
- break;
- }
- }
-
- public function getURI() {
- $uri = static::URI;
-
- if($this->getInput('lang')) {
- $uri .= '/' . $this->getInput('lang');
- }
-
- switch($this->queriedContext) {
- case 'News':
- $uri .= '/news';
- break;
- case 'Profile':
- $uri .= '/etf-profile.html?' . http_build_query(array(
- 'isin' => strtoupper($this->getInput('isin'))
- ));
- break;
- }
-
- return $uri;
- }
-
- public function getName() {
- $name = static::NAME;
-
- $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : '';
-
- switch($this->queriedContext) {
- case 'News': break;
- case 'Profile':
- if($this->getInput('isin')) {
- $name .= ' ISIN ' . strtoupper($this->getInput('isin'));
- }
- }
-
- if($this->getInput('lang')) {
- $name .= ' (' . strtoupper($this->getInput('lang')) . ')';
- }
-
- return $name;
- }
-
- #region Common
-
- /**
- * Fixes dates depending on the choosen language:
- *
- * de : dd.mm.yy
- * en : dd.mm.yy
- * it : dd/mm/yy
- *
- * Basically strtotime doesn't convert dates correctly due to formats
- * being hard to interpret. So we use the DateTime object, manually
- * fixing dates and times (set to 00:00:00.000).
- *
- * We don't know the timezone, so just assume +00:00 (or whatever
- * DateTime chooses)
- */
- private function fixDate($date) {
- switch($this->getInput('lang')) {
- case 'en':
- case 'de':
- $df = date_create_from_format('d.m.y', $date);
- break;
- case 'it':
- $df = date_create_from_format('d/m/y', $date);
- break;
- }
- date_time_set($df, 0, 0);
+class JustETFBridge extends BridgeAbstract
+{
+ const NAME = 'justETF Bridge';
+ const URI = 'https://www.justetf.com';
+ const DESCRIPTION = 'Currently only supports the news feed';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = [
+ 'News' => [
+ 'full' => [
+ 'name' => 'Full Article',
+ 'type' => 'checkbox',
+ 'title' => 'Enable to load full articles'
+ ]
+ ],
+ 'Profile' => [
+ 'isin' => [
+ 'name' => 'ISIN',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'IE00B4X9L533',
+ 'pattern' => '[a-zA-Z]{2}[a-zA-Z0-9]{10}',
+ 'title' => 'ISIN, consisting of 2-letter country code, 9-character identifier, check character'
+ ],
+ 'strategy' => [
+ 'name' => 'Include Strategy',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'description' => [
+ 'name' => 'Include Description',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ]
+ ],
+ 'global' => [
+ 'lang' => [
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'values' => [
+ 'Englisch' => 'en',
+ 'Deutsch' => 'de',
+ 'Italiano' => 'it'
+ ],
+ 'defaultValue' => 'Englisch'
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ defaultLinkTo($html, static::URI);
+
+ switch ($this->queriedContext) {
+ case 'News':
+ $this->collectNews($html);
+ break;
+ case 'Profile':
+ $this->collectProfile($html);
+ break;
+ }
+ }
+
+ public function getURI()
+ {
+ $uri = static::URI;
+
+ if ($this->getInput('lang')) {
+ $uri .= '/' . $this->getInput('lang');
+ }
+
+ switch ($this->queriedContext) {
+ case 'News':
+ $uri .= '/news';
+ break;
+ case 'Profile':
+ $uri .= '/etf-profile.html?' . http_build_query([
+ 'isin' => strtoupper($this->getInput('isin'))
+ ]);
+ break;
+ }
+
+ return $uri;
+ }
+
+ public function getName()
+ {
+ $name = static::NAME;
+
+ $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : '';
+
+ switch ($this->queriedContext) {
+ case 'News':
+ break;
+ case 'Profile':
+ if ($this->getInput('isin')) {
+ $name .= ' ISIN ' . strtoupper($this->getInput('isin'));
+ }
+ }
+
+ if ($this->getInput('lang')) {
+ $name .= ' (' . strtoupper($this->getInput('lang')) . ')';
+ }
+
+ return $name;
+ }
+
+ #region Common
+
+ /**
+ * Fixes dates depending on the choosen language:
+ *
+ * de : dd.mm.yy
+ * en : dd.mm.yy
+ * it : dd/mm/yy
+ *
+ * Basically strtotime doesn't convert dates correctly due to formats
+ * being hard to interpret. So we use the DateTime object, manually
+ * fixing dates and times (set to 00:00:00.000).
+ *
+ * We don't know the timezone, so just assume +00:00 (or whatever
+ * DateTime chooses)
+ */
+ private function fixDate($date)
+ {
+ switch ($this->getInput('lang')) {
+ case 'en':
+ case 'de':
+ $df = date_create_from_format('d.m.y', $date);
+ break;
+ case 'it':
+ $df = date_create_from_format('d/m/y', $date);
+ break;
+ }
+
+ date_time_set($df, 0, 0);
+
+ // Debug::log(date_format($df, 'U'));
+
+ return date_format($df, 'U');
+ }
+
+ private function extractImages($article)
+ {
+ // Notice: We can have zero or more images (though it should mostly be 1)
+ $elements = $article->find('img');
+
+ $images = [];
+
+ foreach ($elements as $img) {
+ // Skip the logo (mostly provided part of a hidden div)
+ if (substr($img->src, strrpos($img->src, '/') + 1) === 'logo.png') {
+ continue;
+ }
+
+ $images[] = $img->src;
+ }
+
+ return $images;
+ }
+
+ #endregion
+
+ #region News
+
+ private function collectNews($html)
+ {
+ $articles = $html->find('div.newsTopArticle')
+ or returnServerError('No articles found! Layout might have changed!');
+
+ foreach ($articles as $article) {
+ $item = [];
+
+ // Common data
+
+ $item['uri'] = $this->extractNewsUri($article);
+ $item['timestamp'] = $this->extractNewsDate($article);
+ $item['title'] = $this->extractNewsTitle($article);
- // Debug::log(date_format($df, 'U'));
+ if ($this->getInput('full')) {
+ $uri = $this->extractNewsUri($article);
- return date_format($df, 'U');
- }
+ $html = getSimpleHTMLDOMCached($uri)
+ or returnServerError('Failed loading full article from ' . $uri);
- private function extractImages($article) {
- // Notice: We can have zero or more images (though it should mostly be 1)
- $elements = $article->find('img');
+ $fullArticle = $html->find('div.article', 0)
+ or returnServerError('No content found! Layout might have changed!');
- $images = array();
+ defaultLinkTo($fullArticle, static::URI);
+
+ $item['author'] = $this->extractFullArticleAuthor($fullArticle);
+ $item['content'] = $this->extractFullArticleContent($fullArticle);
+ $item['enclosures'] = $this->extractImages($fullArticle);
+ } else {
+ $item['content'] = $this->extractNewsDescription($article);
+ $item['enclosures'] = $this->extractImages($article);
+ }
- foreach($elements as $img) {
- // Skip the logo (mostly provided part of a hidden div)
- if(substr($img->src, strrpos($img->src, '/') + 1) === 'logo.png')
- continue;
+ $this->items[] = $item;
+ }
+ }
- $images[] = $img->src;
- }
+ private function extractNewsUri($article)
+ {
+ $element = $article->find('a', 0)
+ or returnServerError('Anchor not found!');
- return $images;
- }
+ return $element->href;
+ }
- #endregion
+ private function extractNewsDate($article)
+ {
+ $element = $article->find('div.subheadline', 0)
+ or returnServerError('Date not found!');
- #region News
+ // Debug::log($element->plaintext);
- private function collectNews($html) {
- $articles = $html->find('div.newsTopArticle')
- or returnServerError('No articles found! Layout might have changed!');
+ $date = trim(explode('|', $element->plaintext)[0]);
- foreach($articles as $article) {
+ return $this->fixDate($date);
+ }
- $item = array();
+ private function extractNewsDescription($article)
+ {
+ $element = $article->find('span.newsText', 0)
+ or returnServerError('Description not found!');
- // Common data
+ $element->find('a', 0)->onclick = '';
- $item['uri'] = $this->extractNewsUri($article);
- $item['timestamp'] = $this->extractNewsDate($article);
- $item['title'] = $this->extractNewsTitle($article);
+ // Debug::log($element->innertext);
- if($this->getInput('full')) {
+ return $element->innertext;
+ }
- $uri = $this->extractNewsUri($article);
+ private function extractNewsTitle($article)
+ {
+ $element = $article->find('h3', 0)
+ or returnServerError('Title not found!');
- $html = getSimpleHTMLDOMCached($uri)
- or returnServerError('Failed loading full article from ' . $uri);
+ return $element->plaintext;
+ }
- $fullArticle = $html->find('div.article', 0)
- or returnServerError('No content found! Layout might have changed!');
+ private function extractFullArticleContent($article)
+ {
+ $element = $article->find('div.article_body', 0)
+ or returnServerError('Article body not found!');
- defaultLinkTo($fullArticle, static::URI);
+ // Remove teaser image
+ $element->find('img.teaser-img', 0)->outertext = '';
- $item['author'] = $this->extractFullArticleAuthor($fullArticle);
- $item['content'] = $this->extractFullArticleContent($fullArticle);
- $item['enclosures'] = $this->extractImages($fullArticle);
+ // Remove self advertisements
+ foreach ($element->find('.call-action') as $adv) {
+ $adv->outertext = '';
+ }
- } else {
+ // Remove tips
+ foreach ($element->find('.panel-edu') as $tip) {
+ $tip->outertext = '';
+ }
- $item['content'] = $this->extractNewsDescription($article);
- $item['enclosures'] = $this->extractImages($article);
+ // Remove inline scripts (used for i.e. interactive graphs) as they are
+ // rendered as a long series of strings
+ foreach ($element->find('script') as $script) {
+ $script->outertext = '[Content removed! Visit site to see full contents!]';
+ }
- }
+ return $element->innertext;
+ }
- $this->items[] = $item;
- }
- }
+ private function extractFullArticleAuthor($article)
+ {
+ $element = $article->find('span[itemprop=name]', 0)
+ or returnServerError('Author not found!');
- private function extractNewsUri($article) {
- $element = $article->find('a', 0)
- or returnServerError('Anchor not found!');
+ return $element->plaintext;
+ }
- return $element->href;
- }
+ #endregion
- private function extractNewsDate($article) {
- $element = $article->find('div.subheadline', 0)
- or returnServerError('Date not found!');
+ #region Profile
- // Debug::log($element->plaintext);
+ private function collectProfile($html)
+ {
+ $item = [];
- $date = trim(explode('|', $element->plaintext)[0]);
+ $item['uri'] = $this->getURI();
+ $item['timestamp'] = $this->extractProfileDate($html);
+ $item['title'] = $this->extractProfiletitle($html);
+ $item['author'] = $this->extractProfileAuthor($html);
+ $item['content'] = $this->extractProfileContent($html);
- return $this->fixDate($date);
- }
+ $this->items[] = $item;
+ }
- private function extractNewsDescription($article) {
- $element = $article->find('span.newsText', 0)
- or returnServerError('Description not found!');
+ private function extractProfileDate($html)
+ {
+ $element = $html->find('div.infobox div.vallabel', 0)
+ or returnServerError('Date not found!');
- $element->find('a', 0)->onclick = '';
+ // Debug::log($element->plaintext);
- // Debug::log($element->innertext);
+ $date = trim(explode("\r\n", $element->plaintext)[1]);
- return $element->innertext;
- }
+ return $this->fixDate($date);
+ }
- private function extractNewsTitle($article) {
- $element = $article->find('h3', 0)
- or returnServerError('Title not found!');
+ private function extractProfileTitle($html)
+ {
+ $element = $html->find('span.h1', 0)
+ or returnServerError('Title not found!');
- return $element->plaintext;
- }
+ return $element->plaintext;
+ }
- private function extractFullArticleContent($article) {
- $element = $article->find('div.article_body', 0)
- or returnServerError('Article body not found!');
+ private function extractProfileContent($html)
+ {
+ // There are a few thins we are interested:
+ // - Investment Strategy
+ // - Description
+ // - Quote
- // Remove teaser image
- $element->find('img.teaser-img', 0)->outertext = '';
+ $strategy = $html->find('div.tab-container div.col-sm-6 p', 0)
+ or returnServerError('Investment Strategy not found!');
- // Remove self advertisements
- foreach($element->find('.call-action') as $adv) {
- $adv->outertext = '';
- }
+ // Description requires a bit of cleanup due to lack of propper identification
- // Remove tips
- foreach($element->find('.panel-edu') as $tip) {
- $tip->outertext = '';
- }
+ $description = $html->find('div.headline', 5)
+ or returnServerError('Description container not found!');
- // Remove inline scripts (used for i.e. interactive graphs) as they are
- // rendered as a long series of strings
- foreach($element->find('script') as $script) {
- $script->outertext = '[Content removed! Visit site to see full contents!]';
- }
+ $description = $description->parent();
- return $element->innertext;
- }
+ foreach ($description->find('div') as $div) {
+ $div->outertext = '';
+ }
- private function extractFullArticleAuthor($article) {
- $element = $article->find('span[itemprop=name]', 0)
- or returnServerError('Author not found!');
+ $quote = $html->find('div.infobox div.val', 0)
+ or returnServerError('Quote not found!');
- return $element->plaintext;
- }
+ $quote_html = '<strong>Quote</strong><br><p>' . $quote . '</p>';
+ $strategy_html = '';
+ $description_html = '';
- #endregion
+ if ($this->getInput('strategy') === true) {
+ $strategy_html = '<strong>Strategy</strong><br><p>' . $strategy . '</p><br>';
+ }
- #region Profile
+ if ($this->getInput('description') === true) {
+ $description_html = '<strong>Description</strong><br><p>' . $description . '</p><br>';
+ }
- private function collectProfile($html) {
- $item = array();
+ return $strategy_html . $description_html . $quote_html;
+ }
- $item['uri'] = $this->getURI();
- $item['timestamp'] = $this->extractProfileDate($html);
- $item['title'] = $this->extractProfiletitle($html);
- $item['author'] = $this->extractProfileAuthor($html);
- $item['content'] = $this->extractProfileContent($html);
+ private function extractProfileAuthor($html)
+ {
+ // Use ISIN + WKN as author
+ // Notice: "identfier" is not a typo [sic]!
+ $element = $html->find('span.identfier', 0)
+ or returnServerError('Author not found!');
- $this->items[] = $item;
- }
+ return $element->plaintext;
+ }
- private function extractProfileDate($html) {
- $element = $html->find('div.infobox div.vallabel', 0)
- or returnServerError('Date not found!');
-
- // Debug::log($element->plaintext);
-
- $date = trim(explode("\r\n", $element->plaintext)[1]);
-
- return $this->fixDate($date);
- }
-
- private function extractProfileTitle($html) {
- $element = $html->find('span.h1', 0)
- or returnServerError('Title not found!');
-
- return $element->plaintext;
- }
-
- private function extractProfileContent($html) {
- // There are a few thins we are interested:
- // - Investment Strategy
- // - Description
- // - Quote
-
- $strategy = $html->find('div.tab-container div.col-sm-6 p', 0)
- or returnServerError('Investment Strategy not found!');
-
- // Description requires a bit of cleanup due to lack of propper identification
-
- $description = $html->find('div.headline', 5)
- or returnServerError('Description container not found!');
-
- $description = $description->parent();
-
- foreach($description->find('div') as $div) {
- $div->outertext = '';
- }
-
- $quote = $html->find('div.infobox div.val', 0)
- or returnServerError('Quote not found!');
-
- $quote_html = '<strong>Quote</strong><br><p>' . $quote . '</p>';
- $strategy_html = '';
- $description_html = '';
-
- if($this->getInput('strategy') === true) {
- $strategy_html = '<strong>Strategy</strong><br><p>' . $strategy . '</p><br>';
- }
-
- if($this->getInput('description') === true) {
- $description_html = '<strong>Description</strong><br><p>' . $description . '</p><br>';
- }
-
- return $strategy_html . $description_html . $quote_html;
- }
-
- private function extractProfileAuthor($html) {
- // Use ISIN + WKN as author
- // Notice: "identfier" is not a typo [sic]!
- $element = $html->find('span.identfier', 0)
- or returnServerError('Author not found!');
-
- return $element->plaintext;
- }
-
- #endregion
+ #endregion
}
diff --git a/bridges/Kanali6Bridge.php b/bridges/Kanali6Bridge.php
index 267c7d5e..e3b7998d 100644
--- a/bridges/Kanali6Bridge.php
+++ b/bridges/Kanali6Bridge.php
@@ -1,20 +1,22 @@
<?php
-class Kanali6Bridge extends XPathAbstract {
- const NAME = 'Kanali6 Latest Podcasts';
- const DESCRIPTION = 'Returns the latest podcasts';
- const URI = 'https://kanali6.com.cy/mp3/TOC.html';
+class Kanali6Bridge extends XPathAbstract
+{
+ const NAME = 'Kanali6 Latest Podcasts';
+ const DESCRIPTION = 'Returns the latest podcasts';
+ const URI = 'https://kanali6.com.cy/mp3/TOC.html';
- const FEED_SOURCE_URL = 'https://kanali6.com.cy/mp3/TOC.xml';
- const XPATH_EXPRESSION_ITEM = '//recording[position() <= 50]';
- const XPATH_EXPRESSION_ITEM_TITLE = './title';
- const XPATH_EXPRESSION_ITEM_CONTENT = './durationvisual';
- const XPATH_EXPRESSION_ITEM_URI = './filename';
- const XPATH_EXPRESSION_ITEM_AUTHOR = './/producersname';
- const XPATH_EXPRESSION_ITEM_TIMESTAMP = './recfinisheddatetime';
- const XPATH_EXPRESSION_ITEM_ENCLOSURES = './filename';
+ const FEED_SOURCE_URL = 'https://kanali6.com.cy/mp3/TOC.xml';
+ const XPATH_EXPRESSION_ITEM = '//recording[position() <= 50]';
+ const XPATH_EXPRESSION_ITEM_TITLE = './title';
+ const XPATH_EXPRESSION_ITEM_CONTENT = './durationvisual';
+ const XPATH_EXPRESSION_ITEM_URI = './filename';
+ const XPATH_EXPRESSION_ITEM_AUTHOR = './/producersname';
+ const XPATH_EXPRESSION_ITEM_TIMESTAMP = './recfinisheddatetime';
+ const XPATH_EXPRESSION_ITEM_ENCLOSURES = './filename';
- public function getURI() {
- return self::URI;
- }
+ public function getURI()
+ {
+ return self::URI;
+ }
}
diff --git a/bridges/KernelBugTrackerBridge.php b/bridges/KernelBugTrackerBridge.php
index 2677d717..02d31cff 100644
--- a/bridges/KernelBugTrackerBridge.php
+++ b/bridges/KernelBugTrackerBridge.php
@@ -1,147 +1,158 @@
<?php
-class KernelBugTrackerBridge extends BridgeAbstract {
- const NAME = 'Kernel Bug Tracker';
- const URI = 'https://bugzilla.kernel.org';
- const DESCRIPTION = 'DEPRECATED: Use BugzillaBridge instead.
+class KernelBugTrackerBridge extends BridgeAbstract
+{
+ const NAME = 'Kernel Bug Tracker';
+ const URI = 'https://bugzilla.kernel.org';
+ const DESCRIPTION = 'DEPRECATED: Use BugzillaBridge instead.
Returns feeds for bug comments';
- const MAINTAINER = 'logmanoriginal';
- const PARAMETERS = array(
- 'Bug comments' => array(
- 'id' => array(
- 'name' => 'Bug tracking ID',
- 'type' => 'number',
- 'required' => true,
- 'title' => 'Insert bug tracking ID',
- 'exampleValue' => 121241
- ),
- 'limit' => array(
- 'name' => 'Number of comments to return',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Specify number of comments to return',
- 'defaultValue' => -1
- ),
- 'sorting' => array(
- 'name' => 'Sorting',
- 'type' => 'list',
- 'required' => false,
- 'title' => 'Defines the sorting order of the comments returned',
- 'defaultValue' => 'of',
- 'values' => array(
- 'Oldest first' => 'of',
- 'Latest first' => 'lf'
- )
- )
- )
- );
-
- private $bugid = '';
- private $bugdesc = '';
-
- public function getIcon() {
- return self::URI . '/images/favicon.ico';
- }
-
- public function collectData(){
- $limit = $this->getInput('limit');
- $sorting = $this->getInput('sorting');
-
- // We use the print preview page for simplicity
- $html = getSimpleHTMLDOMCached($this->getURI() . '&format=multiple',
- 86400,
- null,
- null,
- true,
- true,
- DEFAULT_TARGET_CHARSET,
- false, // Do NOT remove line breaks
- DEFAULT_BR_TEXT,
- DEFAULT_SPAN_TEXT);
-
- if($html === false)
- returnServerError('Failed to load page!');
-
- $html = defaultLinkTo($html, self::URI);
-
- // Store header information into private members
- $this->bugid = $html->find('#bugzilla-body', 0)->find('a', 0)->innertext;
- $this->bugdesc = $html->find('table.bugfields', 0)->find('tr', 0)->find('td', 0)->innertext;
-
- // Get and limit comments
- $comments = $html->find('div.bz_comment');
-
- if($limit > 0 && count($comments) > $limit) {
- $comments = array_slice($comments, count($comments) - $limit, $limit);
- }
-
- // Order comments
- switch($sorting) {
- case 'lf': $comments = array_reverse($comments, true);
- // fall-through
- case 'of':
- // fall-through
- default: // Nothing to do, keep original order
- }
-
- foreach($comments as $comment) {
- $comment = $this->inlineStyles($comment);
-
- $item = array();
- $item['uri'] = $this->getURI() . '#' . $comment->id;
- $item['author'] = $comment->find('span.bz_comment_user', 0)->innertext;
- $item['title'] = $comment->find('span.bz_comment_number', 0)->find('a', 0)->innertext;
- $item['timestamp'] = strtotime($comment->find('span.bz_comment_time', 0)->innertext);
- $item['content'] = $comment->find('pre.bz_comment_text', 0)->innertext;
-
- // Fix line breaks (they use LF)
- $item['content'] = str_replace("\n", '<br>', $item['content']);
-
- // Fix relative URIs
- $item['content'] = $item['content'];
-
- $this->items[] = $item;
- }
-
- }
-
- public function getURI(){
- switch($this->queriedContext) {
- case 'Bug comments':
- return parent::getURI()
- . '/show_bug.cgi?id='
- . $this->getInput('id');
- break;
- default: return parent::getURI();
- }
- }
-
- public function getName(){
- switch($this->queriedContext) {
- case 'Bug comments':
- return 'Bug '
- . $this->bugid
- . ' tracker for '
- . $this->bugdesc
- . ' - '
- . parent::getName();
- break;
- default: return parent::getName();
- }
- }
-
- /**
- * Adds styles as attributes to tags with known classes
- *
- * @param object $html A simplehtmldom object
- * @return object Returns the original object with styles added as
- * attributes.
- */
- private function inlineStyles($html){
- foreach($html->find('.bz_obsolete') as $element) {
- $element->style = 'text-decoration:line-through;';
- }
-
- return $html;
- }
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = [
+ '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
+ ],
+ 'sorting' => [
+ 'name' => 'Sorting',
+ 'type' => 'list',
+ 'required' => false,
+ 'title' => 'Defines the sorting order of the comments returned',
+ 'defaultValue' => 'of',
+ 'values' => [
+ 'Oldest first' => 'of',
+ 'Latest first' => 'lf'
+ ]
+ ]
+ ]
+ ];
+
+ private $bugid = '';
+ private $bugdesc = '';
+
+ public function getIcon()
+ {
+ return self::URI . '/images/favicon.ico';
+ }
+
+ public function collectData()
+ {
+ $limit = $this->getInput('limit');
+ $sorting = $this->getInput('sorting');
+
+ // We use the print preview page for simplicity
+ $html = getSimpleHTMLDOMCached(
+ $this->getURI() . '&format=multiple',
+ 86400,
+ null,
+ null,
+ true,
+ true,
+ DEFAULT_TARGET_CHARSET,
+ false, // Do NOT remove line breaks
+ DEFAULT_BR_TEXT,
+ DEFAULT_SPAN_TEXT
+ );
+
+ if ($html === false) {
+ returnServerError('Failed to load page!');
+ }
+
+ $html = defaultLinkTo($html, self::URI);
+
+ // Store header information into private members
+ $this->bugid = $html->find('#bugzilla-body', 0)->find('a', 0)->innertext;
+ $this->bugdesc = $html->find('table.bugfields', 0)->find('tr', 0)->find('td', 0)->innertext;
+
+ // Get and limit comments
+ $comments = $html->find('div.bz_comment');
+
+ if ($limit > 0 && count($comments) > $limit) {
+ $comments = array_slice($comments, count($comments) - $limit, $limit);
+ }
+
+ // Order comments
+ switch ($sorting) {
+ case 'lf':
+ $comments = array_reverse($comments, true);
+ // fall-through
+ case 'of':
+ // fall-through
+ default: // Nothing to do, keep original order
+ }
+
+ foreach ($comments as $comment) {
+ $comment = $this->inlineStyles($comment);
+
+ $item = [];
+ $item['uri'] = $this->getURI() . '#' . $comment->id;
+ $item['author'] = $comment->find('span.bz_comment_user', 0)->innertext;
+ $item['title'] = $comment->find('span.bz_comment_number', 0)->find('a', 0)->innertext;
+ $item['timestamp'] = strtotime($comment->find('span.bz_comment_time', 0)->innertext);
+ $item['content'] = $comment->find('pre.bz_comment_text', 0)->innertext;
+
+ // Fix line breaks (they use LF)
+ $item['content'] = str_replace("\n", '<br>', $item['content']);
+
+ // Fix relative URIs
+ $item['content'] = $item['content'];
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Bug comments':
+ return parent::getURI()
+ . '/show_bug.cgi?id='
+ . $this->getInput('id');
+ break;
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Bug comments':
+ return 'Bug '
+ . $this->bugid
+ . ' tracker for '
+ . $this->bugdesc
+ . ' - '
+ . parent::getName();
+ break;
+ default:
+ return parent::getName();
+ }
+ }
+
+ /**
+ * Adds styles as attributes to tags with known classes
+ *
+ * @param object $html A simplehtmldom object
+ * @return object Returns the original object with styles added as
+ * attributes.
+ */
+ private function inlineStyles($html)
+ {
+ foreach ($html->find('.bz_obsolete') as $element) {
+ $element->style = 'text-decoration:line-through;';
+ }
+
+ return $html;
+ }
}
diff --git a/bridges/KhinsiderBridge.php b/bridges/KhinsiderBridge.php
index 73c297cd..8493eadc 100644
--- a/bridges/KhinsiderBridge.php
+++ b/bridges/KhinsiderBridge.php
@@ -2,40 +2,40 @@
class KhinsiderBridge extends BridgeAbstract
{
- const MAINTAINER = 'Chouchenos';
- const NAME = 'Khinsider';
- const URI = 'https://downloads.khinsider.com/';
- const CACHE_TIMEOUT = 14400; // 4 h
- const DESCRIPTION = 'Fetch daily game OST from Khinsider';
+ const MAINTAINER = 'Chouchenos';
+ const NAME = 'Khinsider';
+ const URI = 'https://downloads.khinsider.com/';
+ const CACHE_TIMEOUT = 14400; // 4 h
+ const DESCRIPTION = 'Fetch daily game OST from Khinsider';
- public function collectData()
- {
- $html = getSimpleHTMLDOM(self::URI);
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
- $dates = $html->find('.latestSoundtrackHeading');
- $tables = $html->find('.albumList');
- // $dates is empty
- foreach ($dates as $i => $date) {
- $item = array();
- $item['uri'] = self::URI;
- $item['timestamp'] = DateTime::createFromFormat('F jS, Y', $date->plaintext)->setTime(1, 1)->format('U');
- $item['title'] = sprintf('OST for %s', $date->plaintext);
- $item['author'] = 'Khinsider';
- $trs = $tables[$i]->find('tr');
- $content = '<ul>';
- foreach ($trs as $tr) {
- $td = $tr->find('td', 1);
- if (null !== $td) {
- $link = $td->find('a', 0);
- $content .= sprintf('<li><a href="%s">%s</a></li>', $link->href, $link->plaintext);
- }
- }
- $content .= '</ul>';
- $item['content'] = $content;
- $item['uid'] = $item['timestamp'];
- $item['categories'] = array('Video games', 'Music', 'OST', 'download');
+ $dates = $html->find('.latestSoundtrackHeading');
+ $tables = $html->find('.albumList');
+ // $dates is empty
+ foreach ($dates as $i => $date) {
+ $item = [];
+ $item['uri'] = self::URI;
+ $item['timestamp'] = DateTime::createFromFormat('F jS, Y', $date->plaintext)->setTime(1, 1)->format('U');
+ $item['title'] = sprintf('OST for %s', $date->plaintext);
+ $item['author'] = 'Khinsider';
+ $trs = $tables[$i]->find('tr');
+ $content = '<ul>';
+ foreach ($trs as $tr) {
+ $td = $tr->find('td', 1);
+ if (null !== $td) {
+ $link = $td->find('a', 0);
+ $content .= sprintf('<li><a href="%s">%s</a></li>', $link->href, $link->plaintext);
+ }
+ }
+ $content .= '</ul>';
+ $item['content'] = $content;
+ $item['uid'] = $item['timestamp'];
+ $item['categories'] = ['Video games', 'Music', 'OST', 'download'];
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/KilledbyGoogleBridge.php b/bridges/KilledbyGoogleBridge.php
index dc9d33f7..54c5b59f 100644
--- a/bridges/KilledbyGoogleBridge.php
+++ b/bridges/KilledbyGoogleBridge.php
@@ -1,78 +1,81 @@
<?php
-class KilledbyGoogleBridge extends BridgeAbstract {
- const NAME = 'Killed by Google Bridge';
- const URI = 'https://killedbygoogle.com';
- const DESCRIPTION = 'Returns list of recently discontinued Google services, products, devices, and apps.';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array();
- const CACHE_TIMEOUT = 3600;
-
- public function collectData() {
-
- $json = getContents(self::URI . '/graveyard.json')
- or returnServerError('Could not request: ' . self::URI . '/graveyard.json');
-
- $this->handleJson($json);
- $this->orderItems();
- $this->limitItems();
- }
-
- /**
- * Handle JSON
- */
- private function handleJson($json) {
-
- $graveyard = json_decode($json, true);
-
- foreach($graveyard as $tombstone) {
- $item = array();
-
- $openDate = new DateTime($tombstone['dateOpen']);
- $closeDate = new DateTime($tombstone['dateClose']);
- $currentDate = new DateTime();
-
- $yearOpened = $openDate->format('Y');
- $yearClosed = $closeDate->format('Y');
-
- if ($closeDate > $currentDate) {
- continue;
- }
-
- $item['title'] = $tombstone['name'] . ' (' . $yearOpened . ' - ' . $yearClosed . ')';
- $item['uid'] = $tombstone['slug'];
- $item['uri'] = $tombstone['link'];
- $item['timestamp'] = strtotime($tombstone['dateClose']);
-
- $item['content'] = <<<EOD
+class KilledbyGoogleBridge extends BridgeAbstract
+{
+ const NAME = 'Killed by Google Bridge';
+ const URI = 'https://killedbygoogle.com';
+ const DESCRIPTION = 'Returns list of recently discontinued Google services, products, devices, and apps.';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [];
+
+ const CACHE_TIMEOUT = 3600;
+
+ public function collectData()
+ {
+ $json = getContents(self::URI . '/graveyard.json')
+ or returnServerError('Could not request: ' . self::URI . '/graveyard.json');
+
+ $this->handleJson($json);
+ $this->orderItems();
+ $this->limitItems();
+ }
+
+ /**
+ * Handle JSON
+ */
+ private function handleJson($json)
+ {
+ $graveyard = json_decode($json, true);
+
+ foreach ($graveyard as $tombstone) {
+ $item = [];
+
+ $openDate = new DateTime($tombstone['dateOpen']);
+ $closeDate = new DateTime($tombstone['dateClose']);
+ $currentDate = new DateTime();
+
+ $yearOpened = $openDate->format('Y');
+ $yearClosed = $closeDate->format('Y');
+
+ if ($closeDate > $currentDate) {
+ continue;
+ }
+
+ $item['title'] = $tombstone['name'] . ' (' . $yearOpened . ' - ' . $yearClosed . ')';
+ $item['uid'] = $tombstone['slug'];
+ $item['uri'] = $tombstone['link'];
+ $item['timestamp'] = strtotime($tombstone['dateClose']);
+
+ $item['content'] = <<<EOD
<p>{$tombstone['description']}</p><p><a href="{$tombstone['link']}">{$tombstone['link']}</a></p>
EOD;
- $item['enclosures'][] = 'https://static.killedbygoogle.com/com/tombstone.svg';
-
- $this->items[] = $item;
- }
- }
-
- /**
- * Order items by timestamp
- */
- private function orderItems() {
-
- $sort = array();
-
- foreach ($this->items as $key => $item) {
- $sort[$key] = $item['timestamp'];
- }
-
- array_multisort($sort, SORT_DESC, $this->items);
- $this->items = array_slice($this->items, 0, 15);
- }
-
- /**
- * Limit items to 15
- */
- private function limitItems() {
- $this->items = array_slice($this->items, 0, 15);
- }
+ $item['enclosures'][] = 'https://static.killedbygoogle.com/com/tombstone.svg';
+
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Order items by timestamp
+ */
+ private function orderItems()
+ {
+ $sort = [];
+
+ foreach ($this->items as $key => $item) {
+ $sort[$key] = $item['timestamp'];
+ }
+
+ array_multisort($sort, SORT_DESC, $this->items);
+ $this->items = array_slice($this->items, 0, 15);
+ }
+
+ /**
+ * Limit items to 15
+ */
+ private function limitItems()
+ {
+ $this->items = array_slice($this->items, 0, 15);
+ }
}
diff --git a/bridges/KonachanBridge.php b/bridges/KonachanBridge.php
index db6c0767..afddf9ca 100644
--- a/bridges/KonachanBridge.php
+++ b/bridges/KonachanBridge.php
@@ -1,10 +1,9 @@
<?php
-class KonachanBridge extends MoebooruBridge {
-
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Konachan';
- const URI = 'https://konachan.com/';
- const DESCRIPTION = 'Returns images from given page';
-
+class KonachanBridge extends MoebooruBridge
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Konachan';
+ const URI = 'https://konachan.com/';
+ const DESCRIPTION = 'Returns images from given page';
}
diff --git a/bridges/KoreusBridge.php b/bridges/KoreusBridge.php
index 4cfb8c21..874c2c92 100644
--- a/bridges/KoreusBridge.php
+++ b/bridges/KoreusBridge.php
@@ -1,22 +1,25 @@
<?php
-class KoreusBridge extends FeedExpander {
- const MAINTAINER = 'pit-fgfjiudghdf';
- const NAME = 'Koreus';
- const URI = 'https://www.koreus.com/';
- const DESCRIPTION = 'Returns the newest posts from Koreus (full text)';
+class KoreusBridge extends FeedExpander
+{
+ const MAINTAINER = 'pit-fgfjiudghdf';
+ const NAME = 'Koreus';
+ const URI = 'https://www.koreus.com/';
+ const DESCRIPTION = 'Returns the newest posts from Koreus (full text)';
- protected function parseItem($item){
- $item = parent::parseItem($item);
+ protected function parseItem($item)
+ {
+ $item = parent::parseItem($item);
- $html = getSimpleHTMLDOMCached($item['uri']);
- $text = $html->find('p.itemText', 0)->innertext;
- $item['content'] = utf8_encode($text);
+ $html = getSimpleHTMLDOMCached($item['uri']);
+ $text = $html->find('p.itemText', 0)->innertext;
+ $item['content'] = utf8_encode($text);
- return $item;
- }
+ return $item;
+ }
- public function collectData(){
- $this->collectExpandableDatas('https://feeds.feedburner.com/Koreus-articles');
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas('https://feeds.feedburner.com/Koreus-articles');
+ }
}
diff --git a/bridges/KununuBridge.php b/bridges/KununuBridge.php
index 4352ab26..e1b228dc 100644
--- a/bridges/KununuBridge.php
+++ b/bridges/KununuBridge.php
@@ -1,147 +1,156 @@
<?php
-class KununuBridge extends BridgeAbstract {
- const MAINTAINER = 'logmanoriginal';
- const NAME = 'Kununu Bridge';
- const URI = 'https://www.kununu.com/';
- const CACHE_TIMEOUT = 86400; // 24h
- const DESCRIPTION = 'Returns the latest reviews for a company and site of your choice.';
-
- const PARAMETERS = array(
- 'global' => array(
- 'site' => array(
- 'name' => 'Site',
- 'type' => 'list',
- 'title' => 'Select your site',
- 'values' => array(
- 'Austria' => 'at',
- 'Germany' => 'de',
- 'Switzerland' => 'ch'
- ),
- 'exampleValue' => 'de',
- ),
- 'include_ratings' => array(
- 'name' => 'Include ratings',
- 'type' => 'checkbox',
- 'title' => 'Activate to include ratings in the feed'
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'defaultValue' => 3,
- 'title' => "Maximum number of items to return in the feed.\n0 = unlimited"
- )
- ),
- array(
- 'company' => array(
- 'name' => 'Company',
- 'required' => true,
- 'exampleValue' => 'kununu',
- 'title' => 'Insert company name (i.e. Kununu) or URI path (i.e. kununu)'
- )
- )
- );
-
- private $companyName = '';
-
- public function getURI() {
- if(!is_null($this->getInput('company')) && !is_null($this->getInput('site'))) {
-
- $company = $this->fixCompanyName($this->getInput('company'));
- $site = $this->getInput('site');
-
- return sprintf('%s%s/%s', self::URI, $site, $company);
- }
-
- return parent::getURI();
- }
-
- public function getName() {
- if(!is_null($this->getInput('company'))) {
- $company = $this->fixCompanyName($this->getInput('company'));
- return ($this->companyName ?: $company) . ' - ' . self::NAME;
- }
-
- return parent::getName();
- }
-
- public function getIcon() {
- return 'https://www.kununu.com/favicon-196x196.png';
- }
-
- public function collectData(){
- $full = $this->getInput('full');
-
- // Load page
- $json = json_decode(getContents($this->getAPI()), true);
- $this->companyName = $json['common']['name'];
- $baseURI = $this->getURI() . '/bewertung/';
-
- $limit = $this->getInput('limit') ?: 0;
-
- // Go through all articles
- foreach($json['reviews'] as $review) {
- $item = array();
- $item['author'] = $review['position'] . ' / ' . $review['department'];
- $item['timestamp'] = $review['createdAt'];
- $item['title'] = $review['roundedScore'] . ' : ' . $review['title'];
- $item['uri'] = $baseURI . $review['uuid'];
- $item['content'] = $this->extractArticleDescription($review);
- $this->items[] = $item;
-
- if ($limit > 0 && count($this->items) >= $limit) break;
-
- }
- }
-
- /**
- * Returns JSON API url
- */
- private function getAPI() {
- $company = $this->fixCompanyName($this->getInput('company'));
- $site = $this->getInput('site');
-
- return self::URI . 'middlewares/profiles/' .
- $site . '/' . $company .
- '/reviews?reviewType=employees&urlParams=sort=newest&sort=newest&page=1';
- }
-
- /*
- * Returns a fixed version of the provided company name
- */
- private function fixCompanyName($company){
- $company = trim($company);
- $company = str_replace(' ', '-', $company);
- $company = strtolower($company);
-
- $umlauts = Array('/ä/','/ö/','/ü/','/Ä/','/Ö/','/Ü/','/ß/');
- $replace = Array('ae','oe','ue','Ae','Oe','Ue','ss');
-
- return preg_replace($umlauts, $replace, $company);
- }
-
- /**
- * Returns the description from a given article
- */
- private function extractArticleDescription($json){
- $retVal = '';
- foreach($json['texts'] as $text) {
- $retVal .= '<h4>' . $text['id'] . '</h4><p>' . $text['text'] . '</p>';
- }
-
- if($this->getInput('include_ratings') && !empty($json['ratings'])) {
- $retVal .= (empty($retVal) ? '' : '<hr>') . '<table>';
- foreach($json['ratings'] as $rating) {
- $retVal .= <<<EOD
+
+class KununuBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Kununu Bridge';
+ const URI = 'https://www.kununu.com/';
+ const CACHE_TIMEOUT = 86400; // 24h
+ const DESCRIPTION = 'Returns the latest reviews for a company and site of your choice.';
+
+ const PARAMETERS = [
+ 'global' => [
+ 'site' => [
+ 'name' => 'Site',
+ 'type' => 'list',
+ 'title' => 'Select your site',
+ 'values' => [
+ 'Austria' => 'at',
+ 'Germany' => 'de',
+ 'Switzerland' => 'ch'
+ ],
+ 'exampleValue' => 'de',
+ ],
+ 'include_ratings' => [
+ 'name' => 'Include ratings',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to include ratings in the feed'
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'defaultValue' => 3,
+ 'title' => "Maximum number of items to return in the feed.\n0 = unlimited"
+ ]
+ ],
+ [
+ 'company' => [
+ 'name' => 'Company',
+ 'required' => true,
+ 'exampleValue' => 'kununu',
+ 'title' => 'Insert company name (i.e. Kununu) or URI path (i.e. kununu)'
+ ]
+ ]
+ ];
+
+ private $companyName = '';
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('company')) && !is_null($this->getInput('site'))) {
+ $company = $this->fixCompanyName($this->getInput('company'));
+ $site = $this->getInput('site');
+
+ return sprintf('%s%s/%s', self::URI, $site, $company);
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('company'))) {
+ $company = $this->fixCompanyName($this->getInput('company'));
+ return ($this->companyName ?: $company) . ' - ' . self::NAME;
+ }
+
+ return parent::getName();
+ }
+
+ public function getIcon()
+ {
+ return 'https://www.kununu.com/favicon-196x196.png';
+ }
+
+ public function collectData()
+ {
+ $full = $this->getInput('full');
+
+ // Load page
+ $json = json_decode(getContents($this->getAPI()), true);
+ $this->companyName = $json['common']['name'];
+ $baseURI = $this->getURI() . '/bewertung/';
+
+ $limit = $this->getInput('limit') ?: 0;
+
+ // Go through all articles
+ foreach ($json['reviews'] as $review) {
+ $item = [];
+ $item['author'] = $review['position'] . ' / ' . $review['department'];
+ $item['timestamp'] = $review['createdAt'];
+ $item['title'] = $review['roundedScore'] . ' : ' . $review['title'];
+ $item['uri'] = $baseURI . $review['uuid'];
+ $item['content'] = $this->extractArticleDescription($review);
+ $this->items[] = $item;
+
+ if ($limit > 0 && count($this->items) >= $limit) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Returns JSON API url
+ */
+ private function getAPI()
+ {
+ $company = $this->fixCompanyName($this->getInput('company'));
+ $site = $this->getInput('site');
+
+ return self::URI . 'middlewares/profiles/' .
+ $site . '/' . $company .
+ '/reviews?reviewType=employees&urlParams=sort=newest&sort=newest&page=1';
+ }
+
+ /*
+ * Returns a fixed version of the provided company name
+ */
+ private function fixCompanyName($company)
+ {
+ $company = trim($company);
+ $company = str_replace(' ', '-', $company);
+ $company = strtolower($company);
+
+ $umlauts = ['/ä/','/ö/','/ü/','/Ä/','/Ö/','/Ü/','/ß/'];
+ $replace = ['ae','oe','ue','Ae','Oe','Ue','ss'];
+
+ return preg_replace($umlauts, $replace, $company);
+ }
+
+ /**
+ * Returns the description from a given article
+ */
+ private function extractArticleDescription($json)
+ {
+ $retVal = '';
+ foreach ($json['texts'] as $text) {
+ $retVal .= '<h4>' . $text['id'] . '</h4><p>' . $text['text'] . '</p>';
+ }
+
+ if ($this->getInput('include_ratings') && !empty($json['ratings'])) {
+ $retVal .= (empty($retVal) ? '' : '<hr>') . '<table>';
+ foreach ($json['ratings'] as $rating) {
+ $retVal .= <<<EOD
<tr>
<td>{$rating['id']}
<td>{$rating['roundedScore']}
<td>{$rating['text']}
</tr>
EOD;
- }
- $retVal .= '</table>';
- }
+ }
+ $retVal .= '</table>';
+ }
- return $retVal;
- }
+ return $retVal;
+ }
}
diff --git a/bridges/LWNprevBridge.php b/bridges/LWNprevBridge.php
index 40b1b129..358f841a 100644
--- a/bridges/LWNprevBridge.php
+++ b/bridges/LWNprevBridge.php
@@ -1,266 +1,278 @@
<?php
-class LWNprevBridge extends BridgeAbstract{
- const MAINTAINER = 'Pierre Mazière';
- const NAME = 'LWN Free Weekly Edition';
- const URI = 'https://lwn.net/';
- const CACHE_TIMEOUT = 604800; // 1 week
- const DESCRIPTION = 'LWN Free Weekly Edition available one week late';
-
- private $editionTimeStamp;
-
- public function getURI(){
- return self::URI . 'free/bigpage';
- }
-
- private function jumpToNextTag(&$node){
- while($node && $node->nodeType === XML_TEXT_NODE) {
- $nextNode = $node->nextSibling;
- if(!$nextNode) {
- break;
- }
- $node = $nextNode;
- }
- }
-
- private function jumpToPreviousTag(&$node){
- while($node && $node->nodeType === XML_TEXT_NODE) {
- $previousNode = $node->previousSibling;
- if(!$previousNode) {
- break;
- }
- $node = $previousNode;
- }
- }
-
- public function collectData(){
- // Because the LWN page is written in loose HTML and not XHTML,
- // Simple HTML Dom is not accurate enough for the job
- $content = getContents($this->getURI());
-
- $contents = explode('<b>Page editor</b>', $content);
-
- foreach($contents as $content) {
- if(strpos($content, '<html>') === false) {
- $content = <<<EOD
+
+class LWNprevBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Pierre Mazière';
+ const NAME = 'LWN Free Weekly Edition';
+ const URI = 'https://lwn.net/';
+ const CACHE_TIMEOUT = 604800; // 1 week
+ const DESCRIPTION = 'LWN Free Weekly Edition available one week late';
+
+ private $editionTimeStamp;
+
+ public function getURI()
+ {
+ return self::URI . 'free/bigpage';
+ }
+
+ private function jumpToNextTag(&$node)
+ {
+ while ($node && $node->nodeType === XML_TEXT_NODE) {
+ $nextNode = $node->nextSibling;
+ if (!$nextNode) {
+ break;
+ }
+ $node = $nextNode;
+ }
+ }
+
+ private function jumpToPreviousTag(&$node)
+ {
+ while ($node && $node->nodeType === XML_TEXT_NODE) {
+ $previousNode = $node->previousSibling;
+ if (!$previousNode) {
+ break;
+ }
+ $node = $previousNode;
+ }
+ }
+
+ public function collectData()
+ {
+ // Because the LWN page is written in loose HTML and not XHTML,
+ // Simple HTML Dom is not accurate enough for the job
+ $content = getContents($this->getURI());
+
+ $contents = explode('<b>Page editor</b>', $content);
+
+ foreach ($contents as $content) {
+ if (strpos($content, '<html>') === false) {
+ $content = <<<EOD
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html><head><title>LWN</title></head><body>{$content}</body></html>
EOD;
- } else {
- $content = $content . '</body></html>';
- }
-
- libxml_use_internal_errors(true);
- $html = new DOMDocument();
- $html->loadHTML($content);
- libxml_clear_errors();
-
- $edition = $html->getElementsByTagName('h1');
- if($edition->length !== 0) {
- $text = $edition->item(0)->textContent;
- $this->editionTimeStamp = strtotime(
- substr($text, strpos($text, 'for ') + strlen('for '))
- );
- }
-
- if(strpos($content, 'Cat1HL') === false) {
- $items = $this->getFeatureContents($html);
- } elseif(strpos($content, 'Cat3HL') === false) {
- $items = $this->getBriefItems($html);
- } else {
- $items = $this->getAnnouncements($html);
- }
-
- $this->items = array_merge($this->items, $items);
- }
- }
-
- private function getArticleContent(&$title){
- $link = $title->firstChild;
- $this->jumpToNextTag($link);
- $item['uri'] = self::URI;
- if($link->nodeName === 'a') {
- $item['uri'] .= $link->getAttribute('href');
- }
-
- $item['timestamp'] = $this->editionTimeStamp;
-
- $node = $title;
- $content = '';
- $contentEnd = false;
- while(!$contentEnd) {
- $node = $node->nextSibling;
- if(!$node || (
- $node->nodeType !== XML_TEXT_NODE &&
- $node->nodeName === 'h2' || (
- !is_null($node->attributes) &&
- !is_null($class = $node->attributes->getNamedItem('class')) &&
- in_array($class->nodeValue, array('Cat1HL','Cat2HL'))
- )
- )
- ) {
- $contentEnd = true;
- } else {
- $content .= $node->C14N();
- }
- }
- $item['content'] = $content;
- return $item;
- }
-
- private function getFeatureContents(&$html){
- $items = array();
- foreach($html->getElementsByTagName('h3') as $title) {
- if($title->getAttribute('class') !== 'SummaryHL') {
- continue;
- }
-
- $item = array();
-
- $author = $title->nextSibling;
- $this->jumpToNextTag($author);
- if($author->getAttribute('class') === 'FeatureByline') {
- $item['author'] = $author->getElementsByTagName('b')->item(0)->textContent;
- } else {
- continue;
- }
-
- $item['title'] = $title->textContent;
-
- $items[] = array_merge($item, $this->getArticleContent($title));
- }
- return $items;
- }
-
- private function getItemPrefix(&$cat, &$cats){
- $cat1 = '';
- $cat2 = '';
- $cat3 = '';
- switch($cat->getAttribute('class')) {
- case 'Cat3HL':
- $cat3 = $cat->textContent;
- $cat = $cat->previousSibling;
- $this->jumpToPreviousTag($cat);
- $cats[2] = $cat3;
- if($cat->getAttribute('class') !== 'Cat2HL') {
- break;
- }
- // fall-through? Looks like a bug
- case 'Cat2HL':
- $cat2 = $cat->textContent;
- $cat = $cat->previousSibling;
- $this->jumpToPreviousTag($cat);
- $cats[1] = $cat2;
- if(empty($cat3)) {
- $cats[2] = '';
- }
- if($cat->getAttribute('class') !== 'Cat1HL') {
- break;
- }
- // fall-through? Looks like a bug
- case 'Cat1HL':
- $cat1 = $cat->textContent;
- $cats[0] = $cat1;
- if(empty($cat3)) {
- $cats[2] = '';
- }
- if(empty($cat2)) {
- $cats[1] = '';
- }
- break;
- default:
- break;
- }
-
- $prefix = '';
- if(!empty($cats[0])) {
- $prefix .= '[' . $cats[0] . ($cats[1] ? '/' . $cats[1] : '') . '] ';
- }
- return $prefix;
- }
-
- private function getAnnouncements(&$html){
- $items = array();
- $cats = array('','','');
-
- foreach($html->getElementsByTagName('p') as $newsletters) {
- if($newsletters->getAttribute('class') !== 'Cat3HL') {
- continue;
- }
-
- $item = array();
-
- $item['uri'] = self::URI . '#' . count($items);
-
- $item['timestamp'] = $this->editionTimeStamp;
-
- $item['author'] = 'LWN';
-
- $cat = $newsletters->previousSibling;
- $this->jumpToPreviousTag($cat);
- $prefix = $this->getItemPrefix($cat, $cats);
- $item['title'] = $prefix . ' ' . $newsletters->textContent;
-
- $node = $newsletters;
- $content = '';
- $contentEnd = false;
- while(!$contentEnd) {
- $node = $node->nextSibling;
- if(!$node || (
- $node->nodeType !== XML_TEXT_NODE && (
- !is_null($node->attributes) &&
- !is_null($class = $node->attributes->getNamedItem('class')) &&
- in_array($class->nodeValue, array('Cat1HL','Cat2HL','Cat3HL'))
- )
- )
- ) {
- $contentEnd = true;
- } else {
- $content .= $node->C14N();
- }
- }
- $item['content'] = $content;
- $items[] = $item;
- }
-
- foreach($html->getElementsByTagName('h2') as $title) {
- if($title->getAttribute('class') !== 'SummaryHL') {
- continue;
- }
-
- $item = array();
-
- $cat = $title->previousSibling;
- $this->jumpToPreviousTag($cat);
- $cat = $cat->previousSibling;
- $this->jumpToPreviousTag($cat);
- $prefix = $this->getItemPrefix($cat, $cats);
- $item['title'] = $prefix . ' ' . $title->textContent;
- $items[] = array_merge($item, $this->getArticleContent($title));
- }
-
- return $items;
- }
-
- private function getBriefItems(&$html){
- $items = array();
- $cats = array('','','');
- foreach($html->getElementsByTagName('h2') as $title) {
- if($title->getAttribute('class') !== 'SummaryHL') {
- continue;
- }
-
- $item = array();
-
- $cat = $title->previousSibling;
- $this->jumpToPreviousTag($cat);
- $cat = $cat->previousSibling;
- $this->jumpToPreviousTag($cat);
- $prefix = $this->getItemPrefix($cat, $cats);
- $item['title'] = $prefix . ' ' . $title->textContent;
- $items[] = array_merge($item, $this->getArticleContent($title));
- }
-
- return $items;
- }
+ } else {
+ $content = $content . '</body></html>';
+ }
+
+ libxml_use_internal_errors(true);
+ $html = new DOMDocument();
+ $html->loadHTML($content);
+ libxml_clear_errors();
+
+ $edition = $html->getElementsByTagName('h1');
+ if ($edition->length !== 0) {
+ $text = $edition->item(0)->textContent;
+ $this->editionTimeStamp = strtotime(
+ substr($text, strpos($text, 'for ') + strlen('for '))
+ );
+ }
+
+ if (strpos($content, 'Cat1HL') === false) {
+ $items = $this->getFeatureContents($html);
+ } elseif (strpos($content, 'Cat3HL') === false) {
+ $items = $this->getBriefItems($html);
+ } else {
+ $items = $this->getAnnouncements($html);
+ }
+
+ $this->items = array_merge($this->items, $items);
+ }
+ }
+
+ private function getArticleContent(&$title)
+ {
+ $link = $title->firstChild;
+ $this->jumpToNextTag($link);
+ $item['uri'] = self::URI;
+ if ($link->nodeName === 'a') {
+ $item['uri'] .= $link->getAttribute('href');
+ }
+
+ $item['timestamp'] = $this->editionTimeStamp;
+
+ $node = $title;
+ $content = '';
+ $contentEnd = false;
+ while (!$contentEnd) {
+ $node = $node->nextSibling;
+ if (
+ !$node || (
+ $node->nodeType !== XML_TEXT_NODE &&
+ $node->nodeName === 'h2' || (
+ !is_null($node->attributes) &&
+ !is_null($class = $node->attributes->getNamedItem('class')) &&
+ in_array($class->nodeValue, ['Cat1HL','Cat2HL'])
+ )
+ )
+ ) {
+ $contentEnd = true;
+ } else {
+ $content .= $node->C14N();
+ }
+ }
+ $item['content'] = $content;
+ return $item;
+ }
+
+ private function getFeatureContents(&$html)
+ {
+ $items = [];
+ foreach ($html->getElementsByTagName('h3') as $title) {
+ if ($title->getAttribute('class') !== 'SummaryHL') {
+ continue;
+ }
+
+ $item = [];
+
+ $author = $title->nextSibling;
+ $this->jumpToNextTag($author);
+ if ($author->getAttribute('class') === 'FeatureByline') {
+ $item['author'] = $author->getElementsByTagName('b')->item(0)->textContent;
+ } else {
+ continue;
+ }
+
+ $item['title'] = $title->textContent;
+
+ $items[] = array_merge($item, $this->getArticleContent($title));
+ }
+ return $items;
+ }
+
+ private function getItemPrefix(&$cat, &$cats)
+ {
+ $cat1 = '';
+ $cat2 = '';
+ $cat3 = '';
+ switch ($cat->getAttribute('class')) {
+ case 'Cat3HL':
+ $cat3 = $cat->textContent;
+ $cat = $cat->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $cats[2] = $cat3;
+ if ($cat->getAttribute('class') !== 'Cat2HL') {
+ break;
+ }
+ // fall-through? Looks like a bug
+ case 'Cat2HL':
+ $cat2 = $cat->textContent;
+ $cat = $cat->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $cats[1] = $cat2;
+ if (empty($cat3)) {
+ $cats[2] = '';
+ }
+ if ($cat->getAttribute('class') !== 'Cat1HL') {
+ break;
+ }
+ // fall-through? Looks like a bug
+ case 'Cat1HL':
+ $cat1 = $cat->textContent;
+ $cats[0] = $cat1;
+ if (empty($cat3)) {
+ $cats[2] = '';
+ }
+ if (empty($cat2)) {
+ $cats[1] = '';
+ }
+ break;
+ default:
+ break;
+ }
+
+ $prefix = '';
+ if (!empty($cats[0])) {
+ $prefix .= '[' . $cats[0] . ($cats[1] ? '/' . $cats[1] : '') . '] ';
+ }
+ return $prefix;
+ }
+
+ private function getAnnouncements(&$html)
+ {
+ $items = [];
+ $cats = ['','',''];
+
+ foreach ($html->getElementsByTagName('p') as $newsletters) {
+ if ($newsletters->getAttribute('class') !== 'Cat3HL') {
+ continue;
+ }
+
+ $item = [];
+
+ $item['uri'] = self::URI . '#' . count($items);
+
+ $item['timestamp'] = $this->editionTimeStamp;
+
+ $item['author'] = 'LWN';
+
+ $cat = $newsletters->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $prefix = $this->getItemPrefix($cat, $cats);
+ $item['title'] = $prefix . ' ' . $newsletters->textContent;
+
+ $node = $newsletters;
+ $content = '';
+ $contentEnd = false;
+ while (!$contentEnd) {
+ $node = $node->nextSibling;
+ if (
+ !$node || (
+ $node->nodeType !== XML_TEXT_NODE && (
+ !is_null($node->attributes) &&
+ !is_null($class = $node->attributes->getNamedItem('class')) &&
+ in_array($class->nodeValue, ['Cat1HL','Cat2HL','Cat3HL'])
+ )
+ )
+ ) {
+ $contentEnd = true;
+ } else {
+ $content .= $node->C14N();
+ }
+ }
+ $item['content'] = $content;
+ $items[] = $item;
+ }
+
+ foreach ($html->getElementsByTagName('h2') as $title) {
+ if ($title->getAttribute('class') !== 'SummaryHL') {
+ continue;
+ }
+
+ $item = [];
+
+ $cat = $title->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $cat = $cat->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $prefix = $this->getItemPrefix($cat, $cats);
+ $item['title'] = $prefix . ' ' . $title->textContent;
+ $items[] = array_merge($item, $this->getArticleContent($title));
+ }
+
+ return $items;
+ }
+
+ private function getBriefItems(&$html)
+ {
+ $items = [];
+ $cats = ['','',''];
+ foreach ($html->getElementsByTagName('h2') as $title) {
+ if ($title->getAttribute('class') !== 'SummaryHL') {
+ continue;
+ }
+
+ $item = [];
+
+ $cat = $title->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $cat = $cat->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $prefix = $this->getItemPrefix($cat, $cats);
+ $item['title'] = $prefix . ' ' . $title->textContent;
+ $items[] = array_merge($item, $this->getArticleContent($title));
+ }
+
+ return $items;
+ }
}
-?>
diff --git a/bridges/LaCentraleBridge.php b/bridges/LaCentraleBridge.php
index dc6c1a97..cf898a06 100644
--- a/bridges/LaCentraleBridge.php
+++ b/bridges/LaCentraleBridge.php
@@ -1,473 +1,474 @@
<?php
-class LaCentraleBridge extends BridgeAbstract {
+class LaCentraleBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'jacknumber';
+ const NAME = 'La Centrale';
+ const URI = 'https://www.lacentrale.fr/';
+ const DESCRIPTION = 'Returns most recent vehicules ads from LaCentrale';
- const MAINTAINER = 'jacknumber';
- const NAME = 'La Centrale';
- const URI = 'https://www.lacentrale.fr/';
- const DESCRIPTION = 'Returns most recent vehicules ads from LaCentrale';
+ const PARAMETERS = [ [
+ 'type' => [
+ 'name' => 'Type de véhicule',
+ 'type' => 'list',
+ 'values' => [
+ 'Voiture' => 'car',
+ 'Camion/Pickup' => 'truck',
+ 'Moto' => 'moto',
+ 'Scooter' => 'scooter',
+ 'Quad' => 'quad',
+ 'Caravane/Camping-car' => 'mobileHome'
+ ]
+ ],
+ 'brand' => [
+ 'name' => 'Marque',
+ 'type' => 'list',
+ 'values' => [
+ '' => '',
+ 'ABARTH' => 'ABARTH',
+ 'AC' => 'AC',
+ 'AIXAM' => 'AIXAM',
+ 'ALFA ROMEO' => 'ALFA ROMEO',
+ 'ALKE' => 'ALKE',
+ 'ALPINA' => 'ALPINA',
+ 'ALPINE' => 'ALPINE',
+ 'AMC' => 'AMC',
+ 'ANAIG' => 'ANAIG',
+ 'APRILIA' => 'APRILIA',
+ 'ARIEL' => 'ARIEL',
+ 'ASTON MARTIN' => 'ASTON MARTIN',
+ 'AUDI' => 'AUDI',
+ 'AUSTIN HEALEY' => 'AUSTIN HEALEY',
+ 'AUSTIN' => 'AUSTIN',
+ 'AUTOBIANCHI' => 'AUTOBIANCHI',
+ 'AVINTON' => 'AVINTON',
+ 'BELLIER' => 'BELLIER',
+ 'BENELLI' => 'BENELLI',
+ 'BENTLEY' => 'BENTLEY',
+ 'BETA' => 'BETA',
+ 'BMW' => 'BMW',
+ 'BOLLORE' => 'BOLLORE',
+ 'BRIXTON' => 'BRIXTON',
+ 'BUELL' => 'BUELL',
+ 'BUGATTI' => 'BUGATTI',
+ 'BUICK' => 'BUICK',
+ 'BULLIT' => 'BULLIT',
+ 'CADILLAC' => 'CADILLAC',
+ 'CASALINI' => 'CASALINI',
+ 'CATERHAM' => 'CATERHAM',
+ 'CHATENET' => 'CHATENET',
+ 'CHEVROLET' => 'CHEVROLET',
+ 'CHRYSLER' => 'CHRYSLER',
+ 'CHUNLAN' => 'CHUNLAN',
+ 'CITROEN' => 'CITROEN',
+ 'COURB' => 'COURB',
+ 'CR&S' => 'CR&S',
+ 'CUPRA' => 'CUPRA',
+ 'CYCLONE' => 'CYCLONE',
+ 'DACIA' => 'DACIA',
+ 'DAELIM' => 'DAELIM',
+ 'DAEWOO' => 'DAEWOO',
+ 'DAF' => 'DAF',
+ 'DAIHATSU' => 'DAIHATSU',
+ 'DANGEL' => 'DANGEL',
+ 'DATSUN' => 'DATSUN',
+ 'DE SOTO' => 'DE SOTO',
+ 'DE TOMASO' => 'DE TOMASO',
+ 'DERBI' => 'DERBI',
+ 'DEVINCI' => 'DEVINCI',
+ 'DODGE' => 'DODGE',
+ 'DONKERVOORT' => 'DONKERVOORT',
+ 'DS' => 'DS',
+ 'DUCATI' => 'DUCATI',
+ 'DUCATY' => 'DUCATY',
+ 'DUE' => 'DUE',
+ 'ENFIELD' => 'ENFIELD',
+ 'EXCALIBUR' => 'EXCALIBUR',
+ 'FACEL VEGA' => 'FACEL VEGA',
+ 'FANTIC MOTOR' => 'FANTIC MOTOR',
+ 'FERRARI' => 'FERRARI',
+ 'FIAT' => 'FIAT',
+ 'FISKER' => 'FISKER',
+ 'FORD' => 'FORD',
+ 'FUSO' => 'FUSO',
+ 'GAS GAS' => 'GAS GAS',
+ 'GILERA' => 'GILERA',
+ 'GMC' => 'GMC',
+ 'GOWINN' => 'GOWINN',
+ 'GRANDIN' => 'GRANDIN',
+ 'HARLEY DAVIDSON' => 'HARLEY DAVIDSON',
+ 'HOMMELL' => 'HOMMELL',
+ 'HONDA' => 'HONDA',
+ 'HUMMER' => 'HUMMER',
+ 'HUSABERG' => 'HUSABERG',
+ 'HUSQVARNA' => 'HUSQVARNA',
+ 'HYOSUNG' => 'HYOSUNG',
+ 'HYUNDAI' => 'HYUNDAI',
+ 'INDIAN' => 'INDIAN',
+ 'INFINITI' => 'INFINITI',
+ 'INNOCENTI' => 'INNOCENTI',
+ 'ISUZU' => 'ISUZU',
+ 'IVECO' => 'IVECO',
+ 'JAGUAR' => 'JAGUAR',
+ 'JDM SIMPA' => 'JDM SIMPA',
+ 'JEEP' => 'JEEP',
+ 'JENSEN' => 'JENSEN',
+ 'JIAYUAN' => 'JIAYUAN',
+ 'KAWASAKI' => 'KAWASAKI',
+ 'KEEWAY' => 'KEEWAY',
+ 'KIA' => 'KIA',
+ 'KSR' => 'KSR',
+ 'KTM' => 'KTM',
+ 'KYMCO' => 'KYMCO',
+ 'LADA' => 'LADA',
+ 'LAMBORGHINI' => 'LAMBORGHINI',
+ 'LANCIA' => 'LANCIA',
+ 'LAND ROVER' => 'LAND ROVER',
+ 'LEXUS' => 'LEXUS',
+ 'LIGIER' => 'LIGIER',
+ 'LINCOLN' => 'LINCOLN',
+ 'LONDON TAXI COMPANY' => 'LONDON TAXI COMPANY',
+ 'LOTUS' => 'LOTUS',
+ 'MAGPOWER' => 'MAGPOWER',
+ 'MAN' => 'MAN',
+ 'MASAI' => 'MASAI',
+ 'MASERATI' => 'MASERATI',
+ 'MASH' => 'MASH',
+ 'MATRA' => 'MATRA',
+ 'MAYBACH' => 'MAYBACH',
+ 'MAZDA' => 'MAZDA',
+ 'MCLAREN' => 'MCLAREN',
+ 'MEGA' => 'MEGA',
+ 'MERCEDES' => 'MERCEDES',
+ 'MERCEDES-AMG' => 'MERCEDES-AMG',
+ 'MERCURY' => 'MERCURY',
+ 'MEYERS MANX' => 'MEYERS MANX',
+ 'MG' => 'MG',
+ 'MIA ELECTRIC' => 'MIA ELECTRIC',
+ 'MICROCAR' => 'MICROCAR',
+ 'MINAUTO' => 'MINAUTO',
+ 'MINI' => 'MINI',
+ 'MITSUBISHI' => 'MITSUBISHI',
+ 'MORGAN' => 'MORGAN',
+ 'MORRIS' => 'MORRIS',
+ 'MOTO GUZZI' => 'MOTO GUZZI',
+ 'MOTO MORINI' => 'MOTO MORINI',
+ 'MOTOBECANE' => 'MOTOBECANE',
+ 'MPM MOTORS' => 'MPM MOTORS',
+ 'MV AGUSTA' => 'MV AGUSTA',
+ 'NISSAN' => 'NISSAN',
+ 'NORTON' => 'NORTON',
+ 'NSU' => 'NSU',
+ 'OLDSMOBILE' => 'OLDSMOBILE',
+ 'OPEL' => 'OPEL',
+ 'ORCAL' => 'ORCAL',
+ 'OSSA' => 'OSSA',
+ 'PACKARD' => 'PACKARD',
+ 'PANTHER' => 'PANTHER',
+ 'PEUGEOT' => 'PEUGEOT',
+ 'PGO' => 'PGO',
+ 'PIAGGIO' => 'PIAGGIO',
+ 'PLYMOUTH' => 'PLYMOUTH',
+ 'POLARIS' => 'POLARIS',
+ 'PONTIAC' => 'PONTIAC',
+ 'PORSCHE' => 'PORSCHE',
+ 'REALM' => 'REALM',
+ 'REGAL RAPTOR' => 'REGAL RAPTOR',
+ 'RENAULT' => 'RENAULT',
+ 'RIEJU' => 'RIEJU',
+ 'ROLLS ROYCE' => 'ROLLS ROYCE',
+ 'ROVER' => 'ROVER',
+ 'ROYAL ENFIELD' => 'ROYAL ENFIELD',
+ 'SAAB' => 'SAAB',
+ 'SANTANA' => 'SANTANA',
+ 'SCANIA' => 'SCANIA',
+ 'SEAT' => 'SEAT',
+ 'SECMA' => 'SECMA',
+ 'SHELBY' => 'SHELBY',
+ 'SHERCO' => 'SHERCO',
+ 'SIMCA' => 'SIMCA',
+ 'SKODA' => 'SKODA',
+ 'SMART' => 'SMART',
+ 'SPYKER' => 'SPYKER',
+ 'SSANGYONG' => 'SSANGYONG',
+ 'STUDEBAKER' => 'STUDEBAKER',
+ 'SUBARU' => 'SUBARU',
+ 'SUNBEAM' => 'SUNBEAM',
+ 'SUZUKI' => 'SUZUKI',
+ 'SWM' => 'SWM',
+ 'SYM' => 'SYM',
+ 'TALBOT SIMCA' => 'TALBOT SIMCA',
+ 'TALBOT' => 'TALBOT',
+ 'TEILHOL' => 'TEILHOL',
+ 'TESLA' => 'TESLA',
+ 'TM' => 'TM',
+ 'TNT MOTOR' => 'TNT MOTOR',
+ 'TOYOTA' => 'TOYOTA',
+ 'TRIUMPH' => 'TRIUMPH',
+ 'TVR' => 'TVR',
+ 'VAUXHALL' => 'VAUXHALL',
+ 'VESPA' => 'VESPA',
+ 'VICTORY' => 'VICTORY',
+ 'VOLKSWAGEN' => 'VOLKSWAGEN',
+ 'VOLVO' => 'VOLVO',
+ 'VOXAN' => 'VOXAN',
+ 'WIESMANN' => 'WIESMANN',
+ 'YAMAHA' => 'YAMAHA',
+ 'YCF' => 'YCF',
+ 'ZERO' => 'ZERO',
+ 'ZONGSHEN' => 'ZONGSHEN'
+ ]
+ ],
+ 'model' => [
+ 'name' => 'Modèle',
+ 'type' => 'text',
+ 'title' => 'Get the exact name on LaCentrale'
+ ],
+ 'versions' => [
+ 'name' => 'Version(s)',
+ 'type' => 'text',
+ 'title' => 'Get the exact name(s) on LaCentrale. Separate by comma'
+ ],
+ 'category' => [
+ 'name' => 'Catégorie',
+ 'type' => 'list',
+ 'values' => [
+ '' => '',
+ 'Voiture' => [
+ '4x4, SUV & Crossover' => '47',
+ 'Citadine' => '40',
+ 'Berline' => '41_42',
+ 'Break' => '43',
+ 'Cabriolet' => '46',
+ 'Coupé' => '45',
+ 'Monospace' => '44',
+ 'Bus et minibus' => '82',
+ 'Fourgonnette' => '85',
+ 'Fourgon (< 3,5 tonnes)' => '81',
+ 'Pick-up' => '50',
+ 'Voiture société, commerciale' => '80',
+ 'Sans permis' => '48',
+ 'Camion (> 3,5 tonnes)' => '83',
+ ],
+ 'Camion/Pickup' => [
+ 'Camion (> 3,5 tonnes)' => '83',
+ 'Fourgon (< 3,5 tonnes)' => '81',
+ 'Bus et minibus' => '82',
+ 'Fourgonnette' => '85',
+ 'Pick-up' => '50',
+ 'Voiture société, commerciale' => '80'
+ ],
+ 'Moto' => [
+ 'Custom' => '60',
+ 'Offroad' => '61',
+ 'Roadster' => '62',
+ 'GT' => '63',
+ 'Mini moto' => '64',
+ 'Mobylette' => '65',
+ 'Supermotard' => '66',
+ 'Trail' => '67',
+ 'Side-car' => '69',
+ 'Sportive' => '68'
+ ],
+ 'Caravane/Camping-car' => [
+ 'Caravane' => '423',
+ 'Profilé' => '506',
+ 'Fourgon aménagé' => '507',
+ 'Intégral' => '508',
+ 'Capucine' => '510'
+ ]
+ ]
+ ],
+ 'pricemin' => [
+ 'name' => 'Prix min',
+ 'type' => 'number'
+ ],
+ 'pricemax' => [
+ 'name' => 'Prix max',
+ 'type' => 'number'
+ ],
+ 'location' => [
+ 'name' => 'CP ou département',
+ 'type' => 'number',
+ 'title' => 'Only one'
+ ],
+ 'distance' => [
+ 'name' => 'Rayon de recherche',
+ 'type' => 'list',
+ 'values' => [
+ '' => '',
+ '10 km' => '1',
+ '20 km' => '2',
+ '50 km' => '3',
+ '100 km' => '4',
+ '200 km' => '5'
+ ]
+ ],
+ 'region' => [
+ 'name' => 'Région',
+ 'type' => 'list',
+ 'values' => [
+ '' => '',
+ 'Auvergne-Rhône-Alpes' => 'FR-ARA',
+ 'Bourgogne-Franche-Comté' => 'FR-BFC',
+ 'Bretagne' => 'FR-BRE',
+ 'Centre-Val de Loire' => 'FR-CVL',
+ 'Corse' => 'FR-COR',
+ 'Grand Est' => 'FR-GES',
+ 'Hauts-de-France' => 'FR-HDF',
+ 'Île-de-France' => 'FR-IDF',
+ 'Normandie' => 'FR-NOR',
+ 'Nouvelle-Aquitaine' => 'FR-PAC',
+ 'Occitanie' => 'FR-PDL',
+ 'Pays de la Loire' => 'FR-OCC',
+ 'Provence-Alpes-Côte d\'Azur' => 'FR-NAQ'
+ ]
+ ],
+ 'mileagemin' => [
+ 'name' => 'Kilométrage min',
+ 'type' => 'number'
+ ],
+ 'mileagemax' => [
+ 'name' => 'Kilométrage max',
+ 'type' => 'number'
+ ],
+ 'yearmin' => [
+ 'name' => 'Année min',
+ 'type' => 'number'
+ ],
+ 'yearmax' => [
+ 'name' => 'Année max',
+ 'type' => 'number'
+ ],
+ 'cubiccapacitymin' => [
+ 'name' => 'Cylindrée min',
+ 'type' => 'number'
+ ],
+ 'cubiccapacitymax' => [
+ 'name' => 'Cylindrée max',
+ 'type' => 'number'
+ ],
+ 'fuel' => [
+ 'name' => 'Énergie',
+ 'type' => 'list',
+ 'values' => [
+ '' => '',
+ 'Diesel' => 'dies',
+ 'Essence' => 'ess',
+ 'Électrique' => 'elec',
+ 'Hybride' => 'hyb',
+ 'GPL' => 'gpl',
+ 'Bioéthanol' => 'eth',
+ 'Autre' => 'alt'
+ ]
+ ],
+ 'gearbox' => [
+ 'name' => 'Boite de vitesse',
+ 'type' => 'list',
+ 'values' => [
+ '' => '',
+ 'Boite automatique' => 'AUTO',
+ 'Boite mécanique' => 'MANUAL'
+ ]
+ ],
+ 'doors' => [
+ 'name' => 'Nombre de portes',
+ 'type' => 'list',
+ 'values' => [
+ '' => '',
+ '2 portes' => '2',
+ '3 portes' => '3',
+ '4 portes' => '4',
+ '5 portes' => '5',
+ '6 portes ou plus' => '6'
+ ]
+ ],
+ 'firsthand' => [
+ 'name' => 'Première main',
+ 'type' => 'checkbox'
+ ],
+ 'seller' => [
+ 'name' => 'Vendeur',
+ 'type' => 'list',
+ 'values' => [
+ '' => '',
+ 'Particulier' => 'PART',
+ 'Professionel' => 'PRO'
+ ]
+ ],
+ 'sort' => [
+ 'name' => 'Tri',
+ 'type' => 'list',
+ 'values' => [
+ 'Prix (croissant)' => 'priceAsc',
+ 'Prix (décroissant)' => 'priceDesc',
+ 'Marque (croissant)' => 'makeAsc',
+ 'Marque (décroissant)' => 'makeDesc',
+ 'Kilométrage (croissant)' => 'mileageAsc',
+ 'Kilométrage (décroissant)' => 'mileageDesc',
+ 'Année (croissant)' => 'yearAsc',
+ 'Année (décroissant)' => 'yearDesc',
+ 'Département (croissant)' => 'visitPlaceAsc',
+ 'Département (décroissant)' => 'visitPlaceDesc'
+ ]
+ ],
+ ]];
- const PARAMETERS = array( array(
- 'type' => array(
- 'name' => 'Type de véhicule',
- 'type' => 'list',
- 'values' => array(
- 'Voiture' => 'car',
- 'Camion/Pickup' => 'truck',
- 'Moto' => 'moto',
- 'Scooter' => 'scooter',
- 'Quad' => 'quad',
- 'Caravane/Camping-car' => 'mobileHome'
- )
- ),
- 'brand' => array(
- 'name' => 'Marque',
- 'type' => 'list',
- 'values' => array(
- '' => '',
- 'ABARTH' => 'ABARTH',
- 'AC' => 'AC',
- 'AIXAM' => 'AIXAM',
- 'ALFA ROMEO' => 'ALFA ROMEO',
- 'ALKE' => 'ALKE',
- 'ALPINA' => 'ALPINA',
- 'ALPINE' => 'ALPINE',
- 'AMC' => 'AMC',
- 'ANAIG' => 'ANAIG',
- 'APRILIA' => 'APRILIA',
- 'ARIEL' => 'ARIEL',
- 'ASTON MARTIN' => 'ASTON MARTIN',
- 'AUDI' => 'AUDI',
- 'AUSTIN HEALEY' => 'AUSTIN HEALEY',
- 'AUSTIN' => 'AUSTIN',
- 'AUTOBIANCHI' => 'AUTOBIANCHI',
- 'AVINTON' => 'AVINTON',
- 'BELLIER' => 'BELLIER',
- 'BENELLI' => 'BENELLI',
- 'BENTLEY' => 'BENTLEY',
- 'BETA' => 'BETA',
- 'BMW' => 'BMW',
- 'BOLLORE' => 'BOLLORE',
- 'BRIXTON' => 'BRIXTON',
- 'BUELL' => 'BUELL',
- 'BUGATTI' => 'BUGATTI',
- 'BUICK' => 'BUICK',
- 'BULLIT' => 'BULLIT',
- 'CADILLAC' => 'CADILLAC',
- 'CASALINI' => 'CASALINI',
- 'CATERHAM' => 'CATERHAM',
- 'CHATENET' => 'CHATENET',
- 'CHEVROLET' => 'CHEVROLET',
- 'CHRYSLER' => 'CHRYSLER',
- 'CHUNLAN' => 'CHUNLAN',
- 'CITROEN' => 'CITROEN',
- 'COURB' => 'COURB',
- 'CR&S' => 'CR&S',
- 'CUPRA' => 'CUPRA',
- 'CYCLONE' => 'CYCLONE',
- 'DACIA' => 'DACIA',
- 'DAELIM' => 'DAELIM',
- 'DAEWOO' => 'DAEWOO',
- 'DAF' => 'DAF',
- 'DAIHATSU' => 'DAIHATSU',
- 'DANGEL' => 'DANGEL',
- 'DATSUN' => 'DATSUN',
- 'DE SOTO' => 'DE SOTO',
- 'DE TOMASO' => 'DE TOMASO',
- 'DERBI' => 'DERBI',
- 'DEVINCI' => 'DEVINCI',
- 'DODGE' => 'DODGE',
- 'DONKERVOORT' => 'DONKERVOORT',
- 'DS' => 'DS',
- 'DUCATI' => 'DUCATI',
- 'DUCATY' => 'DUCATY',
- 'DUE' => 'DUE',
- 'ENFIELD' => 'ENFIELD',
- 'EXCALIBUR' => 'EXCALIBUR',
- 'FACEL VEGA' => 'FACEL VEGA',
- 'FANTIC MOTOR' => 'FANTIC MOTOR',
- 'FERRARI' => 'FERRARI',
- 'FIAT' => 'FIAT',
- 'FISKER' => 'FISKER',
- 'FORD' => 'FORD',
- 'FUSO' => 'FUSO',
- 'GAS GAS' => 'GAS GAS',
- 'GILERA' => 'GILERA',
- 'GMC' => 'GMC',
- 'GOWINN' => 'GOWINN',
- 'GRANDIN' => 'GRANDIN',
- 'HARLEY DAVIDSON' => 'HARLEY DAVIDSON',
- 'HOMMELL' => 'HOMMELL',
- 'HONDA' => 'HONDA',
- 'HUMMER' => 'HUMMER',
- 'HUSABERG' => 'HUSABERG',
- 'HUSQVARNA' => 'HUSQVARNA',
- 'HYOSUNG' => 'HYOSUNG',
- 'HYUNDAI' => 'HYUNDAI',
- 'INDIAN' => 'INDIAN',
- 'INFINITI' => 'INFINITI',
- 'INNOCENTI' => 'INNOCENTI',
- 'ISUZU' => 'ISUZU',
- 'IVECO' => 'IVECO',
- 'JAGUAR' => 'JAGUAR',
- 'JDM SIMPA' => 'JDM SIMPA',
- 'JEEP' => 'JEEP',
- 'JENSEN' => 'JENSEN',
- 'JIAYUAN' => 'JIAYUAN',
- 'KAWASAKI' => 'KAWASAKI',
- 'KEEWAY' => 'KEEWAY',
- 'KIA' => 'KIA',
- 'KSR' => 'KSR',
- 'KTM' => 'KTM',
- 'KYMCO' => 'KYMCO',
- 'LADA' => 'LADA',
- 'LAMBORGHINI' => 'LAMBORGHINI',
- 'LANCIA' => 'LANCIA',
- 'LAND ROVER' => 'LAND ROVER',
- 'LEXUS' => 'LEXUS',
- 'LIGIER' => 'LIGIER',
- 'LINCOLN' => 'LINCOLN',
- 'LONDON TAXI COMPANY' => 'LONDON TAXI COMPANY',
- 'LOTUS' => 'LOTUS',
- 'MAGPOWER' => 'MAGPOWER',
- 'MAN' => 'MAN',
- 'MASAI' => 'MASAI',
- 'MASERATI' => 'MASERATI',
- 'MASH' => 'MASH',
- 'MATRA' => 'MATRA',
- 'MAYBACH' => 'MAYBACH',
- 'MAZDA' => 'MAZDA',
- 'MCLAREN' => 'MCLAREN',
- 'MEGA' => 'MEGA',
- 'MERCEDES' => 'MERCEDES',
- 'MERCEDES-AMG' => 'MERCEDES-AMG',
- 'MERCURY' => 'MERCURY',
- 'MEYERS MANX' => 'MEYERS MANX',
- 'MG' => 'MG',
- 'MIA ELECTRIC' => 'MIA ELECTRIC',
- 'MICROCAR' => 'MICROCAR',
- 'MINAUTO' => 'MINAUTO',
- 'MINI' => 'MINI',
- 'MITSUBISHI' => 'MITSUBISHI',
- 'MORGAN' => 'MORGAN',
- 'MORRIS' => 'MORRIS',
- 'MOTO GUZZI' => 'MOTO GUZZI',
- 'MOTO MORINI' => 'MOTO MORINI',
- 'MOTOBECANE' => 'MOTOBECANE',
- 'MPM MOTORS' => 'MPM MOTORS',
- 'MV AGUSTA' => 'MV AGUSTA',
- 'NISSAN' => 'NISSAN',
- 'NORTON' => 'NORTON',
- 'NSU' => 'NSU',
- 'OLDSMOBILE' => 'OLDSMOBILE',
- 'OPEL' => 'OPEL',
- 'ORCAL' => 'ORCAL',
- 'OSSA' => 'OSSA',
- 'PACKARD' => 'PACKARD',
- 'PANTHER' => 'PANTHER',
- 'PEUGEOT' => 'PEUGEOT',
- 'PGO' => 'PGO',
- 'PIAGGIO' => 'PIAGGIO',
- 'PLYMOUTH' => 'PLYMOUTH',
- 'POLARIS' => 'POLARIS',
- 'PONTIAC' => 'PONTIAC',
- 'PORSCHE' => 'PORSCHE',
- 'REALM' => 'REALM',
- 'REGAL RAPTOR' => 'REGAL RAPTOR',
- 'RENAULT' => 'RENAULT',
- 'RIEJU' => 'RIEJU',
- 'ROLLS ROYCE' => 'ROLLS ROYCE',
- 'ROVER' => 'ROVER',
- 'ROYAL ENFIELD' => 'ROYAL ENFIELD',
- 'SAAB' => 'SAAB',
- 'SANTANA' => 'SANTANA',
- 'SCANIA' => 'SCANIA',
- 'SEAT' => 'SEAT',
- 'SECMA' => 'SECMA',
- 'SHELBY' => 'SHELBY',
- 'SHERCO' => 'SHERCO',
- 'SIMCA' => 'SIMCA',
- 'SKODA' => 'SKODA',
- 'SMART' => 'SMART',
- 'SPYKER' => 'SPYKER',
- 'SSANGYONG' => 'SSANGYONG',
- 'STUDEBAKER' => 'STUDEBAKER',
- 'SUBARU' => 'SUBARU',
- 'SUNBEAM' => 'SUNBEAM',
- 'SUZUKI' => 'SUZUKI',
- 'SWM' => 'SWM',
- 'SYM' => 'SYM',
- 'TALBOT SIMCA' => 'TALBOT SIMCA',
- 'TALBOT' => 'TALBOT',
- 'TEILHOL' => 'TEILHOL',
- 'TESLA' => 'TESLA',
- 'TM' => 'TM',
- 'TNT MOTOR' => 'TNT MOTOR',
- 'TOYOTA' => 'TOYOTA',
- 'TRIUMPH' => 'TRIUMPH',
- 'TVR' => 'TVR',
- 'VAUXHALL' => 'VAUXHALL',
- 'VESPA' => 'VESPA',
- 'VICTORY' => 'VICTORY',
- 'VOLKSWAGEN' => 'VOLKSWAGEN',
- 'VOLVO' => 'VOLVO',
- 'VOXAN' => 'VOXAN',
- 'WIESMANN' => 'WIESMANN',
- 'YAMAHA' => 'YAMAHA',
- 'YCF' => 'YCF',
- 'ZERO' => 'ZERO',
- 'ZONGSHEN' => 'ZONGSHEN'
- )
- ),
- 'model' => array(
- 'name' => 'Modèle',
- 'type' => 'text',
- 'title' => 'Get the exact name on LaCentrale'
- ),
- 'versions' => array(
- 'name' => 'Version(s)',
- 'type' => 'text',
- 'title' => 'Get the exact name(s) on LaCentrale. Separate by comma'
- ),
- 'category' => array(
- 'name' => 'Catégorie',
- 'type' => 'list',
- 'values' => array(
- '' => '',
- 'Voiture' => array(
- '4x4, SUV & Crossover' => '47',
- 'Citadine' => '40',
- 'Berline' => '41_42',
- 'Break' => '43',
- 'Cabriolet' => '46',
- 'Coupé' => '45',
- 'Monospace' => '44',
- 'Bus et minibus' => '82',
- 'Fourgonnette' => '85',
- 'Fourgon (< 3,5 tonnes)' => '81',
- 'Pick-up' => '50',
- 'Voiture société, commerciale' => '80',
- 'Sans permis' => '48',
- 'Camion (> 3,5 tonnes)' => '83',
- ),
- 'Camion/Pickup' => array(
- 'Camion (> 3,5 tonnes)' => '83',
- 'Fourgon (< 3,5 tonnes)' => '81',
- 'Bus et minibus' => '82',
- 'Fourgonnette' => '85',
- 'Pick-up' => '50',
- 'Voiture société, commerciale' => '80'
- ),
- 'Moto' => array(
- 'Custom' => '60',
- 'Offroad' => '61',
- 'Roadster' => '62',
- 'GT' => '63',
- 'Mini moto' => '64',
- 'Mobylette' => '65',
- 'Supermotard' => '66',
- 'Trail' => '67',
- 'Side-car' => '69',
- 'Sportive' => '68'
- ),
- 'Caravane/Camping-car' => array(
- 'Caravane' => '423',
- 'Profilé' => '506',
- 'Fourgon aménagé' => '507',
- 'Intégral' => '508',
- 'Capucine' => '510'
- )
- )
- ),
- 'pricemin' => array(
- 'name' => 'Prix min',
- 'type' => 'number'
- ),
- 'pricemax' => array(
- 'name' => 'Prix max',
- 'type' => 'number'
- ),
- 'location' => array(
- 'name' => 'CP ou département',
- 'type' => 'number',
- 'title' => 'Only one'
- ),
- 'distance' => array(
- 'name' => 'Rayon de recherche',
- 'type' => 'list',
- 'values' => array(
- '' => '',
- '10 km' => '1',
- '20 km' => '2',
- '50 km' => '3',
- '100 km' => '4',
- '200 km' => '5'
- )
- ),
- 'region' => array(
- 'name' => 'Région',
- 'type' => 'list',
- 'values' => array(
- '' => '',
- 'Auvergne-Rhône-Alpes' => 'FR-ARA',
- 'Bourgogne-Franche-Comté' => 'FR-BFC',
- 'Bretagne' => 'FR-BRE',
- 'Centre-Val de Loire' => 'FR-CVL',
- 'Corse' => 'FR-COR',
- 'Grand Est' => 'FR-GES',
- 'Hauts-de-France' => 'FR-HDF',
- 'Île-de-France' => 'FR-IDF',
- 'Normandie' => 'FR-NOR',
- 'Nouvelle-Aquitaine' => 'FR-PAC',
- 'Occitanie' => 'FR-PDL',
- 'Pays de la Loire' => 'FR-OCC',
- 'Provence-Alpes-Côte d\'Azur' => 'FR-NAQ'
- )
- ),
- 'mileagemin' => array(
- 'name' => 'Kilométrage min',
- 'type' => 'number'
- ),
- 'mileagemax' => array(
- 'name' => 'Kilométrage max',
- 'type' => 'number'
- ),
- 'yearmin' => array(
- 'name' => 'Année min',
- 'type' => 'number'
- ),
- 'yearmax' => array(
- 'name' => 'Année max',
- 'type' => 'number'
- ),
- 'cubiccapacitymin' => array(
- 'name' => 'Cylindrée min',
- 'type' => 'number'
- ),
- 'cubiccapacitymax' => array(
- 'name' => 'Cylindrée max',
- 'type' => 'number'
- ),
- 'fuel' => array(
- 'name' => 'Énergie',
- 'type' => 'list',
- 'values' => array(
- '' => '',
- 'Diesel' => 'dies',
- 'Essence' => 'ess',
- 'Électrique' => 'elec',
- 'Hybride' => 'hyb',
- 'GPL' => 'gpl',
- 'Bioéthanol' => 'eth',
- 'Autre' => 'alt'
- )
- ),
- 'gearbox' => array(
- 'name' => 'Boite de vitesse',
- 'type' => 'list',
- 'values' => array(
- '' => '',
- 'Boite automatique' => 'AUTO',
- 'Boite mécanique' => 'MANUAL'
- )
- ),
- 'doors' => array(
- 'name' => 'Nombre de portes',
- 'type' => 'list',
- 'values' => array(
- '' => '',
- '2 portes' => '2',
- '3 portes' => '3',
- '4 portes' => '4',
- '5 portes' => '5',
- '6 portes ou plus' => '6'
- )
- ),
- 'firsthand' => array(
- 'name' => 'Première main',
- 'type' => 'checkbox'
- ),
- 'seller' => array(
- 'name' => 'Vendeur',
- 'type' => 'list',
- 'values' => array(
- '' => '',
- 'Particulier' => 'PART',
- 'Professionel' => 'PRO'
- )
- ),
- 'sort' => array(
- 'name' => 'Tri',
- 'type' => 'list',
- 'values' => array(
- 'Prix (croissant)' => 'priceAsc',
- 'Prix (décroissant)' => 'priceDesc',
- 'Marque (croissant)' => 'makeAsc',
- 'Marque (décroissant)' => 'makeDesc',
- 'Kilométrage (croissant)' => 'mileageAsc',
- 'Kilométrage (décroissant)' => 'mileageDesc',
- 'Année (croissant)' => 'yearAsc',
- 'Année (décroissant)' => 'yearDesc',
- 'Département (croissant)' => 'visitPlaceAsc',
- 'Département (décroissant)' => 'visitPlaceDesc'
- )
- ),
- ));
+ public function collectData()
+ {
+ if (
+ !empty($this->getInput('distance'))
+ && is_null($this->getInput('location'))
+ ) {
+ returnClientError('You need a place ("CP ou département") to search arround.');
+ }
- public function collectData(){
- if(!empty($this->getInput('distance'))
- && is_null($this->getInput('location'))
- ) {
- returnClientError('You need a place ("CP ou département") to search arround.');
- }
+ $params = [
+ 'vertical' => $this->getInput('type'),
+ 'makesModelsCommercialNames' => $this->getInput('brand') . ':' . $this->getInput('model'),
+ 'versions' => $this->getInput('versions'),
+ 'categories' => $this->getInput('category'),
+ 'priceMin' => $this->getInput('pricemin'),
+ 'priceMax' => $this->getInput('pricemax'),
+ 'dptCp' => $this->getInput('location'),
+ 'distance' => $this->getInput('distance'),
+ 'regions' => $this->getInput('region'),
+ 'mileageMin' => $this->getInput('mileagemin'),
+ 'mileageMax' => $this->getInput('mileagemax'),
+ 'yearMin' => $this->getInput('yearmin'),
+ 'yearMax' => $this->getInput('yearmax'),
+ 'cubicMin' => $this->getInput('cubiccapacitymin'),
+ 'cubicMax' => $this->getInput('cubiccapacitymax'),
+ 'energies' => $this->getInput('fuel'),
+ 'firstHand' => $this->getInput('firsthand') ? 'true' : 'false',
+ 'gearbox' => $this->getInput('gearbox'),
+ 'doors' => $this->getInput('doors'),
+ 'sortBy' => $this->getInput('sort')
+ ];
+ $url = sprintf('%slisting?%s', self::URI, http_build_query($params));
+ $html = getSimpleHTMLDOM($url);
- $params = array(
- 'vertical' => $this->getInput('type'),
- 'makesModelsCommercialNames' => $this->getInput('brand') . ':' . $this->getInput('model'),
- 'versions' => $this->getInput('versions'),
- 'categories' => $this->getInput('category'),
- 'priceMin' => $this->getInput('pricemin'),
- 'priceMax' => $this->getInput('pricemax'),
- 'dptCp' => $this->getInput('location'),
- 'distance' => $this->getInput('distance'),
- 'regions' => $this->getInput('region'),
- 'mileageMin' => $this->getInput('mileagemin'),
- 'mileageMax' => $this->getInput('mileagemax'),
- 'yearMin' => $this->getInput('yearmin'),
- 'yearMax' => $this->getInput('yearmax'),
- 'cubicMin' => $this->getInput('cubiccapacitymin'),
- 'cubicMax' => $this->getInput('cubiccapacitymax'),
- 'energies' => $this->getInput('fuel'),
- 'firstHand' => $this->getInput('firsthand') ? 'true' : 'false',
- 'gearbox' => $this->getInput('gearbox'),
- 'doors' => $this->getInput('doors'),
- 'sortBy' => $this->getInput('sort')
- );
- $url = sprintf('%slisting?%s', self::URI, http_build_query($params));
- $html = getSimpleHTMLDOM($url);
+ $elements = $html->find('.adLineContainer');
+ foreach ($elements as $element) {
+ $item = [];
+ $item['uri'] = trim(self::URI, '/') . $element->find('div > a', 0)->href;
+ $item['title'] = $element->find('.searchCard__makeModel', 0)->plaintext;
+ $item['sellerType'] = $element->find('.searchCard__customer', 0)->plaintext;
+ $item['author'] = $item['sellerType'];
+ $item['version'] = $element->find('.searchCard__version', 0)->plaintext;
+ $item['price'] = $element->find('.searchCard__fieldPrice', 0)->plaintext;
+ $item['year'] = $element->find('.searchCard__year', 0)->plaintext;
+ $item['mileage'] = $element->find('.searchCard__mileage', 0)->plaintext;
+ // The image is lazyloaded with ajax
- $elements = $html->find('.adLineContainer');
- foreach($elements as $element) {
-
- $item = array();
- $item['uri'] = trim(self::URI, '/') . $element->find('div > a', 0)->href;
- $item['title'] = $element->find('.searchCard__makeModel', 0)->plaintext;
- $item['sellerType'] = $element->find('.searchCard__customer', 0)->plaintext;
- $item['author'] = $item['sellerType'];
- $item['version'] = $element->find('.searchCard__version', 0)->plaintext;
- $item['price'] = $element->find('.searchCard__fieldPrice', 0)->plaintext;
- $item['year'] = $element->find('.searchCard__year', 0)->plaintext;
- $item['mileage'] = $element->find('.searchCard__mileage', 0)->plaintext;
- // The image is lazyloaded with ajax
-
- $item['content'] = '
+ $item['content'] = '
<br>Variation : ' . $item['version']
- . '<br>Prix : ' . $item['price']
- . '<br>Année : ' . $item['year']
- . '<br>Kilométrage : ' . $item['mileage']
- . '<br>Type de vendeur : ' . $item['sellerType'];
+ . '<br>Prix : ' . $item['price']
+ . '<br>Année : ' . $item['year']
+ . '<br>Kilométrage : ' . $item['mileage']
+ . '<br>Type de vendeur : ' . $item['sellerType'];
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/LaTeX3ProjectNewslettersBridge.php b/bridges/LaTeX3ProjectNewslettersBridge.php
index 61bc1f6d..dcf1ba0d 100644
--- a/bridges/LaTeX3ProjectNewslettersBridge.php
+++ b/bridges/LaTeX3ProjectNewslettersBridge.php
@@ -1,33 +1,37 @@
<?php
-class LaTeX3ProjectNewslettersBridge extends BridgeAbstract {
- const MAINTAINER = 'µKöff';
- const NAME = 'LaTeX3 Project Newsletters';
- const URI = 'https://www.latex-project.org';
- const DESCRIPTION = 'Newsletters by the LaTeX3 project team covering topics of interest in the area of
+class LaTeX3ProjectNewslettersBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'µKöff';
+ const NAME = 'LaTeX3 Project Newsletters';
+ const URI = 'https://www.latex-project.org';
+ const DESCRIPTION = 'Newsletters by the LaTeX3 project team covering topics of interest in the area of
LaTeX3/expl3 development. They appear in irregular intervals and are not necessarily tied to individual
releases of the software (as the LaTeX3 kernel code is updated rather often).';
- public function collectData(){
- $html = getSimpleHTMLDOM(static::URI . '/news/latex3-news/') or returnServerError('No contents received!');
- $newsContainer = $html->find('article tbody', 0);
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(static::URI . '/news/latex3-news/') or returnServerError('No contents received!');
+ $newsContainer = $html->find('article tbody', 0);
- foreach($newsContainer->find('tr') as $row) {
- $this->items[] = $this->collectArticle($row);
- }
- }
+ foreach ($newsContainer->find('tr') as $row) {
+ $this->items[] = $this->collectArticle($row);
+ }
+ }
- private function collectArticle($element) {
- $item = array();
- $item['uri'] = static::URI . $element->find('td', 1)->find('a', 0)->href;
- $item['title'] = $element->find('td', 1)->find('a', 0)->plaintext;
- $item['timestamp'] = DateTime::createFromFormat('Y/m/d', $element->find('td', 0)->plaintext)->getTimestamp();
- $item['content'] = $element->find('td', 2)->plaintext;
- $item['author'] = 'LaTeX3 Project';
- return $item;
- }
+ private function collectArticle($element)
+ {
+ $item = [];
+ $item['uri'] = static::URI . $element->find('td', 1)->find('a', 0)->href;
+ $item['title'] = $element->find('td', 1)->find('a', 0)->plaintext;
+ $item['timestamp'] = DateTime::createFromFormat('Y/m/d', $element->find('td', 0)->plaintext)->getTimestamp();
+ $item['content'] = $element->find('td', 2)->plaintext;
+ $item['author'] = 'LaTeX3 Project';
+ return $item;
+ }
- public function getIcon(){
- return self::URI . '/favicon.ico';
- }
+ public function getIcon()
+ {
+ return self::URI . '/favicon.ico';
+ }
}
diff --git a/bridges/LeBonCoinBridge.php b/bridges/LeBonCoinBridge.php
index 87389d18..62416b98 100644
--- a/bridges/LeBonCoinBridge.php
+++ b/bridges/LeBonCoinBridge.php
@@ -1,538 +1,532 @@
<?php
-class LeBonCoinBridge extends BridgeAbstract {
-
- const MAINTAINER = 'jacknumber';
- const NAME = 'LeBonCoin';
- const URI = 'https://www.leboncoin.fr/';
- const DESCRIPTION = 'Returns most recent results from LeBonCoin';
-
- const PARAMETERS = array(
- array(
- 'keywords' => array('name' => 'Mots-Clés'),
- 'region' => array(
- 'name' => 'Région',
- 'type' => 'list',
- 'values' => array(
- 'Toute la France' => '',
- 'Alsace' => '1',
- 'Aquitaine' => '2',
- 'Auvergne' => '3',
- 'Basse Normandie' => '4',
- 'Bourgogne' => '5',
- 'Bretagne' => '6',
- 'Centre' => '7',
- 'Champagne Ardenne' => '8',
- 'Corse' => '9',
- 'Franche Comté' => '10',
- 'Haute Normandie' => '11',
- 'Ile de France' => '12',
- 'Languedoc Roussillon' => '13',
- 'Limousin' => '14',
- 'Lorraine' => '15',
- 'Midi Pyrénées' => '16',
- 'Nord Pas De Calais' => '17',
- 'Pays de la Loire' => '18',
- 'Picardie' => '19',
- 'Poitou Charentes' => '20',
- 'Provence Alpes Côte d\'Azur' => '21',
- 'Rhône-Alpes' => '22',
- 'Guadeloupe' => '23',
- 'Martinique' => '24',
- 'Guyane' => '25',
- 'Réunion' => '26'
- )
- ),
- 'department' => array(
- 'name' => 'Département',
- 'type' => 'list',
- 'values' => array(
- '' => '',
- 'Ain' => '1',
- 'Aisne' => '2',
- 'Allier' => '3',
- 'Alpes-de-Haute-Provence' => '4',
- 'Hautes-Alpes' => '5',
- 'Alpes-Maritimes' => '6',
- 'Ardèche' => '7',
- 'Ardennes' => '8',
- 'Ariège' => '9',
- 'Aube' => '10',
- 'Aude' => '11',
- 'Aveyron' => '12',
- 'Bouches-du-Rhône' => '13',
- 'Calvados' => '14',
- 'Cantal' => '15',
- 'Charente' => '16',
- 'Charente-Maritime' => '17',
- 'Cher' => '18',
- 'Corrèze' => '19',
- 'Corse-du-Sud' => '2A',
- 'Haute-Corse' => '2B',
- 'Côte-d\'Or' => '21',
- 'Côtes-d\'Armor' => '22',
- 'Creuse' => '23',
- 'Dordogne' => '24',
- 'Doubs' => '25',
- 'Drôme' => '26',
- 'Eure' => '27',
- 'Eure-et-Loir' => '28',
- 'Finistère' => '29',
- 'Gard' => '30',
- 'Haute-Garonne' => '31',
- 'Gers' => '32',
- 'Gironde' => '33',
- 'Hérault' => '34',
- 'Ille-et-Vilaine' => '35',
- 'Indre' => '36',
- 'Indre-et-Loire' => '37',
- 'Isère' => '38',
- 'Jura' => '39',
- 'Landes' => '40',
- 'Loir-et-Cher' => '41',
- 'Loire' => '42',
- 'Haute-Loire' => '43',
- 'Loire-Atlantique' => '44',
- 'Loiret' => '45',
- 'Lot' => '46',
- 'Lot-et-Garonne' => '47',
- 'Lozère' => '48',
- 'Maine-et-Loire' => '49',
- 'Manche' => '50',
- 'Marne' => '51',
- 'Haute-Marne' => '52',
- 'Mayenne' => '53',
- 'Meurthe-et-Moselle' => '54',
- 'Meuse' => '55',
- 'Morbihan' => '56',
- 'Moselle' => '57',
- 'Nièvre' => '58',
- 'Nord' => '59',
- 'Oise' => '60',
- 'Orne' => '61',
- 'Pas-de-Calais' => '62',
- 'Puy-de-Dôme' => '63',
- 'Pyrénées-Atlantiques' => '64',
- 'Hautes-Pyrénées' => '65',
- 'Pyrénées-Orientales' => '66',
- 'Bas-Rhin' => '67',
- 'Haut-Rhin' => '68',
- 'Rhône' => '69',
- 'Haute-Saône' => '70',
- 'Saône-et-Loire' => '71',
- 'Sarthe' => '72',
- 'Savoie' => '73',
- 'Haute-Savoie' => '74',
- 'Paris' => '75',
- 'Seine-Maritime' => '76',
- 'Seine-et-Marne' => '77',
- 'Yvelines' => '78',
- 'Deux-Sèvres' => '79',
- 'Somme' => '80',
- 'Tarn' => '81',
- 'Tarn-et-Garonne' => '82',
- 'Var' => '83',
- 'Vaucluse' => '84',
- 'Vendée' => '85',
- 'Vienne' => '86',
- 'Haute-Vienne' => '87',
- 'Vosges' => '88',
- 'Yonne' => '89',
- 'Territoire de Belfort' => '90',
- 'Essonne' => '91',
- 'Hauts-de-Seine' => '92',
- 'Seine-Saint-Denis' => '93',
- 'Val-de-Marne' => '94',
- 'Val-d\'Oise' => '95'
- )
- ),
- 'cities' => array(
- 'name' => 'Villes',
- 'title' => 'Codes postaux séparés par des virgules'
- ),
- 'category' => array(
- 'name' => 'Catégorie',
- 'type' => 'list',
- 'values' => array(
- 'Toutes catégories' => '',
- 'EMPLOI' => array(
- 'Emploi et recrutement' => '71',
- 'Offres d\'emploi et jobs' => '33'
- ),
- 'VÉHICULES' => array(
- 'Tous' => '1',
- 'Voitures' => '2',
- 'Motos' => '3',
- 'Caravaning' => '4',
- 'Utilitaires' => '5',
- 'Equipement Auto' => '6',
- 'Equipement Moto' => '44',
- 'Equipement Caravaning' => '50',
- 'Nautisme' => '7',
- 'Equipement Nautisme' => '51'
- ),
- 'IMMOBILIER' => array(
- 'Tous' => '8',
- 'Ventes immobilières' => '9',
- 'Locations' => '10',
- 'Colocations' => '11',
- 'Bureaux & Commerces' => '13'
- ),
- 'VACANCES' => array(
- 'Tous' => '66',
- 'Locations & Gîtes' => '12',
- 'Chambres d\'hôtes' => '67',
- 'Campings' => '68',
- 'Hôtels' => '69',
- 'Hébergements insolites' => '70'
- ),
- 'MULTIMÉDIA' => array(
- 'Tous' => '14',
- 'Informatique' => '15',
- 'Consoles & Jeux vidéo' => '43',
- 'Image & Son' => '16',
- 'Téléphonie' => '17'
- ),
- 'LOISIRS' => array(
- 'Tous' => '24',
- 'DVD / Films' => '25',
- 'CD / Musique' => '26',
- 'Livres' => '27',
- 'Animaux' => '28',
- 'Vélos' => '55',
- 'Sports & Hobbies' => '29',
- 'Instruments de musique' => '30',
- 'Collection' => '40',
- 'Jeux & Jouets' => '41',
- 'Vins & Gastronomie' => '48'
- ),
- 'MATÉRIEL PROFESSIONNEL' => array(
- 'Tous' => '56',
- 'Matériel Agricole' => '57',
- 'Transport - Manutention' => '58',
- 'BTP - Chantier Gros-oeuvre' => '59',
- 'Outillage - Matériaux 2nd-oeuvre' => '60',
- 'Équipements Industriels' => '32',
- 'Restauration - Hôtellerie' => '61',
- 'Fournitures de Bureau' => '62',
- 'Commerces & Marchés' => '63',
- 'Matériel Médical' => '64'
- ),
- 'SERVICES' => array(
- 'Tous' => '31',
- 'Prestations de services' => '34',
- 'Billetterie' => '35',
- 'Événements' => '49',
- 'Cours particuliers' => '36',
- 'Covoiturage' => '65'
- ),
- 'MAISON' => array(
- 'Tous' => '18',
- 'Ameublement' => '19',
- 'Électroménager' => '20',
- 'Arts de la table' => '45',
- 'Décoration' => '39',
- 'Linge de maison' => '46',
- 'Bricolage' => '21',
- 'Jardinage' => '52',
- 'Vêtements' => '22',
- 'Chaussures' => '53',
- 'Accessoires & Bagagerie' => '47',
- 'Montres & Bijoux' => '42',
- 'Équipement bébé' => '23',
- 'Vêtements bébé' => '54',
- ),
- 'AUTRES' => '37'
- )
- ),
- 'pricemin' => array(
- 'name' => 'Prix min',
- 'type' => 'number'
- ),
- 'pricemax' => array(
- 'name' => 'Prix max',
- 'type' => 'number'
- ),
- 'estate' => array(
- 'name' => 'Type de bien',
- 'type' => 'list',
- 'values' => array(
- '' => '',
- 'Maison' => '1',
- 'Appartement' => '2',
- 'Terrain' => '3',
- 'Parking' => '4',
- 'Autre' => '5'
- )
- ),
- 'roomsmin' => array(
- 'name' => 'Pièces min',
- 'type' => 'number'
- ),
- 'roomsmax' => array(
- 'name' => 'Pièces max',
- 'type' => 'number'
- ),
- 'squaremin' => array(
- 'name' => 'Surface min',
- 'type' => 'number'
- ),
- 'squaremax' => array(
- 'name' => 'Surface max',
- 'type' => 'number'
- ),
- 'mileagemin' => array(
- 'name' => 'Kilométrage min',
- 'type' => 'number'
- ),
- 'mileagemax' => array(
- 'name' => 'Kilométrage max',
- 'type' => 'number'
- ),
- 'yearmin' => array(
- 'name' => 'Année min',
- 'type' => 'number'
- ),
- 'yearmax' => array(
- 'name' => 'Année max',
- 'type' => 'number'
- ),
- 'cubiccapacitymin' => array(
- 'name' => 'Cylindrée min',
- 'type' => 'number'
- ),
- 'cubiccapacitymax' => array(
- 'name' => 'Cylindrée max',
- 'type' => 'number'
- ),
- 'fuel' => array(
- 'name' => 'Énergie',
- 'type' => 'list',
- 'values' => array(
- '' => '',
- 'Essence' => '1',
- 'Diesel' => '2',
- 'GPL' => '3',
- 'Électrique' => '4',
- 'Hybride' => '6',
- 'Autre' => '5'
- )
- ),
- 'owner' => array(
- 'name' => 'Vendeur',
- 'type' => 'list',
- 'values' => array(
- 'Tous' => '',
- 'Particuliers' => 'private',
- 'Professionnels' => 'pro'
- )
- )
- )
- );
-
- public static $LBC_API_KEY = 'ba0c2dad52b3ec';
-
- private function getRange($field, $range_min, $range_max){
-
- if(!is_null($range_min)
- && !is_null($range_max)
- && $range_min > $range_max) {
- returnClientError('Min-' . $field . ' must be lower than max-' . $field . '.');
- }
-
- if(!is_null($range_min)
- && is_null($range_max)) {
- returnClientError('Max-' . $field . ' is needed when min-' . $field . ' is setted (range).');
- }
-
- return array(
- 'min' => $range_min,
- 'max' => $range_max
- );
- }
-
- public function collectData(){
-
- $url = 'https://api.leboncoin.fr/api/adfinder/v1/search';
- $data = $this->buildRequestJson();
-
- $header = array(
- 'User-Agent: LBC;Android;10;SAMSUNG;phone;0aaaaaaaaaaaaaaa;wifi;8.24.3.8;152437;0',
- 'Content-Type: application/json',
- 'X-LBC-CC: 7',
- 'Accept: application/json,application/hal+json',
- 'Content-Length: ' . strlen($data),
- 'api_key: ' . self::$LBC_API_KEY
- );
-
- $opts = array(
- CURLOPT_CUSTOMREQUEST => 'POST',
- CURLOPT_POSTFIELDS => $data
-
- );
-
- $content = getContents($url, $header, $opts);
-
- $json = json_decode($content);
-
- if($json->total === 0) {
- return;
- }
-
- foreach($json->ads as $element) {
-
- $item['title'] = $element->subject;
- $item['content'] = $element->body;
- $item['date'] = $element->index_date;
- $item['timestamp'] = strtotime($element->index_date);
- $item['uri'] = $element->url;
- $item['ad_type'] = $element->ad_type;
- $item['author'] = $element->owner->name;
-
- if(isset($element->location->city)) {
-
- $item['city'] = $element->location->city;
- $item['content'] .= ' -- ' . $element->location->city;
-
- }
-
- if(isset($element->location->zipcode)) {
- $item['zipcode'] = $element->location->zipcode;
- }
- if(isset($element->price)) {
-
- $item['price'] = $element->price[0];
- $item['content'] .= ' -- ' . current($element->price) . '€';
-
- }
-
- if(isset($element->images->urls)) {
-
- $item['thumbnail'] = $element->images->thumb_url;
- $item['enclosures'] = array();
-
- foreach($element->images->urls as $image) {
- $item['enclosures'][] = $image;
- }
-
- }
-
- $this->items[] = $item;
- }
- }
-
- private function buildRequestJson() {
-
- $requestJson = new StdClass();
- $requestJson->owner_type = $this->getInput('owner');
- $requestJson->filters = new StdClass();
-
- $requestJson->filters->keywords = array(
- 'text' => $this->getInput('keywords')
- );
-
- if($this->getInput('region') != '') {
- $requestJson->filters->location['regions'] = array($this->getInput('region'));
- }
-
- if($this->getInput('department') != '') {
- $requestJson->filters->location['departments'] = array($this->getInput('department'));
- }
-
- if($this->getInput('cities') != '') {
-
- $requestJson->filters->location['city_zipcodes'] = array();
-
- foreach (explode(',', $this->getInput('cities')) as $zipcode) {
-
- $requestJson->filters->location['city_zipcodes'][] = array(
- 'zipcode' => trim($zipcode)
- );
- }
-
- }
-
- $requestJson->filters->category = array(
- 'id' => $this->getInput('category')
- );
-
- if($this->getInput('pricemin') != ''
- || $this->getInput('pricemax') != '') {
-
- $requestJson->filters->ranges->price = $this->getRange(
- 'price',
- $this->getInput('pricemin'),
- $this->getInput('pricemax')
- );
-
- }
-
- if($this->getInput('estate') != '') {
- $requestJson->filters->enums['real_estate_type'] = array($this->getInput('estate'));
- }
-
- if($this->getInput('roomsmin') != ''
- || $this->getInput('roomsmax') != '') {
-
- $requestJson->filters->ranges->rooms = $this->getRange(
- 'rooms',
- $this->getInput('roomsmin'),
- $this->getInput('roomsmax')
- );
-
- }
-
- if($this->getInput('squaremin') != ''
- || $this->getInput('squaremax') != '') {
-
- $requestJson->filters->ranges->square = $this->getRange(
- 'square',
- $this->getInput('squaremin'),
- $this->getInput('squaremax')
- );
-
- }
-
- if($this->getInput('mileagemin') != ''
- || $this->getInput('mileagemax') != '') {
-
- $requestJson->filters->ranges->mileage = $this->getRange(
- 'mileage',
- $this->getInput('mileagemin'),
- $this->getInput('mileagemax')
- );
-
- }
-
- if($this->getInput('yearmin') != ''
- || $this->getInput('yearmax') != '') {
-
- $requestJson->filters->ranges->regdate = $this->getRange(
- 'year',
- $this->getInput('yearmin'),
- $this->getInput('yearmax')
- );
-
- }
-
- if($this->getInput('cubiccapacitymin') != ''
- || $this->getInput('cubiccapacitymax') != '') {
-
- $requestJson->filters->ranges->cubic_capacity = $this->getRange(
- 'cubic_capacity',
- $this->getInput('cubiccapacitymin'),
- $this->getInput('cubiccapacitymax')
- );
-
- }
-
- if($this->getInput('fuel') != '') {
- $requestJson->filters->enums['fuel'] = array($this->getInput('fuel'));
- }
-
- $requestJson->limit = 30;
-
- return json_encode($requestJson);
-
- }
+class LeBonCoinBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'jacknumber';
+ const NAME = 'LeBonCoin';
+ const URI = 'https://www.leboncoin.fr/';
+ const DESCRIPTION = 'Returns most recent results from LeBonCoin';
+
+ const PARAMETERS = [
+ [
+ 'keywords' => ['name' => 'Mots-Clés'],
+ 'region' => [
+ 'name' => 'Région',
+ 'type' => 'list',
+ 'values' => [
+ 'Toute la France' => '',
+ 'Alsace' => '1',
+ 'Aquitaine' => '2',
+ 'Auvergne' => '3',
+ 'Basse Normandie' => '4',
+ 'Bourgogne' => '5',
+ 'Bretagne' => '6',
+ 'Centre' => '7',
+ 'Champagne Ardenne' => '8',
+ 'Corse' => '9',
+ 'Franche Comté' => '10',
+ 'Haute Normandie' => '11',
+ 'Ile de France' => '12',
+ 'Languedoc Roussillon' => '13',
+ 'Limousin' => '14',
+ 'Lorraine' => '15',
+ 'Midi Pyrénées' => '16',
+ 'Nord Pas De Calais' => '17',
+ 'Pays de la Loire' => '18',
+ 'Picardie' => '19',
+ 'Poitou Charentes' => '20',
+ 'Provence Alpes Côte d\'Azur' => '21',
+ 'Rhône-Alpes' => '22',
+ 'Guadeloupe' => '23',
+ 'Martinique' => '24',
+ 'Guyane' => '25',
+ 'Réunion' => '26'
+ ]
+ ],
+ 'department' => [
+ 'name' => 'Département',
+ 'type' => 'list',
+ 'values' => [
+ '' => '',
+ 'Ain' => '1',
+ 'Aisne' => '2',
+ 'Allier' => '3',
+ 'Alpes-de-Haute-Provence' => '4',
+ 'Hautes-Alpes' => '5',
+ 'Alpes-Maritimes' => '6',
+ 'Ardèche' => '7',
+ 'Ardennes' => '8',
+ 'Ariège' => '9',
+ 'Aube' => '10',
+ 'Aude' => '11',
+ 'Aveyron' => '12',
+ 'Bouches-du-Rhône' => '13',
+ 'Calvados' => '14',
+ 'Cantal' => '15',
+ 'Charente' => '16',
+ 'Charente-Maritime' => '17',
+ 'Cher' => '18',
+ 'Corrèze' => '19',
+ 'Corse-du-Sud' => '2A',
+ 'Haute-Corse' => '2B',
+ 'Côte-d\'Or' => '21',
+ 'Côtes-d\'Armor' => '22',
+ 'Creuse' => '23',
+ 'Dordogne' => '24',
+ 'Doubs' => '25',
+ 'Drôme' => '26',
+ 'Eure' => '27',
+ 'Eure-et-Loir' => '28',
+ 'Finistère' => '29',
+ 'Gard' => '30',
+ 'Haute-Garonne' => '31',
+ 'Gers' => '32',
+ 'Gironde' => '33',
+ 'Hérault' => '34',
+ 'Ille-et-Vilaine' => '35',
+ 'Indre' => '36',
+ 'Indre-et-Loire' => '37',
+ 'Isère' => '38',
+ 'Jura' => '39',
+ 'Landes' => '40',
+ 'Loir-et-Cher' => '41',
+ 'Loire' => '42',
+ 'Haute-Loire' => '43',
+ 'Loire-Atlantique' => '44',
+ 'Loiret' => '45',
+ 'Lot' => '46',
+ 'Lot-et-Garonne' => '47',
+ 'Lozère' => '48',
+ 'Maine-et-Loire' => '49',
+ 'Manche' => '50',
+ 'Marne' => '51',
+ 'Haute-Marne' => '52',
+ 'Mayenne' => '53',
+ 'Meurthe-et-Moselle' => '54',
+ 'Meuse' => '55',
+ 'Morbihan' => '56',
+ 'Moselle' => '57',
+ 'Nièvre' => '58',
+ 'Nord' => '59',
+ 'Oise' => '60',
+ 'Orne' => '61',
+ 'Pas-de-Calais' => '62',
+ 'Puy-de-Dôme' => '63',
+ 'Pyrénées-Atlantiques' => '64',
+ 'Hautes-Pyrénées' => '65',
+ 'Pyrénées-Orientales' => '66',
+ 'Bas-Rhin' => '67',
+ 'Haut-Rhin' => '68',
+ 'Rhône' => '69',
+ 'Haute-Saône' => '70',
+ 'Saône-et-Loire' => '71',
+ 'Sarthe' => '72',
+ 'Savoie' => '73',
+ 'Haute-Savoie' => '74',
+ 'Paris' => '75',
+ 'Seine-Maritime' => '76',
+ 'Seine-et-Marne' => '77',
+ 'Yvelines' => '78',
+ 'Deux-Sèvres' => '79',
+ 'Somme' => '80',
+ 'Tarn' => '81',
+ 'Tarn-et-Garonne' => '82',
+ 'Var' => '83',
+ 'Vaucluse' => '84',
+ 'Vendée' => '85',
+ 'Vienne' => '86',
+ 'Haute-Vienne' => '87',
+ 'Vosges' => '88',
+ 'Yonne' => '89',
+ 'Territoire de Belfort' => '90',
+ 'Essonne' => '91',
+ 'Hauts-de-Seine' => '92',
+ 'Seine-Saint-Denis' => '93',
+ 'Val-de-Marne' => '94',
+ 'Val-d\'Oise' => '95'
+ ]
+ ],
+ 'cities' => [
+ 'name' => 'Villes',
+ 'title' => 'Codes postaux séparés par des virgules'
+ ],
+ 'category' => [
+ 'name' => 'Catégorie',
+ 'type' => 'list',
+ 'values' => [
+ 'Toutes catégories' => '',
+ 'EMPLOI' => [
+ 'Emploi et recrutement' => '71',
+ 'Offres d\'emploi et jobs' => '33'
+ ],
+ 'VÉHICULES' => [
+ 'Tous' => '1',
+ 'Voitures' => '2',
+ 'Motos' => '3',
+ 'Caravaning' => '4',
+ 'Utilitaires' => '5',
+ 'Equipement Auto' => '6',
+ 'Equipement Moto' => '44',
+ 'Equipement Caravaning' => '50',
+ 'Nautisme' => '7',
+ 'Equipement Nautisme' => '51'
+ ],
+ 'IMMOBILIER' => [
+ 'Tous' => '8',
+ 'Ventes immobilières' => '9',
+ 'Locations' => '10',
+ 'Colocations' => '11',
+ 'Bureaux & Commerces' => '13'
+ ],
+ 'VACANCES' => [
+ 'Tous' => '66',
+ 'Locations & Gîtes' => '12',
+ 'Chambres d\'hôtes' => '67',
+ 'Campings' => '68',
+ 'Hôtels' => '69',
+ 'Hébergements insolites' => '70'
+ ],
+ 'MULTIMÉDIA' => [
+ 'Tous' => '14',
+ 'Informatique' => '15',
+ 'Consoles & Jeux vidéo' => '43',
+ 'Image & Son' => '16',
+ 'Téléphonie' => '17'
+ ],
+ 'LOISIRS' => [
+ 'Tous' => '24',
+ 'DVD / Films' => '25',
+ 'CD / Musique' => '26',
+ 'Livres' => '27',
+ 'Animaux' => '28',
+ 'Vélos' => '55',
+ 'Sports & Hobbies' => '29',
+ 'Instruments de musique' => '30',
+ 'Collection' => '40',
+ 'Jeux & Jouets' => '41',
+ 'Vins & Gastronomie' => '48'
+ ],
+ 'MATÉRIEL PROFESSIONNEL' => [
+ 'Tous' => '56',
+ 'Matériel Agricole' => '57',
+ 'Transport - Manutention' => '58',
+ 'BTP - Chantier Gros-oeuvre' => '59',
+ 'Outillage - Matériaux 2nd-oeuvre' => '60',
+ 'Équipements Industriels' => '32',
+ 'Restauration - Hôtellerie' => '61',
+ 'Fournitures de Bureau' => '62',
+ 'Commerces & Marchés' => '63',
+ 'Matériel Médical' => '64'
+ ],
+ 'SERVICES' => [
+ 'Tous' => '31',
+ 'Prestations de services' => '34',
+ 'Billetterie' => '35',
+ 'Événements' => '49',
+ 'Cours particuliers' => '36',
+ 'Covoiturage' => '65'
+ ],
+ 'MAISON' => [
+ 'Tous' => '18',
+ 'Ameublement' => '19',
+ 'Électroménager' => '20',
+ 'Arts de la table' => '45',
+ 'Décoration' => '39',
+ 'Linge de maison' => '46',
+ 'Bricolage' => '21',
+ 'Jardinage' => '52',
+ 'Vêtements' => '22',
+ 'Chaussures' => '53',
+ 'Accessoires & Bagagerie' => '47',
+ 'Montres & Bijoux' => '42',
+ 'Équipement bébé' => '23',
+ 'Vêtements bébé' => '54',
+ ],
+ 'AUTRES' => '37'
+ ]
+ ],
+ 'pricemin' => [
+ 'name' => 'Prix min',
+ 'type' => 'number'
+ ],
+ 'pricemax' => [
+ 'name' => 'Prix max',
+ 'type' => 'number'
+ ],
+ 'estate' => [
+ 'name' => 'Type de bien',
+ 'type' => 'list',
+ 'values' => [
+ '' => '',
+ 'Maison' => '1',
+ 'Appartement' => '2',
+ 'Terrain' => '3',
+ 'Parking' => '4',
+ 'Autre' => '5'
+ ]
+ ],
+ 'roomsmin' => [
+ 'name' => 'Pièces min',
+ 'type' => 'number'
+ ],
+ 'roomsmax' => [
+ 'name' => 'Pièces max',
+ 'type' => 'number'
+ ],
+ 'squaremin' => [
+ 'name' => 'Surface min',
+ 'type' => 'number'
+ ],
+ 'squaremax' => [
+ 'name' => 'Surface max',
+ 'type' => 'number'
+ ],
+ 'mileagemin' => [
+ 'name' => 'Kilométrage min',
+ 'type' => 'number'
+ ],
+ 'mileagemax' => [
+ 'name' => 'Kilométrage max',
+ 'type' => 'number'
+ ],
+ 'yearmin' => [
+ 'name' => 'Année min',
+ 'type' => 'number'
+ ],
+ 'yearmax' => [
+ 'name' => 'Année max',
+ 'type' => 'number'
+ ],
+ 'cubiccapacitymin' => [
+ 'name' => 'Cylindrée min',
+ 'type' => 'number'
+ ],
+ 'cubiccapacitymax' => [
+ 'name' => 'Cylindrée max',
+ 'type' => 'number'
+ ],
+ 'fuel' => [
+ 'name' => 'Énergie',
+ 'type' => 'list',
+ 'values' => [
+ '' => '',
+ 'Essence' => '1',
+ 'Diesel' => '2',
+ 'GPL' => '3',
+ 'Électrique' => '4',
+ 'Hybride' => '6',
+ 'Autre' => '5'
+ ]
+ ],
+ 'owner' => [
+ 'name' => 'Vendeur',
+ 'type' => 'list',
+ 'values' => [
+ 'Tous' => '',
+ 'Particuliers' => 'private',
+ 'Professionnels' => 'pro'
+ ]
+ ]
+ ]
+ ];
+
+ public static $LBC_API_KEY = 'ba0c2dad52b3ec';
+
+ private function getRange($field, $range_min, $range_max)
+ {
+ if (
+ !is_null($range_min)
+ && !is_null($range_max)
+ && $range_min > $range_max
+ ) {
+ returnClientError('Min-' . $field . ' must be lower than max-' . $field . '.');
+ }
+
+ if (
+ !is_null($range_min)
+ && is_null($range_max)
+ ) {
+ returnClientError('Max-' . $field . ' is needed when min-' . $field . ' is setted (range).');
+ }
+
+ return [
+ 'min' => $range_min,
+ 'max' => $range_max
+ ];
+ }
+
+ public function collectData()
+ {
+ $url = 'https://api.leboncoin.fr/api/adfinder/v1/search';
+ $data = $this->buildRequestJson();
+
+ $header = [
+ 'User-Agent: LBC;Android;10;SAMSUNG;phone;0aaaaaaaaaaaaaaa;wifi;8.24.3.8;152437;0',
+ 'Content-Type: application/json',
+ 'X-LBC-CC: 7',
+ 'Accept: application/json,application/hal+json',
+ 'Content-Length: ' . strlen($data),
+ 'api_key: ' . self::$LBC_API_KEY
+ ];
+
+ $opts = [
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_POSTFIELDS => $data
+
+ ];
+
+ $content = getContents($url, $header, $opts);
+
+ $json = json_decode($content);
+
+ if ($json->total === 0) {
+ return;
+ }
+
+ foreach ($json->ads as $element) {
+ $item['title'] = $element->subject;
+ $item['content'] = $element->body;
+ $item['date'] = $element->index_date;
+ $item['timestamp'] = strtotime($element->index_date);
+ $item['uri'] = $element->url;
+ $item['ad_type'] = $element->ad_type;
+ $item['author'] = $element->owner->name;
+
+ if (isset($element->location->city)) {
+ $item['city'] = $element->location->city;
+ $item['content'] .= ' -- ' . $element->location->city;
+ }
+
+ if (isset($element->location->zipcode)) {
+ $item['zipcode'] = $element->location->zipcode;
+ }
+
+ if (isset($element->price)) {
+ $item['price'] = $element->price[0];
+ $item['content'] .= ' -- ' . current($element->price) . '€';
+ }
+
+ if (isset($element->images->urls)) {
+ $item['thumbnail'] = $element->images->thumb_url;
+ $item['enclosures'] = [];
+
+ foreach ($element->images->urls as $image) {
+ $item['enclosures'][] = $image;
+ }
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function buildRequestJson()
+ {
+ $requestJson = new StdClass();
+ $requestJson->owner_type = $this->getInput('owner');
+ $requestJson->filters = new StdClass();
+
+ $requestJson->filters->keywords = [
+ 'text' => $this->getInput('keywords')
+ ];
+
+ if ($this->getInput('region') != '') {
+ $requestJson->filters->location['regions'] = [$this->getInput('region')];
+ }
+
+ if ($this->getInput('department') != '') {
+ $requestJson->filters->location['departments'] = [$this->getInput('department')];
+ }
+
+ if ($this->getInput('cities') != '') {
+ $requestJson->filters->location['city_zipcodes'] = [];
+
+ foreach (explode(',', $this->getInput('cities')) as $zipcode) {
+ $requestJson->filters->location['city_zipcodes'][] = [
+ 'zipcode' => trim($zipcode)
+ ];
+ }
+ }
+
+ $requestJson->filters->category = [
+ 'id' => $this->getInput('category')
+ ];
+
+ if (
+ $this->getInput('pricemin') != ''
+ || $this->getInput('pricemax') != ''
+ ) {
+ $requestJson->filters->ranges->price = $this->getRange(
+ 'price',
+ $this->getInput('pricemin'),
+ $this->getInput('pricemax')
+ );
+ }
+
+ if ($this->getInput('estate') != '') {
+ $requestJson->filters->enums['real_estate_type'] = [$this->getInput('estate')];
+ }
+
+ if (
+ $this->getInput('roomsmin') != ''
+ || $this->getInput('roomsmax') != ''
+ ) {
+ $requestJson->filters->ranges->rooms = $this->getRange(
+ 'rooms',
+ $this->getInput('roomsmin'),
+ $this->getInput('roomsmax')
+ );
+ }
+
+ if (
+ $this->getInput('squaremin') != ''
+ || $this->getInput('squaremax') != ''
+ ) {
+ $requestJson->filters->ranges->square = $this->getRange(
+ 'square',
+ $this->getInput('squaremin'),
+ $this->getInput('squaremax')
+ );
+ }
+
+ if (
+ $this->getInput('mileagemin') != ''
+ || $this->getInput('mileagemax') != ''
+ ) {
+ $requestJson->filters->ranges->mileage = $this->getRange(
+ 'mileage',
+ $this->getInput('mileagemin'),
+ $this->getInput('mileagemax')
+ );
+ }
+
+ if (
+ $this->getInput('yearmin') != ''
+ || $this->getInput('yearmax') != ''
+ ) {
+ $requestJson->filters->ranges->regdate = $this->getRange(
+ 'year',
+ $this->getInput('yearmin'),
+ $this->getInput('yearmax')
+ );
+ }
+
+ if (
+ $this->getInput('cubiccapacitymin') != ''
+ || $this->getInput('cubiccapacitymax') != ''
+ ) {
+ $requestJson->filters->ranges->cubic_capacity = $this->getRange(
+ 'cubic_capacity',
+ $this->getInput('cubiccapacitymin'),
+ $this->getInput('cubiccapacitymax')
+ );
+ }
+
+ if ($this->getInput('fuel') != '') {
+ $requestJson->filters->enums['fuel'] = [$this->getInput('fuel')];
+ }
+
+ $requestJson->limit = 30;
+
+ return json_encode($requestJson);
+ }
}
diff --git a/bridges/LeMondeInformatiqueBridge.php b/bridges/LeMondeInformatiqueBridge.php
index 823872fb..678e405f 100644
--- a/bridges/LeMondeInformatiqueBridge.php
+++ b/bridges/LeMondeInformatiqueBridge.php
@@ -1,39 +1,43 @@
<?php
-class LeMondeInformatiqueBridge extends FeedExpander {
- const MAINTAINER = 'ORelio';
- const NAME = 'Le Monde Informatique';
- const URI = 'https://www.lemondeinformatique.fr/';
- const DESCRIPTION = 'Returns the newest articles.';
+class LeMondeInformatiqueBridge extends FeedExpander
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = 'Le Monde Informatique';
+ const URI = 'https://www.lemondeinformatique.fr/';
+ const DESCRIPTION = 'Returns the newest articles.';
- public function collectData(){
- $this->collectExpandableDatas(self::URI . 'rss/rss.xml', 10);
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas(self::URI . 'rss/rss.xml', 10);
+ }
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- $article_html = getSimpleHTMLDOMCached($item['uri']);
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ $article_html = getSimpleHTMLDOMCached($item['uri']);
- //Deduce thumbnail URL from article image URL
- $item['enclosures'] = array(
- str_replace(
- '/grande/',
- '/petite/',
- $article_html->find('.article-image > img, figure > img', 0)->src
- )
- );
+ //Deduce thumbnail URL from article image URL
+ $item['enclosures'] = [
+ str_replace(
+ '/grande/',
+ '/petite/',
+ $article_html->find('.article-image > img, figure > img', 0)->src
+ )
+ ];
- //No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail
- $content_node = $article_html->find('div.col-primary, div.col-sm-9', 0);
- $item['content'] = $this->cleanArticle($content_node->innertext);
- $item['author'] = $article_html->find('div.author-infos', 0)->find('b', 0)->plaintext;
+ //No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail
+ $content_node = $article_html->find('div.col-primary, div.col-sm-9', 0);
+ $item['content'] = $this->cleanArticle($content_node->innertext);
+ $item['author'] = $article_html->find('div.author-infos', 0)->find('b', 0)->plaintext;
- return $item;
- }
+ return $item;
+ }
- private function cleanArticle($article_html){
- $article_html = stripWithDelimiters($article_html, '<script', '</script>');
- $article_html = explode('<p class="contact-error', $article_html)[0] . '</div>';
- return $article_html;
- }
+ private function cleanArticle($article_html)
+ {
+ $article_html = stripWithDelimiters($article_html, '<script', '</script>');
+ $article_html = explode('<p class="contact-error', $article_html)[0] . '</div>';
+ return $article_html;
+ }
}
diff --git a/bridges/LegifranceJOBridge.php b/bridges/LegifranceJOBridge.php
index cfbfad46..2d86c2ce 100644
--- a/bridges/LegifranceJOBridge.php
+++ b/bridges/LegifranceJOBridge.php
@@ -1,73 +1,77 @@
<?php
-class LegifranceJOBridge extends BridgeAbstract {
- const MAINTAINER = 'Pierre Mazière';
- const NAME = 'Journal Officiel de la République Française';
- // This uri returns a snippet of js. Should probably be https://www.legifrance.gouv.fr/jorf/jo/
- const URI = 'https://www.legifrance.gouv.fr/affichJO.do';
- const DESCRIPTION = 'Returns the laws and decrees officially registered daily in France';
+class LegifranceJOBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Pierre Mazière';
+ const NAME = 'Journal Officiel de la République Française';
+ // This uri returns a snippet of js. Should probably be https://www.legifrance.gouv.fr/jorf/jo/
+ const URI = 'https://www.legifrance.gouv.fr/affichJO.do';
+ const DESCRIPTION = 'Returns the laws and decrees officially registered daily in France';
- const PARAMETERS = array();
+ const PARAMETERS = [];
- private $author;
- private $timestamp;
- private $uri;
+ private $author;
+ private $timestamp;
+ private $uri;
- private function extractItem($section, $subsection = null, $origin = null){
- $item = array();
- $item['author'] = $this->author;
- $item['timestamp'] = $this->timestamp;
- $item['uri'] = $this->uri . '#' . count($this->items);
- $item['title'] = $section->plaintext;
+ private function extractItem($section, $subsection = null, $origin = null)
+ {
+ $item = [];
+ $item['author'] = $this->author;
+ $item['timestamp'] = $this->timestamp;
+ $item['uri'] = $this->uri . '#' . count($this->items);
+ $item['title'] = $section->plaintext;
- if(!is_null($origin)) {
- $item['title'] = '[ ' . $item['title'] . ' / ' . $subsection->plaintext . ' ] ' . $origin->plaintext;
- $data = $origin;
- } elseif(!is_null($subsection)) {
- $item['title'] = '[ ' . $item['title'] . ' ] ' . $subsection->plaintext;
- $data = $subsection;
- } else {
- $data = $section;
- }
+ if (!is_null($origin)) {
+ $item['title'] = '[ ' . $item['title'] . ' / ' . $subsection->plaintext . ' ] ' . $origin->plaintext;
+ $data = $origin;
+ } elseif (!is_null($subsection)) {
+ $item['title'] = '[ ' . $item['title'] . ' ] ' . $subsection->plaintext;
+ $data = $subsection;
+ } else {
+ $data = $section;
+ }
- $item['content'] = '';
- foreach($data->nextSibling()->find('a') as $content) {
- $text = $content->plaintext;
- $href = $content->nextSibling()->getAttribute('resource');
- $item['content'] .= '<p><a href="' . $href . '">' . $text . '</a></p>';
- }
- return $item;
- }
+ $item['content'] = '';
+ foreach ($data->nextSibling()->find('a') as $content) {
+ $text = $content->plaintext;
+ $href = $content->nextSibling()->getAttribute('resource');
+ $item['content'] .= '<p><a href="' . $href . '">' . $text . '</a></p>';
+ }
+ return $item;
+ }
- public function getIcon() {
- return 'https://www.legifrance.gouv.fr/img/favicon.ico';
- }
+ public function getIcon()
+ {
+ return 'https://www.legifrance.gouv.fr/img/favicon.ico';
+ }
- public function collectData(){
- $html = getSimpleHTMLDOM(self::URI)
- or $this->returnServer('Unable to download ' . self::URI);
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI)
+ or $this->returnServer('Unable to download ' . self::URI);
- $this->author = trim($html->find('h2.titleJO', 0)->plaintext);
- $uri = $html->find('h2.titleELI', 0)->plaintext;
- $this->uri = trim(substr($uri, strpos($uri, 'https')));
- $this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/'), -5));
+ $this->author = trim($html->find('h2.titleJO', 0)->plaintext);
+ $uri = $html->find('h2.titleELI', 0)->plaintext;
+ $this->uri = trim(substr($uri, strpos($uri, 'https')));
+ $this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/'), -5));
- foreach($html->find('h3') as $section) {
- $subsections = $section->nextSibling()->find('h4');
- foreach($subsections as $subsection) {
- $origins = $subsection->nextSibling()->find('h5');
- foreach($origins as $origin) {
- $this->items[] = $this->extractItem($section, $subsection, $origin);
- }
- if(!empty($origins)) {
- continue;
- }
- $this->items[] = $this->extractItem($section, $subsection);
- }
- if(!empty($subsections)) {
- continue;
- }
- $this->items[] = $this->extractItem($section);
- }
- }
+ foreach ($html->find('h3') as $section) {
+ $subsections = $section->nextSibling()->find('h4');
+ foreach ($subsections as $subsection) {
+ $origins = $subsection->nextSibling()->find('h5');
+ foreach ($origins as $origin) {
+ $this->items[] = $this->extractItem($section, $subsection, $origin);
+ }
+ if (!empty($origins)) {
+ continue;
+ }
+ $this->items[] = $this->extractItem($section, $subsection);
+ }
+ if (!empty($subsections)) {
+ continue;
+ }
+ $this->items[] = $this->extractItem($section);
+ }
+ }
}
diff --git a/bridges/LegoIdeasBridge.php b/bridges/LegoIdeasBridge.php
index 442aba64..c4361f1f 100644
--- a/bridges/LegoIdeasBridge.php
+++ b/bridges/LegoIdeasBridge.php
@@ -1,96 +1,101 @@
<?php
-class LegoIdeasBridge extends BridgeAbstract {
- const NAME = 'Lego Ideas';
- const URI = 'https://ideas.lego.com/';
- const DESCRIPTION = 'Community Supported Lego Builds';
- const MAINTAINER = 'sal0max';
- const CACHE_TIMEOUT = 60 * 60 * 2; // 2h
- const PARAMETERS = array( array(
- 'support_value_min' => array(
- 'name' => 'Minimum Supporters',
- 'title' => 'The number of people that need to have supported a project at minimum.
+
+class LegoIdeasBridge extends BridgeAbstract
+{
+ const NAME = 'Lego Ideas';
+ const URI = 'https://ideas.lego.com/';
+ const DESCRIPTION = 'Community Supported Lego Builds';
+ const MAINTAINER = 'sal0max';
+ const CACHE_TIMEOUT = 60 * 60 * 2; // 2h
+ const PARAMETERS = [ [
+ 'support_value_min' => [
+ 'name' => 'Minimum Supporters',
+ 'title' => 'The number of people that need to have supported a project at minimum.
Once a project reaches 10,000 supporters, it gets reviewed by the lego experts.',
- 'type' => 'number',
- 'defaultValue' => 1000
- ),
- 'idea_phase' => array(
- 'name' => 'Idea Phase',
- 'type' => 'list',
- 'values' => array(
- 'Gathering Support' => 'idea_gathering_support',
- 'Achieved Support' => 'idea_achieved_support',
- 'In Review' => 'idea_in_review',
- 'Approved Ideas' => 'idea_idea_approved',
- 'Not Approved Ideas' => 'idea_idea_not_approved',
- 'On Shelves' => 'idea_on_shelves',
- 'Expired Ideas' => 'idea_expired_ideas',
- ),
- 'defaultValue' => 'idea_gathering_support'
- )
- )
- );
+ 'type' => 'number',
+ 'defaultValue' => 1000
+ ],
+ 'idea_phase' => [
+ 'name' => 'Idea Phase',
+ 'type' => 'list',
+ 'values' => [
+ 'Gathering Support' => 'idea_gathering_support',
+ 'Achieved Support' => 'idea_achieved_support',
+ 'In Review' => 'idea_in_review',
+ 'Approved Ideas' => 'idea_idea_approved',
+ 'Not Approved Ideas' => 'idea_idea_not_approved',
+ 'On Shelves' => 'idea_on_shelves',
+ 'Expired Ideas' => 'idea_expired_ideas',
+ ],
+ 'defaultValue' => 'idea_gathering_support'
+ ]
+ ]
+ ];
- public function getURI() {
- // link to the corresponding page on the website, not the api endpoint
- return self::URI . 'search/global_search/ideas'
- . "?support_value={$this->getInput('support_value_min')}"
- . '&support_value=10000'
- . "&idea_phase={$this->getInput('idea_phase')}"
- . '&sort=most_recent';
- }
+ public function getURI()
+ {
+ // link to the corresponding page on the website, not the api endpoint
+ return self::URI . 'search/global_search/ideas'
+ . "?support_value={$this->getInput('support_value_min')}"
+ . '&support_value=10000'
+ . "&idea_phase={$this->getInput('idea_phase')}"
+ . '&sort=most_recent';
+ }
- public function collectData() {
- $header = array(
- 'Content-Type: application/json',
- 'Accept: application/json'
- );
- $opts = array(
- CURLOPT_POST => 1,
- CURLOPT_POSTFIELDS => $this->getHttpPostData()
- );
- $responseData = getContents($this->getHttpPostURI(), $header, $opts) or
- returnServerError('Unable to query Lego Ideas API.');
+ public function collectData()
+ {
+ $header = [
+ 'Content-Type: application/json',
+ 'Accept: application/json'
+ ];
+ $opts = [
+ CURLOPT_POST => 1,
+ CURLOPT_POSTFIELDS => $this->getHttpPostData()
+ ];
+ $responseData = getContents($this->getHttpPostURI(), $header, $opts) or
+ returnServerError('Unable to query Lego Ideas API.');
- foreach (json_decode($responseData)->results as $project) {
- preg_match('/datetime=\"(\S+)\"/', $project->entity->published_at, $date_matches);
- $datetime = $date_matches[1];
- $link = self::URI . $project->entity->view_url;
- $title = $project->entity->title;
- $desc = $project->entity->content;
- $imageUrl = $project->entity->image_url;
- $creator = $project->entity->creator->alias;
- $uuid = $project->entity->uuid;
+ foreach (json_decode($responseData)->results as $project) {
+ preg_match('/datetime=\"(\S+)\"/', $project->entity->published_at, $date_matches);
+ $datetime = $date_matches[1];
+ $link = self::URI . $project->entity->view_url;
+ $title = $project->entity->title;
+ $desc = $project->entity->content;
+ $imageUrl = $project->entity->image_url;
+ $creator = $project->entity->creator->alias;
+ $uuid = $project->entity->uuid;
- $item = array(
- 'uri' => $link,
- 'title' => $title,
- 'timestamp' => strtotime($datetime),
- 'author' => $creator,
- 'content' => <<<EOD
+ $item = [
+ 'uri' => $link,
+ 'title' => $title,
+ 'timestamp' => strtotime($datetime),
+ 'author' => $creator,
+ 'content' => <<<EOD
<p><img src="{$imageUrl}" alt="{$title}"/></p>
<p>{$desc}</p>
EOD
- );
- $this->items[] = $item;
- }
- }
-
- /**
- * Returns the API endpoint
- */
- private function getHttpPostURI() {
- return self::URI . '/search/global_search/ideas';
- }
+ ];
+ $this->items[] = $item;
+ }
+ }
- /**
- * Returns the API query
- */
- private function getHttpPostData() {
+ /**
+ * Returns the API endpoint
+ */
+ private function getHttpPostURI()
+ {
+ return self::URI . '/search/global_search/ideas';
+ }
- $phase = $this->getInput('idea_phase');
- $minSupporters = $this->getInput('support_value_min');
+ /**
+ * Returns the API query
+ */
+ private function getHttpPostData()
+ {
+ $phase = $this->getInput('idea_phase');
+ $minSupporters = $this->getInput('support_value_min');
- return <<<EOD
+ return <<<EOD
{ "filters": {
"idea_phase": [ "$phase" ],
"support_value": [ $minSupporters, 10000 ]
@@ -98,5 +103,5 @@ EOD
"sort": [ "most_recent:desc" ]
}
EOD;
- }
+ }
}
diff --git a/bridges/LesJoiesDuCodeBridge.php b/bridges/LesJoiesDuCodeBridge.php
index 3f62de9b..a2a5e4b6 100644
--- a/bridges/LesJoiesDuCodeBridge.php
+++ b/bridges/LesJoiesDuCodeBridge.php
@@ -1,36 +1,38 @@
<?php
-class LesJoiesDuCodeBridge extends BridgeAbstract {
- const MAINTAINER = 'superbaillot.net';
- const NAME = 'Les Joies Du Code';
- const URI = 'https://lesjoiesducode.fr/';
- const CACHE_TIMEOUT = 7200; // 2h
- const DESCRIPTION = 'LesJoiesDuCode';
+class LesJoiesDuCodeBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'superbaillot.net';
+ const NAME = 'Les Joies Du Code';
+ const URI = 'https://lesjoiesducode.fr/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'LesJoiesDuCode';
- public function collectData(){
- $html = getSimpleHTMLDOM(self::URI);
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
- foreach($html->find('article.blog-post') as $element) {
- $item = array();
- $temp = $element->find('h1 a', 0);
- $titre = html_entity_decode($temp->innertext);
- $url = $temp->href;
+ foreach ($html->find('article.blog-post') as $element) {
+ $item = [];
+ $temp = $element->find('h1 a', 0);
+ $titre = html_entity_decode($temp->innertext);
+ $url = $temp->href;
- $temp = $element->find('div.blog-post-content', 0);
+ $temp = $element->find('div.blog-post-content', 0);
- // retrieve .gif instead of static .jpg
- $images = $temp->find('p img');
- foreach($images as $image) {
- $img_src = str_replace('.jpg', '.gif', $image->src);
- $image->src = $img_src;
- }
- $content = $temp->innertext;
+ // retrieve .gif instead of static .jpg
+ $images = $temp->find('p img');
+ foreach ($images as $image) {
+ $img_src = str_replace('.jpg', '.gif', $image->src);
+ $image->src = $img_src;
+ }
+ $content = $temp->innertext;
- $item['content'] = trim($content);
- $item['uri'] = $url;
- $item['title'] = trim($titre);
+ $item['content'] = trim($content);
+ $item['uri'] = $url;
+ $item['title'] = trim($titre);
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/ListverseBridge.php b/bridges/ListverseBridge.php
index f597c0b4..ba6d7397 100644
--- a/bridges/ListverseBridge.php
+++ b/bridges/ListverseBridge.php
@@ -1,22 +1,25 @@
<?php
-class ListverseBridge extends FeedExpander {
- const MAINTAINER = 'IceWreck';
- const NAME = 'Listverse Bridge';
- const URI = 'https://listverse.com/';
- const CACHE_TIMEOUT = 3600;
- const DESCRIPTION = 'RSS feed for Listverse';
+class ListverseBridge extends FeedExpander
+{
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'Listverse Bridge';
+ const URI = 'https://listverse.com/';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'RSS feed for Listverse';
- public function collectData(){
- $this->collectExpandableDatas('https://listverse.com/feed/', 15);
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas('https://listverse.com/feed/', 15);
+ }
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- // $articlePage gets the entire page's contents
- $articlePage = getSimpleHTMLDOM($newsItem->link);
- $article = $articlePage->find('#articlecontentonly', 0);
- $item['content'] = $article;
- return $item;
- }
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+ $article = $articlePage->find('#articlecontentonly', 0);
+ $item['content'] = $article;
+ return $item;
+ }
}
diff --git a/bridges/LolibooruBridge.php b/bridges/LolibooruBridge.php
index fb4689be..92d5fe9e 100644
--- a/bridges/LolibooruBridge.php
+++ b/bridges/LolibooruBridge.php
@@ -1,10 +1,9 @@
<?php
-class LolibooruBridge extends MoebooruBridge {
-
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Lolibooru';
- const URI = 'https://lolibooru.moe/';
- const DESCRIPTION = 'Returns images from given page and tags';
-
+class LolibooruBridge extends MoebooruBridge
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Lolibooru';
+ const URI = 'https://lolibooru.moe/';
+ const DESCRIPTION = 'Returns images from given page and tags';
}
diff --git a/bridges/MallTvBridge.php b/bridges/MallTvBridge.php
index 4556a62f..93a07c25 100644
--- a/bridges/MallTvBridge.php
+++ b/bridges/MallTvBridge.php
@@ -1,71 +1,76 @@
<?php
-class MallTvBridge extends BridgeAbstract {
-
- const NAME = 'MALL.TV Bridge';
- const URI = 'https://www.mall.tv';
- const CACHE_TIMEOUT = 3600;
- const DESCRIPTION = 'Return newest videos';
- const MAINTAINER = 'kolarcz';
-
- const PARAMETERS = array(
- array(
- 'url' => array(
- 'name' => 'url to the show',
- 'required' => true,
- 'exampleValue' => 'https://www.mall.tv/zivot-je-hra'
- )
- )
- );
-
- private function fixChars($text) {
- return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
- }
-
- private function getUploadTimeFromUrl($url) {
- $html = getSimpleHTMLDOM($url);
-
- $scriptLdJson = $html->find('script[type="application/ld+json"]', 0)->innertext;
- if (!preg_match('/[\'"]uploadDate[\'"]\s*:\s*[\'"](\d{4}-\d{2}-\d{2})[\'"]/', $scriptLdJson, $match)) {
- returnServerError('Could not get date from MALL.TV detail page');
- }
-
- return strtotime($match[1]);
- }
-
- public function collectData() {
- $url = $this->getInput('url');
-
- if (!preg_match('/^https:\/\/www\.mall\.tv\/[a-z0-9-]+(\/[a-z0-9-]+)?\/?$/', $url)) {
- returnServerError('Invalid url');
- }
-
- $html = getSimpleHTMLDOM($url);
-
- $this->feedUri = $url;
- $this->feedName = $this->fixChars($html->find('title', 0)->plaintext);
-
- foreach ($html->find('section.isVideo .video-card') as $element) {
- $itemTitle = $element->find('.video-card__details-link', 0);
- $itemThumbnail = $element->find('.video-card__thumbnail', 0);
- $itemUri = self::URI . $itemTitle->getAttribute('href');
-
- $item = array(
- 'title' => $this->fixChars($itemTitle->plaintext),
- 'uri' => $itemUri,
- 'content' => '<img src="' . $itemThumbnail->getAttribute('data-src') . '" />',
- 'timestamp' => $this->getUploadTimeFromUrl($itemUri)
- );
-
- $this->items[] = $item;
- }
- }
-
- public function getURI() {
- return isset($this->feedUri) ? $this->feedUri : parent::getURI();
- }
-
- public function getName() {
- return isset($this->feedName) ? $this->feedName : parent::getName();
- }
+class MallTvBridge extends BridgeAbstract
+{
+ const NAME = 'MALL.TV Bridge';
+ const URI = 'https://www.mall.tv';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'Return newest videos';
+ const MAINTAINER = 'kolarcz';
+
+ const PARAMETERS = [
+ [
+ 'url' => [
+ 'name' => 'url to the show',
+ 'required' => true,
+ 'exampleValue' => 'https://www.mall.tv/zivot-je-hra'
+ ]
+ ]
+ ];
+
+ private function fixChars($text)
+ {
+ return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
+ }
+
+ private function getUploadTimeFromUrl($url)
+ {
+ $html = getSimpleHTMLDOM($url);
+
+ $scriptLdJson = $html->find('script[type="application/ld+json"]', 0)->innertext;
+ if (!preg_match('/[\'"]uploadDate[\'"]\s*:\s*[\'"](\d{4}-\d{2}-\d{2})[\'"]/', $scriptLdJson, $match)) {
+ returnServerError('Could not get date from MALL.TV detail page');
+ }
+
+ return strtotime($match[1]);
+ }
+
+ public function collectData()
+ {
+ $url = $this->getInput('url');
+
+ if (!preg_match('/^https:\/\/www\.mall\.tv\/[a-z0-9-]+(\/[a-z0-9-]+)?\/?$/', $url)) {
+ returnServerError('Invalid url');
+ }
+
+ $html = getSimpleHTMLDOM($url);
+
+ $this->feedUri = $url;
+ $this->feedName = $this->fixChars($html->find('title', 0)->plaintext);
+
+ foreach ($html->find('section.isVideo .video-card') as $element) {
+ $itemTitle = $element->find('.video-card__details-link', 0);
+ $itemThumbnail = $element->find('.video-card__thumbnail', 0);
+ $itemUri = self::URI . $itemTitle->getAttribute('href');
+
+ $item = [
+ 'title' => $this->fixChars($itemTitle->plaintext),
+ 'uri' => $itemUri,
+ 'content' => '<img src="' . $itemThumbnail->getAttribute('data-src') . '" />',
+ 'timestamp' => $this->getUploadTimeFromUrl($itemUri)
+ ];
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ return isset($this->feedUri) ? $this->feedUri : parent::getURI();
+ }
+
+ public function getName()
+ {
+ return isset($this->feedName) ? $this->feedName : parent::getName();
+ }
}
diff --git a/bridges/MangaDexBridge.php b/bridges/MangaDexBridge.php
index 6cfd18e9..143cd732 100644
--- a/bridges/MangaDexBridge.php
+++ b/bridges/MangaDexBridge.php
@@ -1,232 +1,245 @@
<?php
-class MangaDexBridge extends BridgeAbstract {
- const NAME = 'MangaDex Bridge';
- const URI = 'https://mangadex.org/';
- const API_ROOT = 'https://api.mangadex.org/';
- const DESCRIPTION = 'Returns MangaDex items using the API';
-
- const PARAMETERS = array(
- 'global' => array(
- 'limit' => array(
- 'name' => 'Item Limit',
- 'type' => 'number',
- 'defaultValue' => 10,
- 'required' => true
- ),
- 'lang' => array(
- 'name' => 'Chapter Languages (default=all)',
- 'title' => 'comma-separated, two-letter language codes (example "en,jp")',
- 'exampleValue' => 'en,jp',
- 'required' => false
- ),
- ),
- 'Title Chapters' => array(
- 'url' => array(
- 'name' => 'URL to title page',
- 'exampleValue' => 'https://mangadex.org/title/f9c33607-9180-4ba6-b85c-e4b5faee7192/official-test-manga',
- 'required' => true
- ),
- 'external' => array(
- 'name' => 'Allow external feed items',
- 'type' => 'checkbox',
- 'title' => 'Some chapters are inaccessible or only available on an external site. Include these?'
- )
- ),
- 'Search Chapters' => array(
- 'chapter' => array(
- 'name' => 'Chapter Number (default=all)',
- 'title' => 'The example value finds the newest first chapters',
- 'exampleValue' => 1,
- 'required' => false
- ),
- 'groups' => array(
- 'name' => 'Group UUID (default=all)',
- 'title' => 'This can be found in the MangaDex Group Page URL',
- 'exampleValue' => '00e03853-1b96-4f41-9542-c71b8692033b',
- 'required' => false,
- ),
- 'uploader' => array(
- 'name' => 'User UUID (default=all)',
- 'title' => 'This can be found in the MangaDex User Page URL',
- 'exampleValue' => 'd2ae45e0-b5e2-4e7f-a688-17925c2d7d6b',
- 'required' => false,
- ),
- 'external' => array(
- 'name' => 'Allow external feed items',
- 'type' => 'checkbox',
- 'title' => 'Some chapters are inaccessible or only available on an external site. Include these?'
- )
- )
- // Future Manga Contexts:
- // Manga List (by author or tags): https://api.mangadex.org/swagger.html#/Manga/get-search-manga
- // Random Manga: https://api.mangadex.org/swagger.html#/Manga/get-manga-random
- // Future Chapter Contexts:
- // User Lists https://api.mangadex.org/swagger.html#/Feed/get-list-id-feed
- //
- // https://api.mangadex.org/docs/get-covers/
- );
-
- const TITLE_REGEX = '#title/(?<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})#';
-
- protected $feedName = '';
- protected $feedURI = '';
-
- protected function buildArrayQuery($name, $array) {
- $query = '';
- foreach($array as $item) {
- $query .= '&' . $name . '=' . $item;
- }
- return $query;
- }
-
- protected function getAPI() {
- $params = array(
- 'limit' => $this->getInput('limit')
- );
-
- $array_params = array();
- if (!empty($this->getInput('lang'))) {
- $array_params['translatedLanguage[]'] = explode(',', $this->getInput('lang'));
- }
-
- switch($this->queriedContext) {
- case 'Title Chapters':
- preg_match(self::TITLE_REGEX, $this->getInput('url'), $matches)
- or returnClientError('Invalid URL Parameter');
- $this->feedURI = self::URI . 'title/' . $matches['uuid'];
- $params['order[readableAt]'] = 'desc';
- if (!$this->getInput('external')) {
- $params['includeFutureUpdates'] = '0';
- }
- $array_params['includes[]'] = array('manga', 'scanlation_group', 'user');
- $uri = self::API_ROOT . 'manga/' . $matches['uuid'] . '/feed';
- break;
- case 'Search Chapters':
- $params['chapter'] = $this->getInput('chapter');
- $params['groups[]'] = $this->getInput('groups');
- $params['uploader'] = $this->getInput('uploader');
- $params['order[readableAt]'] = 'desc';
- if (!$this->getInput('external')) {
- $params['includeFutureUpdates'] = '0';
- }
- $array_params['includes[]'] = array('manga', 'scanlation_group', 'user');
- $uri = self::API_ROOT . 'chapter';
- break;
- default:
- returnServerError('Unimplemented Context (getAPI)');
- }
-
- // Remove null keys
- $params = array_filter($params, function($v) {
- return !empty($v);
- });
-
- $uri .= '?' . http_build_query($params);
-
- // Arrays are passed as repeated keys to MangaDex
- // This cannot be handled by http_build_query
- foreach($array_params as $name => $array_param) {
- $uri .= $this->buildArrayQuery($name, $array_param);
- }
-
- return $uri;
- }
-
- public function getName() {
- switch($this->queriedContext) {
- case 'Title Chapters':
- return $this->feedName . ' Chapters';
- case 'Search Chapters':
- return 'MangaDex Chapter Search';
- default:
- return parent::getName();
- }
- }
-
- public function getURI() {
- switch($this->queriedContext) {
- case 'Title Chapters':
- return $this->feedURI;
- default:
- return parent::getURI();
- }
- }
-
- public function collectData() {
- $api_uri = $this->getAPI();
- $header = array(
- 'Content-Type: application/json'
- );
- $content = json_decode(getContents($api_uri, $header), true);
- if ($content['result'] == 'ok') {
- $content = $content['data'];
- } else {
- returnServerError('Could not retrieve API results');
- }
-
- switch($this->queriedContext) {
- case 'Title Chapters':
- $this->getChapters($content);
- break;
- case 'Search Chapters':
- $this->getChapters($content);
- break;
- default:
- returnServerError('Unimplemented Context (collectData)');
- }
- }
-
- protected function getChapters($content) {
- foreach($content as $chapter) {
- $item = array();
- $item['uid'] = $chapter['id'];
- $item['uri'] = self::URI . 'chapter/' . $chapter['id'];
-
- // External chapter
- if (!$this->getInput('external') && $chapter['attributes']['pages'] == 0)
- continue;
-
- $item['title'] = '';
- if (isset($chapter['attributes']['volume']))
- $item['title'] .= 'Volume ' . $chapter['attributes']['volume'] . ' ';
- if (isset($chapter['attributes']['chapter']))
- $item['title'] .= 'Chapter ' . $chapter['attributes']['chapter'];
- if (!empty($chapter['attributes']['title'])) {
- $item['title'] .= ' - ' . $chapter['attributes']['title'];
- }
- $item['title'] .= ' [' . $chapter['attributes']['translatedLanguage'] . ']';
-
- $item['timestamp'] = $chapter['attributes']['readableAt'];
-
- $groups = array();
- $users = array();
- foreach($chapter['relationships'] as $rel) {
- switch($rel['type']) {
- case 'scanlation_group':
- $groups[] = $rel['attributes']['name'];
- break;
- case 'manga':
- if (empty($this->feedName))
- $this->feedName = reset($rel['attributes']['title']);
- if ($this->queriedContext !== 'Title Chapters')
- $item['title'] = reset($rel['attributes']['title']) . ' ' . $item['title'];
- break;
- case 'user':
- if (isset($item['author'])) {
- $users[] = $rel['attributes']['username'];
- } else {
- $item['author'] = $rel['attributes']['username'];
- }
- break;
- }
- }
- $item['content'] = 'Groups: ' .
- (empty($groups) ? 'No Group' : implode(', ', $groups));
- if (!empty($users)) {
- $item['content'] .= '<br>Other Users: ' . implode(', ', $users);
- }
-
- $this->items[] = $item;
- }
- }
+
+class MangaDexBridge extends BridgeAbstract
+{
+ const NAME = 'MangaDex Bridge';
+ const URI = 'https://mangadex.org/';
+ const API_ROOT = 'https://api.mangadex.org/';
+ const DESCRIPTION = 'Returns MangaDex items using the API';
+
+ const PARAMETERS = [
+ 'global' => [
+ 'limit' => [
+ 'name' => 'Item Limit',
+ 'type' => 'number',
+ 'defaultValue' => 10,
+ 'required' => true
+ ],
+ 'lang' => [
+ 'name' => 'Chapter Languages (default=all)',
+ 'title' => 'comma-separated, two-letter language codes (example "en,jp")',
+ 'exampleValue' => 'en,jp',
+ 'required' => false
+ ],
+ ],
+ 'Title Chapters' => [
+ 'url' => [
+ 'name' => 'URL to title page',
+ 'exampleValue' => 'https://mangadex.org/title/f9c33607-9180-4ba6-b85c-e4b5faee7192/official-test-manga',
+ 'required' => true
+ ],
+ 'external' => [
+ 'name' => 'Allow external feed items',
+ 'type' => 'checkbox',
+ 'title' => 'Some chapters are inaccessible or only available on an external site. Include these?'
+ ]
+ ],
+ 'Search Chapters' => [
+ 'chapter' => [
+ 'name' => 'Chapter Number (default=all)',
+ 'title' => 'The example value finds the newest first chapters',
+ 'exampleValue' => 1,
+ 'required' => false
+ ],
+ 'groups' => [
+ 'name' => 'Group UUID (default=all)',
+ 'title' => 'This can be found in the MangaDex Group Page URL',
+ 'exampleValue' => '00e03853-1b96-4f41-9542-c71b8692033b',
+ 'required' => false,
+ ],
+ 'uploader' => [
+ 'name' => 'User UUID (default=all)',
+ 'title' => 'This can be found in the MangaDex User Page URL',
+ 'exampleValue' => 'd2ae45e0-b5e2-4e7f-a688-17925c2d7d6b',
+ 'required' => false,
+ ],
+ 'external' => [
+ 'name' => 'Allow external feed items',
+ 'type' => 'checkbox',
+ 'title' => 'Some chapters are inaccessible or only available on an external site. Include these?'
+ ]
+ ]
+ // Future Manga Contexts:
+ // Manga List (by author or tags): https://api.mangadex.org/swagger.html#/Manga/get-search-manga
+ // Random Manga: https://api.mangadex.org/swagger.html#/Manga/get-manga-random
+ // Future Chapter Contexts:
+ // User Lists https://api.mangadex.org/swagger.html#/Feed/get-list-id-feed
+ //
+ // https://api.mangadex.org/docs/get-covers/
+ ];
+
+ const TITLE_REGEX = '#title/(?<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})#';
+
+ protected $feedName = '';
+ protected $feedURI = '';
+
+ protected function buildArrayQuery($name, $array)
+ {
+ $query = '';
+ foreach ($array as $item) {
+ $query .= '&' . $name . '=' . $item;
+ }
+ return $query;
+ }
+
+ protected function getAPI()
+ {
+ $params = [
+ 'limit' => $this->getInput('limit')
+ ];
+
+ $array_params = [];
+ if (!empty($this->getInput('lang'))) {
+ $array_params['translatedLanguage[]'] = explode(',', $this->getInput('lang'));
+ }
+
+ switch ($this->queriedContext) {
+ case 'Title Chapters':
+ preg_match(self::TITLE_REGEX, $this->getInput('url'), $matches)
+ or returnClientError('Invalid URL Parameter');
+ $this->feedURI = self::URI . 'title/' . $matches['uuid'];
+ $params['order[readableAt]'] = 'desc';
+ if (!$this->getInput('external')) {
+ $params['includeFutureUpdates'] = '0';
+ }
+ $array_params['includes[]'] = ['manga', 'scanlation_group', 'user'];
+ $uri = self::API_ROOT . 'manga/' . $matches['uuid'] . '/feed';
+ break;
+ case 'Search Chapters':
+ $params['chapter'] = $this->getInput('chapter');
+ $params['groups[]'] = $this->getInput('groups');
+ $params['uploader'] = $this->getInput('uploader');
+ $params['order[readableAt]'] = 'desc';
+ if (!$this->getInput('external')) {
+ $params['includeFutureUpdates'] = '0';
+ }
+ $array_params['includes[]'] = ['manga', 'scanlation_group', 'user'];
+ $uri = self::API_ROOT . 'chapter';
+ break;
+ default:
+ returnServerError('Unimplemented Context (getAPI)');
+ }
+
+ // Remove null keys
+ $params = array_filter($params, function ($v) {
+ return !empty($v);
+ });
+
+ $uri .= '?' . http_build_query($params);
+
+ // Arrays are passed as repeated keys to MangaDex
+ // This cannot be handled by http_build_query
+ foreach ($array_params as $name => $array_param) {
+ $uri .= $this->buildArrayQuery($name, $array_param);
+ }
+
+ return $uri;
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Title Chapters':
+ return $this->feedName . ' Chapters';
+ case 'Search Chapters':
+ return 'MangaDex Chapter Search';
+ default:
+ return parent::getName();
+ }
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Title Chapters':
+ return $this->feedURI;
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function collectData()
+ {
+ $api_uri = $this->getAPI();
+ $header = [
+ 'Content-Type: application/json'
+ ];
+ $content = json_decode(getContents($api_uri, $header), true);
+ if ($content['result'] == 'ok') {
+ $content = $content['data'];
+ } else {
+ returnServerError('Could not retrieve API results');
+ }
+
+ switch ($this->queriedContext) {
+ case 'Title Chapters':
+ $this->getChapters($content);
+ break;
+ case 'Search Chapters':
+ $this->getChapters($content);
+ break;
+ default:
+ returnServerError('Unimplemented Context (collectData)');
+ }
+ }
+
+ protected function getChapters($content)
+ {
+ foreach ($content as $chapter) {
+ $item = [];
+ $item['uid'] = $chapter['id'];
+ $item['uri'] = self::URI . 'chapter/' . $chapter['id'];
+
+ // External chapter
+ if (!$this->getInput('external') && $chapter['attributes']['pages'] == 0) {
+ continue;
+ }
+
+ $item['title'] = '';
+ if (isset($chapter['attributes']['volume'])) {
+ $item['title'] .= 'Volume ' . $chapter['attributes']['volume'] . ' ';
+ }
+ if (isset($chapter['attributes']['chapter'])) {
+ $item['title'] .= 'Chapter ' . $chapter['attributes']['chapter'];
+ }
+ if (!empty($chapter['attributes']['title'])) {
+ $item['title'] .= ' - ' . $chapter['attributes']['title'];
+ }
+ $item['title'] .= ' [' . $chapter['attributes']['translatedLanguage'] . ']';
+
+ $item['timestamp'] = $chapter['attributes']['readableAt'];
+
+ $groups = [];
+ $users = [];
+ foreach ($chapter['relationships'] as $rel) {
+ switch ($rel['type']) {
+ case 'scanlation_group':
+ $groups[] = $rel['attributes']['name'];
+ break;
+ case 'manga':
+ if (empty($this->feedName)) {
+ $this->feedName = reset($rel['attributes']['title']);
+ }
+ if ($this->queriedContext !== 'Title Chapters') {
+ $item['title'] = reset($rel['attributes']['title']) . ' ' . $item['title'];
+ }
+ break;
+ case 'user':
+ if (isset($item['author'])) {
+ $users[] = $rel['attributes']['username'];
+ } else {
+ $item['author'] = $rel['attributes']['username'];
+ }
+ break;
+ }
+ }
+ $item['content'] = 'Groups: ' .
+ (empty($groups) ? 'No Group' : implode(', ', $groups));
+ if (!empty($users)) {
+ $item['content'] .= '<br>Other Users: ' . implode(', ', $users);
+ }
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/MarktplaatsBridge.php b/bridges/MarktplaatsBridge.php
index ccbb64f9..136b85b4 100644
--- a/bridges/MarktplaatsBridge.php
+++ b/bridges/MarktplaatsBridge.php
@@ -1,130 +1,133 @@
<?php
-class MarktplaatsBridge extends BridgeAbstract {
- const NAME = 'Marktplaats';
- const URI = 'https://marktplaats.nl';
- const DESCRIPTION = 'Read search queries from marktplaats.nl';
- const PARAMETERS = array(
- 'Search' => array(
- 'q' => array(
- 'name' => 'query',
- 'type' => 'text',
- 'exampleValue' => 'lamp',
- 'required' => true,
- 'title' => 'The search string for marktplaats',
- ),
- 'z' => array(
- 'name' => 'zipcode',
- 'type' => 'text',
- 'required' => false,
- 'exampleValue' => '1013AA',
- 'title' => 'Zip code for location limited searches',
- ),
- 'd' => array(
- 'name' => 'distance',
- 'type' => 'number',
- 'required' => false,
- 'exampleValue' => '100000',
- 'title' => 'The distance in meters from the zipcode',
- ),
- 'f' => array(
- 'name' => 'priceFrom',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'The minimal price in cents',
- ),
- 't' => array(
- 'name' => 'priceTo',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'The maximal price in cents',
- ),
- 's' => array(
- 'name' => 'showGlobal',
- 'type' => 'checkbox',
- 'required' => false,
- 'title' => 'Include result with negative distance',
- ),
- 'i' => array(
- 'name' => 'includeImage',
- 'type' => 'checkbox',
- 'required' => false,
- 'title' => 'Include the image at the end of the content',
- ),
- 'r' => array(
- 'name' => 'includeRaw',
- 'type' => 'checkbox',
- 'required' => false,
- 'title' => 'Include the raw data behind the content',
- )
- )
- );
- const CACHE_TIMEOUT = 900;
+class MarktplaatsBridge extends BridgeAbstract
+{
+ const NAME = 'Marktplaats';
+ const URI = 'https://marktplaats.nl';
+ const DESCRIPTION = 'Read search queries from marktplaats.nl';
+ const PARAMETERS = [
+ 'Search' => [
+ 'q' => [
+ 'name' => 'query',
+ 'type' => 'text',
+ 'exampleValue' => 'lamp',
+ 'required' => true,
+ 'title' => 'The search string for marktplaats',
+ ],
+ 'z' => [
+ 'name' => 'zipcode',
+ 'type' => 'text',
+ 'required' => false,
+ 'exampleValue' => '1013AA',
+ 'title' => 'Zip code for location limited searches',
+ ],
+ 'd' => [
+ 'name' => 'distance',
+ 'type' => 'number',
+ 'required' => false,
+ 'exampleValue' => '100000',
+ 'title' => 'The distance in meters from the zipcode',
+ ],
+ 'f' => [
+ 'name' => 'priceFrom',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'The minimal price in cents',
+ ],
+ 't' => [
+ 'name' => 'priceTo',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'The maximal price in cents',
+ ],
+ 's' => [
+ 'name' => 'showGlobal',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'title' => 'Include result with negative distance',
+ ],
+ 'i' => [
+ 'name' => 'includeImage',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'title' => 'Include the image at the end of the content',
+ ],
+ 'r' => [
+ 'name' => 'includeRaw',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'title' => 'Include the raw data behind the content',
+ ]
+ ]
+ ];
+ const CACHE_TIMEOUT = 900;
- public function collectData() {
- $query = '';
- $excludeGlobal = false;
- if(!is_null($this->getInput('z')) && !is_null($this->getInput('d'))) {
- $query = '&postcode=' . $this->getInput('z') . '&distanceMeters=' . $this->getInput('d');
- }
- if(!is_null($this->getInput('f'))) {
- $query .= '&PriceCentsFrom=' . $this->getInput('f');
- }
- if(!is_null($this->getInput('t'))) {
- $query .= '&PriceCentsTo=' . $this->getInput('t');
- }
- if(!is_null($this->getInput('s'))) {
- if(!$this->getInput('s')) {
- $excludeGlobal = true;
- }
- }
- $url = 'https://www.marktplaats.nl/lrp/api/search?query=' . urlencode($this->getInput('q')) . $query;
- $jsonString = getSimpleHTMLDOM($url);
- $jsonObj = json_decode($jsonString);
- foreach($jsonObj->listings as $listing) {
- if(!$excludeGlobal || $listing->location->distanceMeters >= 0) {
- $item = array();
- $item['uri'] = 'https://marktplaats.nl' . $listing->vipUrl;
- $item['title'] = $listing->title;
- $item['timestamp'] = $listing->date;
- $item['author'] = $listing->sellerInformation->sellerName;
- $item['content'] = $listing->description;
- $item['categories'] = $listing->verticals;
- $item['uid'] = $listing->itemId;
- if(!is_null($this->getInput('i')) && !empty($listing->imageUrls)) {
- $item['enclosures'] = $listing->imageUrls;
- if(is_array($listing->imageUrls)) {
- foreach($listing->imageUrls as $imgurl) {
- $item['content'] .= "<br />\n<img src='https:" . $imgurl . "' />";
- }
- } else {
- $item['content'] .= "<br>\n<img src='https:" . $listing->imageUrls . "' />";
- }
- }
- if(!is_null($this->getInput('r'))) {
- if($this->getInput('r')) {
- $item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing);
- }
- }
- $item['content'] .= "<br>\n<br>\nPrice: " . $listing->priceInfo->priceCents / 100;
- $item['content'] .= '&nbsp;&nbsp;(' . $listing->priceInfo->priceType . ')';
- if(!empty($listing->location->cityName)) {
- $item['content'] .= "<br><br>\n" . $listing->location->cityName;
- }
- if(!is_null($this->getInput('r'))) {
- if($this->getInput('r')) {
- $item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing);
- }
- }
- $this->items[] = $item;
- }
- }
- }
+ public function collectData()
+ {
+ $query = '';
+ $excludeGlobal = false;
+ if (!is_null($this->getInput('z')) && !is_null($this->getInput('d'))) {
+ $query = '&postcode=' . $this->getInput('z') . '&distanceMeters=' . $this->getInput('d');
+ }
+ if (!is_null($this->getInput('f'))) {
+ $query .= '&PriceCentsFrom=' . $this->getInput('f');
+ }
+ if (!is_null($this->getInput('t'))) {
+ $query .= '&PriceCentsTo=' . $this->getInput('t');
+ }
+ if (!is_null($this->getInput('s'))) {
+ if (!$this->getInput('s')) {
+ $excludeGlobal = true;
+ }
+ }
+ $url = 'https://www.marktplaats.nl/lrp/api/search?query=' . urlencode($this->getInput('q')) . $query;
+ $jsonString = getSimpleHTMLDOM($url);
+ $jsonObj = json_decode($jsonString);
+ foreach ($jsonObj->listings as $listing) {
+ if (!$excludeGlobal || $listing->location->distanceMeters >= 0) {
+ $item = [];
+ $item['uri'] = 'https://marktplaats.nl' . $listing->vipUrl;
+ $item['title'] = $listing->title;
+ $item['timestamp'] = $listing->date;
+ $item['author'] = $listing->sellerInformation->sellerName;
+ $item['content'] = $listing->description;
+ $item['categories'] = $listing->verticals;
+ $item['uid'] = $listing->itemId;
+ if (!is_null($this->getInput('i')) && !empty($listing->imageUrls)) {
+ $item['enclosures'] = $listing->imageUrls;
+ if (is_array($listing->imageUrls)) {
+ foreach ($listing->imageUrls as $imgurl) {
+ $item['content'] .= "<br />\n<img src='https:" . $imgurl . "' />";
+ }
+ } else {
+ $item['content'] .= "<br>\n<img src='https:" . $listing->imageUrls . "' />";
+ }
+ }
+ if (!is_null($this->getInput('r'))) {
+ if ($this->getInput('r')) {
+ $item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing);
+ }
+ }
+ $item['content'] .= "<br>\n<br>\nPrice: " . $listing->priceInfo->priceCents / 100;
+ $item['content'] .= '&nbsp;&nbsp;(' . $listing->priceInfo->priceType . ')';
+ if (!empty($listing->location->cityName)) {
+ $item['content'] .= "<br><br>\n" . $listing->location->cityName;
+ }
+ if (!is_null($this->getInput('r'))) {
+ if ($this->getInput('r')) {
+ $item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing);
+ }
+ }
+ $this->items[] = $item;
+ }
+ }
+ }
- public function getName(){
- if(!is_null($this->getInput('q'))) {
- return $this->getInput('q') . ' - Marktplaats';
- }
- return parent::getName();
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('q'))) {
+ return $this->getInput('q') . ' - Marktplaats';
+ }
+ return parent::getName();
+ }
}
diff --git a/bridges/MastodonBridge.php b/bridges/MastodonBridge.php
index bbbc5587..04c92ba5 100644
--- a/bridges/MastodonBridge.php
+++ b/bridges/MastodonBridge.php
@@ -1,196 +1,206 @@
<?php
-class MastodonBridge extends BridgeAbstract {
- // This script attempts to imitiate the behaviour of a read-only ActivityPub server
- // to read the outbox.
+class MastodonBridge extends BridgeAbstract
+{
+ // This script attempts to imitiate the behaviour of a read-only ActivityPub server
+ // to read the outbox.
- // Note: Most PixelFed instances have ActivityPub outbox disabled,
- // so use the official feed: https://pixelfed.instance/users/username.atom (Posts only)
+ // Note: Most PixelFed instances have ActivityPub outbox disabled,
+ // so use the official feed: https://pixelfed.instance/users/username.atom (Posts only)
- const MAINTAINER = 'Austin Huang';
- const NAME = 'ActivityPub Bridge';
- const CACHE_TIMEOUT = 900; // 15mn
- const DESCRIPTION = 'Returns recent statuses. Supports Mastodon, Pleroma and Misskey, among others. Access to
+ const MAINTAINER = 'Austin Huang';
+ const NAME = 'ActivityPub Bridge';
+ const CACHE_TIMEOUT = 900; // 15mn
+ const DESCRIPTION = 'Returns recent statuses. Supports Mastodon, Pleroma and Misskey, among others. Access to
instances that have Authorized Fetch enabled requires
<a href="https://rss-bridge.github.io/rss-bridge/Bridge_Specific/ActivityPub_(Mastodon).html">configuration</a>.';
- const URI = 'https://mastodon.social';
+ const URI = 'https://mastodon.social';
- // Some Mastodon instances use Secure Mode which requires all requests to be signed.
- // You do not need this for most instances, but if you want to support every known
- // instance, then you should configure them.
- // See also https://docs.joinmastodon.org/spec/security/#http
- const CONFIGURATION = array(
- 'private_key' => array(
- 'required' => false,
- ),
- 'key_id' => array(
- 'required' => false,
- ),
- );
+ // Some Mastodon instances use Secure Mode which requires all requests to be signed.
+ // You do not need this for most instances, but if you want to support every known
+ // instance, then you should configure them.
+ // See also https://docs.joinmastodon.org/spec/security/#http
+ const CONFIGURATION = [
+ 'private_key' => [
+ 'required' => false,
+ ],
+ 'key_id' => [
+ 'required' => false,
+ ],
+ ];
- const PARAMETERS = array(array(
- 'canusername' => array(
- 'name' => 'Canonical username',
- 'exampleValue' => '@sebsauvage@framapiaf.org',
- 'required' => true,
- ),
- 'norep' => array(
- 'name' => 'Without replies',
- 'type' => 'checkbox',
- 'title' => 'Only return statuses that are not replies, as determined by relations (not mentions).'
- ),
- 'noboost' => array(
- 'name' => 'Without boosts',
- 'required' => false,
- 'type' => 'checkbox',
- 'title' => 'Hide boosts. Note that RSS-Bridge will fetch the original status from other federated instances.'
- )
- ));
+ const PARAMETERS = [[
+ 'canusername' => [
+ 'name' => 'Canonical username',
+ 'exampleValue' => '@sebsauvage@framapiaf.org',
+ 'required' => true,
+ ],
+ 'norep' => [
+ 'name' => 'Without replies',
+ 'type' => 'checkbox',
+ 'title' => 'Only return statuses that are not replies, as determined by relations (not mentions).'
+ ],
+ 'noboost' => [
+ 'name' => 'Without boosts',
+ 'required' => false,
+ 'type' => 'checkbox',
+ 'title' => 'Hide boosts. Note that RSS-Bridge will fetch the original status from other federated instances.'
+ ]
+ ]];
- public function getName() {
- if($this->getInput('canusername')) {
- return $this->getInput('canusername');
- }
- return parent::getName();
- }
+ public function getName()
+ {
+ if ($this->getInput('canusername')) {
+ return $this->getInput('canusername');
+ }
+ return parent::getName();
+ }
- private function getInstance() {
- preg_match('/^@[a-zA-Z0-9_]+@(.+)/', $this->getInput('canusername'), $matches);
- return $matches[1];
- }
+ private function getInstance()
+ {
+ preg_match('/^@[a-zA-Z0-9_]+@(.+)/', $this->getInput('canusername'), $matches);
+ return $matches[1];
+ }
- private function getUsername() {
- preg_match('/^@([a-zA-Z_0-9_]+)@.+/', $this->getInput('canusername'), $matches);
- return $matches[1];
- }
+ private function getUsername()
+ {
+ preg_match('/^@([a-zA-Z_0-9_]+)@.+/', $this->getInput('canusername'), $matches);
+ return $matches[1];
+ }
- public function getURI(){
- if($this->getInput('canusername')) {
- // We parse webfinger to make sure the URL is correct. This is mostly because
- // MissKey uses user ID instead of the username in the endpoint, domain delegations,
- // and also to be compatible with future ActivityPub implementations.
- $resource = 'acct:' . $this->getUsername() . '@' . $this->getInstance();
- $webfingerUrl = 'https://' . $this->getInstance() . '/.well-known/webfinger?resource=' . $resource;
- $webfingerHeader = array(
- 'Content-Type: application/jrd+json'
- );
- $webfinger = json_decode(getContents($webfingerUrl, $webfingerHeader), true);
- foreach ($webfinger['links'] as $link) {
- if ($link['type'] === 'application/activity+json') {
- return $link['href'];
- }
- }
- }
+ public function getURI()
+ {
+ if ($this->getInput('canusername')) {
+ // We parse webfinger to make sure the URL is correct. This is mostly because
+ // MissKey uses user ID instead of the username in the endpoint, domain delegations,
+ // and also to be compatible with future ActivityPub implementations.
+ $resource = 'acct:' . $this->getUsername() . '@' . $this->getInstance();
+ $webfingerUrl = 'https://' . $this->getInstance() . '/.well-known/webfinger?resource=' . $resource;
+ $webfingerHeader = [
+ 'Content-Type: application/jrd+json'
+ ];
+ $webfinger = json_decode(getContents($webfingerUrl, $webfingerHeader), true);
+ foreach ($webfinger['links'] as $link) {
+ if ($link['type'] === 'application/activity+json') {
+ return $link['href'];
+ }
+ }
+ }
- return parent::getURI();
- }
+ return parent::getURI();
+ }
- public function collectData() {
- $url = $this->getURI() . '/outbox?page=true';
- $content = $this->fetchAP($url);
- if ($content['id'] === $url) {
- foreach ($content['orderedItems'] as $status) {
- $this->items[] = $this->parseItem($status);
- }
- } else {
- throw new \Exception('Unexpected response from server.');
- }
- }
+ public function collectData()
+ {
+ $url = $this->getURI() . '/outbox?page=true';
+ $content = $this->fetchAP($url);
+ if ($content['id'] === $url) {
+ foreach ($content['orderedItems'] as $status) {
+ $this->items[] = $this->parseItem($status);
+ }
+ } else {
+ throw new \Exception('Unexpected response from server.');
+ }
+ }
- protected function parseItem($content) {
- $item = array();
- switch ($content['type']) {
- case 'Announce': // boost
- if ($this->getInput('noboost')) {
- return null;
- }
- // We fetch the boosted content.
- try {
- $rtContent = $this->fetchAP($content['object']);
- $rtUser = $this->loadCacheValue($rtContent['attributedTo'], 86400);
- if (!isset($rtUser)) {
- // We fetch the author, since we cannot always assume the format of the URL.
- $user = $this->fetchAP($rtContent['attributedTo']);
- preg_match('/https?:\/\/([a-z0-9-\.]{0,})\//', $rtContent['attributedTo'], $matches);
- // We assume that the server name as indicated by the path is the actual server name,
- // since using webfinger to delegate domains is not officially supported, and it only
- // seems to work in one way.
- $rtUser = '@' . $user['preferredUsername'] . '@' . $matches[1];
- $this->saveCacheValue($rtContent['attributedTo'], $rtUser);
- }
- $item['author'] = $rtUser;
- $item['title'] = 'Shared a status by ' . $rtUser . ': ';
- $item = $this->parseObject($rtContent, $item);
- } catch (UnexpectedResponseException $th) {
- $item['title'] = 'Shared an unreachable status: ' . $content['object'];
- $item['content'] = $content['object'];
- $item['uri'] = $content['object'];
- }
- break;
- case 'Create': // posts
- if ($this->getInput('norep') && isset($content['object']['inReplyTo'])) {
- return null;
- }
- $item['author'] = $this->getInput('canusername');
- $item['title'] = '';
- $item = $this->parseObject($content['object'], $item);
- }
- $item['timestamp'] = $content['published'];
- $item['uid'] = $content['id'];
- return $item;
- }
+ protected function parseItem($content)
+ {
+ $item = [];
+ switch ($content['type']) {
+ case 'Announce': // boost
+ if ($this->getInput('noboost')) {
+ return null;
+ }
+ // We fetch the boosted content.
+ try {
+ $rtContent = $this->fetchAP($content['object']);
+ $rtUser = $this->loadCacheValue($rtContent['attributedTo'], 86400);
+ if (!isset($rtUser)) {
+ // We fetch the author, since we cannot always assume the format of the URL.
+ $user = $this->fetchAP($rtContent['attributedTo']);
+ preg_match('/https?:\/\/([a-z0-9-\.]{0,})\//', $rtContent['attributedTo'], $matches);
+ // We assume that the server name as indicated by the path is the actual server name,
+ // since using webfinger to delegate domains is not officially supported, and it only
+ // seems to work in one way.
+ $rtUser = '@' . $user['preferredUsername'] . '@' . $matches[1];
+ $this->saveCacheValue($rtContent['attributedTo'], $rtUser);
+ }
+ $item['author'] = $rtUser;
+ $item['title'] = 'Shared a status by ' . $rtUser . ': ';
+ $item = $this->parseObject($rtContent, $item);
+ } catch (UnexpectedResponseException $th) {
+ $item['title'] = 'Shared an unreachable status: ' . $content['object'];
+ $item['content'] = $content['object'];
+ $item['uri'] = $content['object'];
+ }
+ break;
+ case 'Create': // posts
+ if ($this->getInput('norep') && isset($content['object']['inReplyTo'])) {
+ return null;
+ }
+ $item['author'] = $this->getInput('canusername');
+ $item['title'] = '';
+ $item = $this->parseObject($content['object'], $item);
+ }
+ $item['timestamp'] = $content['published'];
+ $item['uid'] = $content['id'];
+ return $item;
+ }
- protected function parseObject($object, $item) {
- $item['content'] = $object['content'];
- $strippedContent = strip_tags(str_replace('<br>', ' ', $object['content']));
+ protected function parseObject($object, $item)
+ {
+ $item['content'] = $object['content'];
+ $strippedContent = strip_tags(str_replace('<br>', ' ', $object['content']));
- if (mb_strlen($strippedContent) > 75) {
- $contentSubstring = mb_substr($strippedContent, 0, mb_strpos(wordwrap($strippedContent, 75), "\n"));
- $item['title'] .= $contentSubstring . '...';
- } else {
- $item['title'] .= $strippedContent;
- }
- $item['uri'] = $object['id'];
- foreach ($object['attachment'] as $attachment) {
- // Only process REMOTE pictures (prevent xss)
- if ($attachment['mediaType']
- && preg_match('/^image\//', $attachment['mediaType'], $match)
- && preg_match('/^http(s|):\/\//', $attachment['url'], $match)
- ) {
- $item['content'] = $item['content'] . '<br /><img ';
- if ($attachment['name']) {
- $item['content'] .= sprintf('alt="%s" ', $attachment['name']);
- }
- $item['content'] .= sprintf('src="%s" />', $attachment['url']);
- }
- }
- return $item;
- }
+ if (mb_strlen($strippedContent) > 75) {
+ $contentSubstring = mb_substr($strippedContent, 0, mb_strpos(wordwrap($strippedContent, 75), "\n"));
+ $item['title'] .= $contentSubstring . '...';
+ } else {
+ $item['title'] .= $strippedContent;
+ }
+ $item['uri'] = $object['id'];
+ foreach ($object['attachment'] as $attachment) {
+ // Only process REMOTE pictures (prevent xss)
+ if (
+ $attachment['mediaType']
+ && preg_match('/^image\//', $attachment['mediaType'], $match)
+ && preg_match('/^http(s|):\/\//', $attachment['url'], $match)
+ ) {
+ $item['content'] = $item['content'] . '<br /><img ';
+ if ($attachment['name']) {
+ $item['content'] .= sprintf('alt="%s" ', $attachment['name']);
+ }
+ $item['content'] .= sprintf('src="%s" />', $attachment['url']);
+ }
+ }
+ return $item;
+ }
- protected function fetchAP($url) {
- $d = new DateTime();
- $d->setTimezone(new DateTimeZone('GMT'));
- $date = $d->format('D, d M Y H:i:s e');
- preg_match('/https?:\/\/([a-z0-9-\.]{0,})(\/[^?#]+)/', $url, $matches);
- $headers = array(
- 'Accept: application/activity+json',
- 'Host: ' . $matches[1],
- 'Date: ' . $date
- );
- $privateKey = $this->getOption('private_key');
- $keyId = $this->getOption('key_id');
- if ($privateKey && $keyId) {
- $pkey = openssl_pkey_get_private('file://' . $privateKey);
- $toSign = '(request-target): get ' . $matches[2] . "\nhost: " . $matches[1] . "\ndate: " . $date;
- $result = openssl_sign($toSign, $signature, $pkey, 'RSA-SHA256');
- if ($result) {
- Debug::log($toSign);
- $sig = 'Signature: keyId="' . $keyId . '",headers="(request-target) host date",signature="' .
- base64_encode($signature) . '"';
- Debug::log($sig);
- array_push($headers, $sig);
- }
- }
- return json_decode(getContents($url, $headers), true);
- }
+ protected function fetchAP($url)
+ {
+ $d = new DateTime();
+ $d->setTimezone(new DateTimeZone('GMT'));
+ $date = $d->format('D, d M Y H:i:s e');
+ preg_match('/https?:\/\/([a-z0-9-\.]{0,})(\/[^?#]+)/', $url, $matches);
+ $headers = [
+ 'Accept: application/activity+json',
+ 'Host: ' . $matches[1],
+ 'Date: ' . $date
+ ];
+ $privateKey = $this->getOption('private_key');
+ $keyId = $this->getOption('key_id');
+ if ($privateKey && $keyId) {
+ $pkey = openssl_pkey_get_private('file://' . $privateKey);
+ $toSign = '(request-target): get ' . $matches[2] . "\nhost: " . $matches[1] . "\ndate: " . $date;
+ $result = openssl_sign($toSign, $signature, $pkey, 'RSA-SHA256');
+ if ($result) {
+ Debug::log($toSign);
+ $sig = 'Signature: keyId="' . $keyId . '",headers="(request-target) host date",signature="' .
+ base64_encode($signature) . '"';
+ Debug::log($sig);
+ array_push($headers, $sig);
+ }
+ }
+ return json_decode(getContents($url, $headers), true);
+ }
}
diff --git a/bridges/MediapartBlogsBridge.php b/bridges/MediapartBlogsBridge.php
index b46ef2a2..fa8c3d5f 100644
--- a/bridges/MediapartBlogsBridge.php
+++ b/bridges/MediapartBlogsBridge.php
@@ -1,48 +1,53 @@
<?php
-class MediapartBlogsBridge extends BridgeAbstract {
- const NAME = 'Mediapart Blogs';
- const BASE_URI = 'https://blogs.mediapart.fr';
- const URI = self::BASE_URI . '/blogs';
- const MAINTAINER = 'somini';
- const PARAMETERS = array(
- array(
- 'slug' => array(
- 'name' => 'Blog Slug',
- 'type' => 'text',
- 'title' => 'Blog user name',
- 'required' => true,
- 'exampleValue' => 'jean-vincot',
- )
- )
- );
-
- public function getIcon() {
- return 'https://static.mediapart.fr/favicon/favicon-club.ico?v=2';
- }
-
- public function collectData() {
- $html = getSimpleHTMLDOM(self::BASE_URI . '/' . $this->getInput('slug') . '/blog');
-
- foreach($html->find('ul.post-list li') as $element) {
- $item = array();
-
- $item_title = $element->find('h3.title a', 0);
- $item_divs = $element->find('div');
-
- $item['title'] = $item_title->innertext;
- $item['uri'] = self::BASE_URI . trim($item_title->href);
- $item['author'] = $element->find('.author .subscriber', 0)->innertext;
- $item['content'] = $item_divs[count($item_divs) - 2] . $item_divs[count($item_divs) - 1];
- $item['timestamp'] = strtotime($element->find('.author time', 0)->datetime);
-
- $this->items[] = $item;
- }
- }
-
- public function getName() {
- if ($this->getInput('slug')) {
- return self::NAME . ' | ' . $this->getInput('slug');
- }
- return parent::getName();
- }
+
+class MediapartBlogsBridge extends BridgeAbstract
+{
+ const NAME = 'Mediapart Blogs';
+ const BASE_URI = 'https://blogs.mediapart.fr';
+ const URI = self::BASE_URI . '/blogs';
+ const MAINTAINER = 'somini';
+ const PARAMETERS = [
+ [
+ 'slug' => [
+ 'name' => 'Blog Slug',
+ 'type' => 'text',
+ 'title' => 'Blog user name',
+ 'required' => true,
+ 'exampleValue' => 'jean-vincot',
+ ]
+ ]
+ ];
+
+ public function getIcon()
+ {
+ return 'https://static.mediapart.fr/favicon/favicon-club.ico?v=2';
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::BASE_URI . '/' . $this->getInput('slug') . '/blog');
+
+ foreach ($html->find('ul.post-list li') as $element) {
+ $item = [];
+
+ $item_title = $element->find('h3.title a', 0);
+ $item_divs = $element->find('div');
+
+ $item['title'] = $item_title->innertext;
+ $item['uri'] = self::BASE_URI . trim($item_title->href);
+ $item['author'] = $element->find('.author .subscriber', 0)->innertext;
+ $item['content'] = $item_divs[count($item_divs) - 2] . $item_divs[count($item_divs) - 1];
+ $item['timestamp'] = strtotime($element->find('.author time', 0)->datetime);
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName()
+ {
+ if ($this->getInput('slug')) {
+ return self::NAME . ' | ' . $this->getInput('slug');
+ }
+ return parent::getName();
+ }
}
diff --git a/bridges/MediapartBridge.php b/bridges/MediapartBridge.php
index f7fff4ab..3c8c8317 100644
--- a/bridges/MediapartBridge.php
+++ b/bridges/MediapartBridge.php
@@ -1,65 +1,69 @@
<?php
-class MediapartBridge extends FeedExpander {
- const MAINTAINER = 'killruana';
- const NAME = 'Mediapart Bridge';
- const URI = 'https://www.mediapart.fr/';
- const PARAMETERS = array(
- array(
- 'single_page_mode' => array(
- 'name' => 'Single page article',
- 'type' => 'checkbox',
- 'title' => 'Display long articles on a single page',
- 'defaultValue' => 'checked'
- ),
- 'mpsessid' => array(
- 'name' => 'MPSESSID',
- 'type' => 'text',
- 'title' => 'Value of the session cookie MPSESSID'
- )
- )
- );
- const CACHE_TIMEOUT = 7200; // 2h
- const DESCRIPTION = 'Returns the newest articles.';
+class MediapartBridge extends FeedExpander
+{
+ const MAINTAINER = 'killruana';
+ const NAME = 'Mediapart Bridge';
+ const URI = 'https://www.mediapart.fr/';
+ const PARAMETERS = [
+ [
+ 'single_page_mode' => [
+ 'name' => 'Single page article',
+ 'type' => 'checkbox',
+ 'title' => 'Display long articles on a single page',
+ 'defaultValue' => 'checked'
+ ],
+ 'mpsessid' => [
+ 'name' => 'MPSESSID',
+ 'type' => 'text',
+ 'title' => 'Value of the session cookie MPSESSID'
+ ]
+ ]
+ ];
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Returns the newest articles.';
- public function collectData() {
- $url = self::URI . 'articles/feed';
- $this->collectExpandableDatas($url);
- }
+ public function collectData()
+ {
+ $url = self::URI . 'articles/feed';
+ $this->collectExpandableDatas($url);
+ }
- protected function parseItem($newsItem) {
- $item = parent::parseItem($newsItem);
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
- // Mediapart provide multiple type of contents.
- // We only process items relative to the newspaper
- // See issue #1292 - https://github.com/RSS-Bridge/rss-bridge/issues/1292
- if (strpos($item['uri'], self::URI . 'journal/') === 0) {
- // Enable single page mode?
- if ($this->getInput('single_page_mode') === true) {
- $item['uri'] .= '?onglet=full';
- }
+ // Mediapart provide multiple type of contents.
+ // We only process items relative to the newspaper
+ // See issue #1292 - https://github.com/RSS-Bridge/rss-bridge/issues/1292
+ if (strpos($item['uri'], self::URI . 'journal/') === 0) {
+ // Enable single page mode?
+ if ($this->getInput('single_page_mode') === true) {
+ $item['uri'] .= '?onglet=full';
+ }
- // If a session cookie is defined, get the full article
- $mpsessid = $this->getInput('mpsessid');
- if (!empty($mpsessid)) {
- // Set the session cookie
- $opt = array();
- $opt[CURLOPT_COOKIE] = 'MPSESSID=' . $mpsessid;
+ // If a session cookie is defined, get the full article
+ $mpsessid = $this->getInput('mpsessid');
+ if (!empty($mpsessid)) {
+ // Set the session cookie
+ $opt = [];
+ $opt[CURLOPT_COOKIE] = 'MPSESSID=' . $mpsessid;
- // Get the page
- $articlePage = getSimpleHTMLDOM(
- $newsItem->link . '?onglet=full',
- array(),
- $opt);
+ // Get the page
+ $articlePage = getSimpleHTMLDOM(
+ $newsItem->link . '?onglet=full',
+ [],
+ $opt
+ );
- // Extract the article content
- $content = $articlePage->find('div.content-article', 0)->innertext;
- $content = sanitize($content);
- $content = defaultLinkTo($content, static::URI);
- $item['content'] .= $content;
- }
- }
+ // Extract the article content
+ $content = $articlePage->find('div.content-article', 0)->innertext;
+ $content = sanitize($content);
+ $content = defaultLinkTo($content, static::URI);
+ $item['content'] .= $content;
+ }
+ }
- return $item;
- }
+ return $item;
+ }
}
diff --git a/bridges/MilbooruBridge.php b/bridges/MilbooruBridge.php
index 24571279..54833b30 100644
--- a/bridges/MilbooruBridge.php
+++ b/bridges/MilbooruBridge.php
@@ -1,10 +1,9 @@
<?php
-class MilbooruBridge extends Shimmie2Bridge {
-
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Milbooru';
- const URI = 'http://sheslostcontrol.net/moe/shimmie/';
- const DESCRIPTION = 'Returns images from given page';
-
+class MilbooruBridge extends Shimmie2Bridge
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Milbooru';
+ const URI = 'http://sheslostcontrol.net/moe/shimmie/';
+ const DESCRIPTION = 'Returns images from given page';
}
diff --git a/bridges/MixCloudBridge.php b/bridges/MixCloudBridge.php
index 2296af2f..d2b35989 100644
--- a/bridges/MixCloudBridge.php
+++ b/bridges/MixCloudBridge.php
@@ -1,61 +1,64 @@
<?php
-class MixCloudBridge extends BridgeAbstract {
-
- const MAINTAINER = 'Alexis CHEMEL';
- const NAME = 'MixCloud';
- const URI = 'https://www.mixcloud.com';
- const API_URI = 'https://api.mixcloud.com/';
- const CACHE_TIMEOUT = 3600; // 1h
- const DESCRIPTION = 'Returns latest musics on user stream';
-
- const PARAMETERS = array(array(
- 'u' => array(
- 'name' => 'username',
- 'required' => true,
- 'exampleValue' => 'DJJazzyJeff',
- )
- ));
-
- public function getName(){
- if(!is_null($this->getInput('u'))) {
- return 'MixCloud - ' . $this->getInput('u');
- }
-
- return parent::getName();
- }
-
- private static function compareDate($stream1, $stream2) {
- return (strtotime($stream1['timestamp']) < strtotime($stream2['timestamp']) ? 1 : -1);
- }
-
- public function collectData(){
- $user = urlencode($this->getInput('u'));
- // Get Cloudcasts
- $mixcloudUri = self::API_URI . $user . '/cloudcasts/';
- $content = getContents($mixcloudUri);
- $casts = json_decode($content)->data;
-
- // Get Listens
- $mixcloudUri = self::API_URI . $user . '/listens/';
- $content = getContents($mixcloudUri);
- $listens = json_decode($content)->data;
-
- $streams = array_merge($casts, $listens);
-
- foreach($streams as $stream) {
- $item = array();
-
- $item['uri'] = $stream->url;
- $item['title'] = $stream->name;
- $item['content'] = '<img src="' . $stream->pictures->thumbnail . '" />';
- $item['author'] = $stream->user->name;
- $item['timestamp'] = $stream->created_time;
-
- $this->items[] = $item;
- }
-
- // Sort items by date
- usort($this->items, array('MixCloudBridge', 'compareDate'));
- }
+class MixCloudBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Alexis CHEMEL';
+ const NAME = 'MixCloud';
+ const URI = 'https://www.mixcloud.com';
+ const API_URI = 'https://api.mixcloud.com/';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Returns latest musics on user stream';
+
+ const PARAMETERS = [[
+ 'u' => [
+ 'name' => 'username',
+ 'required' => true,
+ 'exampleValue' => 'DJJazzyJeff',
+ ]
+ ]];
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('u'))) {
+ return 'MixCloud - ' . $this->getInput('u');
+ }
+
+ return parent::getName();
+ }
+
+ private static function compareDate($stream1, $stream2)
+ {
+ return (strtotime($stream1['timestamp']) < strtotime($stream2['timestamp']) ? 1 : -1);
+ }
+
+ public function collectData()
+ {
+ $user = urlencode($this->getInput('u'));
+ // Get Cloudcasts
+ $mixcloudUri = self::API_URI . $user . '/cloudcasts/';
+ $content = getContents($mixcloudUri);
+ $casts = json_decode($content)->data;
+
+ // Get Listens
+ $mixcloudUri = self::API_URI . $user . '/listens/';
+ $content = getContents($mixcloudUri);
+ $listens = json_decode($content)->data;
+
+ $streams = array_merge($casts, $listens);
+
+ foreach ($streams as $stream) {
+ $item = [];
+
+ $item['uri'] = $stream->url;
+ $item['title'] = $stream->name;
+ $item['content'] = '<img src="' . $stream->pictures->thumbnail . '" />';
+ $item['author'] = $stream->user->name;
+ $item['timestamp'] = $stream->created_time;
+
+ $this->items[] = $item;
+ }
+
+ // Sort items by date
+ usort($this->items, ['MixCloudBridge', 'compareDate']);
+ }
}
diff --git a/bridges/ModelKarteiBridge.php b/bridges/ModelKarteiBridge.php
index 2a1bee9c..04b03fcf 100644
--- a/bridges/ModelKarteiBridge.php
+++ b/bridges/ModelKarteiBridge.php
@@ -1,102 +1,118 @@
<?php
-class ModelKarteiBridge extends BridgeAbstract {
- const NAME = 'model-kartei.de';
- const URI = 'https://www.model-kartei.de/';
- const DESCRIPTION = 'Get the public comp card gallery';
- const MAINTAINER = 'fulmeek';
- const PARAMETERS = array(array(
- 'model_id' => array(
- 'name' => 'Model ID',
- 'required' => true,
- 'exampleValue' => '614931'
- )
- ));
-
- const LIMIT_ITEMS = 10;
-
- private $feedName = '';
-
- public function collectData() {
- $model_id = preg_replace('/[^0-9]/', '', $this->getInput('model_id'));
- if (empty($model_id))
- returnServerError('Invalid model ID');
-
- $html = getSimpleHTMLDOM(self::URI . 'sedcards/model/' . $model_id . '/');
-
- $objTitle = $html->find('.sTitle', 0);
- if ($objTitle)
- $this->feedName = $objTitle->plaintext;
-
- $itemlist = $html->find('#photoList .photoPreview');
- if (!$itemlist)
- returnServerError('No gallery');
-
- foreach($itemlist as $idx => $element) {
- if ($idx >= self::LIMIT_ITEMS)
- break;
-
- $item = array();
-
- $title = $element->title;
- $date = $element->{'data-date'};
- $author = $this->feedName;
- $text = '';
-
- $objImage = $element->find('a.photoLink img', 0);
- $objLink = $element->find('a.photoLink', 0);
-
- if ($objLink) {
- $page = getSimpleHTMLDOMCached($objLink->href);
-
- if (empty($title)) {
- $objTitle = $page->find('.p-title', 0);
- if ($objTitle)
- $title = $objTitle->plaintext;
- }
- if (empty($date)) {
- $objDate = $page->find('.cameraDetails .date', 0);
- if ($objDate)
- $date = strtotime($objDate->parent()->plaintext);
- }
- if (empty($author)) {
- $objAuthor = $page->find('.p-publisher a', 0);
- if ($objAuthor)
- $author = $objAuthor->plaintext;
- }
-
- $objFullImage = $page->find('img#gofullscreen', 0);
- if ($objFullImage)
- $objImage = $objFullImage;
-
- $objText = $page->find('.p-desc', 0);
- if ($objText)
- $text = $objText->plaintext;
- }
-
- $item['title'] = $title;
- $item['timestamp'] = $date;
- $item['author'] = $author;
-
- if ($objImage)
- $item['content'] = '<img src="' . $objImage->src . '"/>';
- if ($objLink) {
- $item['uri'] = $objLink->href;
- if (!empty($item['content']))
- $item['content'] = '<a href="' . $objLink->href . '" target="_blank">' . $item['content'] . '</a>';
- } else {
- $item['uri'] = 'urn:sha1:' . hash('sha1', $item['content']);
- }
- if (!empty($text))
- $item['content'] = '<p>' . $text . '</p>' . $item['content'];
-
- $this->items[] = $item;
- }
- }
-
- public function getName(){
- if(!empty($this->feedName)) {
- return $this->feedName . ' - ' . self::NAME;
- }
- return parent::getName();
- }
+
+class ModelKarteiBridge extends BridgeAbstract
+{
+ const NAME = 'model-kartei.de';
+ const URI = 'https://www.model-kartei.de/';
+ const DESCRIPTION = 'Get the public comp card gallery';
+ const MAINTAINER = 'fulmeek';
+ const PARAMETERS = [[
+ 'model_id' => [
+ 'name' => 'Model ID',
+ 'required' => true,
+ 'exampleValue' => '614931'
+ ]
+ ]];
+
+ const LIMIT_ITEMS = 10;
+
+ private $feedName = '';
+
+ public function collectData()
+ {
+ $model_id = preg_replace('/[^0-9]/', '', $this->getInput('model_id'));
+ if (empty($model_id)) {
+ returnServerError('Invalid model ID');
+ }
+
+ $html = getSimpleHTMLDOM(self::URI . 'sedcards/model/' . $model_id . '/');
+
+ $objTitle = $html->find('.sTitle', 0);
+ if ($objTitle) {
+ $this->feedName = $objTitle->plaintext;
+ }
+
+ $itemlist = $html->find('#photoList .photoPreview');
+ if (!$itemlist) {
+ returnServerError('No gallery');
+ }
+
+ foreach ($itemlist as $idx => $element) {
+ if ($idx >= self::LIMIT_ITEMS) {
+ break;
+ }
+
+ $item = [];
+
+ $title = $element->title;
+ $date = $element->{'data-date'};
+ $author = $this->feedName;
+ $text = '';
+
+ $objImage = $element->find('a.photoLink img', 0);
+ $objLink = $element->find('a.photoLink', 0);
+
+ if ($objLink) {
+ $page = getSimpleHTMLDOMCached($objLink->href);
+
+ if (empty($title)) {
+ $objTitle = $page->find('.p-title', 0);
+ if ($objTitle) {
+ $title = $objTitle->plaintext;
+ }
+ }
+ if (empty($date)) {
+ $objDate = $page->find('.cameraDetails .date', 0);
+ if ($objDate) {
+ $date = strtotime($objDate->parent()->plaintext);
+ }
+ }
+ if (empty($author)) {
+ $objAuthor = $page->find('.p-publisher a', 0);
+ if ($objAuthor) {
+ $author = $objAuthor->plaintext;
+ }
+ }
+
+ $objFullImage = $page->find('img#gofullscreen', 0);
+ if ($objFullImage) {
+ $objImage = $objFullImage;
+ }
+
+ $objText = $page->find('.p-desc', 0);
+ if ($objText) {
+ $text = $objText->plaintext;
+ }
+ }
+
+ $item['title'] = $title;
+ $item['timestamp'] = $date;
+ $item['author'] = $author;
+
+ if ($objImage) {
+ $item['content'] = '<img src="' . $objImage->src . '"/>';
+ }
+ if ($objLink) {
+ $item['uri'] = $objLink->href;
+ if (!empty($item['content'])) {
+ $item['content'] = '<a href="' . $objLink->href . '" target="_blank">' . $item['content'] . '</a>';
+ }
+ } else {
+ $item['uri'] = 'urn:sha1:' . hash('sha1', $item['content']);
+ }
+ if (!empty($text)) {
+ $item['content'] = '<p>' . $text . '</p>' . $item['content'];
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName()
+ {
+ if (!empty($this->feedName)) {
+ return $this->feedName . ' - ' . self::NAME;
+ }
+ return parent::getName();
+ }
}
diff --git a/bridges/MoebooruBridge.php b/bridges/MoebooruBridge.php
index 90b6a6ca..1af08575 100644
--- a/bridges/MoebooruBridge.php
+++ b/bridges/MoebooruBridge.php
@@ -1,55 +1,59 @@
<?php
-class MoebooruBridge extends BridgeAbstract {
- const NAME = 'Moebooru';
- const URI = 'https://moe.dev.myconan.net/';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Returns images from given page';
- const MAINTAINER = 'pmaziere';
+class MoebooruBridge extends BridgeAbstract
+{
+ const NAME = 'Moebooru';
+ const URI = 'https://moe.dev.myconan.net/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns images from given page';
+ const MAINTAINER = 'pmaziere';
- const PARAMETERS = array( array(
- 'p' => array(
- 'name' => 'page',
- 'defaultValue' => 1,
- 'type' => 'number'
- ),
- 't' => array(
- 'name' => 'tags'
- )
- ));
+ const PARAMETERS = [ [
+ 'p' => [
+ 'name' => 'page',
+ 'defaultValue' => 1,
+ 'type' => 'number'
+ ],
+ 't' => [
+ 'name' => 'tags'
+ ]
+ ]];
- protected function getFullURI(){
- return $this->getURI()
- . 'post?page='
- . $this->getInput('p')
- . '&tags='
- . urlencode($this->getInput('t'));
- }
+ protected function getFullURI()
+ {
+ return $this->getURI()
+ . 'post?page='
+ . $this->getInput('p')
+ . '&tags='
+ . urlencode($this->getInput('t'));
+ }
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getFullURI());
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getFullURI());
- $input_json = explode('Post.register(', $html);
- foreach($input_json as $element)
- $data[] = preg_replace('/}\)(.*)/', '}', $element);
- unset($data[0]);
+ $input_json = explode('Post.register(', $html);
+ foreach ($input_json as $element) {
+ $data[] = preg_replace('/}\)(.*)/', '}', $element);
+ }
+ unset($data[0]);
- foreach($data as $datai) {
- $json = json_decode($datai, true);
- $item = array();
- $item['uri'] = $this->getURI() . '/post/show/' . $json['id'];
- $item['postid'] = $json['id'];
- $item['timestamp'] = $json['created_at'];
- $item['imageUri'] = $json['file_url'];
- $item['title'] = $this->getName() . ' | ' . $json['id'];
- $item['content'] = '<a href="'
- . $item['imageUri']
- . '"><img src="'
- . $json['preview_url']
- . '" /></a><br>Tags: '
- . $json['tags'];
+ foreach ($data as $datai) {
+ $json = json_decode($datai, true);
+ $item = [];
+ $item['uri'] = $this->getURI() . '/post/show/' . $json['id'];
+ $item['postid'] = $json['id'];
+ $item['timestamp'] = $json['created_at'];
+ $item['imageUri'] = $json['file_url'];
+ $item['title'] = $this->getName() . ' | ' . $json['id'];
+ $item['content'] = '<a href="'
+ . $item['imageUri']
+ . '"><img src="'
+ . $json['preview_url']
+ . '" /></a><br>Tags: '
+ . $json['tags'];
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/MoinMoinBridge.php b/bridges/MoinMoinBridge.php
index 1920c5a1..c8053587 100644
--- a/bridges/MoinMoinBridge.php
+++ b/bridges/MoinMoinBridge.php
@@ -1,327 +1,346 @@
<?php
-class MoinMoinBridge extends BridgeAbstract {
-
- const MAINTAINER = 'logmanoriginal';
- const NAME = 'MoinMoin Bridge';
- const URI = 'https://moinmo.in';
- const DESCRIPTION = 'Generates feeds for pages of a MoinMoin (compatible) wiki';
- const PARAMETERS = array(
- array(
- 'source' => array(
- 'name' => 'Source',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Insert wiki page URI (e.g.: https://moinmo.in/MoinMoin)',
- 'exampleValue' => 'https://moinmo.in/MoinMoin'
- ),
- 'separator' => array(
- 'name' => 'Separator',
- 'type' => 'list',
- 'requied' => true,
- 'title' => 'Defines the separtor for splitting content into feeds',
- 'defaultValue' => 'h2',
- 'values' => array(
- 'Header (h1)' => 'h1',
- 'Header (h2)' => 'h2',
- 'Header (h3)' => 'h3',
- 'List element (li)' => 'li',
- 'Anchor (a)' => 'a'
- )
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Number of items to return (from top)',
- 'defaultValue' => -1
- ),
- 'content' => array(
- 'name' => 'Content',
- 'type' => 'list',
- 'required' => false,
- 'title' => 'Defines how feed contents are build',
- 'defaultValue' => 'separator',
- 'values' => array(
- 'By separator' => 'separator',
- 'Follow link (only for anchor)' => 'follow',
- 'None' => 'none'
- )
- )
- )
- );
-
- private $title = '';
-
- public function collectData(){
- /* MoinMoin uses a rather unpleasent representation of HTML. Instead of
- * using tags like <article/>, <navigation/>, <header/>, etc... it uses
- * <div/>, <span/> and <p/>. Also each line is literaly identified via
- * IDs. The only way to distinguish content is via headers, though not
- * in all cases.
- *
- * Example (indented for the sake of readability):
- * ...
- * <span class="anchor" id="line-1"></span>
- * <span class="anchor" id="line-2"></span>
- * <span class="anchor" id="line-3"></span>
- * <span class="anchor" id="line-4"></span>
- * <span class="anchor" id="line-5"></span>
- * <span class="anchor" id="line-6"></span>
- * <span class="anchor" id="line-7"></span>
- * <span class="anchor" id="line-8"></span>
- * <span class="anchor" id="line-9"></span>
- * <p class="line867">MoinMoin is a Wiki software implemented in
- * <a class="interwiki" href="/Python" title="MoinMoin">Python</a>
- * and distributed as Free Software under
- * <a class="interwiki" href="/GPL" title="MoinMoin">GNU GPL license</a>.
- * ...
- */
- $html = getSimpleHTMLDOM($this->getInput('source'));
-
- // Some anchors link to local sites or local IDs (both don't work well
- // in feeds)
- $html = $this->fixAnchors($html);
-
- $this->title = $html->find('title', 0)->innertext . ' | ' . self::NAME;
-
- // Here we focus on simple author and timestamp information from the given
- // page. Later we update this information in case the anchor is followed.
- $author = $this->findAuthor($html);
- $timestamp = $this->findTimestamp($html);
-
- $sections = $this->splitSections($html);
-
- foreach($sections as $section) {
- $item = array();
-
- $item['uri'] = $this->findSectionAnchor($section[0]);
-
- switch($this->getInput('content')) {
- case 'none': // Do not return any content
- break;
- case 'follow': // Follow the anchor
- // We can only follow anchors (use default otherwise)
- if($this->getInput('separator') === 'a') {
- $content = $this->followAnchor($item['uri']);
-
- // Return only actual content
- $item['content'] = $content->find('div#page', 0)->innertext;
-
- // Each page could have its own author and timestamp
- $author = $this->findAuthor($content);
- $timestamp = $this->findTimestamp($content);
-
- break;
- }
- // fall-through
- case 'separator':
- default: // Use contents from the current page
- $item['content'] = $this->cleanArticle($section[2]);
- }
-
- if(!is_null($author)) $item['author'] = $author;
- if(!is_null($timestamp)) $item['timestamp'] = $timestamp;
- $item['title'] = strip_tags($section[1]);
-
- // Skip items with empty title
- if(empty(trim($item['title']))) {
- continue;
- }
-
- $this->items[] = $item;
-
- if($this->getInput('limit') > 0
- && count($this->items) >= $this->getInput('limit')) {
- break;
- }
- }
- }
-
- public function getName(){
- return $this->title ?: parent::getName();
- }
-
- public function getURI(){
- return $this->getInput('source') ?: parent::getURI();
- }
-
- /**
- * Splits the html into sections.
- *
- * Returns an array with one element per section. Each element consists of:
- * [0] The entire section
- * [1] The section title
- * [2] The section content
- */
- private function splitSections($html){
- $content = $html->find('div#page', 0)->innertext
- or returnServerError('Unable to find <div id="page"/>!');
-
- $sections = array();
-
- $regex = implode(
- '',
- array(
- "\<{$this->getInput('separator')}.+?(?=\>)\>",
- "(.+?)(?=\<\/{$this->getInput('separator')}\>)",
- "\<\/{$this->getInput('separator')}\>",
- "(.+?)((?=\<{$this->getInput('separator')})|(?=\<div\sid=\"pagebottom\")){1}"
- )
- );
-
- preg_match_all(
- '/' . $regex . '/m',
- $content,
- $sections,
- PREG_SET_ORDER
- );
-
- // Some pages don't use headers, return page as one feed
- if(count($sections) === 0) {
- return array(
- array(
- $content,
- $html->find('title', 0)->innertext,
- $content
- )
- );
- }
-
- return $sections;
- }
-
- /**
- * Returns the anchor for a given section
- */
- private function findSectionAnchor($section){
- $html = str_get_html($section);
-
- // For IDs
- $anchor = $html->find($this->getInput('separator') . '[id=]', 0);
- if(!is_null($anchor)) {
- return $this->getInput('source') . '#' . $anchor->id;
- }
-
- // For actual anchors
- $anchor = $html->find($this->getInput('separator') . '[href=]', 0);
- if(!is_null($anchor)) {
- return $anchor->href;
- }
-
- // Nothing found
- return $this->getInput('source');
- }
-
- /**
- * Returns the author
- *
- * Notice: Some pages don't provide author information
- */
- private function findAuthor($html){
- /* Example:
- * <p id="pageinfo" class="info" dir="ltr" lang="en">MoinMoin: LocalSpellingWords
- * (last edited 2017-02-16 15:36:31 by <span title="??? @ hosted-by.leaseweb.com
- * [178.162.199.143]">hosted-by</span>)</p>
- */
- $pageinfo = $html->find('[id="pageinfo"]', 0);
-
- if(is_null($pageinfo)) {
- return null;
- } else {
- $author = $pageinfo->find('[title=]', 0);
- if(is_null($author)) {
- return null;
- } else {
- return trim(explode('@', $author->title)[0]);
- }
- }
- }
-
- /**
- * Returns the time of last edit
- *
- * Notice: Some pages don't provide this information
- */
- private function findTimestamp($html){
- // See example of findAuthor()
- $pageinfo = $html->find('[id="pageinfo"]', 0);
-
- if(is_null($pageinfo)) {
- return null;
- } else {
- $timestamp = $pageinfo->innertext;
- $matches = array();
- preg_match('/.+?(?=\().+?(?=\d)([0-9\-\s\:]+)/m', $pageinfo, $matches);
- return strtotime($matches[1]);
- }
- }
-
- /**
- * Returns the original HTML with all anchors fixed (makes relative anchors
- * absolute)
- */
- private function fixAnchors($html, $source = null){
-
- $source = $source ?: $this->getURI();
-
- foreach($html->find('a') as $anchor) {
- switch(substr($anchor->href, 0, 1)) {
- case 'h': // http or https, no actions required
- break;
- case '/': // some relative path
- $anchor->href = $this->findDomain($source) . $anchor->href;
- break;
- case '#': // it's an ID
- default: // probably something like ? or &, skip empty ones
- if(!isset($anchor->href))
- break;
- $anchor->href = $source . $anchor->href;
- }
- }
-
- return $html;
- }
-
- /**
- * Loads the full article of a given anchor (if the anchor is from the same
- * wiki domain)
- */
- private function followAnchor($anchor){
- if(strrpos($anchor, $this->findDomain($this->getInput('source')) === false)) {
- return null;
- }
-
- $html = getSimpleHTMLDOMCached($anchor);
- if(!$html) { // Cannot load article
- return null;
- }
-
- return $this->fixAnchors($html, $anchor);
- }
-
- /**
- * Finds the domain for a given URI
- */
- private function findDomain($uri){
- $matches = array();
- preg_match('/(http[s]{0,1}:\/\/.+?(?=\/))/', $uri, $matches);
- return $matches[1];
- }
-
- /* This function is a copy from CNETBridge */
- private function stripWithDelimiters($string, $start, $end){
- while(strpos($string, $start) !== false) {
- $section_to_remove = substr($string, strpos($string, $start));
- $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
- $string = str_replace($section_to_remove, '', $string);
- }
-
- return $string;
- }
-
- /* This function is based on CNETBridge */
- private function cleanArticle($article_html){
- $article_html = $this->stripWithDelimiters($article_html, '<script', '</script>');
- return $article_html;
- }
+
+class MoinMoinBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'MoinMoin Bridge';
+ const URI = 'https://moinmo.in';
+ const DESCRIPTION = 'Generates feeds for pages of a MoinMoin (compatible) wiki';
+ const PARAMETERS = [
+ [
+ 'source' => [
+ 'name' => 'Source',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert wiki page URI (e.g.: https://moinmo.in/MoinMoin)',
+ 'exampleValue' => 'https://moinmo.in/MoinMoin'
+ ],
+ 'separator' => [
+ 'name' => 'Separator',
+ 'type' => 'list',
+ 'requied' => true,
+ 'title' => 'Defines the separtor for splitting content into feeds',
+ 'defaultValue' => 'h2',
+ 'values' => [
+ 'Header (h1)' => 'h1',
+ 'Header (h2)' => 'h2',
+ 'Header (h3)' => 'h3',
+ 'List element (li)' => 'li',
+ 'Anchor (a)' => 'a'
+ ]
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Number of items to return (from top)',
+ 'defaultValue' => -1
+ ],
+ 'content' => [
+ 'name' => 'Content',
+ 'type' => 'list',
+ 'required' => false,
+ 'title' => 'Defines how feed contents are build',
+ 'defaultValue' => 'separator',
+ 'values' => [
+ 'By separator' => 'separator',
+ 'Follow link (only for anchor)' => 'follow',
+ 'None' => 'none'
+ ]
+ ]
+ ]
+ ];
+
+ private $title = '';
+
+ public function collectData()
+ {
+ /* MoinMoin uses a rather unpleasent representation of HTML. Instead of
+ * using tags like <article/>, <navigation/>, <header/>, etc... it uses
+ * <div/>, <span/> and <p/>. Also each line is literaly identified via
+ * IDs. The only way to distinguish content is via headers, though not
+ * in all cases.
+ *
+ * Example (indented for the sake of readability):
+ * ...
+ * <span class="anchor" id="line-1"></span>
+ * <span class="anchor" id="line-2"></span>
+ * <span class="anchor" id="line-3"></span>
+ * <span class="anchor" id="line-4"></span>
+ * <span class="anchor" id="line-5"></span>
+ * <span class="anchor" id="line-6"></span>
+ * <span class="anchor" id="line-7"></span>
+ * <span class="anchor" id="line-8"></span>
+ * <span class="anchor" id="line-9"></span>
+ * <p class="line867">MoinMoin is a Wiki software implemented in
+ * <a class="interwiki" href="/Python" title="MoinMoin">Python</a>
+ * and distributed as Free Software under
+ * <a class="interwiki" href="/GPL" title="MoinMoin">GNU GPL license</a>.
+ * ...
+ */
+ $html = getSimpleHTMLDOM($this->getInput('source'));
+
+ // Some anchors link to local sites or local IDs (both don't work well
+ // in feeds)
+ $html = $this->fixAnchors($html);
+
+ $this->title = $html->find('title', 0)->innertext . ' | ' . self::NAME;
+
+ // Here we focus on simple author and timestamp information from the given
+ // page. Later we update this information in case the anchor is followed.
+ $author = $this->findAuthor($html);
+ $timestamp = $this->findTimestamp($html);
+
+ $sections = $this->splitSections($html);
+
+ foreach ($sections as $section) {
+ $item = [];
+
+ $item['uri'] = $this->findSectionAnchor($section[0]);
+
+ switch ($this->getInput('content')) {
+ case 'none': // Do not return any content
+ break;
+ case 'follow': // Follow the anchor
+ // We can only follow anchors (use default otherwise)
+ if ($this->getInput('separator') === 'a') {
+ $content = $this->followAnchor($item['uri']);
+
+ // Return only actual content
+ $item['content'] = $content->find('div#page', 0)->innertext;
+
+ // Each page could have its own author and timestamp
+ $author = $this->findAuthor($content);
+ $timestamp = $this->findTimestamp($content);
+
+ break;
+ }
+ // fall-through
+ case 'separator':
+ default: // Use contents from the current page
+ $item['content'] = $this->cleanArticle($section[2]);
+ }
+
+ if (!is_null($author)) {
+ $item['author'] = $author;
+ }
+ if (!is_null($timestamp)) {
+ $item['timestamp'] = $timestamp;
+ }
+ $item['title'] = strip_tags($section[1]);
+
+ // Skip items with empty title
+ if (empty(trim($item['title']))) {
+ continue;
+ }
+
+ $this->items[] = $item;
+
+ if (
+ $this->getInput('limit') > 0
+ && count($this->items) >= $this->getInput('limit')
+ ) {
+ break;
+ }
+ }
+ }
+
+ public function getName()
+ {
+ return $this->title ?: parent::getName();
+ }
+
+ public function getURI()
+ {
+ return $this->getInput('source') ?: parent::getURI();
+ }
+
+ /**
+ * Splits the html into sections.
+ *
+ * Returns an array with one element per section. Each element consists of:
+ * [0] The entire section
+ * [1] The section title
+ * [2] The section content
+ */
+ private function splitSections($html)
+ {
+ $content = $html->find('div#page', 0)->innertext
+ or returnServerError('Unable to find <div id="page"/>!');
+
+ $sections = [];
+
+ $regex = implode(
+ '',
+ [
+ "\<{$this->getInput('separator')}.+?(?=\>)\>",
+ "(.+?)(?=\<\/{$this->getInput('separator')}\>)",
+ "\<\/{$this->getInput('separator')}\>",
+ "(.+?)((?=\<{$this->getInput('separator')})|(?=\<div\sid=\"pagebottom\")){1}"
+ ]
+ );
+
+ preg_match_all(
+ '/' . $regex . '/m',
+ $content,
+ $sections,
+ PREG_SET_ORDER
+ );
+
+ // Some pages don't use headers, return page as one feed
+ if (count($sections) === 0) {
+ return [
+ [
+ $content,
+ $html->find('title', 0)->innertext,
+ $content
+ ]
+ ];
+ }
+
+ return $sections;
+ }
+
+ /**
+ * Returns the anchor for a given section
+ */
+ private function findSectionAnchor($section)
+ {
+ $html = str_get_html($section);
+
+ // For IDs
+ $anchor = $html->find($this->getInput('separator') . '[id=]', 0);
+ if (!is_null($anchor)) {
+ return $this->getInput('source') . '#' . $anchor->id;
+ }
+
+ // For actual anchors
+ $anchor = $html->find($this->getInput('separator') . '[href=]', 0);
+ if (!is_null($anchor)) {
+ return $anchor->href;
+ }
+
+ // Nothing found
+ return $this->getInput('source');
+ }
+
+ /**
+ * Returns the author
+ *
+ * Notice: Some pages don't provide author information
+ */
+ private function findAuthor($html)
+ {
+ /* Example:
+ * <p id="pageinfo" class="info" dir="ltr" lang="en">MoinMoin: LocalSpellingWords
+ * (last edited 2017-02-16 15:36:31 by <span title="??? @ hosted-by.leaseweb.com
+ * [178.162.199.143]">hosted-by</span>)</p>
+ */
+ $pageinfo = $html->find('[id="pageinfo"]', 0);
+
+ if (is_null($pageinfo)) {
+ return null;
+ } else {
+ $author = $pageinfo->find('[title=]', 0);
+ if (is_null($author)) {
+ return null;
+ } else {
+ return trim(explode('@', $author->title)[0]);
+ }
+ }
+ }
+
+ /**
+ * Returns the time of last edit
+ *
+ * Notice: Some pages don't provide this information
+ */
+ private function findTimestamp($html)
+ {
+ // See example of findAuthor()
+ $pageinfo = $html->find('[id="pageinfo"]', 0);
+
+ if (is_null($pageinfo)) {
+ return null;
+ } else {
+ $timestamp = $pageinfo->innertext;
+ $matches = [];
+ preg_match('/.+?(?=\().+?(?=\d)([0-9\-\s\:]+)/m', $pageinfo, $matches);
+ return strtotime($matches[1]);
+ }
+ }
+
+ /**
+ * Returns the original HTML with all anchors fixed (makes relative anchors
+ * absolute)
+ */
+ private function fixAnchors($html, $source = null)
+ {
+ $source = $source ?: $this->getURI();
+
+ foreach ($html->find('a') as $anchor) {
+ switch (substr($anchor->href, 0, 1)) {
+ case 'h': // http or https, no actions required
+ break;
+ case '/': // some relative path
+ $anchor->href = $this->findDomain($source) . $anchor->href;
+ break;
+ case '#': // it's an ID
+ default: // probably something like ? or &, skip empty ones
+ if (!isset($anchor->href)) {
+ break;
+ }
+ $anchor->href = $source . $anchor->href;
+ }
+ }
+
+ return $html;
+ }
+
+ /**
+ * Loads the full article of a given anchor (if the anchor is from the same
+ * wiki domain)
+ */
+ private function followAnchor($anchor)
+ {
+ if (strrpos($anchor, $this->findDomain($this->getInput('source')) === false)) {
+ return null;
+ }
+
+ $html = getSimpleHTMLDOMCached($anchor);
+ if (!$html) { // Cannot load article
+ return null;
+ }
+
+ return $this->fixAnchors($html, $anchor);
+ }
+
+ /**
+ * Finds the domain for a given URI
+ */
+ private function findDomain($uri)
+ {
+ $matches = [];
+ preg_match('/(http[s]{0,1}:\/\/.+?(?=\/))/', $uri, $matches);
+ return $matches[1];
+ }
+
+ /* This function is a copy from CNETBridge */
+ private function stripWithDelimiters($string, $start, $end)
+ {
+ while (strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ }
+
+ return $string;
+ }
+
+ /* This function is based on CNETBridge */
+ private function cleanArticle($article_html)
+ {
+ $article_html = $this->stripWithDelimiters($article_html, '<script', '</script>');
+ return $article_html;
+ }
}
diff --git a/bridges/MondeDiploBridge.php b/bridges/MondeDiploBridge.php
index ad3967df..7c897f8f 100644
--- a/bridges/MondeDiploBridge.php
+++ b/bridges/MondeDiploBridge.php
@@ -1,29 +1,32 @@
<?php
-class MondeDiploBridge extends BridgeAbstract {
- const MAINTAINER = 'Pitchoule';
- const NAME = 'Monde Diplomatique';
- const URI = 'https://www.monde-diplomatique.fr';
- const CACHE_TIMEOUT = 21600; //6h
- const DESCRIPTION = 'Returns most recent results from MondeDiplo.';
+class MondeDiploBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Pitchoule';
+ const NAME = 'Monde Diplomatique';
+ const URI = 'https://www.monde-diplomatique.fr';
+ const CACHE_TIMEOUT = 21600; //6h
+ const DESCRIPTION = 'Returns most recent results from MondeDiplo.';
- private function cleanText($text) {
- return trim(str_replace(array('&nbsp;', '&nbsp'), ' ', $text));
- }
+ private function cleanText($text)
+ {
+ return trim(str_replace(['&nbsp;', '&nbsp'], ' ', $text));
+ }
- public function collectData(){
- $html = getSimpleHTMLDOM(self::URI);
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
- foreach($html->find('div.unarticle') as $article) {
- $element = $article->parent();
- $title = $element->find('h3', 0)->plaintext;
- $datesAuteurs = $element->find('div.dates_auteurs', 0)->plaintext;
- $item = array();
- $item['uri'] = urljoin(self::URI, $element->href);
- $item['title'] = $this->cleanText($title) . ' - ' . $this->cleanText($datesAuteurs);
- $item['content'] = $this->cleanText(str_replace(array($title, $datesAuteurs), '', $element->plaintext));
+ foreach ($html->find('div.unarticle') as $article) {
+ $element = $article->parent();
+ $title = $element->find('h3', 0)->plaintext;
+ $datesAuteurs = $element->find('div.dates_auteurs', 0)->plaintext;
+ $item = [];
+ $item['uri'] = urljoin(self::URI, $element->href);
+ $item['title'] = $this->cleanText($title) . ' - ' . $this->cleanText($datesAuteurs);
+ $item['content'] = $this->cleanText(str_replace([$title, $datesAuteurs], '', $element->plaintext));
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/MozillaBugTrackerBridge.php b/bridges/MozillaBugTrackerBridge.php
index d284e460..cf6d7f73 100644
--- a/bridges/MozillaBugTrackerBridge.php
+++ b/bridges/MozillaBugTrackerBridge.php
@@ -1,147 +1,158 @@
<?php
-class MozillaBugTrackerBridge extends BridgeAbstract {
- const NAME = 'Mozilla Bug Tracker';
- const URI = 'https://bugzilla.mozilla.org';
- const DESCRIPTION = 'DEPRECATED: Use BugzillaBridge instead.
+class MozillaBugTrackerBridge extends BridgeAbstract
+{
+ const NAME = 'Mozilla Bug Tracker';
+ const URI = 'https://bugzilla.mozilla.org';
+ const DESCRIPTION = 'DEPRECATED: Use BugzillaBridge instead.
Returns feeds for bug comments';
- const MAINTAINER = 'AntoineTurmel';
- const PARAMETERS = array(
- 'Bug comments' => array(
- 'id' => array(
- 'name' => 'Bug tracking ID',
- 'type' => 'number',
- 'required' => true,
- 'title' => 'Insert bug tracking ID',
- 'exampleValue' => 121241
- ),
- 'limit' => array(
- 'name' => 'Number of comments to return',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Specify number of comments to return',
- 'defaultValue' => -1
- ),
- 'sorting' => array(
- 'name' => 'Sorting',
- 'type' => 'list',
- 'required' => false,
- 'title' => 'Defines the sorting order of the comments returned',
- 'defaultValue' => 'of',
- 'values' => array(
- 'Oldest first' => 'of',
- 'Latest first' => 'lf'
- )
- )
- )
- );
-
- private $bugid = '';
- private $bugdesc = '';
-
- public function getIcon() {
- return self::URI . '/extensions/BMO/web/images/favicon.ico';
- }
-
- public function collectData(){
- $limit = $this->getInput('limit');
- $sorting = $this->getInput('sorting');
-
- // We use the print preview page for simplicity
- $html = getSimpleHTMLDOMCached($this->getURI() . '&format=multiple',
- 86400,
- null,
- null,
- true,
- true,
- DEFAULT_TARGET_CHARSET,
- false, // Do NOT remove line breaks
- DEFAULT_BR_TEXT,
- DEFAULT_SPAN_TEXT);
-
- if($html === false)
- returnServerError('Failed to load page!');
-
- // Fix relative URLs
- defaultLinkTo($html, self::URI);
-
- // Store header information into private members
- $this->bugid = trim($html->find('#field-value-bug_id', 0)->plaintext);
- $this->bugdesc = $html->find('h1#field-value-short_desc', 0)->plaintext;
-
- // Get and limit comments
- $comments = $html->find('div.change-set');
-
- if($limit > 0 && count($comments) > $limit) {
- $comments = array_slice($comments, count($comments) - $limit, $limit);
- }
-
- if ($sorting === 'lf') {
- $comments = array_reverse($comments, true);
- }
-
- foreach($comments as $comment) {
- $comment = $this->inlineStyles($comment);
-
- $item = array();
- $item['uri'] = $comment->find('h3.change-name', 0)->find('a', 0)->href;
- $item['author'] = $comment->find('td.change-author', 0)->plaintext;
- $item['title'] = $comment->find('h3.change-name', 0)->plaintext;
- $item['timestamp'] = strtotime($comment->find('span.rel-time', 0)->title);
- $item['content'] = '';
-
- if ($comment->find('.comment-text', 0)) {
- $item['content'] = $comment->find('.comment-text', 0)->outertext;
- }
-
- if ($comment->find('div.activity', 0)) {
- $item['content'] .= $comment->find('div.activity', 0)->innertext;
- }
-
- $this->items[] = $item;
- }
- }
-
- public function getURI(){
- switch($this->queriedContext) {
- case 'Bug comments':
- return parent::getURI()
- . '/show_bug.cgi?id='
- . $this->getInput('id');
- break;
- default: return parent::getURI();
- }
- }
-
- public function getName(){
- switch($this->queriedContext) {
- case 'Bug comments':
- return $this->bugid
- . ' - '
- . $this->bugdesc
- . ' - '
- . parent::getName();
- break;
- default: return parent::getName();
- }
- }
-
- /**
- * Adds styles as attributes to tags with known classes
- *
- * @param object $html A simplehtmldom object
- * @return object Returns the original object with styles added as
- * attributes.
- */
- private function inlineStyles($html){
- foreach($html->find('.bz_closed') as $element) {
- $element->style = 'text-decoration:line-through;';
- }
-
- foreach($html->find('pre') as $element) {
- $element->style = 'white-space: pre-wrap;';
- }
-
- return $html;
- }
+ const MAINTAINER = 'AntoineTurmel';
+ const PARAMETERS = [
+ '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
+ ],
+ 'sorting' => [
+ 'name' => 'Sorting',
+ 'type' => 'list',
+ 'required' => false,
+ 'title' => 'Defines the sorting order of the comments returned',
+ 'defaultValue' => 'of',
+ 'values' => [
+ 'Oldest first' => 'of',
+ 'Latest first' => 'lf'
+ ]
+ ]
+ ]
+ ];
+
+ private $bugid = '';
+ private $bugdesc = '';
+
+ public function getIcon()
+ {
+ return self::URI . '/extensions/BMO/web/images/favicon.ico';
+ }
+
+ public function collectData()
+ {
+ $limit = $this->getInput('limit');
+ $sorting = $this->getInput('sorting');
+
+ // We use the print preview page for simplicity
+ $html = getSimpleHTMLDOMCached(
+ $this->getURI() . '&format=multiple',
+ 86400,
+ null,
+ null,
+ true,
+ true,
+ DEFAULT_TARGET_CHARSET,
+ false, // Do NOT remove line breaks
+ DEFAULT_BR_TEXT,
+ DEFAULT_SPAN_TEXT
+ );
+
+ if ($html === false) {
+ returnServerError('Failed to load page!');
+ }
+
+ // Fix relative URLs
+ defaultLinkTo($html, self::URI);
+
+ // Store header information into private members
+ $this->bugid = trim($html->find('#field-value-bug_id', 0)->plaintext);
+ $this->bugdesc = $html->find('h1#field-value-short_desc', 0)->plaintext;
+
+ // Get and limit comments
+ $comments = $html->find('div.change-set');
+
+ if ($limit > 0 && count($comments) > $limit) {
+ $comments = array_slice($comments, count($comments) - $limit, $limit);
+ }
+
+ if ($sorting === 'lf') {
+ $comments = array_reverse($comments, true);
+ }
+
+ foreach ($comments as $comment) {
+ $comment = $this->inlineStyles($comment);
+
+ $item = [];
+ $item['uri'] = $comment->find('h3.change-name', 0)->find('a', 0)->href;
+ $item['author'] = $comment->find('td.change-author', 0)->plaintext;
+ $item['title'] = $comment->find('h3.change-name', 0)->plaintext;
+ $item['timestamp'] = strtotime($comment->find('span.rel-time', 0)->title);
+ $item['content'] = '';
+
+ if ($comment->find('.comment-text', 0)) {
+ $item['content'] = $comment->find('.comment-text', 0)->outertext;
+ }
+
+ if ($comment->find('div.activity', 0)) {
+ $item['content'] .= $comment->find('div.activity', 0)->innertext;
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Bug comments':
+ return parent::getURI()
+ . '/show_bug.cgi?id='
+ . $this->getInput('id');
+ break;
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Bug comments':
+ return $this->bugid
+ . ' - '
+ . $this->bugdesc
+ . ' - '
+ . parent::getName();
+ break;
+ default:
+ return parent::getName();
+ }
+ }
+
+ /**
+ * Adds styles as attributes to tags with known classes
+ *
+ * @param object $html A simplehtmldom object
+ * @return object Returns the original object with styles added as
+ * attributes.
+ */
+ private function inlineStyles($html)
+ {
+ foreach ($html->find('.bz_closed') as $element) {
+ $element->style = 'text-decoration:line-through;';
+ }
+
+ foreach ($html->find('pre') as $element) {
+ $element->style = 'white-space: pre-wrap;';
+ }
+
+ return $html;
+ }
}
diff --git a/bridges/MozillaSecurityBridge.php b/bridges/MozillaSecurityBridge.php
index ab798f00..1b7e7de3 100644
--- a/bridges/MozillaSecurityBridge.php
+++ b/bridges/MozillaSecurityBridge.php
@@ -1,32 +1,34 @@
<?php
-class MozillaSecurityBridge extends BridgeAbstract {
- const MAINTAINER = 'm0le.net';
- const NAME = 'Mozilla Security Advisories';
- const URI = 'https://www.mozilla.org/en-US/security/advisories/';
- const CACHE_TIMEOUT = 7200; // 2h
- const DESCRIPTION = 'Mozilla Security Advisories';
- const WEBROOT = 'https://www.mozilla.org';
+class MozillaSecurityBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'm0le.net';
+ const NAME = 'Mozilla Security Advisories';
+ const URI = 'https://www.mozilla.org/en-US/security/advisories/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Mozilla Security Advisories';
+ const WEBROOT = 'https://www.mozilla.org';
- public function collectData(){
- $html = getSimpleHTMLDOM(self::URI);
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
- $html = defaultLinkTo($html, self::WEBROOT);
+ $html = defaultLinkTo($html, self::WEBROOT);
- $item = array();
- $articles = $html->find('div[id="main-content"] h2');
+ $item = [];
+ $articles = $html->find('div[id="main-content"] h2');
- foreach ($articles as $element) {
- //Limit total amount of requests
- if(count($this->items) >= 20) {
- break;
- }
- $item['title'] = $element->innertext;
- $item['timestamp'] = strtotime($element->innertext);
- $item['content'] = $element->next_sibling()->innertext;
- $item['uri'] = self::URI . '?' . $item['timestamp'];
- $item['uid'] = self::URI . '?' . $item['timestamp'];
- $this->items[] = $item;
- }
- }
+ foreach ($articles as $element) {
+ //Limit total amount of requests
+ if (count($this->items) >= 20) {
+ break;
+ }
+ $item['title'] = $element->innertext;
+ $item['timestamp'] = strtotime($element->innertext);
+ $item['content'] = $element->next_sibling()->innertext;
+ $item['uri'] = self::URI . '?' . $item['timestamp'];
+ $item['uid'] = self::URI . '?' . $item['timestamp'];
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/MsnMondeBridge.php b/bridges/MsnMondeBridge.php
index 817f13ad..844aa4a2 100644
--- a/bridges/MsnMondeBridge.php
+++ b/bridges/MsnMondeBridge.php
@@ -1,40 +1,46 @@
<?php
-class MsnMondeBridge extends FeedExpander {
- const MAINTAINER = 'kranack';
- const NAME = 'MSN Actu Monde';
- const DESCRIPTION = 'Returns the 10 newest posts from MSN Actualités (full text)';
- const URI = 'https://www.msn.com/fr-fr/actualite';
- const FEED_URL = 'https://rss.msn.com/fr-fr';
- const JSON_URL = 'https://assets.msn.com/content/view/v2/Detail/fr-fr/';
- const LIMIT = 10;
+class MsnMondeBridge extends FeedExpander
+{
+ const MAINTAINER = 'kranack';
+ const NAME = 'MSN Actu Monde';
+ const DESCRIPTION = 'Returns the 10 newest posts from MSN Actualités (full text)';
+ const URI = 'https://www.msn.com/fr-fr/actualite';
+ const FEED_URL = 'https://rss.msn.com/fr-fr';
+ const JSON_URL = 'https://assets.msn.com/content/view/v2/Detail/fr-fr/';
+ const LIMIT = 10;
- public function getName() {
- return 'MSN Actualités';
- }
+ public function getName()
+ {
+ return 'MSN Actualités';
+ }
- public function getURI() {
- return self::URI;
- }
+ public function getURI()
+ {
+ return self::URI;
+ }
- public function collectData() {
- $this->collectExpandableDatas(self::FEED_URL, self::LIMIT);
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas(self::FEED_URL, self::LIMIT);
+ }
- protected function parseItem($newsItem) {
- $item = parent::parseItem($newsItem);
- if (!preg_match('#fr-fr/actualite.*/ar-(?<id>[\w]*)\?#', $item['uri'], $matches)) {
- return;
- }
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ if (!preg_match('#fr-fr/actualite.*/ar-(?<id>[\w]*)\?#', $item['uri'], $matches)) {
+ return;
+ }
- $json = json_decode(getContents(self::JSON_URL . $matches['id']), true);
- $item['content'] = $json['body'];
- if (!empty($json['authors']))
- $item['author'] = reset($json['authors'])['name'];
- $item['timestamp'] = $json['createdDateTime'];
- foreach($json['tags'] as $tag) {
- $item['categories'][] = $tag['label'];
- }
- return $item;
- }
+ $json = json_decode(getContents(self::JSON_URL . $matches['id']), true);
+ $item['content'] = $json['body'];
+ if (!empty($json['authors'])) {
+ $item['author'] = reset($json['authors'])['name'];
+ }
+ $item['timestamp'] = $json['createdDateTime'];
+ foreach ($json['tags'] as $tag) {
+ $item['categories'][] = $tag['label'];
+ }
+ return $item;
+ }
}
diff --git a/bridges/MspabooruBridge.php b/bridges/MspabooruBridge.php
index 1c830c0a..76b9a600 100644
--- a/bridges/MspabooruBridge.php
+++ b/bridges/MspabooruBridge.php
@@ -1,14 +1,15 @@
<?php
-class MspabooruBridge extends GelbooruBridge {
+class MspabooruBridge extends GelbooruBridge
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Mspabooru';
+ const URI = 'https://mspabooru.com/';
+ const DESCRIPTION = 'Returns images from given page';
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Mspabooru';
- const URI = 'https://mspabooru.com/';
- const DESCRIPTION = 'Returns images from given page';
-
- protected function buildThumbnailURI($element){
- return $this->getURI() . 'thumbnails/' . $element->directory
- . '/thumbnail_' . $element->image;
- }
+ protected function buildThumbnailURI($element)
+ {
+ return $this->getURI() . 'thumbnails/' . $element->directory
+ . '/thumbnail_' . $element->image;
+ }
}
diff --git a/bridges/MydealsBridge.php b/bridges/MydealsBridge.php
index 5fd27670..257ef561 100644
--- a/bridges/MydealsBridge.php
+++ b/bridges/MydealsBridge.php
@@ -1,2081 +1,2080 @@
<?php
-class MydealsBridge extends PepperBridgeAbstract {
+class MydealsBridge extends PepperBridgeAbstract
+{
+ const NAME = 'Mydeals bridge';
+ const URI = 'https://www.mydealz.de/';
+ const DESCRIPTION = 'Zeigt die Deals von mydeals.de';
+ const MAINTAINER = 'sysadminstory';
+ const PARAMETERS = [
+ 'Suche nach Stichworten' => [
+ 'q' => [
+ 'name' => 'Stichworten',
+ 'type' => 'text',
+ 'exampleValue' => 'lamp',
+ 'required' => true
+ ],
+ 'hide_expired' => [
+ 'name' => 'Abgelaufenes ausblenden',
+ 'type' => 'checkbox',
+ ],
+ 'hide_local' => [
+ 'name' => 'Lokales ausblenden',
+ 'type' => 'checkbox',
+ 'title' => 'Deals im physischen Geschäft ausblenden',
+ ],
+ 'priceFrom' => [
+ 'name' => 'Minimaler Preis',
+ 'type' => 'text',
+ 'title' => 'Minmaler Preis in Euros',
+ 'required' => false
+ ],
+ 'priceTo' => [
+ 'name' => 'Maximaler Preis',
+ 'type' => 'text',
+ 'title' => 'maximaler Preis in Euro',
+ 'required' => false
+ ],
+ ],
- const NAME = 'Mydeals bridge';
- const URI = 'https://www.mydealz.de/';
- const DESCRIPTION = 'Zeigt die Deals von mydeals.de';
- const MAINTAINER = 'sysadminstory';
- const PARAMETERS = array(
- 'Suche nach Stichworten' => array (
- 'q' => array(
- 'name' => 'Stichworten',
- 'type' => 'text',
- 'exampleValue' => 'lamp',
- 'required' => true
- ),
- 'hide_expired' => array(
- 'name' => 'Abgelaufenes ausblenden',
- 'type' => 'checkbox',
- ),
- 'hide_local' => array(
- 'name' => 'Lokales ausblenden',
- 'type' => 'checkbox',
- 'title' => 'Deals im physischen Geschäft ausblenden',
- ),
- 'priceFrom' => array(
- 'name' => 'Minimaler Preis',
- 'type' => 'text',
- 'title' => 'Minmaler Preis in Euros',
- 'required' => false
- ),
- 'priceTo' => array(
- 'name' => 'Maximaler Preis',
- 'type' => 'text',
- 'title' => 'maximaler Preis in Euro',
- 'required' => false
- ),
- ),
-
- 'Deals pro Gruppen' => array(
- 'group' => array(
- 'name' => 'Gruppen',
- 'type' => 'list',
- 'title' => 'Gruppe, deren Deals angezeigt werden müssen',
- 'values' => array(
- '1Password' => '1password',
- '3D Drucker' => '3d-drucker',
- '4K Fernseher' => '4k-fernseher',
- '4K Monitore' => '4k-monitor',
- '4K Ultra HD Blu-ray' => 'ultra-hd-blu-ray',
- '8K Fernseher' => '8k-fernseher',
- '32 Zoll Fernseher' => '32-zoll-fernseher',
- '55 Zoll Fernseher' => '55-zoll-fernseher',
- '65 Zoll Fernseher' => '65-zoll-fernseher',
- '75 Zoll Fernseher' => '75-zoll-fernseher',
- '1151 Mainboard' => '1151-mainboard',
- 'Abus' => 'abus',
- 'ABUS Fahrradschlösser' => 'abus-fahrradschloss',
- 'Accessoires' => 'accessoires',
- 'Acer' => 'acer',
- 'Acer Aspire' => 'acer-aspire',
- 'Acer Laptops' => 'acer-laptop',
- 'Acer Monitore' => 'acer-monitor',
- 'Acer Predator' => 'acer-predator',
- 'Action Cameras' => 'actioncam',
- 'Actionfiguren' => 'actionfiguren',
- 'adidas' => 'adidas',
- 'adidas Essentials' => 'adidas-neo',
- 'adidas Iniki' => 'adidas-iniki',
- 'adidas NMD' => 'adidas-nmd',
- 'adidas Originals' => 'adidas-originals',
- 'adidas Schuhe' => 'adidas-schuhe',
- 'adidas Superstar' => 'adidas-superstar',
- 'adidas Ultraboost' => 'adidas-ultraboost',
- 'adidas ZX Flux' => 'adidas-zx-flux',
- 'Adventskalender' => 'adventskalender',
- 'AEG' => 'aeg',
- 'AEG Waschmaschinen' => 'aeg-waschmaschine',
- 'Age of Empires' => 'age-of-empires',
- 'AiO Wasserkühlung' => 'aio-wasserkuehlung',
- 'AKG' => 'akg',
- 'Akkus' => 'akkus',
- 'Akkuschrauber' => 'akkuschrauber',
- 'Alfa Romeo' => 'alfa-romeo',
- 'Alienware' => 'alienware',
- 'Alkohol' => 'alkohol',
- 'All Inclusive Reisen' => 'all-inclusive',
- 'All in One PCs' => 'all-in-one-pcs',
- 'AM4 Mainboard' => 'am4-mainboard',
- 'Amazfit' => 'xiaomi-amazfit',
- 'Amazfit Bip' => 'amazfit-bip',
- 'Amazfit GTS' => 'amazfit-gts',
- 'Amazon Echo' => 'amazon-echo',
- 'Amazon Echo Dot' => 'amazon-echo-dot',
- 'Amazon Echo Plus' => 'amazon-echo-plus',
- 'Amazon Echo Show' => 'amazon-echo-show',
- 'Amazon Echo Show 5' => 'amazon-echo-show-5',
- 'Amazon Echo Show 8' => 'amazon-echo-show-8',
- 'Amazon Echo Spot' => 'amazon-echo-spot',
- 'Amazon Fire TV Cube' => 'fire-tv-cube',
- 'Amazon Fire TV Stick' => 'fire-tv',
- 'Amazon Fire TV Stick 4K' => 'fire-tv-stick-4k',
- 'Amazon Tablets' => 'amazon-tablet',
- 'Amazon Warehouse Deals' => 'amazon-warehouse-deals',
- 'AMD' => 'amd',
- 'AMD Radeon' => 'amd-radeon',
- 'AMD Radeon VII' => 'vega-7',
- 'AMD RX Vega' => 'amd-vega',
- 'AMD Ryzen' => 'amd-ryzen',
- 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x',
- 'American Express' => 'american-express',
- 'amiibo' => 'amiibo',
- 'Analoguhren' => 'analoguhren',
- 'Android Apps' => 'android-apps',
- 'Android Smartphones' => 'android-smartphones',
- 'Angelzubehör' => 'angelsport',
- 'Animal Crossing' => 'animal-crossing',
- 'Animal Crossing: New Horizons' => 'animal-crossing-new-horizons',
- 'Anime' => 'anime',
- 'Ankündigungen' => 'ankundigungen',
- 'Anno 1800' => 'anno-1800',
- 'Anthem' => 'anthem',
- 'Anzug' => 'anzug',
- 'AOC' => 'aoc',
- 'Apex Legends' => 'apex-legends',
- 'Apotheke' => 'apotheke',
- 'Apple' => 'apple',
- 'Apple AirPods' => 'airpods',
- 'Apple AirPods 2' => 'airpods-2',
- 'Apple AirPods Max' => 'airpods-max',
- 'Apple AirPods Pro' => 'airpods-pro',
- 'Apple EarPods' => 'apple-earpods',
- 'Apple HomePod' => 'homepod',
- 'Apple HomePod mini' => 'apple-homepod-mini',
- 'Apple Kopfhörer' => 'apple-kopfhoerer',
- 'Apple Magic Mouse 2' => 'apple-magic-mouse-2',
- 'Apple Pencil' => 'apple-pencil',
- 'Apple Pencil 2' => 'apple-pencil-2',
- 'Apple TV' => 'apple-tv',
- 'Apple Watch' => 'apple-watch',
- 'Apple Watch 3' => 'apple-watch-3',
- 'Apple Watch 4' => 'apple-watch-4',
- 'Apple Watch 5' => 'apple-watch-5',
- 'Apple Watch 6' => 'apple-watch-6',
- 'Apple Watch SE' => 'apple-watch-se',
- 'Apps' => 'apps',
- 'Aquaristik' => 'aquaristik',
- 'Arbeitsspeicher' => 'arbeitsspeicher',
- 'Arbeitszimmermöbel' => 'arbeitszimmer',
- 'ASICS' => 'asics',
- 'Assassin&#039;s Creed' => 'assassins-creed',
- 'Assassin&#039;s Creed: Valhalla' => 'assassins-creed-valhalla',
- 'Assassin&#039;s Creed Odyssey' => 'assassins-creed-odyssey',
- 'Assassin&#039;s Creed Origins' => 'assassins-creed-origins',
- 'ASTRO Gaming A50' => 'astro-gaming-a50',
- 'ASUS' => 'asus',
- 'ASUS Laptops' => 'asus-laptop',
- 'Asus Mainboard' => 'asus-mainboard',
- 'Asus Monitore' => 'asus-monitor',
- 'ASUS ROG' => 'asus-rog',
- 'ASUS Smartphones' => 'asus-smartphones',
- 'Asus ZenBook' => 'asus-zenbook',
- 'ASUS ZenFone 5' => 'asus-zenfone-5',
- 'ASUS ZenFone 5Z' => 'asus-zenfone-5z',
- 'Audi' => 'audi',
- 'Audio &amp; HiFi' => 'audio-hifi',
- 'Audioverstärker' => 'audioverstaerker',
- 'Audio Zubehör' => 'audio-zubehoer',
- 'Aukey' => 'aukey',
- 'Außenleuchten' => 'aussenleuchten',
- 'Auto &amp; Motorrad' => 'auto-motorrad',
- 'Auto Bild' => 'auto-bild',
- 'Auto Leasing' => 'auto-leasing',
- 'Auto Leasing Gewerbe' => 'gewerbe-leasing',
- 'Auto Leasing Privat' => 'privat-leasing',
- 'Automatikuhren' => 'automatikuhr',
- 'auto motor und sport' => 'auto-motor-sport',
- 'Autoradio' => 'autoradio',
- 'Auto Teile' => 'autoteile',
- 'Autowäsche' => 'autowaesche',
- 'Auto Zubehör' => 'auto',
- 'AVM FRITZ!Box' => 'avm-fritz-box',
- 'AVM FRITZ!Box 7490' => 'avm-fritz-box-7490',
- 'AVM FRITZ!Box 7530' => 'avm-fritz-box-7530',
- 'AVM FRITZ!Box 7580' => 'avm-fritz-box-7580',
- 'AVM FRITZ!Box 7590' => 'avm-fritz-box-7590',
- 'AVM FRITZ! DECT 301' => 'avm-fritz-dect-301',
- 'AV Receiver' => 'av-receiver',
- 'Baby &amp; Kind' => 'kinder',
- 'Baby-Erstausstattung' => 'baby-erstausstattung',
- 'Babybetten' => 'babybetten',
- 'Baby Born' => 'baby-born',
- 'Babykleidung' => 'babybekleidung',
- 'Babynahrung' => 'babynahrung',
- 'Babyphone' => 'babyphone',
- 'Backofen &amp; Herd' => 'backofen-herd',
- 'Backwaren' => 'backwaren',
- 'Backzubehör' => 'backzubehoer',
- 'Bademode' => 'bademode',
- 'Badmöbel' => 'badezimmer',
- 'Bahn-Tickets' => 'bahntickets',
- 'Bahncard' => 'bahncard',
- 'Balkonmöbel' => 'balkonmoebel',
- 'Ballerinas' => 'ballerinas',
- 'Bang &amp; Olufsen' => 'bang-olufsen',
- 'Bank' => 'bank',
- 'Barbie' => 'barbie',
- 'Barclaycard' => 'barclaycard',
- 'Bartschneider' => 'bartschneider',
- 'Batterien' => 'batterien',
- 'Battle.net' => 'battle-net',
- 'Battlefield' => 'battlefield',
- 'Battlefield 1' => 'battlefield-1',
- 'Battlefield 5' => 'battlefield-5',
- 'Bauknecht' => 'bauknecht',
- 'Bauknecht Waschmaschinen' => 'bauknecht-waschmaschine',
- 'Baumarkt' => 'baumarkt',
- 'Bayonetta' => 'bayonetta',
- 'Bayonetta 2' => 'bayonetta-2',
- 'Beamer' => 'beamer',
- 'Beamer Leinwand' => 'beamer-leinwand',
- 'Beats by Dre' => 'beats-by-dre',
- 'Beats Solo3' => 'beats-solo3',
- 'Beats Solo Pro' => 'beats-solo-pro',
- 'Beats Studio3' => 'beats-studio3',
- 'Beauty &amp; Gesundheit' => 'beauty',
- 'Beko' => 'beko',
- 'Beleuchtung' => 'beleuchtung',
- 'Belkin' => 'belkin',
- 'Ben &amp; Jerry&#039;s' => 'ben-jerrys',
- 'Bench' => 'bench',
- 'BenQ' => 'benq',
- 'BenQ Monitore' => 'benq-monitor',
- 'be quiet!' => 'be-quiet',
- 'be quiet! Netzteile' => 'be-quiet-netzteil',
- 'Besteck' => 'besteck',
- 'Bethesda' => 'bethesda',
- 'Betten' => 'betten',
- 'Bettwäsche' => 'bettwaesche',
- 'beyerdynamic' => 'beyerdynamic',
- 'Beyerdynamic MMX 300' => 'beyerdynamic-mmx-300',
- 'BHs' => 'bhs',
- 'Bier' => 'bier',
- 'Biking &amp; Urban Sport' => 'biking-urban-sport',
- 'Bildbearbeitungsprogramme' => 'bildbearbeitungsprogramme',
- 'Birkenstock' => 'birkenstock',
- 'Black &amp; Decker' => 'black-and-decker',
- 'Blackberry Smartphones' => 'blackberry',
- 'Black Desert Online' => 'black-desert-online',
- 'Blazer' => 'blazer',
- 'Blood &amp; Truth' => 'blood-truth',
- 'Blu-ray' => 'blu-ray',
- 'Blu-ray Player' => 'blu-ray-player',
- 'Bluetooth Kopfhörer' => 'bluetooth-kopfhoerer',
- 'Bluetooth Lautsprecher' => 'bluetooth-lautsprecher',
- 'Blumen' => 'blumen',
- 'Blusen' => 'blusen',
- 'BMW' => 'bmw',
- 'Bodenbelag' => 'bodenbelag',
- 'Boho-Chic wohnen' => 'boho-chich-wohnen',
- 'Bohrer' => 'bohrer',
- 'Bohrhämmer' => 'bohrhaemmer',
- 'Bohrmaschinen' => 'bohrmaschinen',
- 'Bollerwagen' => 'bollerwagen',
- 'Bombay Gin' => 'bombay',
- 'Borderlands' => 'borderlands',
- 'Borderlands 3' => 'borderlands-3',
- 'Bosch' => 'bosch',
- 'Bosch Akkuschrauber' => 'bosch-akkuschrauber',
- 'Bosch Geschirrspüler' => 'bosch-geschirrspueler',
- 'Bosch Kühlschränke' => 'bosch-kuehlschrank',
- 'Bosch Waschmaschinen' => 'bosch-waschmaschine',
- 'Bose' => 'bose',
- 'Bose Headphones 700' => 'bose-headphones-700',
- 'Bose Home Speaker 500' => 'bose-home-speaker-500',
- 'Bose Kopfhörer' => 'bose-kopfhoerer',
- 'Bose QuietComfort' => 'bose-quietcomfort',
- 'Bose QuietComfort 35 II' => 'bose-quiet-comfort-35-ii',
- 'Bose Solo 5' => 'bose-solo-5',
- 'Bose SoundLink' => 'bose-soundlink',
- 'Bose SoundTouch' => 'bose-soundtouch',
- 'BOSS' => 'boss',
- 'Bourbon' => 'bourbon',
- 'Bowers &amp; Wilkins' => 'bowers-wilkins',
- 'Boxershorts' => 'boxershorts',
- 'Boxspringbetten' => 'boxspringbetten',
- 'Braun' => 'braun',
- 'Braun Rasierer' => 'braun-rasierer',
- 'Braun Series 3' => 'braun-series-3',
- 'Braun Series 5' => 'braun-series-5',
- 'Braun Series 7' => 'braun-series-7',
- 'Braun Series 9' => 'braun-series-9',
- 'Bridgekameras' => 'bridgekamera',
- 'Brigitte' => 'brigitte',
- 'Brillen &amp; Kontaktlinsen' => 'brillen',
- 'Brita' => 'brita',
- 'Britax Römer' => 'britax-roemer',
- 'Brotaufstrich' => 'brotaufstrich',
- 'Brother Drucker' => 'brother-drucker',
- 'Bücher' => 'buecher',
- 'Bücher, Magazine &amp; Zeitschriften' => 'buecher-zeitschriften',
- 'bugatti' => 'bugatti',
- 'Bügeleisen' => 'buegeleisen',
- 'Bügeln' => 'buegeln',
- 'Buggy' => 'buggy',
- 'Burger' => 'burger',
- 'BURNHARD' => 'burnhard',
- 'Bürobedarf' => 'buerobedarf',
- 'Bürostühle' => 'buerostuhl',
- 'Bus &amp; Bahn' => 'bus-bahn',
- 'Business Mode' => 'business-mode',
- 'c&#039;t – Magazin für Computertechnik' => 'ct-magazin-computertechnik',
- 'Cafissimo' => 'cafissimo',
- 'Call of Duty' => 'call-of-duty',
- 'Call of Duty: Black Ops 4' => 'call-of-duty-black-ops-4',
- 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war',
- 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare',
- 'Call of Duty: Modern Warfare' => 'call-of-duty-modern-warfare',
- 'Call of Duty: Warzone' => 'call-of-duty-warzone',
- 'Call of Duty: WW2' => 'call-of-duty-ww2',
- 'Calvin Klein' => 'calvin-klein',
- 'Camcorder' => 'camcorder',
- 'Campen' => 'campen',
- 'Canon' => 'canon',
- 'Canon Drucker' => 'canon-drucker',
- 'Canon EOS' => 'canon-eos',
- 'Canon Kameras' => 'canon-kameras',
- 'Canon PowerShot' => 'canon-powershot',
- 'CANTON' => 'canton',
- 'Caps' => 'caps',
- 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker',
- 'Capture One' => 'capture-one',
- 'Carhartt' => 'carhartt',
- 'Carsharing' => 'carsharing',
- 'Casio' => 'casio',
- 'Cheap Monday' => 'cheapmonday',
- 'Chevrolet' => 'chevrolet',
- 'China Handys' => 'china-handys',
- 'Chip (Magazin)' => 'chip-magazin',
- 'Chips' => 'chips',
- 'Christbaumschmuck' => 'christbaumschmuck',
- 'Christbaumständer' => 'christbaumstaender',
- 'Chromebook' => 'chromebook',
- 'Chronographen' => 'chronograph',
- 'Chucks' => 'chucks',
- 'Citroen' => 'citroen',
- 'Coca-Cola' => 'coca-cola',
- 'Comics' => 'comics',
- 'Computer' => 'computer',
- 'Computer &amp; Tablets' => 'computer-tablet',
- 'Computer Bild' => 'computer-bild',
- 'Controller' => 'controller',
- 'Converse' => 'converse',
- 'Convertibles' => 'convertibles',
- 'Corsair' => 'corsair',
- 'Corsair VOID PRO' => 'corsair-void-pro',
- 'Couchtische' => 'couchtische',
- 'Coupons' => 'coupons',
- 'CPU-Kühler' => 'cpu-kuehler',
- 'Craghoppers' => 'craghoppers',
- 'Crocs' => 'crocs',
- 'Crucial' => 'crucial',
- 'Cupra' => 'cupra',
- 'Cyberpunk 2077' => 'cyberpunk-2077',
- 'cybex' => 'cybex',
- 'D-Link' => 'd-link',
- 'DAB Radios' => 'dab-radios',
- 'Dacia' => 'dacia',
- 'Damenbekleidung' => 'fashion-frauen',
- 'Damenschuhe' => 'damenschuhe',
- 'Dampfbügelstation' => 'dampfbuegelstation',
- 'Dampfgarer' => 'dampfgarer',
- 'Dampfreiniger' => 'dampfreiniger',
- 'Dark Souls' => 'dark-souls',
- 'Dashcam' => 'dashcam',
- 'Datentarif' => 'datentarif',
- 'Daypack' => 'daypack',
- 'Days Gone' => 'days-gone',
- 'DC Shoes' => 'dc-shoes',
- 'DDR3 RAM' => 'ddr3-ram',
- 'DDR4 RAM' => 'ddr4-ram',
- 'De&#039;Longhi' => 'delonghi',
- 'Death Stranding' => 'death-stranding',
- 'Deckenlampen' => 'deckenlampen',
- 'DECT Telefone' => 'telefone',
- 'Dekoration' => 'dekoration',
- 'Dell' => 'dell',
- 'Dell Laptops' => 'dell-laptop',
- 'Dell Monitore' => 'dell-monitor',
- 'Dell XPS' => 'dell-xps',
- 'Denon' => 'denon',
- 'Deo' => 'deo',
- 'Depot' => 'depot',
- 'DER SPIEGEL' => 'der-spiegel',
- 'Designermöbel' => 'designermoebel',
- 'Desigual' => 'desigual',
- 'Desinfektionsmittel' => 'desinfektionsmittel',
- 'Desktop PCs' => 'desktop-pc',
- 'Dessous' => 'dessous',
- 'Destiny' => 'destiny',
- 'Destiny 2' => 'destiny-2',
- 'Deus Ex' => 'deus-ex',
- 'Deus Ex: Mankind' => 'deus-ex-mankind',
- 'Deuter' => 'deuter',
- 'DeutschlandCard' => 'deutschlandcard',
- 'devolo' => 'devolo',
- 'DeWalt' => 'dewalt',
- 'Die drei Fragezeichen' => 'die-drei-fragezeichen',
- 'Die Eiskönigin' => 'die-eiskoenigin',
- 'Dienstleistungen &amp; Verträge' => 'dienstleistungen-vertraege',
- 'Dies &amp; Das' => 'dies-das',
- 'Diesel' => 'diesel',
- 'Die Sims' => 'die-sims',
- 'Die Sims 4' => 'die-sims-4',
- 'Die Zeit' => 'die-zeit',
- 'Digitalreceiver' => 'digitalreceiver',
- 'Digitaluhren' => 'digitaluhr',
- 'Direktflüge' => 'direktfluege',
- 'Dirt Devil' => 'dirt-devil',
- 'Dishonored' => 'dishonored',
- 'Dishonored 2: Das Vermächtnis der Maske' => 'dishonored-2',
- 'Disney' => 'disney',
- 'Disney+' => 'disney-plus',
- 'DJI' => 'dji',
- 'DJI Osmo Pocket' => 'dji-osmo-pocket',
- 'Dockers' => 'dockers',
- 'Dolce Gusto' => 'dolce-gusto',
- 'DOOM Eternal' => 'doom-eternal',
- 'Douglas Adventskalender' => 'douglas-adventskalender',
- 'Dr. Martens' => 'dr-martens',
- 'Dragon Ball' => 'dragon-ball',
- 'Dragon Ball FighterZ' => 'dragon-ball-fighterz',
- 'Dragon Ball Z: Kakarot' => 'dragon-ball-z-kakarot',
- 'Dragon Quest Builders' => 'dragon-quest-builders',
- 'Dragon Quest Builders 2' => 'dragon-quest-builders-2',
- 'Dreame Staubsauger' => 'xiaomi-staubsauger',
- 'Dreame T20' => 'dreame-t20',
- 'Dreame V9' => 'xiaomi-dreame-v9',
- 'Dreame V10' => 'xiaomi-dreame-v10',
- 'Dreame V11' => 'xiaomi-dreame-v11',
- 'Drohnen' => 'drohnen',
- 'Drucker' => 'drucker',
- 'Druckerpatronen' => 'druckerpatronen',
- 'Druckerzubehör' => 'druckerzubehoer',
- 'DSL &amp; Kabel' => 'dsl',
- 'Dunstabzugshauben' => 'dunstabzugshauben',
- 'Durex' => 'durex',
- 'Duscharmaturen' => 'duscharmaturen',
- 'Duschgel' => 'duschgel',
- 'Duschköpfe' => 'duschkoepfe',
- 'DVD' => 'dvd',
- 'Dyson' => 'dyson',
- 'Dyson Staubsauger' => 'dyson-staubsauger',
- 'Dyson V6' => 'dyson-v6',
- 'Dyson V7' => 'dyson-v7',
- 'Dyson V8' => 'dyson-v8',
- 'Dyson V10' => 'dyson-v10',
- 'Dyson V11' => 'dyson-v11',
- 'Dyson V11 Absolute' => 'dyson-v11-absolute',
- 'Dyson V11 Animal' => 'dyson-v11-animal',
- 'E-Bikes' => 'e-bikes',
- 'E-Scooter' => 'e-scooter',
- 'E-Scooter Sharing' => 'e-scooter-sharing',
- 'E-Zigaretten' => 'e-zigaretten',
- 'Eastpak' => 'eastpak',
- 'eBook Reader' => 'ebook-reader',
- 'eBooks' => 'ebooks',
- 'Ecovacs' => 'ecovacs',
- 'Ecovacs Deebot 900' => 'ecovacs-deebot-900',
- 'Ecovacs Deebot OZMO 930' => 'ecovacs-deebot-ozmo-930',
- 'Edifier' => 'edifier',
- 'Edifier R1280DB' => 'edifier-r1280db',
- 'Edifier R1280T' => 'edifier-r1280t',
- 'Einhell' => 'einhell',
- 'Eis' => 'eis',
- 'Elektrische Zahnbürsten' => 'elektrische-zahnbuersten',
- 'Elektrogrills' => 'elektrogrill',
- 'Elektroheizungen' => 'elektroheizungen',
- 'Elektronik' => 'elektronik',
- 'Elektronik Zubehör' => 'elektronikzubehoer',
- 'Elektrorasierer' => 'elektrorasierer',
- 'Elektroroller' => 'elektroroller',
- 'Elektrowerkzeuge' => 'elektrowerkzeug',
- 'Elephone' => 'elephone',
- 'ELLE' => 'elle',
- 'Emsa' => 'emsa',
- 'Energy Drinks' => 'energy-drinks',
- 'Entsafter' => 'entsafter',
- 'Epilierer' => 'epilierer',
- 'Epson' => 'epson',
- 'Epson Drucker' => 'epson-drucker',
- 'Erotik' => 'erotik',
- 'Error Fare' => 'error-fare',
- 'Espressomaschinen' => 'espressomaschinen',
- 'Esprit' => 'esprit',
- 'Esstische' => 'esstisch',
- 'Esszimmer' => 'esszimmer',
- 'Eterna' => 'eterna',
- 'EUROtronic Comet DECT' => 'eurotronic-comet-dect',
- 'Externe Festplatten' => 'externe-festplatten',
- 'F1 2017' => 'f1-2017',
- 'F1 2019' => 'f1-2019',
- 'F1 2020' => 'f1-2020',
- 'Fahrräder' => 'fahrraeder',
- 'Fahrradhelme' => 'fahrradhelme',
- 'Fahrradrucksäcke' => 'fahrradrucksack',
- 'Fahrradschlösser' => 'fahrradschloss',
- 'Fahrradteile' => 'fahrradteile',
- 'Fahrradträger' => 'fahrradtraeger',
- 'Fahrradzubehör' => 'fahrradzubehoer',
- 'Fahrzeuge' => 'fahrzeuge',
- 'Falke' => 'falke',
- 'Fallout' => 'fallout',
- 'Fallout 4' => 'fallout-4',
- 'Fallout 76' => 'fallout-76',
- 'Family &amp; Kids' => 'family-kids',
- 'Far Cry' => 'far-cry',
- 'Far Cry 5' => 'far-cry-5',
- 'Far Cry New Dawn' => 'far-cry-new-dawn',
- 'Fashion &amp; Accessoires' => 'fashion-accessoires',
- 'Fast Food' => 'fast-food',
- 'Felgen' => 'felgen',
- 'Fenstersauger' => 'fenstersauger',
- 'Fernbus-Tickets' => 'fernbus',
- 'Fernseher' => 'fernseher',
- 'Fertiggerichte' => 'fertiggerichte',
- 'Festplatten' => 'festplatten',
- 'Festplattengehäuse' => 'festplattengehaeuse',
- 'FFP2 Masken' => 'ffp2-masken',
- 'Fiat' => 'fiat',
- 'FIFA' => 'fifa',
- 'FIFA 17' => 'fifa-17',
- 'FIFA 18' => 'fifa-18',
- 'FIFA 19' => 'fifa-19',
- 'FIFA 20' => 'fifa-20',
- 'FIFA 21' => 'fifa-21',
- 'FILA' => 'fila',
- 'Filme &amp; Serien' => 'filme-serien',
- 'Filterkaffeemaschinen' => 'filterkaffeemaschinen',
- 'Final Fantasy' => 'final-fantasy',
- 'Final Fantasy 7' => 'final-fantasy-7',
- 'Finanzen- und Steuersoftware' => 'finanzen-und-steuersoftware',
- 'Finish' => 'finish',
- 'Fisch &amp; Meeresfrüchte' => 'fisch-meeresfruechte',
- 'Fischertechnik' => 'fischertechnik',
- 'Fisher-Price' => 'fisher-price',
- 'Fiskars' => 'fiskars',
- 'Fissler' => 'fissler',
- 'fitbit' => 'fitbit',
- 'Fitness &amp; Running' => 'fitness',
- 'Fitness Apps' => 'fitness-apps',
- 'Fitnessstudio' => 'fitnessstudio',
- 'Fitnesstracker' => 'fitnesstracker',
- 'Fjällräven' => 'fjaellraeven',
- 'Fleisch &amp; Wurst' => 'fleisch-wurst',
- 'Fliesenschneider' => 'fliesenschneider',
- 'Flüge' => 'fluege',
- 'Flurmöbel' => 'flurmoebel',
- 'FOCUS' => 'focus',
- 'Ford' => 'ford',
- 'For Honor' => 'for-honor',
- 'Formel 1 Games' => 'formel-1',
- 'Fortnite' => 'fortnite',
- 'Forza' => 'forza',
- 'Forza Horizon' => 'forza-horizon',
- 'Forza Horizon 4' => 'forza-horizon-4',
- 'Forza Motorsport' => 'forza-motorsport',
- 'Forza Motorsport 7' => 'forza-7',
- 'Fossil' => 'fossil',
- 'Foto &amp; Kamera' => 'foto-video',
- 'Foto Apps' => 'foto-apps',
- 'Fotobücher' => 'fotobuecher',
- 'Fototapete' => 'fototapete',
- 'Fragen &amp; Gesuche' => 'gesuche',
- 'Frankfurter Allgemeine Zeitung (F.A.Z.)' => 'frankfurter-allgemeine-zeitung',
- 'FreeSync Monitore' => 'freesync-monitor',
- 'Freizeitpark-Tickets' => 'freizeitpark',
- 'Freizeitsport' => 'freizeitsport',
- 'Fritteusen' => 'fritteusen',
- 'Frontlader' => 'frontlader',
- 'Frühlingsdeko' => 'fruehlingsdeko',
- 'Frühstücksflocken' => 'fruehstuecksflocken',
- 'Fruit of the Loom' => 'fruit-of-the-loom',
- 'Fujifilm' => 'fujifilm',
- 'Füller' => 'fueller',
- 'Full HD-Beamer' => 'full-hd-beamer',
- 'Fun Factory' => 'fun-factory',
- 'FurReal Friends' => 'furreal-friends',
- 'Fußball' => 'fussball',
- 'Fußball-Trikots' => 'fussball-trikots',
- 'Fußballschuhe' => 'fussballschuhe',
- 'G-Star' => 'g-star',
- 'G-Sync Monitore' => 'g-sync-monitor',
- 'Game of Thrones' => 'game-of-thrones',
- 'Gaming' => 'gaming',
- 'Gaming Headsets' => 'gaming-headset',
- 'Gaming Laptops' => 'gaming-laptop',
- 'Gaming Mäuse' => 'gaming-maus',
- 'Gaming Monitore' => 'gaming-monitor',
- 'Gaming PCs' => 'gaming-pc',
- 'Gaming Stühle' => 'gaming-stuhl',
- 'Gaming Tastaturen' => 'gaming-tastatur',
- 'Gaming Zubehör' => 'spielekonsolen-zubehoer',
- 'Ganzjahresreifen' => 'ganzjahresreifen',
- 'GAP' => 'gap',
- 'Gardena' => 'gardena',
- 'Garderobe' => 'garderobe',
- 'Garmin' => 'garmin',
- 'Garmin Fenix' => 'garmin-fenix',
- 'Garten' => 'garten',
- 'Garten &amp; Baumarkt' => 'garten-baumarkt',
- 'Gartenarbeit' => 'gartenarbeit',
- 'Gartenbank' => 'gartenbank',
- 'Gartenliegen' => 'sonnenliegen',
- 'Gartenmöbel' => 'gartenmoebel',
- 'Gartenstühle' => 'gartenstuehle',
- 'Gartentische' => 'gartentische',
- 'Gasgrills' => 'gasgrill',
- 'Gastarif' => 'gastarif',
- 'Gears 5' => 'gears-5',
- 'Gears of War' => 'gears-of-war',
- 'Gefrierschränke' => 'gefrierschrank',
- 'Geld-zurück-Aktionen' => 'geld-zurueck',
- 'Geldbörsen' => 'geldboersen',
- 'Gemüse' => 'gemuese',
- 'Geox' => 'geox',
- 'Geschirr' => 'geschirr',
- 'Geschirrspüler' => 'geschirrspueler',
- 'Gesellschaftsspiele' => 'gesellschaftsspiele',
- 'Gesichtspflege' => 'gesichtspflege',
- 'Gesundheit' => 'gesundheit',
- 'Getränke' => 'getraenke',
- 'Gewinnspiele' => 'gewinnspiele',
- 'GHD' => 'ghd',
- 'Ghost of Tsushima' => 'ghost-of-tsushima',
- 'GIGABYTE' => 'gigabyte',
- 'Gigaset' => 'gigaset',
- 'Gillette' => 'gillette',
- 'Gillette Rasierer' => 'gillette-rasierer',
- 'Gin' => 'gin',
- 'Girokonto' => 'konto',
- 'Glamour' => 'glamour',
- 'Glamourös wohnen' => 'glamouroes-wohnen',
- 'Gläser' => 'glaeser',
- 'Glätteisen' => 'glaetteisen',
- 'Gleitgel' => 'gleitgel',
- 'Glühwein' => 'gluehwein',
- 'God of War' => 'god-of-war',
- 'Google Chromecast' => 'chromecast',
- 'Google Chromecast mit Google TV' => 'chromecast-mit-google-tv',
- 'Google Chromecast Ultra' => 'chromecast-ultra',
- 'Google Home' => 'google-home',
- 'Google Home Max' => 'google-home-max',
- 'Google Home Mini' => 'google-home-mini',
- 'Google Nest Hub' => 'google-nest-hub',
- 'Google Pixel' => 'google-pixel',
- 'Google Pixel 2' => 'google-pixel-2',
- 'Google Pixel 3' => 'google-pixel-3',
- 'Google Pixel 4' => 'google-pixel-4',
- 'Google Pixel 4 XL' => 'google-pixel-4xl',
- 'Google Pixel 4a' => 'google-pixel-4a',
- 'Google Pixel 4a 5G' => 'google-pixel-4a-5g',
- 'Google Pixel 5' => 'google-pixel-5',
- 'Google Smartphones' => 'google-smartphones',
- 'Google Stadia Konsolen' => 'google-stadia',
- 'GoPro Action Cameras' => 'gopro',
- 'GoPro HERO 7' => 'gopro-hero-7',
- 'GoPro HERO 8' => 'gopro-hero-8',
- 'GoPro HERO 9' => 'gopro-hero-9',
- 'Gorenje' => 'gorenje',
- 'Grafikkarten' => 'grafikkarten',
- 'Gran Turismo' => 'gran-turismo',
- 'Gran Turismo Sport' => 'gran-turismo-sport',
- 'Grazia' => 'grazia',
- 'Grills' => 'grill',
- 'Grillzubehör' => 'grillzubehoer',
- 'Grundig' => 'grundig',
- 'GTA' => 'gta',
- 'GTA V' => 'gta-v',
- 'GTX 1060' => 'gtx-1060',
- 'GTX 1070' => 'gtx-1070',
- 'GTX 1080' => 'gtx-1080',
- 'GTX 1080 Ti' => 'gtx-1080-ti',
- 'GTX 1660' => 'gtx-1660',
- 'GTX 1660 Ti' => 'gtx-1660-ti',
- 'Gucci' => 'gucci',
- 'Gummistiefel' => 'gummistiefel',
- 'Gürtel' => 'guertel',
- 'Gutscheinfehler' => 'gutscheinfehler',
- 'Haarentfernung' => 'haarentfernung',
- 'Haargel' => 'haargel',
- 'Haarpflege' => 'haarpflege',
- 'Haarschneidemaschinen' => 'haarschneidemaschinen',
- 'Haarspray' => 'haarspray',
- 'Haartrockner' => 'haartrockner',
- 'Haftpflichtversicherung' => 'haftpflichtversicherung',
- 'Hama' => 'hama',
- 'Handelsblatt' => 'handelsblatt',
- 'Handmixer' => 'handmixer',
- 'Handtaschen' => 'handtaschen',
- 'Handtücher' => 'handtuecher',
- 'Handwerkzeuge' => 'handwerkzeug',
- 'Handy &amp; Smartphone Zubehör' => 'smartphone-zubehoer',
- 'Handyhalterung' => 'handyhalterung',
- 'Handyhüllen' => 'handyhuelle',
- 'Handys mit Vertrag' => 'handys-mit-vertrag',
- 'Handys ohne Vertrag' => 'handys-ohne-vertrag',
- 'Handyversicherung' => 'handyversicherung',
- 'Handyverträge' => 'handyvertraege',
- 'Handyverträge 3 Monate Kündigungsfrist' => 'handyvertraege-3-monate-kuendigungsfrist',
- 'Handyverträge monatlich kündbar' => 'handyvertraege-monatlich-kuendbar',
- 'Hängematten' => 'haengematten',
- 'Hanteln' => 'hanteln',
- 'Haribo' => 'haribo',
- 'Harman Kardon' => 'harman-kardon',
- 'Harry Potter' => 'harry-potter',
- 'Hasbro' => 'hasbro',
- 'Haushaltsartikel' => 'haushaltsartikel',
- 'Haushaltsgeräte' => 'haushaltsgeraete',
- 'Haushaltswaren' => 'haushaltswaren',
- 'Hausratversicherung' => 'hausratsversicherung',
- 'Hausschuhe' => 'hausschuhe',
- 'Haustier' => 'haustier',
- 'Hautpflege' => 'hautpflege',
- 'Head &amp; Shoulders' => 'head-and-shoulders',
- 'Heckenscheren' => 'heckenschere',
- 'Heimkino' => 'heimkino',
- 'Heimtextilien' => 'heimtextilien',
- 'Heißluftfritteusen' => 'heissluftfriteuse',
- 'Heizkörperthermostat' => 'heizkoerperthermostat',
- 'Heizungen' => 'heizungen',
- 'Hemden' => 'hemden',
- 'Hendrick&#039;s Gin' => 'hendricks-gin',
- 'Herbstdeko' => 'herbstdeko',
- 'Herrenbekleidung' => 'fashion-maenner',
- 'Herrenschuhe' => 'herrenschuhe',
- 'HiPP' => 'hipp',
- 'Hisense' => 'hisense',
- 'Hochbetten' => 'hochbetten',
- 'Hochdruckreiniger' => 'hochdruckreiniger',
- 'Hochstuhl' => 'hochstuhl',
- 'Hollywoodschaukel' => 'hollywoodschaukel',
- 'Home &amp; Living' => 'home-living',
- 'homee' => 'homee',
- 'Honda' => 'honda',
- 'Honor' => 'honor',
- 'Honor 5' => 'honor-5',
- 'Honor 6' => 'honor-6',
- 'Honor 7X' => 'honor-7',
- 'Honor 8' => 'honor-8',
- 'Honor 9' => 'honor-9',
- 'Honor 20' => 'honor-20',
- 'Honor 20 Lite' => 'honor-20-lite',
- 'Honor Band 4' => 'honor-band-4',
- 'Honor Band 5' => 'honor-band-5',
- 'Honor Play' => 'honor-play',
- 'Honor Smartphones' => 'honor-smartphones',
- 'Honor View 10' => 'honor-view-10',
- 'Honor View 20' => 'honor-view-20',
- 'Hoodies' => 'hoodies',
- 'Hörbücher' => 'hoerbuecher',
- 'Horizon Zero Dawn' => 'horizon-zero-dawn',
- 'Hörspiele' => 'hoerspiele',
- 'Hörzu' => 'hoerzu',
- 'Hosen' => 'hosen',
- 'Hotels &amp; Unterkünfte' => 'hotel',
- 'Hot Wheels' => 'hot-wheels',
- 'Hoverboards' => 'hoverboards',
- 'HP' => 'hp',
- 'HP Drucker' => 'hp-drucker',
- 'HP Laptops' => 'hp-laptop',
- 'HP OMEN' => 'hp-omen',
- 'HP Pavilion' => 'hp-pavilion',
- 'HTC 10' => 'htc-10',
- 'HTC Desire 12' => 'htc-desire',
- 'HTC Smartphones' => 'htc-smartphones',
- 'HTC U11' => 'htc-u11',
- 'HTC Vive' => 'htc-vive',
- 'Huawei' => 'huawei',
- 'Huawei Kopfhörer' => 'huawei-kopfhoerer',
- 'Huawei Mate 9' => 'huawei-mate-9',
- 'Huawei Mate 10' => 'huawei-mate-10',
- 'Huawei Mate 20' => 'huawei-mate-20',
- 'Huawei Mate 20 Lite' => 'huawei-mate-20-lite',
- 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro',
- 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro',
- 'Huawei MateBook' => 'huawei-matebook',
- 'Huawei P10' => 'huawei-p10',
- 'Huawei P20' => 'huawei-p20',
- 'Huawei P30' => 'huawei-p30',
- 'Huawei P30 Lite' => 'huawei-p30-lite',
- 'Huawei P30 Pro' => 'huawei-p30-pro',
- 'Huawei P40' => 'huawei-p40',
- 'Huawei P40 Lite' => 'huawei-p40-lite',
- 'Huawei P40 Pro' => 'huawei-p40-pro',
- 'Huawei P Smart' => 'huawei-p-smart',
- 'Huawei Smartphones' => 'huawei-smartphones',
- 'Huawei Tablets' => 'huawei-mediapad',
- 'Huawei Watch GT2' => 'huawei-watch-gt2',
- 'Huawei Y7' => 'huawei-y7',
- 'Hunde' => 'hunde',
- 'Hundefutter' => 'hundefutter',
- 'Hüte &amp; Mützen' => 'huete-muetzen',
- 'Hyrule Warriors' => 'hyrule-warriors',
- 'Hyrule Warriors: Zeit der Verheerung' => 'hyrule-warriors-zeit-der-verheerung',
- 'Hyundai' => 'hyundai',
- 'iMac' => 'imac',
- 'Immortals Fenyx Rising' => 'immortals-fenyx-rising',
- 'In-Ear Kopfhörer' => 'in-ear-kopfhoerer',
- 'Industrial Style' => 'industrial-style',
- 'Inline Skates' => 'inline-skates',
- 'Instax Mini' => 'instax-mini',
- 'Intel Core i9-9900K' => 'intel-core-i9-9900k',
- 'Intel i3' => 'intel-i3',
- 'Intel i5' => 'intel-i5',
- 'Intel i7' => 'intel-i7',
- 'Intel i9' => 'intel-i9',
- 'Intenso' => 'intenso',
- 'Internet Security' => 'internet-security',
- 'Intimpflege' => 'intimpflege',
- 'iOS Apps' => 'ios-apps',
- 'iPad' => 'ipad',
- 'iPad 2019' => 'ipad-2019',
- 'iPad 2020' => 'ipad-2020',
- 'iPad Air' => 'ipad-air-2',
- 'iPad Air 2019' => 'ipad-air-2019',
- 'iPad Air 2020' => 'ipad-air-2020',
- 'iPad mini' => 'ipad-mini',
- 'iPad Pro' => 'ipad-pro',
- 'iPad Pro 11' => 'ipad-pro-11',
- 'iPad Pro 12.9' => 'ipad-pro-12-9',
- 'iPad Pro 2020' => 'ipad-pro-2020',
- 'iPhone' => 'iphone',
- 'iPhone 6' => 'iphone-6',
- 'iPhone 6 Plus' => 'iphone-6-plus',
- 'iPhone 6s' => 'iphone-6s',
- 'iPhone 6s Plus' => 'iphone-6s-plus',
- 'iPhone 7' => 'iphone-7',
- 'iPhone 7 Plus' => 'iphone-7-plus',
- 'iPhone 8' => 'iphone-8',
- 'iPhone 8 Plus' => 'iphone-8-plus',
- 'iPhone 11' => 'iphone-11',
- 'iPhone 11 Pro' => 'iphone-11-pro',
- 'iPhone 11 Pro Max' => 'iphone-11-pro-max',
- 'iPhone 12' => 'iphone-12',
- 'iPhone 12 mini' => 'iphone-12-mini',
- 'iPhone 12 Pro' => 'iphone-12-pro',
- 'iPhone 12 Pro Max' => 'iphone-12-pro-max',
- 'iPhone SE' => 'iphone-se',
- 'iPhone X' => 'iphone-x',
- 'iPhone Xr' => 'iphone-xr',
- 'iPhone Xs' => 'iphone-xs',
- 'iPhone Xs Max' => 'iphone-xs-max',
- 'iPhone Zubehör' => 'iphone-zubehoer',
- 'Irish Whiskey' => 'irish-whiskey',
- 'iRobot' => 'irobot',
- 'iRobot Roomba' => 'irobot-roomba',
- 'iRobot Roomba 980' => 'irobot-roomba-980',
- 'iRobot Roomba i7' => 'irobot-roomba-i7',
- 'Isomatten' => 'isomatten',
- 'iTunes Guthaben' => 'itunes-guthaben',
- 'Jabra Elite 75t' => 'jabra-elite-75t',
- 'Jabra Elite 85h' => 'jabra-elite-85h',
- 'Jabra Elite 85t' => 'jabra-elite-85t',
- 'Jabra Elite Active 75t' => 'jabra-elite-active-75t',
- 'Jabra Kopfhörer' => 'jabra-kopfhoerer',
- 'JACK &amp; JONES' => 'jack-jones',
- 'Jacken' => 'jacken',
- 'JACK WOLFSKIN' => 'jack-wolfskin',
- 'Jagdzubehör' => 'jagdzubehoer',
- 'JBL' => 'jbl',
- 'JBL Charge 4' => 'jbl-charge-4',
- 'JBL Flip' => 'jbl-flip',
- 'JBL GO' => 'jbl-go',
- 'Jeans' => 'jeans',
- 'Jim Beam' => 'jim-beam',
- 'Jogginghosen' => 'jogginghosen',
- 'Joghurt' => 'joghurt',
- 'Johnnie Walker' => 'johnnie-walker',
- 'Jura Kaffeemaschinen' => 'jura',
- 'Just Cause' => 'just-cause',
- 'Just Cause 4' => 'just-cause-4',
- 'Kaffee' => 'kaffee',
- 'Kaffeekapseln' => 'kaffeekapseln',
- 'Kaffeemaschinen' => 'kaffeemaschinen',
- 'Kaffeemühlen' => 'kaffeemuehlen',
- 'Kaffeepadmaschinen' => 'kaffeepadmaschinen',
- 'Kaffeepads' => 'kaffeepads',
- 'Kaffeevollautomaten' => 'kaffeevollautomaten',
- 'Kameras' => 'kamera',
- 'Kamera Zubehör' => 'kamerazubehoer',
- 'Kamine' => 'kamine',
- 'Kapselmaschinen' => 'kapselmaschinen',
- 'Kärcher' => 'kaercher',
- 'Kärcher Fenstersauger' => 'kaercher-fenstersauger',
- 'Kärcher Hochdruckreiniger' => 'kaercher-hochdruckreiniger',
- 'Kartenspiele' => 'kartenspiel',
- 'Käse' => 'kaese',
- 'Katzen' => 'katzen',
- 'Katzenfutter' => 'katzenfutter',
- 'Kaufen im Ausland' => 'kaufen-ausland',
- 'Ketchup' => 'ketchup',
- 'KFZ Versicherung' => 'kfz-versicherung',
- 'KIA' => 'kia',
- 'kiddy' => 'kiddy',
- 'Kinder Adventskalender' => 'kinder-adventskalender',
- 'Kinderbekleidung' => 'kinderkleidung',
- 'Kinderbetten' => 'kinderbett',
- 'Kinderfahrräder' => 'kinderfahrrad',
- 'Kinderschuhe' => 'kinderschuhe',
- 'Kindersitz' => 'kindersitz',
- 'Kinderwagen' => 'kinderwagen',
- 'Kinderwagen &amp; Autositze' => 'baby-transport',
- 'Kinderzimmermöbel' => 'kinderzimmer',
- 'Kindle' => 'kindle',
- 'Kindle Oasis' => 'kindle-oasis',
- 'Kindle Paperwhite' => 'kindle-paperwhite',
- 'Kingdom Come: Deliverance' => 'kingdom-come-deliverance',
- 'Kingdom Hearts' => 'kingdom-hearts',
- 'Kingdom Hearts 3' => 'kingdom-hearts-3',
- 'Kingston HyperX Cloud Flight' => 'kingston-hyperx-cloud-flight',
- 'Kingston HyperX Cloud II' => 'hyperx-cloud-ii',
- 'Kino' => 'kino',
- 'KitchenAid' => 'kitchenaid',
- 'Kleider' => 'kleider',
- 'Kleiderschränke' => 'kleiderschraenke',
- 'Kleidung' => 'kleidung',
- 'Klemmbausteine' => 'klemmbausteine',
- 'Klimaanlagen' => 'klimaanlagen',
- 'Klimatechnik' => 'klimatechnik',
- 'Klipsch' => 'klipsch',
- 'Kochgeräte' => 'kochgeraete',
- 'Kodak' => 'kodak',
- 'Koffer' => 'koffer',
- 'Kohlenmonoxidmelder' => 'kohlenmonoxidmelder',
- 'Kolonialstil' => 'kolonialstil',
- 'Kommoden &amp; Sideboards' => 'kommoden-sideboards',
- 'Kondome' => 'kondome',
- 'König der Löwen Musical' => 'koenig-der-loewen-musical',
- 'Kontaktgrills' => 'kontaktgrill',
- 'Konto &amp; Kreditkarten' => 'konto-kreditkarten',
- 'Konzert-Tickets' => 'konzerte',
- 'Kopfhörer' => 'kopfhoerer',
- 'Körperpflege &amp; Hygiene' => 'koerperpflege',
- 'Kosmetik' => 'kosmetik',
- 'Kostüme' => 'kostuem',
- 'Kraftstoffe &amp; Betriebsstoffe' => 'kraftstoffe-betriebsstoffe',
- 'Krafttraining' => 'krafttraining',
- 'Kredit' => 'kredit',
- 'Kreditkarten' => 'kreditkarten',
- 'Kreissägen' => 'kreissaegen',
- 'Kreuzfahrten' => 'kreuzfahrten',
- 'Krups' => 'krups',
- 'Küche' => 'kueche',
- 'Küchengeräte' => 'kuechengeraete',
- 'Küchenhelfer' => 'kuechenhelfer',
- 'Küchenmaschinen' => 'kuechenmaschinen',
- 'Küchenmesser' => 'messer',
- 'Küchenutensilien' => 'kuechenutensilien',
- 'Kugelschreiber' => 'kugelschreiber',
- 'Kühl-Gefrierkombinationen' => 'kuehl-gefrierkombination',
- 'Kühlboxen' => 'kuehlboxen',
- 'Kühlschränke' => 'kuehlschrank',
- 'Kultur &amp; Freizeit' => 'kultur-freizeit',
- 'Kunst &amp; Hobby' => 'hobby',
- 'Kurse &amp; Trainings' => 'kurse-trainings',
- 'Lacoste' => 'lacoste',
- 'Ladegeräte' => 'ladegeraete',
- 'Lampen' => 'lampen',
- 'Landhausstil' => 'landhausstil',
- 'Landwirtschafts-Simulator' => 'landwirtschafts-simulator',
- 'Laptops' => 'laptop',
- 'Laserdrucker' => 'laserdrucker',
- 'Last Minute Reisen' => 'last-minute',
- 'Lattenroste' => 'lattenroste',
- 'Laubsauger' => 'laubsauger',
- 'Laufräder' => 'laufraeder',
- 'Laufschuhe' => 'laufschuhe',
- 'Laufsport' => 'laufsport',
- 'Lautsprecher' => 'lautsprecher',
- 'Lavazza' => 'lavazza',
- 'Lay-Z-Spa Whirlpools' => 'lay-z-spa-whirlpools',
- 'Lebensmittel' => 'lebensmittel',
- 'Lebensmittel &amp; Haushalt' => 'food',
- 'LED Lampen' => 'led-lampen',
- 'LEGO' => 'lego',
- 'LEGO Adventskalender' => 'lego-adventskalender',
- 'LEGO Architecture' => 'lego-architecture',
- 'LEGO Batman' => 'lego-batman',
- 'LEGO City' => 'lego-city',
- 'LEGO Creator' => 'lego-creator',
- 'LEGO Dimensions' => 'lego-dimensions',
- 'LEGO DUPLO' => 'lego-duplo',
- 'LEGO Friends' => 'lego-friends',
- 'LEGO Harry Potter' => 'lego-harry-potter',
- 'LEGO Marvel Super Heroes' => 'lego-marvel-super-heroes',
- 'LEGO Nexo Knights' => 'lego-nexo-knights',
- 'LEGO NINJAGO' => 'lego-ninjago',
- 'LEGO Star Wars' => 'lego-star-wars',
- 'LEGO Star Wars Millennium Falcon' => 'lego-star-wars-millennium-falcon',
- 'LEGO Super Mario' => 'lego-super-mario',
- 'LEGO Technic' => 'lego-technic',
- 'LEGO The Simpsons' => 'lego-simpsons',
- 'Leifheit' => 'leifheit',
- 'Lenovo' => 'lenovo',
- 'Lenovo Laptops' => 'lenovo-laptop',
- 'Lenovo Tablets' => 'lenovo-tablet',
- 'Lenovo ThinkPad' => 'lenovo-thinkpad',
- 'Lenovo Yoga' => 'lenovo-yoga',
- 'Leonardo' => 'leonardo',
- 'Leuchtmittel' => 'leuchten',
- 'Levi&#039;s' => 'levis',
- 'Lexar' => 'lexar',
- 'Lexmark' => 'lexmark',
- 'LG' => 'lg',
- 'LG Fernseher' => 'lg-fernsher',
- 'LG G5' => 'lg-g5',
- 'LG G6' => 'lg-g6',
- 'LG G7 ThinQ' => 'lg-g7-thinq',
- 'LG OLED Fernseher' => 'lg-oled-tv',
- 'LG Smartphones' => 'lg-smartphones',
- 'LG V30' => 'lg-v30',
- 'Lichterketten' => 'lichterketten',
- 'Liebeskind' => 'liebeskind',
- 'Lieferservice' => 'lieferservice',
- 'Lindt' => 'lindt',
- 'Lindt Adventskalender' => 'lindt-adventskalender',
- 'Logitech' => 'logitech',
- 'Logitech G413' => 'logitech-g413',
- 'Logitech G430' => 'logitech-g430',
- 'Logitech G502 Proteus Spectrum' => 'logitech-g502',
- 'Logitech G513' => 'logitech-g513',
- 'Logitech G533' => 'logitech-g533',
- 'Logitech G633 Artemis Spectrum' => 'logitech-g633',
- 'Logitech G703' => 'logitech-g703',
- 'Logitech G903' => 'logitech-g903',
- 'Logitech G910 Orion Spectrum' => 'logitech-g910',
- 'Logitech G915' => 'logitech-g915',
- 'Logitech G933 Artemis Spectrum' => 'logitech-g933',
- 'Logitech Harmony' => 'logitech-harmony',
- 'Logitech Mäuse' => 'logitech-maeuse',
- 'Logitech MX Master' => 'logitech-mx-master',
- 'Logitech MX Master 2S' => 'logitech-mx-master-2s',
- 'Logitech Tastaturen' => 'logitech-tastaturen',
- 'Logitech Z333' => 'logitech-z333',
- 'Logitech Z337' => 'logitech-z337',
- 'Logitech Z906' => 'logitech-z906',
- 'Luftbefeuchter' => 'luftbefeuchter',
- 'Luftentfeuchter' => 'luftentfeuchter',
- 'Luftmatratzen' => 'luftmatratzen',
- 'Luftreiniger' => 'luftreiniger',
- 'Luigi&#039;s Mansion' => 'luigis-mansion',
- 'Luigi&#039;s Mansion 3' => 'luigis-mansion-3',
- 'Lustiges Taschenbuch' => 'lustiges-taschenbuch',
- 'M.2 SSD' => 'm2-ssd',
- 'MacBook' => 'macbook',
- 'MacBook Air' => 'macbook-air',
- 'MacBook Pro' => 'macbook-pro',
- 'MacBook Pro 13' => 'macbook-pro-13',
- 'MacBook Pro 15' => 'macbook-pro-15',
- 'MacBook Pro 16' => 'macbook-pro-16',
- 'Mac mini' => 'mac-mini',
- 'Mac Software' => 'mac-software',
- 'Madden NFL' => 'madden-nfl',
- 'Magazine' => 'magazine',
- 'Magnat' => 'magnat',
- 'Magnum Eis' => 'magnum-eis',
- 'Mähroboter' => 'maehroboter',
- 'Mainboards' => 'mainboards',
- 'Make Up Adventskalender' => 'make-up-adventskalender',
- 'Makita' => 'makita',
- 'Makita Akkuschrauber' => 'makita-akkuschrauber',
- 'Malerwerkzeuge' => 'malerpinsel',
- 'Mangas' => 'mangas',
- 'Marantz' => 'marantz',
- 'Mario Kart' => 'mario-kart',
- 'Mario Kart 8 Deluxe' => 'mario-kart-8-deluxe',
- 'Marken' => 'marken',
- 'Marvel' => 'marvel',
- 'Marvel&#039;s Spider-Man: Miles Morales' => 'marvels-spider-man-miles-morales',
- 'Mass Effect' => 'mass-effect',
- 'Mass Effect: Andromeda' => 'mass-effect-andromeda',
- 'Massivholzmöbel' => 'massivholzmoebel',
- 'Mastercard' => 'mastercard',
- 'Matratzen' => 'matratzen',
- 'Maxi Cosi' => 'maxi-cosi',
- 'Mazda' => 'mazda',
- 'Medion' => 'medion',
- 'Mercedes-Benz' => 'mercedes-benz',
- 'Mesh WLAN Router' => 'mesh-wlan-router',
- 'Metabo' => 'metabo',
- 'Metro (Spiel)' => 'metro',
- 'Metro Exodus' => 'metro-exodus',
- 'Michael Kors' => 'michael-kors',
- 'microSD' => 'microsd',
- 'microSDHC' => 'microsdhc',
- 'microSDXC' => 'microsdxc',
- 'Microsoft Flight Simulator' => 'microsoft-flight-simulator',
- 'Microsoft Software' => 'microsoft-software',
- 'Microsoft Surface Notebooks' => 'microsoft-surface-notebooks',
- 'Microsoft Surface Pro 4' => 'surface-pro-4',
- 'Microsoft Surface Pro 6' => 'surface-pro-6',
- 'Microsoft Surface Pro 7' => 'microsoft-surface-pro-7',
- 'Microsoft Surface Tablets' => 'microsoft-surface',
- 'Miele' => 'miele',
- 'Miele Geschirrspüler' => 'miele-geschirrspueler',
- 'Miele Staubsauger' => 'miele-staubsauger',
- 'Miele Waschmaschinen' => 'miele-waschmaschine',
- 'Mietwagen' => 'mietwagen',
- 'Mikrofone' => 'mikrofone',
- 'Mikrowellen' => 'mikrowelle',
- 'Milchaufschäumer' => 'milchaufschaeumer',
- 'Milka' => 'milka',
- 'Minecraft' => 'minecraft',
- 'Mineralwasser' => 'mineralwasser',
- 'Minions' => 'minions',
- 'Mini PCs' => 'mini-pc',
- 'Mitsubishi' => 'mitsubishi',
- 'Mittelerde' => 'middle-earth',
- 'Mittelerde: Mordors Schatten' => 'mittelerde-mordors-schatten',
- 'Mittelerde: Schatten des Krieges' => 'mittelerde-schatten-des-krieges',
- 'Mixer &amp; Rührer' => 'mixer',
- 'Möbel' => 'moebel-deko',
- 'Modellbau' => 'modellbau',
- 'Modern wohnen' => 'modern-wohnen',
- 'Monitore' => 'monitor',
- 'Monkey 47' => 'monkey-47',
- 'Monopoly' => 'monopoly',
- 'Monster Hunter' => 'monster-hunter',
- 'Monster Hunter: World' => 'monster-hunter-world',
- 'Mortal Kombat' => 'mortal-kombat',
- 'Mortal Kombat 11' => 'mortal-kombat-11',
- 'Motorola' => 'motorola',
- 'Motorola Smartphones' => 'motorola-smartphones',
- 'Motorradbekleidung' => 'motorradbekleidung',
- 'Motorradhelm' => 'motorradhelm',
- 'Motorrad Zubehör' => 'motorrad',
- 'Moto Z' => 'moto-z',
- 'Mountainbikes' => 'mountainbikes',
- 'MSI' => 'msi',
- 'Mülleimer' => 'muelleimer',
- 'Multifunktionsdrucker' => 'multifunktionsdrucker',
- 'Multiroom Speaker' => 'multiroom',
- 'Mund- &amp; Zahnpflege' => 'mund-zahnpflege',
- 'Mundschutzmasken' => 'mundschutzmasken',
- 'Museums-Tickets' => 'museum',
- 'Musical Tickets' => 'musical',
- 'Musik' => 'musik',
- 'Musik Apps' => 'musik-apps',
- 'Musikinstrumente' => 'musikinstrumente',
- 'Musik Streaming' => 'musik-streaming',
- 'Müsli' => 'muesli',
- 'Mustang' => 'mustang',
- 'Mützen' => 'muetzen',
- 'Nachtwäsche' => 'nachtwaesche',
- 'Nähbedarf' => 'naehen',
- 'Nähmaschinen' => 'naehmaschine',
- 'Nahrungsergänzungsmittel' => 'nahrungsergaenzungsmittel',
- 'Nahverkehr' => 'nahverkehr',
- 'Naketano' => 'naketano',
- 'NAS' => 'nas',
- 'Nassrasierer' => 'rasierer',
- 'Navigationsgeräte' => 'navigationsgeraete',
- 'Neato' => 'neato',
- 'Neato Robotics Botvac D7 Connected' => 'neato-botvac-d7',
- 'Need for Speed' => 'need-for-speed',
- 'Need for Speed Heat' => 'need-for-speed-heat',
- 'Need for Speed Payback' => 'need-for-speed-payback',
- 'Nerf' => 'nerf',
- 'Nescafé' => 'nescafe',
- 'Nespresso' => 'nespresso',
- 'Nespresso Kaffeemaschinen' => 'nespresso-kaffeemaschinen',
- 'Netflix' => 'netflix',
- 'NETGEAR' => 'netgear',
- 'NETGEAR Nighthawk' => 'netgear-nighthawk',
- 'NETGEAR Orbi' => 'netgear-orbi',
- 'NETGEAR Router' => 'netgear-router',
- 'Netzteile' => 'netzteile',
- 'Netzwerk' => 'netzwerk',
- 'New Balance' => 'new-balance',
- 'Nike' => 'nike',
- 'Nike Air Force 1' => 'nike-air-force',
- 'Nike Air Max' => 'nike-air-max',
- 'Nike Air Max 270' => 'nike-air-max-270',
- 'Nike Air Max 720' => 'nike-air-max-720',
- 'Nike Air Max Thea' => 'nike-air-max-thea',
- 'Nike Air Presto' => 'nike-presto',
- 'Nike Free' => 'nike-free',
- 'Nike Huarache' => 'nike-huarache',
- 'Nike Roshe Run' => 'nike-roshe-run',
- 'Nike Schuhe' => 'nike-schuhe',
- 'Nikon' => 'nikon',
- 'Nikon DSLR' => 'nikon-dslr',
- 'Ni No Kuni' => 'ni-no-kuni',
- 'Ni No Kuni: Der Fluch der Weißen Königin' => 'ni-no-kuni-der-fluch-der-weissen-koenigin',
- 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-ii',
- 'Nintendo' => 'nintendo',
- 'Nintendo 2DS Konsolen' => 'nintendo-2ds',
- 'Nintendo 3DS Konsolen' => 'nintendo-3ds',
- 'Nintendo 3DS Spiele' => 'nintendo-3ds-spiele',
- 'Nintendo 3DS Zubehör' => 'nintendo-3ds-zubehoer',
- 'Nintendo Classic Mini NES Konsolen' => 'nintendo-classic-mini-nes',
- 'Nintendo Classic Mini SNES Konsolen' => 'nintendo-classic-mini-snes',
- 'Nintendo eShop Guthaben' => 'nintendo-eshop-guthaben',
- 'Nintendo Switch Controller' => 'nintendo-switch-controller',
- 'Nintendo Switch Konsolen' => 'nintendo-switch',
- 'Nintendo Switch Lite Konsolen' => 'nintendo-switch-lite',
- 'Nintendo Switch Pro Controller' => 'nintendo-switch-pro-controller',
- 'Nintendo Switch Spiele' => 'nintendo-switch-spiele',
- 'Nintendo Switch Zubehör' => 'nintendo-switch-zubehoer',
- 'Nintendo Zubehör' => 'nintendo-zubehoer',
- 'Nissan' => 'nissan',
- 'Nivea' => 'nivea',
- 'Nokia' => 'nokia',
- 'Nokia Handys' => 'nokia-handys',
- 'Nudeln' => 'nudeln',
- 'Nuki Smart Locks' => 'nuki-smart-lock',
- 'Nüsse' => 'nuesse',
- 'Nutella' => 'nutella',
- 'Nvidia' => 'nvidia',
- 'Nvidia GeForce' => 'nvidia-geforce',
- 'Nvidia SHIELD TV' => 'nvidia-shield',
- 'o2' => 'o2-netz',
- 'Objektive' => 'objektiv',
- 'Obst' => 'obst',
- 'Obst &amp; Gemüse' => 'obst-gemuese',
- 'Oculus Quest' => 'oculus-quest',
- 'Oculus Rift' => 'oculus-rift',
- 'Office Programme' => 'office-programme',
- 'OLED Fernseher' => 'oled-fernseher',
- 'Olympus' => 'olympus',
- 'On-Ear Kopfhörer' => 'on-ear-kopfhoerer',
- 'OnePlus 3' => 'oneplus-3',
- 'OnePlus 5' => 'oneplus-5',
- 'OnePlus 6' => 'oneplus-6',
- 'OnePlus 7' => 'oneplus-7',
- 'OnePlus 7 Pro' => 'oneplus-7-pro',
- 'OnePlus 7T' => 'oneplus-7t',
- 'OnePlus 7T Pro' => 'oneplus-7t-pro',
- 'OnePlus 8' => 'oneplus-8',
- 'OnePlus 8 Pro' => 'one-plus-8-pro',
- 'OnePlus 8T' => 'oneplus-8t',
- 'OnePlus Nord' => 'oneplus-nord',
- 'OnePlus Smartphones' => 'oneplus-smartphones',
- 'Onkyo' => 'onkyo',
- 'Opel' => 'opel',
- 'OPPO Find X2 Lite' => 'oppo-find-x2-lite',
- 'OPPO Find X2 Neo' => 'oppo-find-x2-neo',
- 'OPPO Find X2 Pro' => 'oppo-find-x2-pro',
- 'OPPO Reno2' => 'oppo-reno2',
- 'OPPO Reno2 Z' => 'oppo-reno2-z',
- 'OPPO Reno4 5G' => 'oppo-reno4-5g',
- 'OPPO Reno4 Pro 5G' => 'oppo-reno4-pro-5g',
- 'OPPO Reno4 Z 5G' => 'oppo-reno4-z-5g',
- 'OPPO Smartphones' => 'oppo-smartphones',
- 'Oral-B' => 'oral-b',
- 'Oral-B Elektrische Zahnbürsten' => 'oral-b-elektrische-zahnbuersten',
- 'Origin' => 'origin',
- 'Osram' => 'osram',
- 'Osram Smart+' => 'osram-smart-plus',
- 'Osterdeko' => 'osterdeko',
- 'Outdoor &amp; Camping' => 'outdoor',
- 'Outdoorbekleidung' => 'outdoorbekleidung',
- 'Outdoorjacken' => 'outdoorjacken',
- 'Outdoor Spielzeuge' => 'outdoor-spielzeug',
- 'Over-Ear Kopfhörer' => 'over-ear-kopfhoerer',
- 'Pampers' => 'pampers',
- 'Panama Jack' => 'panama-jack',
- 'Panasonic' => 'panasonic',
- 'Panasonic Fernseher' => 'panasonic-fernseher',
- 'Panasonic Kameras' => 'panasonic-kameras',
- 'Panasonic Lumix' => 'panasonic-lumix',
- 'Paper Mario: The Origami King' => 'paper-mario-the-origami-king',
- 'Papiertapete' => 'papiertapete',
- 'Parfum' => 'parfum',
- 'Parfum Damen' => 'parfum-damen',
- 'Parfum Herren' => 'parfum-herren',
- 'Pauschalreisen' => 'pauschalreise',
- 'Pavillons' => 'pavillons',
- 'Paw Patrol' => 'paw-patrol',
- 'PAYBACK' => 'payback',
- 'Payday' => 'payday',
- 'Payday 2' => 'payday-2',
- 'paydirekt' => 'paydirekt',
- 'PC Gaming Systeme' => 'pc-gaming-systeme',
- 'PC Gaming Zubehör' => 'pc-gaming-zubehoer',
- 'PC Gehäuse' => 'pc-gehaeuse',
- 'PC Komponenten' => 'hardware',
- 'PC Lautsprecher' => 'pc-lautsprecher',
- 'PC Mäuse' => 'pc-maus',
- 'PC Spiele' => 'pc-spiele',
- 'PC Zubehör' => 'pc-zubehoer',
- 'Pendelleuchten' => 'pendelleuchten',
- 'Pentax' => 'pentax',
- 'Pepe Jeans' => 'pepe-jeans',
- 'Peppa Wutz' => 'peppa-wutz',
- 'PepperBonus' => 'pepperbonus',
- 'Pestos' => 'pestos',
- 'Peugeot' => 'peugeot',
- 'Pfannen' => 'pfannen',
- 'Pflanzen' => 'pflanzen',
- 'Philips' => 'philips',
- 'Philips Fernseher' => 'philips-fernseher',
- 'Philips Hue' => 'philips-hue',
- 'Philips Hue E14' => 'philips-hue-e14',
- 'Philips Hue E27' => 'philips-hue-e27',
- 'Philips Hue Go' => 'philips-hue-go',
- 'Philips Hue GU10' => 'philips-hue-gu10',
- 'Philips Hue LightStrip' => 'philips-hue-lightstrip',
- 'Philips Hue Play Gradient LightStrip' => 'philips-hue-play-gradient-lightstrip',
- 'Philips Hue Play HDMI Sync Box' => 'philips-hue-play-hdmi-sync-box',
- 'Philips Hue Play Lightbar' => 'philips-hue-play',
- 'Philips OneBlade' => 'philips-oneblade',
- 'Philips Rasierer' => 'philips-rasierer',
- 'Philips Sonicare' => 'philips-sonicare',
- 'Philips Staubsauger' => 'philips-staubsauger',
- 'Philips Wecker' => 'philips-wecker',
- 'Photoshop' => 'photoshop',
- 'Pioneer' => 'pioneer',
- 'Pizza' => 'pizza',
- 'Plattenspieler' => 'plattenspieler',
- 'Playboy' => 'playboy',
- 'Playerunknown&#039;s Battlegrounds' => 'playerunknowns-battlegrounds',
- 'PLAYMOBIL' => 'playmobil',
- 'PLAYMOBIL Adventskalender' => 'playmobil-adventskalender',
- 'PlayStation' => 'playstation',
- 'PlayStation 4 Controller' => 'playstation-4-controller',
- 'PlayStation 4 Konsolen' => 'playstation-4',
- 'PlayStation 4 Pro Konsolen' => 'playstation-4-pro',
- 'PlayStation 4 Spiele' => 'playstation-4-spiele',
- 'PlayStation 5 Konsolen' => 'playstation-5',
- 'PlayStation 5 Spiele' => 'playstation-5-spiele',
- 'PlayStation Classic Konsolen' => 'playstation-classic',
- 'PlayStation Now' => 'playstation-now',
- 'PlayStation Plus' => 'playstation-plus',
- 'PlayStation Zubehör' => 'playstation-zubehoer',
- 'Plüschtiere' => 'plueschtiere',
- 'Plus Size Mode' => 'plus-size-mode',
- 'POCO F2 Pro' => 'poco-f2-pro',
- 'POCO X3' => 'poco-x3',
- 'Pokémon' => 'pokemon',
- 'Pokémon: Let&#039;s Go' => 'pokemon-lets-go',
- 'Pokémon Schwert und Schild' => 'pokemon-schwert-schild',
- 'Pokémon Tekken' => 'pokemon-tekken',
- 'Pokémon Ultrasonne &amp; Ultramond' => 'pokemon-ultrasonne-ultramond',
- 'Poloshirts' => 'poloshirts',
- 'Polsterbetten' => 'polsterbetten',
- 'Polyrattan Möbel' => 'polyrattan',
- 'Pools' => 'pools',
- 'Powerbanks' => 'powerbanks',
- 'Powerbeats Pro' => 'powerbeats',
- 'Preisfehler' => 'preisfehler',
- 'Prepaid-Tarife' => 'prepaid-tarife',
- 'Prime Gaming' => 'twitch-prime',
- 'Pro Evolution Soccer' => 'pro-evolution-soccer',
- 'Pro Evolution Soccer 2018' => 'pes-2018',
- 'Pro Evolution Soccer 2019' => 'pes-2019',
- 'Pro Evolution Soccer 2020' => 'pes-2020',
- 'Proteine' => 'whey-proteine',
- 'Prozessoren' => 'prozessoren',
- 'PSN Guthaben' => 'psn-guthaben',
- 'Puky' => 'puky',
- 'Pullover' => 'pullover',
- 'PUMA' => 'puma',
- 'Pumps' => 'pumps',
- 'Puppen' => 'puppen',
- 'Puppenhäuser' => 'puppenhaeuser',
- 'Puzzles' => 'puzzle',
- 'Qeridoo' => 'qeridoo',
- 'Qeridoo Fahrradanhänger' => 'qeridoo-fahrradanhaenger',
- 'Qeridoo KidGoo 2' => 'qeridoo-kidgoo-2',
- 'Qeridoo Sportrex 2' => 'qeridoo-sportrex-2',
- 'Quiksilver' => 'quiksilver',
- 'Raclettes' => 'raclettes',
- 'Radios' => 'radios',
- 'Radsport' => 'radsport',
- 'Rasenmäher' => 'rasenmaeher',
- 'Rasentrimmer' => 'rasentrimmer',
- 'Rasierklingen' => 'rasierklingen',
- 'Raspberry Pi' => 'raspberry-pi',
- 'Rasur, Enthaarung &amp; Trimmen' => 'rasur-enthaarung',
- 'Rauchmelder' => 'rauchmelder',
- 'Ravensburger' => 'ravensburger',
- 'Ray-Ban' => 'ray-ban',
- 'Razer DeathAdder' => 'razer-deathadder',
- 'RC Autos' => 'rc-autos',
- 'Red Bull' => 'red-bull',
- 'Red Dead Redemption' => 'red-dead-redemption',
- 'Red Dead Redemption 2' => 'red-dead-redemption-2',
- 'Reebok' => 'reebok',
- 'Regale' => 'regale',
- 'Reifen' => 'reifen',
- 'Reinigungsmittel' => 'reinigungsmittel',
- 'Reise Apps' => 'reise-apps',
- 'Reisen' => 'reisen',
- 'Reiskocher' => 'reiskocher',
- 'Remington' => 'remington',
- 'Renault' => 'renault',
- 'Rennräder' => 'rennraeder',
- 'Repeater' => 'repeater',
- 'Resident Evil' => 'resident-evil',
- 'Resident Evil 2' => 'resident-evil-2',
- 'Resident Evil 7' => 'resident-evil-7',
- 'Restaurant' => 'restaurant',
- 'Retro Stil' => 'retro-stil',
- 'Rimowa' => 'rimowa',
- 'Ring Fit Adventure' => 'ring-fit-adventure',
- 'Rituals' => 'rituals',
- 'Rituals Adventskalender' => 'rituals-adventskalender',
- 'Roborock' => 'xiaomi-roborock',
- 'Roborock S5 Max' => 'roborock-s5-max',
- 'Roborock S6' => 'roborock-s6',
- 'Roborock S6 MaxV' => 'roborock-s6-maxv',
- 'ROCCAT' => 'roccat',
- 'ROCCAT Tyon' => 'roccat-tyon',
- 'Röcke' => 'roecke',
- 'Rocket League' => 'rocket-league',
- 'Roidmi Staubsauger' => 'roidmi-staubsauger',
- 'Rollei' => 'rollei',
- 'Rösle' => 'roesle',
- 'Router' => 'router',
- 'Roxy' => 'roxy',
- 'RTX 2060' => 'rtx-2060',
- 'RTX 2070' => 'rtx-2070',
- 'RTX 2080' => 'rtx-2080',
- 'RTX 2080 Ti' => 'rtx-2080-ti',
- 'RTX 3070' => 'rtx-3070',
- 'RTX 3080' => 'rtx-3080',
- 'RTX 3090' => 'rtx-3090',
- 'Rucksäcke' => 'rucksaecke',
- 'Russell Hobbs' => 'russell-hobbs',
- 'RX 480' => 'rx-480',
- 'RX 570' => 'rx-570',
- 'RX 580' => 'rx-580',
- 'RX 590' => 'rx-590',
- 'RX 5700 XT' => 'rx-5700-xt',
- 'RX 6800' => 'rx-6800',
- 'RX 6800 XT' => 'rx-6800-xt',
- 'RX 6900 XT' => 'rx-6900-xt',
- 'RX Vega 56' => 'rx-vega-56',
- 'RX Vega 64' => 'rx-vega-64',
- 'Sägen' => 'saegen',
- 'Salomon' => 'salomon',
- 'Samsonite' => 'samsonite',
- 'Samsung' => 'samsung',
- 'Samsung Fernseher' => 'samsung-fernseher',
- 'Samsung Galaxy A7' => 'samsung-galaxy-a7',
- 'Samsung Galaxy A8' => 'samsung-galaxy-a8',
- 'Samsung Galaxy A51' => 'samsung-galaxy-a51',
- 'Samsung Galaxy A71' => 'samsung-galaxy-a71',
- 'Samsung Galaxy Buds' => 'samsung-galaxy-buds',
- 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus',
- 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live',
- 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro',
- 'Samsung Galaxy Note9' => 'samsung-galaxy-note-9',
- 'Samsung Galaxy Note20' => 'samsung-galaxy-note20',
- 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note20-ultra',
- 'Samsung Galaxy S7' => 'samsung-galaxy-s7',
- 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge',
- 'Samsung Galaxy S8' => 'samsung-galaxy-s8',
- 'Samsung Galaxy S8+' => 'samsung-galaxy-s8-plus',
- 'Samsung Galaxy S9' => 'samsung-galaxy-s9',
- 'Samsung Galaxy S9+' => 'samsung-galaxy-s9-plus',
- 'Samsung Galaxy S10' => 'samsung-galaxy-s10',
- 'Samsung Galaxy S10+' => 'samsung-galaxy-s10-plus',
- 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e',
- 'Samsung Galaxy S20' => 'samsung-galaxy-s20',
- 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe',
- 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra',
- 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus',
- 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g',
- 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g',
- 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g',
- 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4',
- 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6',
- 'Samsung Galaxy Watch' => 'samsung-galaxy-watch',
- 'Samsung Galaxy Watch Active2' => 'samsung-galaxy-watch-active-2',
- 'Samsung Gear' => 'samsung-gear',
- 'Samsung Gear S3' => 'samsung-gear-s3',
- 'Samsung Gear VR' => 'samsung-gear-vr',
- 'Samsung Kopfhörer' => 'samsung-kopfhoerer',
- 'Samsung Kühlschränke' => 'samsung-kuehlschrank',
- 'Samsung Monitore' => 'samsung-monitor',
- 'Samsung QLED Fernseher' => 'samsung-qled-fernseher',
- 'Samsung Smartphones' => 'samsung-smartphone',
- 'Samsung SSD' => 'samsung-ssd',
- 'Samsung Tablets' => 'samsung-tablet',
- 'Samsung The Frame Fernseher' => 'samsung-the-frame-fernseher',
- 'Samsung Waschmaschinen' => 'samsung-waschmaschine',
- 'Sandalen' => 'sandalen',
- 'SanDisk' => 'sandisk',
- 'SanDisk SSD' => 'sandisk-ssd',
- 'Sanitär &amp; Armaturen' => 'sanitaer-armaturen',
- 'Saucen' => 'saucen',
- 'Saugroboter' => 'saugroboter',
- 'Scanner' => 'scanner',
- 'Schallplatten' => 'schallplatten',
- 'Scheppach' => 'scheppach',
- 'Schlafsäcke' => 'schlafsack',
- 'Schlafsofas' => 'schlafsofas',
- 'Schlafzimmer' => 'schlafzimmer',
- 'Schlagschrauber' => 'schlagschrauber',
- 'Schlauchboote' => 'schlauchboote',
- 'Schleich' => 'schleich',
- 'Schlitten' => 'schlitten',
- 'Schmuck' => 'schmuck',
- 'Schneefräsen' => 'schneefraesen',
- 'Schnellkochtöpfe' => 'schnellkochtoepfe',
- 'Schnürhalbschuhe' => 'schnuerhalbschuhe',
- 'Schokolade' => 'schokolade',
- 'Schraubendreher' => 'schraubendreher',
- 'Schreibgeräte' => 'schreibgeraete',
- 'Schreibtische' => 'schreibtisch',
- 'Schuhe' => 'schuhe',
- 'Schuhschränke' => 'schuhschraenke',
- 'Schulbedarf' => 'schulbedarf',
- 'Schulranzen' => 'schulranzen',
- 'Schutzfolien' => 'schutzfolien',
- 'Schwangerschaft' => 'schwangerschaft',
- 'Schwerlastregale' => 'schwerlastregale',
- 'Scooter' => 'scooter',
- 'Scotch Whisky' => 'scotch-whisky',
- 'SDHC Speicherkarten' => 'sdhc-speicherkarten',
- 'SD Karten' => 'sd-karten',
- 'Seagate' => 'seagate',
- 'Sea of Thieves' => 'sea-of-thieves',
- 'Seat' => 'seat',
- 'Sega Mega Drive Mini Konsolen' => 'sega-mega-drive-mini',
- 'Seidensticker' => 'seidensticker',
- 'Sekiro: Shadows Die Twice' => 'sekiro',
- 'Senf' => 'senf',
- 'Sennheiser' => 'sennheiser',
- 'Senseo' => 'senseo',
- 'Service-Verträge' => 'service-vertraege',
- 'Sessel' => 'sessel',
- 'Sextoys' => 'sextoys',
- 'Shadow of the Tomb Raider' => 'shadow-of-the-tomb-raider',
- 'Shampoo' => 'shampoo',
- 'Sharkoon' => 'sharkoon',
- 'Sharp' => 'sharp',
- 'Shenmue' => 'shenmue',
- 'Shenmue I &amp; II' => 'shenmue-i-ii',
- 'Shenmue III' => 'shenmue-iii',
- 'Shishas' => 'shishas',
- 'Shishas &amp; Zubehör' => 'shishas-zubehoer',
- 'Shoop' => 'shoop',
- 'Shops: Erfahrungen' => 'shops',
- 'Shorts' => 'shorts',
- 'Sicherheitstechnik' => 'sicherheitstechnik',
- 'Side-by-Side-Kühlschränke' => 'side-by-side-kuehlschrank',
- 'Sid Meier&#039;s Civilization VI' => 'sid-meiers-civilization-vi',
- 'Sid Meier’s Civilization' => 'sid-meiers-civilization',
- 'Siemens' => 'siemens',
- 'Siemens Geschirrspüler' => 'siemens-geschirrspueler',
- 'Siemens Kühlschränke' => 'siemens-kuehlschrank',
- 'Siemens Waschmaschinen' => 'siemens-waschmaschine',
- 'Silit' => 'silit',
- 'Skandi Stil' => 'skandi-stil',
- 'Skateboards' => 'skateboard',
- 'Skaten' => 'skaten',
- 'Ski &amp; Snowboard' => 'snowboard',
- 'Skoda' => 'skoda',
- 'Sky' => 'sky',
- 'Sky Ticket' => 'sky-ticket',
- 'Smarte Beleuchtung' => 'smarte-beleuchtung',
- 'Smarte Wecker' => 'smarte-wecker',
- 'Smart Home' => 'smart-home',
- 'Smart Home Steckdosen' => 'smart-home-steckdosen',
- 'Smart Locks' => 'smart-lock',
- 'Smartphones' => 'smartphone',
- 'Smartphones unter 200€' => 'smartphones-unter-200-euro',
- 'Smart Speaker' => 'smart-speaker',
- 'Smart Tech &amp; Gadgets' => 'smart-tech',
- 'Smartwatches' => 'smartwatch',
- 'Smoothie Maker' => 'smoothie-maker',
- 'Snacks &amp; Knabberzeug' => 'snacks-knabberzeug',
- 'Sneakers' => 'sneaker',
- 'Socken' => 'socken',
- 'SodaStream' => 'sodastream',
- 'Sofas' => 'sofa',
- 'Sofortbildkameras' => 'sofortbildkameras',
- 'Softdrinks' => 'softdrinks',
- 'Software' => 'software',
- 'Software &amp; Apps' => 'apps-software',
- 'Solarleuchten' => 'solarleuchten',
- 'Somat' => 'somat',
- 'Sommerreifen' => 'sommerreifen',
- 'Sonnenbrillen' => 'sonnenbrillen',
- 'Sonnencreme' => 'sonnencreme',
- 'Sonnenpflege' => 'sonnenpflege',
- 'Sonnenschirme' => 'sonnenschirme',
- 'Sonoff' => 'sonoff',
- 'Sonos' => 'sonos',
- 'Sonos Beam' => 'sonos-beam',
- 'Sonos Move' => 'sonos-move',
- 'Sonos One' => 'sonos-one',
- 'Sonos PLAY:1' => 'sonos-play-1',
- 'Sonos PLAY:3' => 'sonos-play-3',
- 'Sonos Play:5 (Five)' => 'sonos-play-5',
- 'Sonos Playbar' => 'sonos-playbar',
- 'Sonos Playbase' => 'sonos-playbase',
- 'Sonstiges' => 'diverses',
- 'Sony' => 'sony',
- 'Sony Alpha 7' => 'sony-alpha-7',
- 'Sony Alpha 7 II' => 'sony-alpha-7-ii',
- 'Sony Alpha 7 III' => 'sony-alpha-7-iii',
- 'Sony Alpha 6000' => 'sony-alpha-6000',
- 'Sony Alpha 6300' => 'sony-alpha-6300',
- 'Sony Alpha 6400' => 'sony-alpha-6400',
- 'Sony Alpha 6500' => 'sony-alpha-6500',
- 'Sony DualSense Wireless-Controller' => 'playstation-5-controller',
- 'Sony Fernseher' => 'sony-fernseher',
- 'Sony Kameras' => 'sony-kameras',
- 'Sony Kopfhörer' => 'sony-kopfhoerer',
- 'Sony PlayStation VR' => 'sony-playstation-vr',
- 'Sony PULSE 3D Wireless Headset' => 'sony-pulse-3d-wireless-headset',
- 'Sony WF-1000XM3' => 'sony-wf-1000xm3',
- 'Sony WH-1000XM3' => 'sony-wh-1000xm3',
- 'Sony WH-1000XM4' => 'sony-wh-1000xm4',
- 'Sony Xperia' => 'sony-xperia',
- 'Sony Xperia X' => 'sony-xperia-x',
- 'Sony Xperia XA' => 'sony-xperia-xa',
- 'Sony Xperia XZ' => 'sony-xperia-xz',
- 'Soundbar' => 'soundbar',
- 'Soundbase' => 'soundbase',
- 'Soundkarten' => 'soundkarten',
- 'South Park: Die rektakuläre Zerreißprobe' => 'south-park-die-rektakulaere-zerreissprobe',
- 'Spartipps' => 'spartipps',
- 'Speicherkarten' => 'speicherkarten',
- 'Speichermedien' => 'speichermedien',
- 'Speiseöle' => 'speiseoele',
- 'Spiegelreflexkameras' => 'spiegelreflexkamera',
- 'Spiele &amp; Brettspiele' => 'spiele-brettspiele',
- 'Spiele Apps' => 'spiele-apps',
- 'Spielekonsolen' => 'spielekonsolen',
- 'Spielfiguren &amp; Spielsets' => 'spielfiguren-spielsets',
- 'Spielzeuge' => 'spielzeug',
- 'Spirituosen' => 'spirituosen',
- 'Sport &amp; Outdoor' => 'sport',
- 'Sportbekleidung' => 'sportbekleidung',
- 'Sport Bild' => 'sport-bild',
- 'Sportnahrung' => 'sportlernahrung',
- 'Sporttasche' => 'sporttasche',
- 'Spotify' => 'spotify',
- 'Spülmaschinentabs' => 'spuelmaschinentabs',
- 'Spyro Reignited Trilogy' => 'spyro-reignited-trilogy',
- 'SSD' => 'ssd',
- 'Stabmixer' => 'stabmixer',
- 'Städtereisen' => 'staedtereise',
- 'Standmixer' => 'standmixer',
- 'Star Trek' => 'star-trek',
- 'Star Wars' => 'star-wars',
- 'Star Wars: Battlefront 2' => 'star-wars-battlefront-2',
- 'Star Wars: Squadrons' => 'star-wars-squadrons',
- 'Star Wars Battlefront' => 'star-wars-battlefront',
- 'Star Wars Jedi: Fallen Order' => 'star-wars-jedi-fallen-order',
- 'Staubsauger' => 'staubsauger',
- 'Staubsaugerbeutel' => 'staubsaugerbeutel',
- 'Staubsauger ohne Beutel' => 'staubsauger-ohne-beutel',
- 'Steam' => 'steam',
- 'Steckschlüssel' => 'steckschluessel',
- 'SteelSeries' => 'steelseries',
- 'Stehlampen' => 'stehlampen',
- 'Steiff' => 'steiff',
- 'Stern (Magazin)' => 'stern-magazin',
- 'Stichsägen' => 'stichsaegen',
- 'Stiefel' => 'stiefel',
- 'Stiefeletten' => 'stiefeletten',
- 'Stiftung Warentest' => 'stiftung-warentest-magazin',
- 'Streaming-Dienste' => 'streaming-dienste',
- 'Streaming Lautsprecher' => 'streaming-lautsprecher',
- 'Strom &amp; Gas' => 'strom-gas',
- 'Stromtarif' => 'stromtarif',
- 'Studentenrabatte' => 'studentenrabatte',
- 'Stühle' => 'stuehle',
- 'Subwoofer' => 'subwoofer',
- 'SUP Boards' => 'sup-boards',
- 'Superdry' => 'superdry',
- 'Super Mario' => 'super-mario',
- 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars',
- 'Super Mario Maker 2' => 'super-mario-maker-2',
- 'Super Mario Odyssey' => 'super-mario-odyssey',
- 'Super Mario Party' => 'super-mario-party',
- 'Supermarkt' => 'supermarkt',
- 'Super Smash Bros. Ultimate' => 'super-smash-bros-ultimate',
- 'Süßigkeiten' => 'suessigkeiten',
- 'Synology' => 'synology',
- 'Syoss' => 'syoss',
- 'Systemkameras' => 'systemkamera',
- 'T-Shirts' => 't-shirts',
- 'Tablets' => 'tablet',
- 'Tablet Zubehör' => 'tablet-zubehoer',
- 'tado° Smartes Heizkörper-Thermostat' => 'tado-smartes-thermostat',
- 'Tamaris' => 'tamaris',
- 'Tangle Teezer' => 'tangle-teezer',
- 'Tanqueray' => 'tanqueray',
- 'Tapeten' => 'tapeten',
- 'Taschen' => 'taschen',
- 'Taschenlampen' => 'taschenlampen',
- 'Taschentücher' => 'taschentuecher',
- 'Tassimo' => 'tassimo',
- 'Tassimo Kaffeemaschinen' => 'tassimo-kaffeemaschinen',
- 'Tastaturen' => 'tastatur',
- 'TCL Fernseher' => 'tcl-fernseher',
- 'Team Sonic Racing' => 'team-sonic-racing',
- 'Teamsport' => 'teamsport',
- 'Tee' => 'tee',
- 'Tefal' => 'tefal',
- 'Tefal OptiGrills' => 'tefal-optigrill',
- 'Tefal Pfannen' => 'tefal-pfannen',
- 'Tekken' => 'tekken',
- 'Tekken 7' => 'tekken-7',
- 'Telefon- &amp; Internet-Verträge' => 'telefon-internet',
- 'Telefone &amp; Zubehör' => 'handy-smartphone',
- 'Telekom' => 'telekom-net',
- 'Telekom Magenta' => 'telekom-magenta',
- 'Telekom SmartHome' => 'telekom-smarthome',
- 'Teppiche' => 'teppiche',
- 'Tesla' => 'tesla',
- 'Tetris' => 'tetris',
- 'Teufel' => 'teufel',
- 'The Elder Scrolls' => 'the-elder-scrolls',
- 'The Elder Scrolls V: Skyrim' => 'skyrim',
- 'The Evil Within' => 'the-evil-within',
- 'The Evil Within 2' => 'the-evil-within-2',
- 'The Last of Us' => 'the-last-of-us',
- 'The Last of Us Part II' => 'the-last-of-us-part-ii',
- 'The Legend of Zelda' => 'the-legend-of-zelda',
- 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild',
- 'The Legend of Zelda: Link&#039;s Awakening' => 'zelda-links-awakening',
- 'The Legend of Zelda: Skyward Sword HD' => 'zelda-skyward-sword-hd',
- 'The North Face' => 'the-north-face',
- 'The Outer Worlds' => 'the-outer-worlds',
- 'Thermosflaschen' => 'thermosflaschen',
- 'Thermoskannen' => 'thermoskanne',
- 'The Witcher' => 'the-witcher',
- 'The Witcher 3' => 'the-witcher-3',
- 'Thule' => 'thule',
- 'Thule Chariot Fahrradanhänger' => 'thule-chariot-fahrradanhaenger',
- 'Thule Dachboxen' => 'thule-dachboxen',
- 'Thule Fahrradträger' => 'thule-fahrradtraeger',
- 'Tickets &amp; Shows' => 'erlebnisse',
- 'Tiefkühlkost' => 'tiefkuehkost',
- 'Timberland' => 'timberland',
- 'Tintenstrahldrucker' => 'tintenstrahldrucker',
- 'Tischlampen' => 'tischlampen',
- 'Tischtennis' => 'tischtennis',
- 'Tischtennisplatten' => 'tischtennisplatten',
- 'Tischtennisschläger' => 'tischtennisschlaeger',
- 'Toaster' => 'toaster',
- 'Toilettenpapier' => 'toilettenpapier',
- 'tolino' => 'tolino',
- 'Tomb Raider' => 'tomb-raider',
- 'Tom Clancy&#039;s' => 'tom-clancys',
- 'Tom Clancy&#039;s: Ghost Recon Wildlands' => 'tom-clancys-ghost-recon-wildlands',
- 'Tom Clancy&#039;s Ghost Recon Breakpoint' => 'tom-clancys-ghost-recon-breakpoint',
- 'Tom Clancy&#039;s The Division 2' => 'tom-clancy-the-division-2',
- 'Tommy Hilfiger' => 'tommy-hilfiger',
- 'TOM TAILOR' => 'tom-tailor',
- 'Toner' => 'toner',
- 'Tonic Water' => 'tonic-water',
- 'Toniebox' => 'toniebox',
- 'Tonies Figuren' => 'tonie-figuren',
- 'Töpfe' => 'toepfe',
- 'Töpfe &amp; Pfannen' => 'kochen',
- 'Toplader' => 'toplader',
- 'Toshiba' => 'toshiba',
- 'Total War' => 'total-war',
- 'Toyota' => 'toyota',
- 'TP-Link' => 'tp-link',
- 'TP-Link Router' => 'tp-link-router',
- 'Trampoline' => 'trampolin',
- 'TREKSTOR' => 'trekstor',
- 'Trockner' => 'trockner',
- 'Tropical Islands' => 'tropical-island',
- 'Tropico' => 'tropico',
- 'Tropico 5' => 'tropico-5',
- 'Tropico 6' => 'tropico-6',
- 'TV &amp; Video' => 'tv-video',
- 'TV Boxen' => 'tv-box',
- 'TV Spielfilm' => 'tv-spielfilm',
- 'TV Wandhalterungen' => 'tv-wandhalterung',
- 'TV Zubehör' => 'tv-zubehoer',
- 'Übergangsjacken' => 'uebergangsjacken',
- 'Überwachungskamera' => 'ueberwachungskamera',
- 'UE BLAST' => 'ue-blast',
- 'UE BOOM' => 'ue-boom',
- 'UE BOOM 2' => 'ue-boom-2',
- 'UE BOOM 3' => 'ue-boom-3',
- 'UE MEGABLAST' => 'ue-megablast',
- 'UE MEGABOOM' => 'ue-megaboom',
- 'UE MEGABOOM 3' => 'ue-megaboom-3',
- 'UE WONDERBOOM' => 'ue-wonderboom',
- 'UE WONDERBOOM 2' => 'ue-wonderboom-2',
- 'UGG' => 'ugg',
- 'Uhren' => 'uhren',
- 'Umstandsmode' => 'umstandsmode',
- 'Uncharted' => 'uncharted',
- 'Uncharted 4' => 'uncharted-4',
- 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy',
- 'Under Armour' => 'under-armour',
- 'Universalfernbedienungen' => 'universalfernbedienungen',
- 'Unterwäsche' => 'unterwaesche',
- 'Uplay' => 'uplay',
- 'Urban Sport' => 'urban-sport',
- 'Urlaub' => 'urlaub',
- 'USB Sticks' => 'usb-stick',
- 'Vakuumierer' => 'vakuumierer',
- 'Vans' => 'vans',
- 'Vans Old Skool' => 'vans-old-skool',
- 'Vans Schuhe' => 'vans-schuhe',
- 'Vaude' => 'vaude',
- 'Ventilatoren' => 'ventilator',
- 'Verbandskästen' => 'verbandskaesten',
- 'Versicherung' => 'versicherung',
- 'Versicherung &amp; Finanzen' => 'vertraege-finanzen',
- 'Videobearbeitungsprogramme' => 'videobearbeitungsprogramme',
- 'Video Player' => 'video-player',
- 'Videospiele' => 'videospiele',
- 'Video Streaming' => 'video-streaming',
- 'Vileda' => 'vileda',
- 'Villeroy &amp; Boch' => 'villeroy-boch',
- 'Virenschutz' => 'virenschutz',
- 'VISA' => 'visa',
- 'Vliestapeten' => 'vliestapete',
- 'Vodafone' => 'vodafone-netz',
- 'Vodka' => 'vodka',
- 'Volvo' => 'volvo',
- 'Vorratsdosen' => 'vorratsdosen',
- 'Vorstellungsrunde' => 'vorstellungsrunde',
- 'VPN' => 'vpn',
- 'VPS' => 'vps',
- 'VR Brillen' => 'vr-brille',
- 'VR Spiele' => 'vr-spiele',
- 'VTech' => 'vtech',
- 'VW' => 'vw',
- 'Waffeleisen' => 'waffeleisen',
- 'Wandbilder' => 'wandtattoos',
- 'Wanderrucksäcke' => 'wanderrucksack',
- 'Wanderschuhe' => 'wanderschuhe',
- 'Wandersport' => 'hiking',
- 'Wandfarben' => 'wandfarben',
- 'Wandlampen' => 'wandlampen',
- 'Wäscheständer' => 'waeschestaender',
- 'Waschmaschinen' => 'waschmaschinen',
- 'Waschmittel' => 'waschmittel',
- 'Waschtrockner' => 'waschtrockner',
- 'Wasserfilter' => 'wasserfilter',
- 'Wasserkocher' => 'wasserkocher',
- 'Wasserkühlung' => 'wasserkuehlung',
- 'Wasserspielzeuge' => 'wasserspielzeug',
- 'Wassersport' => 'wassersport',
- 'Watch Dogs' => 'watch-dogs',
- 'Watch Dogs 2' => 'watch-dogs-2',
- 'Watch Dogs: Legion' => 'watch-dogs-legion',
- 'WC Sitze' => 'wc-sitze',
- 'WD-40' => 'wd-40',
- 'Wearables' => 'wearable',
- 'Webcams' => 'webcam',
- 'Weber Gasgrills' => 'weber-gasgrill',
- 'Weber Grills' => 'weber-grill',
- 'Weihnachtsbäume' => 'weihnachtsbaum',
- 'Weihnachtsbeleuchtung' => 'weihnachtsbeleuchtung',
- 'Weihnachtsdeko' => 'weihnachtsdeko',
- 'Weihnachtspullover' => 'weihnachtspullover',
- 'Wein' => 'wein',
- 'Wellensteyn' => 'wellensteyn',
- 'Wellness &amp; Gesundheit' => 'wellness-massagen',
- 'Wera' => 'wera',
- 'Werkstatt &amp; Service' => 'werkstatt-service',
- 'Werkstatteinrichtungen' => 'werkstatteinrichtungen',
- 'Werkzeuge' => 'werkzeug',
- 'Werkzeugkoffer' => 'werkzeugkoffer',
- 'Wesco Mülleimer' => 'wesco-muelleimer',
- 'Western Digital' => 'western-digital',
- 'Wetterstationen' => 'wetterstationen',
- 'Whirlpools' => 'whirlpools',
- 'Whisky' => 'whisky',
- 'Wiko' => 'wiko',
- 'Wilkinson Sword Rasierer' => 'wilkinson-sword',
- 'Windeln' => 'windeln',
- 'Winkelschleifer' => 'winkelschleifer',
- 'Winterdeko' => 'winterdeko',
- 'Winterjacken' => 'winterjacken',
- 'Winterreifen' => 'winterreifen',
- 'Winterstiefel' => 'winterstiefel',
- 'Wireless Charger' => 'wireless-charger',
- 'Wirtschaftswoche' => 'wirtschaftswoche',
- 'WMF' => 'wmf',
- 'WMF Besteck' => 'wmf-besteck',
- 'WMF Topfset' => 'wmf-topfset',
- 'Wohnzimmermöbel' => 'wohnzimmer',
- 'Wolfenstein' => 'wolfenstein',
- 'Wolfenstein II: The New Colossus' => 'wolfenstein-2-the-new-colossus',
- 'Womanizer' => 'womanizer',
- 'World of Warcraft' => 'world-of-warcraft',
- 'Wrangler' => 'wrangler',
- 'X570 Mainboard' => 'x570-mainboard',
- 'Xbox' => 'xbox',
- 'Xbox Controller' => 'xbox-controller',
- 'Xbox Elite Wireless Controller' => 'xbox-one-elite-controller',
- 'Xbox Elite Wireless Controller 2' => 'xbox-one-elite-controller-2',
- 'Xbox Game Pass' => 'xbox-game-pass',
- 'Xbox Game Pass Ultimate' => 'xbox-game-pass-ultimate',
- 'Xbox Guthaben' => 'xbox-guthaben',
- 'Xbox Live Gold' => 'xbox-live',
- 'Xbox One Controller' => 'xbox-one-controller',
- 'Xbox One S Konsolen' => 'xbox-one-s',
- 'Xbox One Spiele' => 'xbox-one-spiele',
- 'Xbox One X Konsolen' => 'xbox-one-x',
- 'Xbox Series S Konsolen' => 'xbox-series-s',
- 'Xbox Series X Controller' => 'xbox-series-x-controller',
- 'Xbox Series X Konsolen' => 'xbox-series-x',
- 'Xbox Series X Spiele' => 'xbox-series-x-spiele',
- 'Xbox Wireless Headset' => 'xbox-wireless-headset',
- 'Xbox Zubehör' => 'xbox-zubehoer',
- 'Xiaomi' => 'xiaomi',
- 'Xiaomi Air Laptop' => 'xiaomi-air',
- 'Xiaomi E-Scooter' => 'xiaomi-e-scooter',
- 'Xiaomi Fernseher' => 'xiaomi-fernseher',
- 'Xiaomi Kopfhörer' => 'xiaomi-kopfhoerer',
- 'Xiaomi Mi 5S' => 'xiaomi-mi-5',
- 'Xiaomi Mi 6' => 'xiaomi-mi-6',
- 'Xiaomi Mi 8' => 'xiaomi-mi-8',
- 'Xiaomi Mi 8 Lite' => 'xiaomi-mi-8-lite',
- 'Xiaomi Mi 8 Pro' => 'xiaomi-mi-8-pro',
- 'Xiaomi Mi 9' => 'xiaomi-mi-9',
- 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite',
- 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se',
- 'Xiaomi Mi 9T' => 'xiaomi-mi-9t',
- 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro',
- 'Xiaomi Mi 10' => 'xiaomi-mi-10',
- 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite',
- 'Xiaomi Mi 10 Pro' => 'xiaomi-mi-10-pro',
- 'Xiaomi Mi 11' => 'xiaomi-mi-11',
- 'Xiaomi Mi A1' => 'xiaomi-mi-a1',
- 'Xiaomi Mi A2' => 'xiaomi-mi-a2',
- 'Xiaomi Mi AirDots' => 'xiaomi-mi-airdots',
- 'Xiaomi Mi AirDots Pro' => 'xiaomi-airdots-pro',
- 'Xiaomi Mi Band' => 'xiaomi-mi-band',
- 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4',
- 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5',
- 'Xiaomi Mi Electric Scooter 1S' => 'xiaomi-mi-scooter-1s',
- 'Xiaomi Mi Electric Scooter M365' => 'xiaomi-mi-electric-scooter-m365',
- 'Xiaomi Mi Electric Scooter Pro 2' => 'xiaomi-mi-electric-scooter-pro-2',
- 'Xiaomi Mi Mix' => 'xiaomi-mi-mix',
- 'Xiaomi Mi Mix 3' => 'xiaomi-mi-mix-3',
- 'Xiaomi Mi Note' => 'xiaomi-mi-note',
- 'Xiaomi Mi Note 10' => 'xiaomi-mi-note-10',
- 'Xiaomi Mi Note 10 Lite' => 'xiaomi-mi-note-10-lite',
- 'Xiaomi Mi Note 10 Pro' => 'xiaomi-mi-note-10-pro',
- 'Xiaomi Mi TV 4S' => 'xiaomi-mi-smart-tv-4s',
- 'Xiaomi Mi TV Stick' => 'xiaomi-mi-tv-stick',
- 'Xiaomi Pocophone F1' => 'xiaomi-pocophone-f1',
- 'Xiaomi Redmi 9' => 'xiaomi-redmi-9',
- 'Xiaomi Redmi 9A' => 'xiaomi-redmi-9a',
- 'Xiaomi Redmi AirDots' => 'xiaomi-redmi-airdots',
- 'Xiaomi Redmi Note 4' => 'xiaomi-redmi-note-4',
- 'Xiaomi Redmi Note 5' => 'xiaomi-redmi-note-5',
- 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8',
- 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro',
- 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9',
- 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro',
- 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s',
- 'Xiaomi Redmi Note 10' => 'xiaomi-redmi-note-10',
- 'Xiaomi Redmi Note 10 Pro' => 'xiaomi-redmi-note-10-pro',
- 'Xiaomi Smart Home' => 'xiaomi-smart-home',
- 'Xiaomi Smartphones' => 'xiaomi-smartphones',
- 'Xiaomi YouPin' => 'xiaomi-youpin',
- 'XMG' => 'xmg',
- 'Yamaha' => 'yamaha',
- 'Yeelight' => 'xiaomi-yeelight',
- 'Yoga' => 'yoga',
- 'Yogamatten' => 'yogamatten',
- 'Yoshi&#039;s Crafted World' => 'yoshis-crafted-world',
- 'Zahnbürsten' => 'zahnbuersten',
- 'Zahnpasta' => 'zahnpasta',
- 'Zahnzusatzversicherung' => 'zahnzusatzversicherung',
- 'Zeitschriften' => 'zeitschriften-magazine',
- 'Zelte' => 'zelte',
- 'Zirkel' => 'zirkel',
- 'Zoo-Tickets' => 'zoo',
- 'Zotac' => 'zotac',
- 'ZTE Smartphones' => 'zte-smartphones',
- 'ZWILLING' => 'zwilling',
- 'ZWILLING Besteck' => 'zwilling-besteck',
- )
- ),
- 'order' => array(
- 'name' => 'sortieren nach',
- 'type' => 'list',
- 'title' => 'Sortierung der deals',
- 'values' => array(
- 'Vom heißesten zum kältesten Deal' => '-hot',
- 'Vom jüngsten Deal zum ältesten' => '-new',
- )
- ),
- ),
- 'Überwachung Diskussion' => array(
- 'url' => array(
- 'name' => 'URL der Diskussion',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'URL-Diskussion zu überwachen: https://www.mydealz.de/diskussion/title-123',
- 'exampleValue' => 'https://www.mydealz.de/diskussion/anleitung-wie-schreibe-ich-einen-deal-1658317',
- ),
- 'only_with_url' => array(
- 'name' => 'Kommentare ohne URL ausschließen',
- 'type' => 'checkbox',
- 'title' => 'Kommentare, die keine URL enthalten, im Feed ausschließen',
- 'defaultValue' => false,
- )
- )
- );
-
- public $lang = array(
- 'bridge-uri' => SELF::URI,
- 'bridge-name' => SELF::NAME,
- 'context-keyword' => 'Suche nach Stichworten',
- 'context-group' => 'Deals pro Gruppen',
- 'context-talk' => 'Überwachung Diskussion',
- 'uri-group' => 'gruppe/',
- 'request-error' => 'Could not request mydeals',
- 'thread-error' => 'Die ID der Diskussion kann nicht ermittelt werden. Überprüfen Sie die eingegebene URL',
- 'no-results' => 'Ups, wir konnten keine Deals zu',
- 'relative-date-indicator' => array(
- 'vor',
- 'seit'
- ),
- 'price' => 'Preis',
- 'shipping' => 'Versand',
- 'origin' => 'Ursprung',
- 'discount' => 'Rabatte',
- 'title-keyword' => 'Suche',
- 'title-group' => 'Gruppe',
- 'title-talk' => 'Überwachung Diskussion',
- 'local-months' => array(
- 'Jan',
- 'Feb',
- 'Mär',
- 'Apr',
- 'Mai',
- 'Jun',
- 'Jul',
- 'Aug',
- 'Sep',
- 'Okt',
- 'Nov',
- 'Dez',
- '.'
- ),
- 'local-time-relative' => array(
- 'eingestellt vor ',
- 'm',
- 'h,',
- 'day',
- 'days',
- 'month',
- 'year',
- 'and '
- ),
- 'date-prefixes' => array(
- 'eingestellt am ',
- 'lokal ',
- 'aktualisiert ',
- ),
- 'relative-date-alt-prefixes' => array(
- 'aktualisiert vor ',
- 'kommentiert vor ',
- 'heiß seit '
- ),
- 'relative-date-ignore-suffix' => array(
- '/von.*$/'
- ),
- 'localdeal' => array(
- 'Lokal ',
- 'Läuft bis '
- )
- );
+ 'Deals pro Gruppen' => [
+ 'group' => [
+ 'name' => 'Gruppen',
+ 'type' => 'list',
+ 'title' => 'Gruppe, deren Deals angezeigt werden müssen',
+ 'values' => [
+ '1Password' => '1password',
+ '3D Drucker' => '3d-drucker',
+ '4K Fernseher' => '4k-fernseher',
+ '4K Monitore' => '4k-monitor',
+ '4K Ultra HD Blu-ray' => 'ultra-hd-blu-ray',
+ '8K Fernseher' => '8k-fernseher',
+ '32 Zoll Fernseher' => '32-zoll-fernseher',
+ '55 Zoll Fernseher' => '55-zoll-fernseher',
+ '65 Zoll Fernseher' => '65-zoll-fernseher',
+ '75 Zoll Fernseher' => '75-zoll-fernseher',
+ '1151 Mainboard' => '1151-mainboard',
+ 'Abus' => 'abus',
+ 'ABUS Fahrradschlösser' => 'abus-fahrradschloss',
+ 'Accessoires' => 'accessoires',
+ 'Acer' => 'acer',
+ 'Acer Aspire' => 'acer-aspire',
+ 'Acer Laptops' => 'acer-laptop',
+ 'Acer Monitore' => 'acer-monitor',
+ 'Acer Predator' => 'acer-predator',
+ 'Action Cameras' => 'actioncam',
+ 'Actionfiguren' => 'actionfiguren',
+ 'adidas' => 'adidas',
+ 'adidas Essentials' => 'adidas-neo',
+ 'adidas Iniki' => 'adidas-iniki',
+ 'adidas NMD' => 'adidas-nmd',
+ 'adidas Originals' => 'adidas-originals',
+ 'adidas Schuhe' => 'adidas-schuhe',
+ 'adidas Superstar' => 'adidas-superstar',
+ 'adidas Ultraboost' => 'adidas-ultraboost',
+ 'adidas ZX Flux' => 'adidas-zx-flux',
+ 'Adventskalender' => 'adventskalender',
+ 'AEG' => 'aeg',
+ 'AEG Waschmaschinen' => 'aeg-waschmaschine',
+ 'Age of Empires' => 'age-of-empires',
+ 'AiO Wasserkühlung' => 'aio-wasserkuehlung',
+ 'AKG' => 'akg',
+ 'Akkus' => 'akkus',
+ 'Akkuschrauber' => 'akkuschrauber',
+ 'Alfa Romeo' => 'alfa-romeo',
+ 'Alienware' => 'alienware',
+ 'Alkohol' => 'alkohol',
+ 'All Inclusive Reisen' => 'all-inclusive',
+ 'All in One PCs' => 'all-in-one-pcs',
+ 'AM4 Mainboard' => 'am4-mainboard',
+ 'Amazfit' => 'xiaomi-amazfit',
+ 'Amazfit Bip' => 'amazfit-bip',
+ 'Amazfit GTS' => 'amazfit-gts',
+ 'Amazon Echo' => 'amazon-echo',
+ 'Amazon Echo Dot' => 'amazon-echo-dot',
+ 'Amazon Echo Plus' => 'amazon-echo-plus',
+ 'Amazon Echo Show' => 'amazon-echo-show',
+ 'Amazon Echo Show 5' => 'amazon-echo-show-5',
+ 'Amazon Echo Show 8' => 'amazon-echo-show-8',
+ 'Amazon Echo Spot' => 'amazon-echo-spot',
+ 'Amazon Fire TV Cube' => 'fire-tv-cube',
+ 'Amazon Fire TV Stick' => 'fire-tv',
+ 'Amazon Fire TV Stick 4K' => 'fire-tv-stick-4k',
+ 'Amazon Tablets' => 'amazon-tablet',
+ 'Amazon Warehouse Deals' => 'amazon-warehouse-deals',
+ 'AMD' => 'amd',
+ 'AMD Radeon' => 'amd-radeon',
+ 'AMD Radeon VII' => 'vega-7',
+ 'AMD RX Vega' => 'amd-vega',
+ 'AMD Ryzen' => 'amd-ryzen',
+ 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x',
+ 'American Express' => 'american-express',
+ 'amiibo' => 'amiibo',
+ 'Analoguhren' => 'analoguhren',
+ 'Android Apps' => 'android-apps',
+ 'Android Smartphones' => 'android-smartphones',
+ 'Angelzubehör' => 'angelsport',
+ 'Animal Crossing' => 'animal-crossing',
+ 'Animal Crossing: New Horizons' => 'animal-crossing-new-horizons',
+ 'Anime' => 'anime',
+ 'Ankündigungen' => 'ankundigungen',
+ 'Anno 1800' => 'anno-1800',
+ 'Anthem' => 'anthem',
+ 'Anzug' => 'anzug',
+ 'AOC' => 'aoc',
+ 'Apex Legends' => 'apex-legends',
+ 'Apotheke' => 'apotheke',
+ 'Apple' => 'apple',
+ 'Apple AirPods' => 'airpods',
+ 'Apple AirPods 2' => 'airpods-2',
+ 'Apple AirPods Max' => 'airpods-max',
+ 'Apple AirPods Pro' => 'airpods-pro',
+ 'Apple EarPods' => 'apple-earpods',
+ 'Apple HomePod' => 'homepod',
+ 'Apple HomePod mini' => 'apple-homepod-mini',
+ 'Apple Kopfhörer' => 'apple-kopfhoerer',
+ 'Apple Magic Mouse 2' => 'apple-magic-mouse-2',
+ 'Apple Pencil' => 'apple-pencil',
+ 'Apple Pencil 2' => 'apple-pencil-2',
+ 'Apple TV' => 'apple-tv',
+ 'Apple Watch' => 'apple-watch',
+ 'Apple Watch 3' => 'apple-watch-3',
+ 'Apple Watch 4' => 'apple-watch-4',
+ 'Apple Watch 5' => 'apple-watch-5',
+ 'Apple Watch 6' => 'apple-watch-6',
+ 'Apple Watch SE' => 'apple-watch-se',
+ 'Apps' => 'apps',
+ 'Aquaristik' => 'aquaristik',
+ 'Arbeitsspeicher' => 'arbeitsspeicher',
+ 'Arbeitszimmermöbel' => 'arbeitszimmer',
+ 'ASICS' => 'asics',
+ 'Assassin&#039;s Creed' => 'assassins-creed',
+ 'Assassin&#039;s Creed: Valhalla' => 'assassins-creed-valhalla',
+ 'Assassin&#039;s Creed Odyssey' => 'assassins-creed-odyssey',
+ 'Assassin&#039;s Creed Origins' => 'assassins-creed-origins',
+ 'ASTRO Gaming A50' => 'astro-gaming-a50',
+ 'ASUS' => 'asus',
+ 'ASUS Laptops' => 'asus-laptop',
+ 'Asus Mainboard' => 'asus-mainboard',
+ 'Asus Monitore' => 'asus-monitor',
+ 'ASUS ROG' => 'asus-rog',
+ 'ASUS Smartphones' => 'asus-smartphones',
+ 'Asus ZenBook' => 'asus-zenbook',
+ 'ASUS ZenFone 5' => 'asus-zenfone-5',
+ 'ASUS ZenFone 5Z' => 'asus-zenfone-5z',
+ 'Audi' => 'audi',
+ 'Audio &amp; HiFi' => 'audio-hifi',
+ 'Audioverstärker' => 'audioverstaerker',
+ 'Audio Zubehör' => 'audio-zubehoer',
+ 'Aukey' => 'aukey',
+ 'Außenleuchten' => 'aussenleuchten',
+ 'Auto &amp; Motorrad' => 'auto-motorrad',
+ 'Auto Bild' => 'auto-bild',
+ 'Auto Leasing' => 'auto-leasing',
+ 'Auto Leasing Gewerbe' => 'gewerbe-leasing',
+ 'Auto Leasing Privat' => 'privat-leasing',
+ 'Automatikuhren' => 'automatikuhr',
+ 'auto motor und sport' => 'auto-motor-sport',
+ 'Autoradio' => 'autoradio',
+ 'Auto Teile' => 'autoteile',
+ 'Autowäsche' => 'autowaesche',
+ 'Auto Zubehör' => 'auto',
+ 'AVM FRITZ!Box' => 'avm-fritz-box',
+ 'AVM FRITZ!Box 7490' => 'avm-fritz-box-7490',
+ 'AVM FRITZ!Box 7530' => 'avm-fritz-box-7530',
+ 'AVM FRITZ!Box 7580' => 'avm-fritz-box-7580',
+ 'AVM FRITZ!Box 7590' => 'avm-fritz-box-7590',
+ 'AVM FRITZ! DECT 301' => 'avm-fritz-dect-301',
+ 'AV Receiver' => 'av-receiver',
+ 'Baby &amp; Kind' => 'kinder',
+ 'Baby-Erstausstattung' => 'baby-erstausstattung',
+ 'Babybetten' => 'babybetten',
+ 'Baby Born' => 'baby-born',
+ 'Babykleidung' => 'babybekleidung',
+ 'Babynahrung' => 'babynahrung',
+ 'Babyphone' => 'babyphone',
+ 'Backofen &amp; Herd' => 'backofen-herd',
+ 'Backwaren' => 'backwaren',
+ 'Backzubehör' => 'backzubehoer',
+ 'Bademode' => 'bademode',
+ 'Badmöbel' => 'badezimmer',
+ 'Bahn-Tickets' => 'bahntickets',
+ 'Bahncard' => 'bahncard',
+ 'Balkonmöbel' => 'balkonmoebel',
+ 'Ballerinas' => 'ballerinas',
+ 'Bang &amp; Olufsen' => 'bang-olufsen',
+ 'Bank' => 'bank',
+ 'Barbie' => 'barbie',
+ 'Barclaycard' => 'barclaycard',
+ 'Bartschneider' => 'bartschneider',
+ 'Batterien' => 'batterien',
+ 'Battle.net' => 'battle-net',
+ 'Battlefield' => 'battlefield',
+ 'Battlefield 1' => 'battlefield-1',
+ 'Battlefield 5' => 'battlefield-5',
+ 'Bauknecht' => 'bauknecht',
+ 'Bauknecht Waschmaschinen' => 'bauknecht-waschmaschine',
+ 'Baumarkt' => 'baumarkt',
+ 'Bayonetta' => 'bayonetta',
+ 'Bayonetta 2' => 'bayonetta-2',
+ 'Beamer' => 'beamer',
+ 'Beamer Leinwand' => 'beamer-leinwand',
+ 'Beats by Dre' => 'beats-by-dre',
+ 'Beats Solo3' => 'beats-solo3',
+ 'Beats Solo Pro' => 'beats-solo-pro',
+ 'Beats Studio3' => 'beats-studio3',
+ 'Beauty &amp; Gesundheit' => 'beauty',
+ 'Beko' => 'beko',
+ 'Beleuchtung' => 'beleuchtung',
+ 'Belkin' => 'belkin',
+ 'Ben &amp; Jerry&#039;s' => 'ben-jerrys',
+ 'Bench' => 'bench',
+ 'BenQ' => 'benq',
+ 'BenQ Monitore' => 'benq-monitor',
+ 'be quiet!' => 'be-quiet',
+ 'be quiet! Netzteile' => 'be-quiet-netzteil',
+ 'Besteck' => 'besteck',
+ 'Bethesda' => 'bethesda',
+ 'Betten' => 'betten',
+ 'Bettwäsche' => 'bettwaesche',
+ 'beyerdynamic' => 'beyerdynamic',
+ 'Beyerdynamic MMX 300' => 'beyerdynamic-mmx-300',
+ 'BHs' => 'bhs',
+ 'Bier' => 'bier',
+ 'Biking &amp; Urban Sport' => 'biking-urban-sport',
+ 'Bildbearbeitungsprogramme' => 'bildbearbeitungsprogramme',
+ 'Birkenstock' => 'birkenstock',
+ 'Black &amp; Decker' => 'black-and-decker',
+ 'Blackberry Smartphones' => 'blackberry',
+ 'Black Desert Online' => 'black-desert-online',
+ 'Blazer' => 'blazer',
+ 'Blood &amp; Truth' => 'blood-truth',
+ 'Blu-ray' => 'blu-ray',
+ 'Blu-ray Player' => 'blu-ray-player',
+ 'Bluetooth Kopfhörer' => 'bluetooth-kopfhoerer',
+ 'Bluetooth Lautsprecher' => 'bluetooth-lautsprecher',
+ 'Blumen' => 'blumen',
+ 'Blusen' => 'blusen',
+ 'BMW' => 'bmw',
+ 'Bodenbelag' => 'bodenbelag',
+ 'Boho-Chic wohnen' => 'boho-chich-wohnen',
+ 'Bohrer' => 'bohrer',
+ 'Bohrhämmer' => 'bohrhaemmer',
+ 'Bohrmaschinen' => 'bohrmaschinen',
+ 'Bollerwagen' => 'bollerwagen',
+ 'Bombay Gin' => 'bombay',
+ 'Borderlands' => 'borderlands',
+ 'Borderlands 3' => 'borderlands-3',
+ 'Bosch' => 'bosch',
+ 'Bosch Akkuschrauber' => 'bosch-akkuschrauber',
+ 'Bosch Geschirrspüler' => 'bosch-geschirrspueler',
+ 'Bosch Kühlschränke' => 'bosch-kuehlschrank',
+ 'Bosch Waschmaschinen' => 'bosch-waschmaschine',
+ 'Bose' => 'bose',
+ 'Bose Headphones 700' => 'bose-headphones-700',
+ 'Bose Home Speaker 500' => 'bose-home-speaker-500',
+ 'Bose Kopfhörer' => 'bose-kopfhoerer',
+ 'Bose QuietComfort' => 'bose-quietcomfort',
+ 'Bose QuietComfort 35 II' => 'bose-quiet-comfort-35-ii',
+ 'Bose Solo 5' => 'bose-solo-5',
+ 'Bose SoundLink' => 'bose-soundlink',
+ 'Bose SoundTouch' => 'bose-soundtouch',
+ 'BOSS' => 'boss',
+ 'Bourbon' => 'bourbon',
+ 'Bowers &amp; Wilkins' => 'bowers-wilkins',
+ 'Boxershorts' => 'boxershorts',
+ 'Boxspringbetten' => 'boxspringbetten',
+ 'Braun' => 'braun',
+ 'Braun Rasierer' => 'braun-rasierer',
+ 'Braun Series 3' => 'braun-series-3',
+ 'Braun Series 5' => 'braun-series-5',
+ 'Braun Series 7' => 'braun-series-7',
+ 'Braun Series 9' => 'braun-series-9',
+ 'Bridgekameras' => 'bridgekamera',
+ 'Brigitte' => 'brigitte',
+ 'Brillen &amp; Kontaktlinsen' => 'brillen',
+ 'Brita' => 'brita',
+ 'Britax Römer' => 'britax-roemer',
+ 'Brotaufstrich' => 'brotaufstrich',
+ 'Brother Drucker' => 'brother-drucker',
+ 'Bücher' => 'buecher',
+ 'Bücher, Magazine &amp; Zeitschriften' => 'buecher-zeitschriften',
+ 'bugatti' => 'bugatti',
+ 'Bügeleisen' => 'buegeleisen',
+ 'Bügeln' => 'buegeln',
+ 'Buggy' => 'buggy',
+ 'Burger' => 'burger',
+ 'BURNHARD' => 'burnhard',
+ 'Bürobedarf' => 'buerobedarf',
+ 'Bürostühle' => 'buerostuhl',
+ 'Bus &amp; Bahn' => 'bus-bahn',
+ 'Business Mode' => 'business-mode',
+ 'c&#039;t – Magazin für Computertechnik' => 'ct-magazin-computertechnik',
+ 'Cafissimo' => 'cafissimo',
+ 'Call of Duty' => 'call-of-duty',
+ 'Call of Duty: Black Ops 4' => 'call-of-duty-black-ops-4',
+ 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war',
+ 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare',
+ 'Call of Duty: Modern Warfare' => 'call-of-duty-modern-warfare',
+ 'Call of Duty: Warzone' => 'call-of-duty-warzone',
+ 'Call of Duty: WW2' => 'call-of-duty-ww2',
+ 'Calvin Klein' => 'calvin-klein',
+ 'Camcorder' => 'camcorder',
+ 'Campen' => 'campen',
+ 'Canon' => 'canon',
+ 'Canon Drucker' => 'canon-drucker',
+ 'Canon EOS' => 'canon-eos',
+ 'Canon Kameras' => 'canon-kameras',
+ 'Canon PowerShot' => 'canon-powershot',
+ 'CANTON' => 'canton',
+ 'Caps' => 'caps',
+ 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker',
+ 'Capture One' => 'capture-one',
+ 'Carhartt' => 'carhartt',
+ 'Carsharing' => 'carsharing',
+ 'Casio' => 'casio',
+ 'Cheap Monday' => 'cheapmonday',
+ 'Chevrolet' => 'chevrolet',
+ 'China Handys' => 'china-handys',
+ 'Chip (Magazin)' => 'chip-magazin',
+ 'Chips' => 'chips',
+ 'Christbaumschmuck' => 'christbaumschmuck',
+ 'Christbaumständer' => 'christbaumstaender',
+ 'Chromebook' => 'chromebook',
+ 'Chronographen' => 'chronograph',
+ 'Chucks' => 'chucks',
+ 'Citroen' => 'citroen',
+ 'Coca-Cola' => 'coca-cola',
+ 'Comics' => 'comics',
+ 'Computer' => 'computer',
+ 'Computer &amp; Tablets' => 'computer-tablet',
+ 'Computer Bild' => 'computer-bild',
+ 'Controller' => 'controller',
+ 'Converse' => 'converse',
+ 'Convertibles' => 'convertibles',
+ 'Corsair' => 'corsair',
+ 'Corsair VOID PRO' => 'corsair-void-pro',
+ 'Couchtische' => 'couchtische',
+ 'Coupons' => 'coupons',
+ 'CPU-Kühler' => 'cpu-kuehler',
+ 'Craghoppers' => 'craghoppers',
+ 'Crocs' => 'crocs',
+ 'Crucial' => 'crucial',
+ 'Cupra' => 'cupra',
+ 'Cyberpunk 2077' => 'cyberpunk-2077',
+ 'cybex' => 'cybex',
+ 'D-Link' => 'd-link',
+ 'DAB Radios' => 'dab-radios',
+ 'Dacia' => 'dacia',
+ 'Damenbekleidung' => 'fashion-frauen',
+ 'Damenschuhe' => 'damenschuhe',
+ 'Dampfbügelstation' => 'dampfbuegelstation',
+ 'Dampfgarer' => 'dampfgarer',
+ 'Dampfreiniger' => 'dampfreiniger',
+ 'Dark Souls' => 'dark-souls',
+ 'Dashcam' => 'dashcam',
+ 'Datentarif' => 'datentarif',
+ 'Daypack' => 'daypack',
+ 'Days Gone' => 'days-gone',
+ 'DC Shoes' => 'dc-shoes',
+ 'DDR3 RAM' => 'ddr3-ram',
+ 'DDR4 RAM' => 'ddr4-ram',
+ 'De&#039;Longhi' => 'delonghi',
+ 'Death Stranding' => 'death-stranding',
+ 'Deckenlampen' => 'deckenlampen',
+ 'DECT Telefone' => 'telefone',
+ 'Dekoration' => 'dekoration',
+ 'Dell' => 'dell',
+ 'Dell Laptops' => 'dell-laptop',
+ 'Dell Monitore' => 'dell-monitor',
+ 'Dell XPS' => 'dell-xps',
+ 'Denon' => 'denon',
+ 'Deo' => 'deo',
+ 'Depot' => 'depot',
+ 'DER SPIEGEL' => 'der-spiegel',
+ 'Designermöbel' => 'designermoebel',
+ 'Desigual' => 'desigual',
+ 'Desinfektionsmittel' => 'desinfektionsmittel',
+ 'Desktop PCs' => 'desktop-pc',
+ 'Dessous' => 'dessous',
+ 'Destiny' => 'destiny',
+ 'Destiny 2' => 'destiny-2',
+ 'Deus Ex' => 'deus-ex',
+ 'Deus Ex: Mankind' => 'deus-ex-mankind',
+ 'Deuter' => 'deuter',
+ 'DeutschlandCard' => 'deutschlandcard',
+ 'devolo' => 'devolo',
+ 'DeWalt' => 'dewalt',
+ 'Die drei Fragezeichen' => 'die-drei-fragezeichen',
+ 'Die Eiskönigin' => 'die-eiskoenigin',
+ 'Dienstleistungen &amp; Verträge' => 'dienstleistungen-vertraege',
+ 'Dies &amp; Das' => 'dies-das',
+ 'Diesel' => 'diesel',
+ 'Die Sims' => 'die-sims',
+ 'Die Sims 4' => 'die-sims-4',
+ 'Die Zeit' => 'die-zeit',
+ 'Digitalreceiver' => 'digitalreceiver',
+ 'Digitaluhren' => 'digitaluhr',
+ 'Direktflüge' => 'direktfluege',
+ 'Dirt Devil' => 'dirt-devil',
+ 'Dishonored' => 'dishonored',
+ 'Dishonored 2: Das Vermächtnis der Maske' => 'dishonored-2',
+ 'Disney' => 'disney',
+ 'Disney+' => 'disney-plus',
+ 'DJI' => 'dji',
+ 'DJI Osmo Pocket' => 'dji-osmo-pocket',
+ 'Dockers' => 'dockers',
+ 'Dolce Gusto' => 'dolce-gusto',
+ 'DOOM Eternal' => 'doom-eternal',
+ 'Douglas Adventskalender' => 'douglas-adventskalender',
+ 'Dr. Martens' => 'dr-martens',
+ 'Dragon Ball' => 'dragon-ball',
+ 'Dragon Ball FighterZ' => 'dragon-ball-fighterz',
+ 'Dragon Ball Z: Kakarot' => 'dragon-ball-z-kakarot',
+ 'Dragon Quest Builders' => 'dragon-quest-builders',
+ 'Dragon Quest Builders 2' => 'dragon-quest-builders-2',
+ 'Dreame Staubsauger' => 'xiaomi-staubsauger',
+ 'Dreame T20' => 'dreame-t20',
+ 'Dreame V9' => 'xiaomi-dreame-v9',
+ 'Dreame V10' => 'xiaomi-dreame-v10',
+ 'Dreame V11' => 'xiaomi-dreame-v11',
+ 'Drohnen' => 'drohnen',
+ 'Drucker' => 'drucker',
+ 'Druckerpatronen' => 'druckerpatronen',
+ 'Druckerzubehör' => 'druckerzubehoer',
+ 'DSL &amp; Kabel' => 'dsl',
+ 'Dunstabzugshauben' => 'dunstabzugshauben',
+ 'Durex' => 'durex',
+ 'Duscharmaturen' => 'duscharmaturen',
+ 'Duschgel' => 'duschgel',
+ 'Duschköpfe' => 'duschkoepfe',
+ 'DVD' => 'dvd',
+ 'Dyson' => 'dyson',
+ 'Dyson Staubsauger' => 'dyson-staubsauger',
+ 'Dyson V6' => 'dyson-v6',
+ 'Dyson V7' => 'dyson-v7',
+ 'Dyson V8' => 'dyson-v8',
+ 'Dyson V10' => 'dyson-v10',
+ 'Dyson V11' => 'dyson-v11',
+ 'Dyson V11 Absolute' => 'dyson-v11-absolute',
+ 'Dyson V11 Animal' => 'dyson-v11-animal',
+ 'E-Bikes' => 'e-bikes',
+ 'E-Scooter' => 'e-scooter',
+ 'E-Scooter Sharing' => 'e-scooter-sharing',
+ 'E-Zigaretten' => 'e-zigaretten',
+ 'Eastpak' => 'eastpak',
+ 'eBook Reader' => 'ebook-reader',
+ 'eBooks' => 'ebooks',
+ 'Ecovacs' => 'ecovacs',
+ 'Ecovacs Deebot 900' => 'ecovacs-deebot-900',
+ 'Ecovacs Deebot OZMO 930' => 'ecovacs-deebot-ozmo-930',
+ 'Edifier' => 'edifier',
+ 'Edifier R1280DB' => 'edifier-r1280db',
+ 'Edifier R1280T' => 'edifier-r1280t',
+ 'Einhell' => 'einhell',
+ 'Eis' => 'eis',
+ 'Elektrische Zahnbürsten' => 'elektrische-zahnbuersten',
+ 'Elektrogrills' => 'elektrogrill',
+ 'Elektroheizungen' => 'elektroheizungen',
+ 'Elektronik' => 'elektronik',
+ 'Elektronik Zubehör' => 'elektronikzubehoer',
+ 'Elektrorasierer' => 'elektrorasierer',
+ 'Elektroroller' => 'elektroroller',
+ 'Elektrowerkzeuge' => 'elektrowerkzeug',
+ 'Elephone' => 'elephone',
+ 'ELLE' => 'elle',
+ 'Emsa' => 'emsa',
+ 'Energy Drinks' => 'energy-drinks',
+ 'Entsafter' => 'entsafter',
+ 'Epilierer' => 'epilierer',
+ 'Epson' => 'epson',
+ 'Epson Drucker' => 'epson-drucker',
+ 'Erotik' => 'erotik',
+ 'Error Fare' => 'error-fare',
+ 'Espressomaschinen' => 'espressomaschinen',
+ 'Esprit' => 'esprit',
+ 'Esstische' => 'esstisch',
+ 'Esszimmer' => 'esszimmer',
+ 'Eterna' => 'eterna',
+ 'EUROtronic Comet DECT' => 'eurotronic-comet-dect',
+ 'Externe Festplatten' => 'externe-festplatten',
+ 'F1 2017' => 'f1-2017',
+ 'F1 2019' => 'f1-2019',
+ 'F1 2020' => 'f1-2020',
+ 'Fahrräder' => 'fahrraeder',
+ 'Fahrradhelme' => 'fahrradhelme',
+ 'Fahrradrucksäcke' => 'fahrradrucksack',
+ 'Fahrradschlösser' => 'fahrradschloss',
+ 'Fahrradteile' => 'fahrradteile',
+ 'Fahrradträger' => 'fahrradtraeger',
+ 'Fahrradzubehör' => 'fahrradzubehoer',
+ 'Fahrzeuge' => 'fahrzeuge',
+ 'Falke' => 'falke',
+ 'Fallout' => 'fallout',
+ 'Fallout 4' => 'fallout-4',
+ 'Fallout 76' => 'fallout-76',
+ 'Family &amp; Kids' => 'family-kids',
+ 'Far Cry' => 'far-cry',
+ 'Far Cry 5' => 'far-cry-5',
+ 'Far Cry New Dawn' => 'far-cry-new-dawn',
+ 'Fashion &amp; Accessoires' => 'fashion-accessoires',
+ 'Fast Food' => 'fast-food',
+ 'Felgen' => 'felgen',
+ 'Fenstersauger' => 'fenstersauger',
+ 'Fernbus-Tickets' => 'fernbus',
+ 'Fernseher' => 'fernseher',
+ 'Fertiggerichte' => 'fertiggerichte',
+ 'Festplatten' => 'festplatten',
+ 'Festplattengehäuse' => 'festplattengehaeuse',
+ 'FFP2 Masken' => 'ffp2-masken',
+ 'Fiat' => 'fiat',
+ 'FIFA' => 'fifa',
+ 'FIFA 17' => 'fifa-17',
+ 'FIFA 18' => 'fifa-18',
+ 'FIFA 19' => 'fifa-19',
+ 'FIFA 20' => 'fifa-20',
+ 'FIFA 21' => 'fifa-21',
+ 'FILA' => 'fila',
+ 'Filme &amp; Serien' => 'filme-serien',
+ 'Filterkaffeemaschinen' => 'filterkaffeemaschinen',
+ 'Final Fantasy' => 'final-fantasy',
+ 'Final Fantasy 7' => 'final-fantasy-7',
+ 'Finanzen- und Steuersoftware' => 'finanzen-und-steuersoftware',
+ 'Finish' => 'finish',
+ 'Fisch &amp; Meeresfrüchte' => 'fisch-meeresfruechte',
+ 'Fischertechnik' => 'fischertechnik',
+ 'Fisher-Price' => 'fisher-price',
+ 'Fiskars' => 'fiskars',
+ 'Fissler' => 'fissler',
+ 'fitbit' => 'fitbit',
+ 'Fitness &amp; Running' => 'fitness',
+ 'Fitness Apps' => 'fitness-apps',
+ 'Fitnessstudio' => 'fitnessstudio',
+ 'Fitnesstracker' => 'fitnesstracker',
+ 'Fjällräven' => 'fjaellraeven',
+ 'Fleisch &amp; Wurst' => 'fleisch-wurst',
+ 'Fliesenschneider' => 'fliesenschneider',
+ 'Flüge' => 'fluege',
+ 'Flurmöbel' => 'flurmoebel',
+ 'FOCUS' => 'focus',
+ 'Ford' => 'ford',
+ 'For Honor' => 'for-honor',
+ 'Formel 1 Games' => 'formel-1',
+ 'Fortnite' => 'fortnite',
+ 'Forza' => 'forza',
+ 'Forza Horizon' => 'forza-horizon',
+ 'Forza Horizon 4' => 'forza-horizon-4',
+ 'Forza Motorsport' => 'forza-motorsport',
+ 'Forza Motorsport 7' => 'forza-7',
+ 'Fossil' => 'fossil',
+ 'Foto &amp; Kamera' => 'foto-video',
+ 'Foto Apps' => 'foto-apps',
+ 'Fotobücher' => 'fotobuecher',
+ 'Fototapete' => 'fototapete',
+ 'Fragen &amp; Gesuche' => 'gesuche',
+ 'Frankfurter Allgemeine Zeitung (F.A.Z.)' => 'frankfurter-allgemeine-zeitung',
+ 'FreeSync Monitore' => 'freesync-monitor',
+ 'Freizeitpark-Tickets' => 'freizeitpark',
+ 'Freizeitsport' => 'freizeitsport',
+ 'Fritteusen' => 'fritteusen',
+ 'Frontlader' => 'frontlader',
+ 'Frühlingsdeko' => 'fruehlingsdeko',
+ 'Frühstücksflocken' => 'fruehstuecksflocken',
+ 'Fruit of the Loom' => 'fruit-of-the-loom',
+ 'Fujifilm' => 'fujifilm',
+ 'Füller' => 'fueller',
+ 'Full HD-Beamer' => 'full-hd-beamer',
+ 'Fun Factory' => 'fun-factory',
+ 'FurReal Friends' => 'furreal-friends',
+ 'Fußball' => 'fussball',
+ 'Fußball-Trikots' => 'fussball-trikots',
+ 'Fußballschuhe' => 'fussballschuhe',
+ 'G-Star' => 'g-star',
+ 'G-Sync Monitore' => 'g-sync-monitor',
+ 'Game of Thrones' => 'game-of-thrones',
+ 'Gaming' => 'gaming',
+ 'Gaming Headsets' => 'gaming-headset',
+ 'Gaming Laptops' => 'gaming-laptop',
+ 'Gaming Mäuse' => 'gaming-maus',
+ 'Gaming Monitore' => 'gaming-monitor',
+ 'Gaming PCs' => 'gaming-pc',
+ 'Gaming Stühle' => 'gaming-stuhl',
+ 'Gaming Tastaturen' => 'gaming-tastatur',
+ 'Gaming Zubehör' => 'spielekonsolen-zubehoer',
+ 'Ganzjahresreifen' => 'ganzjahresreifen',
+ 'GAP' => 'gap',
+ 'Gardena' => 'gardena',
+ 'Garderobe' => 'garderobe',
+ 'Garmin' => 'garmin',
+ 'Garmin Fenix' => 'garmin-fenix',
+ 'Garten' => 'garten',
+ 'Garten &amp; Baumarkt' => 'garten-baumarkt',
+ 'Gartenarbeit' => 'gartenarbeit',
+ 'Gartenbank' => 'gartenbank',
+ 'Gartenliegen' => 'sonnenliegen',
+ 'Gartenmöbel' => 'gartenmoebel',
+ 'Gartenstühle' => 'gartenstuehle',
+ 'Gartentische' => 'gartentische',
+ 'Gasgrills' => 'gasgrill',
+ 'Gastarif' => 'gastarif',
+ 'Gears 5' => 'gears-5',
+ 'Gears of War' => 'gears-of-war',
+ 'Gefrierschränke' => 'gefrierschrank',
+ 'Geld-zurück-Aktionen' => 'geld-zurueck',
+ 'Geldbörsen' => 'geldboersen',
+ 'Gemüse' => 'gemuese',
+ 'Geox' => 'geox',
+ 'Geschirr' => 'geschirr',
+ 'Geschirrspüler' => 'geschirrspueler',
+ 'Gesellschaftsspiele' => 'gesellschaftsspiele',
+ 'Gesichtspflege' => 'gesichtspflege',
+ 'Gesundheit' => 'gesundheit',
+ 'Getränke' => 'getraenke',
+ 'Gewinnspiele' => 'gewinnspiele',
+ 'GHD' => 'ghd',
+ 'Ghost of Tsushima' => 'ghost-of-tsushima',
+ 'GIGABYTE' => 'gigabyte',
+ 'Gigaset' => 'gigaset',
+ 'Gillette' => 'gillette',
+ 'Gillette Rasierer' => 'gillette-rasierer',
+ 'Gin' => 'gin',
+ 'Girokonto' => 'konto',
+ 'Glamour' => 'glamour',
+ 'Glamourös wohnen' => 'glamouroes-wohnen',
+ 'Gläser' => 'glaeser',
+ 'Glätteisen' => 'glaetteisen',
+ 'Gleitgel' => 'gleitgel',
+ 'Glühwein' => 'gluehwein',
+ 'God of War' => 'god-of-war',
+ 'Google Chromecast' => 'chromecast',
+ 'Google Chromecast mit Google TV' => 'chromecast-mit-google-tv',
+ 'Google Chromecast Ultra' => 'chromecast-ultra',
+ 'Google Home' => 'google-home',
+ 'Google Home Max' => 'google-home-max',
+ 'Google Home Mini' => 'google-home-mini',
+ 'Google Nest Hub' => 'google-nest-hub',
+ 'Google Pixel' => 'google-pixel',
+ 'Google Pixel 2' => 'google-pixel-2',
+ 'Google Pixel 3' => 'google-pixel-3',
+ 'Google Pixel 4' => 'google-pixel-4',
+ 'Google Pixel 4 XL' => 'google-pixel-4xl',
+ 'Google Pixel 4a' => 'google-pixel-4a',
+ 'Google Pixel 4a 5G' => 'google-pixel-4a-5g',
+ 'Google Pixel 5' => 'google-pixel-5',
+ 'Google Smartphones' => 'google-smartphones',
+ 'Google Stadia Konsolen' => 'google-stadia',
+ 'GoPro Action Cameras' => 'gopro',
+ 'GoPro HERO 7' => 'gopro-hero-7',
+ 'GoPro HERO 8' => 'gopro-hero-8',
+ 'GoPro HERO 9' => 'gopro-hero-9',
+ 'Gorenje' => 'gorenje',
+ 'Grafikkarten' => 'grafikkarten',
+ 'Gran Turismo' => 'gran-turismo',
+ 'Gran Turismo Sport' => 'gran-turismo-sport',
+ 'Grazia' => 'grazia',
+ 'Grills' => 'grill',
+ 'Grillzubehör' => 'grillzubehoer',
+ 'Grundig' => 'grundig',
+ 'GTA' => 'gta',
+ 'GTA V' => 'gta-v',
+ 'GTX 1060' => 'gtx-1060',
+ 'GTX 1070' => 'gtx-1070',
+ 'GTX 1080' => 'gtx-1080',
+ 'GTX 1080 Ti' => 'gtx-1080-ti',
+ 'GTX 1660' => 'gtx-1660',
+ 'GTX 1660 Ti' => 'gtx-1660-ti',
+ 'Gucci' => 'gucci',
+ 'Gummistiefel' => 'gummistiefel',
+ 'Gürtel' => 'guertel',
+ 'Gutscheinfehler' => 'gutscheinfehler',
+ 'Haarentfernung' => 'haarentfernung',
+ 'Haargel' => 'haargel',
+ 'Haarpflege' => 'haarpflege',
+ 'Haarschneidemaschinen' => 'haarschneidemaschinen',
+ 'Haarspray' => 'haarspray',
+ 'Haartrockner' => 'haartrockner',
+ 'Haftpflichtversicherung' => 'haftpflichtversicherung',
+ 'Hama' => 'hama',
+ 'Handelsblatt' => 'handelsblatt',
+ 'Handmixer' => 'handmixer',
+ 'Handtaschen' => 'handtaschen',
+ 'Handtücher' => 'handtuecher',
+ 'Handwerkzeuge' => 'handwerkzeug',
+ 'Handy &amp; Smartphone Zubehör' => 'smartphone-zubehoer',
+ 'Handyhalterung' => 'handyhalterung',
+ 'Handyhüllen' => 'handyhuelle',
+ 'Handys mit Vertrag' => 'handys-mit-vertrag',
+ 'Handys ohne Vertrag' => 'handys-ohne-vertrag',
+ 'Handyversicherung' => 'handyversicherung',
+ 'Handyverträge' => 'handyvertraege',
+ 'Handyverträge 3 Monate Kündigungsfrist' => 'handyvertraege-3-monate-kuendigungsfrist',
+ 'Handyverträge monatlich kündbar' => 'handyvertraege-monatlich-kuendbar',
+ 'Hängematten' => 'haengematten',
+ 'Hanteln' => 'hanteln',
+ 'Haribo' => 'haribo',
+ 'Harman Kardon' => 'harman-kardon',
+ 'Harry Potter' => 'harry-potter',
+ 'Hasbro' => 'hasbro',
+ 'Haushaltsartikel' => 'haushaltsartikel',
+ 'Haushaltsgeräte' => 'haushaltsgeraete',
+ 'Haushaltswaren' => 'haushaltswaren',
+ 'Hausratversicherung' => 'hausratsversicherung',
+ 'Hausschuhe' => 'hausschuhe',
+ 'Haustier' => 'haustier',
+ 'Hautpflege' => 'hautpflege',
+ 'Head &amp; Shoulders' => 'head-and-shoulders',
+ 'Heckenscheren' => 'heckenschere',
+ 'Heimkino' => 'heimkino',
+ 'Heimtextilien' => 'heimtextilien',
+ 'Heißluftfritteusen' => 'heissluftfriteuse',
+ 'Heizkörperthermostat' => 'heizkoerperthermostat',
+ 'Heizungen' => 'heizungen',
+ 'Hemden' => 'hemden',
+ 'Hendrick&#039;s Gin' => 'hendricks-gin',
+ 'Herbstdeko' => 'herbstdeko',
+ 'Herrenbekleidung' => 'fashion-maenner',
+ 'Herrenschuhe' => 'herrenschuhe',
+ 'HiPP' => 'hipp',
+ 'Hisense' => 'hisense',
+ 'Hochbetten' => 'hochbetten',
+ 'Hochdruckreiniger' => 'hochdruckreiniger',
+ 'Hochstuhl' => 'hochstuhl',
+ 'Hollywoodschaukel' => 'hollywoodschaukel',
+ 'Home &amp; Living' => 'home-living',
+ 'homee' => 'homee',
+ 'Honda' => 'honda',
+ 'Honor' => 'honor',
+ 'Honor 5' => 'honor-5',
+ 'Honor 6' => 'honor-6',
+ 'Honor 7X' => 'honor-7',
+ 'Honor 8' => 'honor-8',
+ 'Honor 9' => 'honor-9',
+ 'Honor 20' => 'honor-20',
+ 'Honor 20 Lite' => 'honor-20-lite',
+ 'Honor Band 4' => 'honor-band-4',
+ 'Honor Band 5' => 'honor-band-5',
+ 'Honor Play' => 'honor-play',
+ 'Honor Smartphones' => 'honor-smartphones',
+ 'Honor View 10' => 'honor-view-10',
+ 'Honor View 20' => 'honor-view-20',
+ 'Hoodies' => 'hoodies',
+ 'Hörbücher' => 'hoerbuecher',
+ 'Horizon Zero Dawn' => 'horizon-zero-dawn',
+ 'Hörspiele' => 'hoerspiele',
+ 'Hörzu' => 'hoerzu',
+ 'Hosen' => 'hosen',
+ 'Hotels &amp; Unterkünfte' => 'hotel',
+ 'Hot Wheels' => 'hot-wheels',
+ 'Hoverboards' => 'hoverboards',
+ 'HP' => 'hp',
+ 'HP Drucker' => 'hp-drucker',
+ 'HP Laptops' => 'hp-laptop',
+ 'HP OMEN' => 'hp-omen',
+ 'HP Pavilion' => 'hp-pavilion',
+ 'HTC 10' => 'htc-10',
+ 'HTC Desire 12' => 'htc-desire',
+ 'HTC Smartphones' => 'htc-smartphones',
+ 'HTC U11' => 'htc-u11',
+ 'HTC Vive' => 'htc-vive',
+ 'Huawei' => 'huawei',
+ 'Huawei Kopfhörer' => 'huawei-kopfhoerer',
+ 'Huawei Mate 9' => 'huawei-mate-9',
+ 'Huawei Mate 10' => 'huawei-mate-10',
+ 'Huawei Mate 20' => 'huawei-mate-20',
+ 'Huawei Mate 20 Lite' => 'huawei-mate-20-lite',
+ 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro',
+ 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro',
+ 'Huawei MateBook' => 'huawei-matebook',
+ 'Huawei P10' => 'huawei-p10',
+ 'Huawei P20' => 'huawei-p20',
+ 'Huawei P30' => 'huawei-p30',
+ 'Huawei P30 Lite' => 'huawei-p30-lite',
+ 'Huawei P30 Pro' => 'huawei-p30-pro',
+ 'Huawei P40' => 'huawei-p40',
+ 'Huawei P40 Lite' => 'huawei-p40-lite',
+ 'Huawei P40 Pro' => 'huawei-p40-pro',
+ 'Huawei P Smart' => 'huawei-p-smart',
+ 'Huawei Smartphones' => 'huawei-smartphones',
+ 'Huawei Tablets' => 'huawei-mediapad',
+ 'Huawei Watch GT2' => 'huawei-watch-gt2',
+ 'Huawei Y7' => 'huawei-y7',
+ 'Hunde' => 'hunde',
+ 'Hundefutter' => 'hundefutter',
+ 'Hüte &amp; Mützen' => 'huete-muetzen',
+ 'Hyrule Warriors' => 'hyrule-warriors',
+ 'Hyrule Warriors: Zeit der Verheerung' => 'hyrule-warriors-zeit-der-verheerung',
+ 'Hyundai' => 'hyundai',
+ 'iMac' => 'imac',
+ 'Immortals Fenyx Rising' => 'immortals-fenyx-rising',
+ 'In-Ear Kopfhörer' => 'in-ear-kopfhoerer',
+ 'Industrial Style' => 'industrial-style',
+ 'Inline Skates' => 'inline-skates',
+ 'Instax Mini' => 'instax-mini',
+ 'Intel Core i9-9900K' => 'intel-core-i9-9900k',
+ 'Intel i3' => 'intel-i3',
+ 'Intel i5' => 'intel-i5',
+ 'Intel i7' => 'intel-i7',
+ 'Intel i9' => 'intel-i9',
+ 'Intenso' => 'intenso',
+ 'Internet Security' => 'internet-security',
+ 'Intimpflege' => 'intimpflege',
+ 'iOS Apps' => 'ios-apps',
+ 'iPad' => 'ipad',
+ 'iPad 2019' => 'ipad-2019',
+ 'iPad 2020' => 'ipad-2020',
+ 'iPad Air' => 'ipad-air-2',
+ 'iPad Air 2019' => 'ipad-air-2019',
+ 'iPad Air 2020' => 'ipad-air-2020',
+ 'iPad mini' => 'ipad-mini',
+ 'iPad Pro' => 'ipad-pro',
+ 'iPad Pro 11' => 'ipad-pro-11',
+ 'iPad Pro 12.9' => 'ipad-pro-12-9',
+ 'iPad Pro 2020' => 'ipad-pro-2020',
+ 'iPhone' => 'iphone',
+ 'iPhone 6' => 'iphone-6',
+ 'iPhone 6 Plus' => 'iphone-6-plus',
+ 'iPhone 6s' => 'iphone-6s',
+ 'iPhone 6s Plus' => 'iphone-6s-plus',
+ 'iPhone 7' => 'iphone-7',
+ 'iPhone 7 Plus' => 'iphone-7-plus',
+ 'iPhone 8' => 'iphone-8',
+ 'iPhone 8 Plus' => 'iphone-8-plus',
+ 'iPhone 11' => 'iphone-11',
+ 'iPhone 11 Pro' => 'iphone-11-pro',
+ 'iPhone 11 Pro Max' => 'iphone-11-pro-max',
+ 'iPhone 12' => 'iphone-12',
+ 'iPhone 12 mini' => 'iphone-12-mini',
+ 'iPhone 12 Pro' => 'iphone-12-pro',
+ 'iPhone 12 Pro Max' => 'iphone-12-pro-max',
+ 'iPhone SE' => 'iphone-se',
+ 'iPhone X' => 'iphone-x',
+ 'iPhone Xr' => 'iphone-xr',
+ 'iPhone Xs' => 'iphone-xs',
+ 'iPhone Xs Max' => 'iphone-xs-max',
+ 'iPhone Zubehör' => 'iphone-zubehoer',
+ 'Irish Whiskey' => 'irish-whiskey',
+ 'iRobot' => 'irobot',
+ 'iRobot Roomba' => 'irobot-roomba',
+ 'iRobot Roomba 980' => 'irobot-roomba-980',
+ 'iRobot Roomba i7' => 'irobot-roomba-i7',
+ 'Isomatten' => 'isomatten',
+ 'iTunes Guthaben' => 'itunes-guthaben',
+ 'Jabra Elite 75t' => 'jabra-elite-75t',
+ 'Jabra Elite 85h' => 'jabra-elite-85h',
+ 'Jabra Elite 85t' => 'jabra-elite-85t',
+ 'Jabra Elite Active 75t' => 'jabra-elite-active-75t',
+ 'Jabra Kopfhörer' => 'jabra-kopfhoerer',
+ 'JACK &amp; JONES' => 'jack-jones',
+ 'Jacken' => 'jacken',
+ 'JACK WOLFSKIN' => 'jack-wolfskin',
+ 'Jagdzubehör' => 'jagdzubehoer',
+ 'JBL' => 'jbl',
+ 'JBL Charge 4' => 'jbl-charge-4',
+ 'JBL Flip' => 'jbl-flip',
+ 'JBL GO' => 'jbl-go',
+ 'Jeans' => 'jeans',
+ 'Jim Beam' => 'jim-beam',
+ 'Jogginghosen' => 'jogginghosen',
+ 'Joghurt' => 'joghurt',
+ 'Johnnie Walker' => 'johnnie-walker',
+ 'Jura Kaffeemaschinen' => 'jura',
+ 'Just Cause' => 'just-cause',
+ 'Just Cause 4' => 'just-cause-4',
+ 'Kaffee' => 'kaffee',
+ 'Kaffeekapseln' => 'kaffeekapseln',
+ 'Kaffeemaschinen' => 'kaffeemaschinen',
+ 'Kaffeemühlen' => 'kaffeemuehlen',
+ 'Kaffeepadmaschinen' => 'kaffeepadmaschinen',
+ 'Kaffeepads' => 'kaffeepads',
+ 'Kaffeevollautomaten' => 'kaffeevollautomaten',
+ 'Kameras' => 'kamera',
+ 'Kamera Zubehör' => 'kamerazubehoer',
+ 'Kamine' => 'kamine',
+ 'Kapselmaschinen' => 'kapselmaschinen',
+ 'Kärcher' => 'kaercher',
+ 'Kärcher Fenstersauger' => 'kaercher-fenstersauger',
+ 'Kärcher Hochdruckreiniger' => 'kaercher-hochdruckreiniger',
+ 'Kartenspiele' => 'kartenspiel',
+ 'Käse' => 'kaese',
+ 'Katzen' => 'katzen',
+ 'Katzenfutter' => 'katzenfutter',
+ 'Kaufen im Ausland' => 'kaufen-ausland',
+ 'Ketchup' => 'ketchup',
+ 'KFZ Versicherung' => 'kfz-versicherung',
+ 'KIA' => 'kia',
+ 'kiddy' => 'kiddy',
+ 'Kinder Adventskalender' => 'kinder-adventskalender',
+ 'Kinderbekleidung' => 'kinderkleidung',
+ 'Kinderbetten' => 'kinderbett',
+ 'Kinderfahrräder' => 'kinderfahrrad',
+ 'Kinderschuhe' => 'kinderschuhe',
+ 'Kindersitz' => 'kindersitz',
+ 'Kinderwagen' => 'kinderwagen',
+ 'Kinderwagen &amp; Autositze' => 'baby-transport',
+ 'Kinderzimmermöbel' => 'kinderzimmer',
+ 'Kindle' => 'kindle',
+ 'Kindle Oasis' => 'kindle-oasis',
+ 'Kindle Paperwhite' => 'kindle-paperwhite',
+ 'Kingdom Come: Deliverance' => 'kingdom-come-deliverance',
+ 'Kingdom Hearts' => 'kingdom-hearts',
+ 'Kingdom Hearts 3' => 'kingdom-hearts-3',
+ 'Kingston HyperX Cloud Flight' => 'kingston-hyperx-cloud-flight',
+ 'Kingston HyperX Cloud II' => 'hyperx-cloud-ii',
+ 'Kino' => 'kino',
+ 'KitchenAid' => 'kitchenaid',
+ 'Kleider' => 'kleider',
+ 'Kleiderschränke' => 'kleiderschraenke',
+ 'Kleidung' => 'kleidung',
+ 'Klemmbausteine' => 'klemmbausteine',
+ 'Klimaanlagen' => 'klimaanlagen',
+ 'Klimatechnik' => 'klimatechnik',
+ 'Klipsch' => 'klipsch',
+ 'Kochgeräte' => 'kochgeraete',
+ 'Kodak' => 'kodak',
+ 'Koffer' => 'koffer',
+ 'Kohlenmonoxidmelder' => 'kohlenmonoxidmelder',
+ 'Kolonialstil' => 'kolonialstil',
+ 'Kommoden &amp; Sideboards' => 'kommoden-sideboards',
+ 'Kondome' => 'kondome',
+ 'König der Löwen Musical' => 'koenig-der-loewen-musical',
+ 'Kontaktgrills' => 'kontaktgrill',
+ 'Konto &amp; Kreditkarten' => 'konto-kreditkarten',
+ 'Konzert-Tickets' => 'konzerte',
+ 'Kopfhörer' => 'kopfhoerer',
+ 'Körperpflege &amp; Hygiene' => 'koerperpflege',
+ 'Kosmetik' => 'kosmetik',
+ 'Kostüme' => 'kostuem',
+ 'Kraftstoffe &amp; Betriebsstoffe' => 'kraftstoffe-betriebsstoffe',
+ 'Krafttraining' => 'krafttraining',
+ 'Kredit' => 'kredit',
+ 'Kreditkarten' => 'kreditkarten',
+ 'Kreissägen' => 'kreissaegen',
+ 'Kreuzfahrten' => 'kreuzfahrten',
+ 'Krups' => 'krups',
+ 'Küche' => 'kueche',
+ 'Küchengeräte' => 'kuechengeraete',
+ 'Küchenhelfer' => 'kuechenhelfer',
+ 'Küchenmaschinen' => 'kuechenmaschinen',
+ 'Küchenmesser' => 'messer',
+ 'Küchenutensilien' => 'kuechenutensilien',
+ 'Kugelschreiber' => 'kugelschreiber',
+ 'Kühl-Gefrierkombinationen' => 'kuehl-gefrierkombination',
+ 'Kühlboxen' => 'kuehlboxen',
+ 'Kühlschränke' => 'kuehlschrank',
+ 'Kultur &amp; Freizeit' => 'kultur-freizeit',
+ 'Kunst &amp; Hobby' => 'hobby',
+ 'Kurse &amp; Trainings' => 'kurse-trainings',
+ 'Lacoste' => 'lacoste',
+ 'Ladegeräte' => 'ladegeraete',
+ 'Lampen' => 'lampen',
+ 'Landhausstil' => 'landhausstil',
+ 'Landwirtschafts-Simulator' => 'landwirtschafts-simulator',
+ 'Laptops' => 'laptop',
+ 'Laserdrucker' => 'laserdrucker',
+ 'Last Minute Reisen' => 'last-minute',
+ 'Lattenroste' => 'lattenroste',
+ 'Laubsauger' => 'laubsauger',
+ 'Laufräder' => 'laufraeder',
+ 'Laufschuhe' => 'laufschuhe',
+ 'Laufsport' => 'laufsport',
+ 'Lautsprecher' => 'lautsprecher',
+ 'Lavazza' => 'lavazza',
+ 'Lay-Z-Spa Whirlpools' => 'lay-z-spa-whirlpools',
+ 'Lebensmittel' => 'lebensmittel',
+ 'Lebensmittel &amp; Haushalt' => 'food',
+ 'LED Lampen' => 'led-lampen',
+ 'LEGO' => 'lego',
+ 'LEGO Adventskalender' => 'lego-adventskalender',
+ 'LEGO Architecture' => 'lego-architecture',
+ 'LEGO Batman' => 'lego-batman',
+ 'LEGO City' => 'lego-city',
+ 'LEGO Creator' => 'lego-creator',
+ 'LEGO Dimensions' => 'lego-dimensions',
+ 'LEGO DUPLO' => 'lego-duplo',
+ 'LEGO Friends' => 'lego-friends',
+ 'LEGO Harry Potter' => 'lego-harry-potter',
+ 'LEGO Marvel Super Heroes' => 'lego-marvel-super-heroes',
+ 'LEGO Nexo Knights' => 'lego-nexo-knights',
+ 'LEGO NINJAGO' => 'lego-ninjago',
+ 'LEGO Star Wars' => 'lego-star-wars',
+ 'LEGO Star Wars Millennium Falcon' => 'lego-star-wars-millennium-falcon',
+ 'LEGO Super Mario' => 'lego-super-mario',
+ 'LEGO Technic' => 'lego-technic',
+ 'LEGO The Simpsons' => 'lego-simpsons',
+ 'Leifheit' => 'leifheit',
+ 'Lenovo' => 'lenovo',
+ 'Lenovo Laptops' => 'lenovo-laptop',
+ 'Lenovo Tablets' => 'lenovo-tablet',
+ 'Lenovo ThinkPad' => 'lenovo-thinkpad',
+ 'Lenovo Yoga' => 'lenovo-yoga',
+ 'Leonardo' => 'leonardo',
+ 'Leuchtmittel' => 'leuchten',
+ 'Levi&#039;s' => 'levis',
+ 'Lexar' => 'lexar',
+ 'Lexmark' => 'lexmark',
+ 'LG' => 'lg',
+ 'LG Fernseher' => 'lg-fernsher',
+ 'LG G5' => 'lg-g5',
+ 'LG G6' => 'lg-g6',
+ 'LG G7 ThinQ' => 'lg-g7-thinq',
+ 'LG OLED Fernseher' => 'lg-oled-tv',
+ 'LG Smartphones' => 'lg-smartphones',
+ 'LG V30' => 'lg-v30',
+ 'Lichterketten' => 'lichterketten',
+ 'Liebeskind' => 'liebeskind',
+ 'Lieferservice' => 'lieferservice',
+ 'Lindt' => 'lindt',
+ 'Lindt Adventskalender' => 'lindt-adventskalender',
+ 'Logitech' => 'logitech',
+ 'Logitech G413' => 'logitech-g413',
+ 'Logitech G430' => 'logitech-g430',
+ 'Logitech G502 Proteus Spectrum' => 'logitech-g502',
+ 'Logitech G513' => 'logitech-g513',
+ 'Logitech G533' => 'logitech-g533',
+ 'Logitech G633 Artemis Spectrum' => 'logitech-g633',
+ 'Logitech G703' => 'logitech-g703',
+ 'Logitech G903' => 'logitech-g903',
+ 'Logitech G910 Orion Spectrum' => 'logitech-g910',
+ 'Logitech G915' => 'logitech-g915',
+ 'Logitech G933 Artemis Spectrum' => 'logitech-g933',
+ 'Logitech Harmony' => 'logitech-harmony',
+ 'Logitech Mäuse' => 'logitech-maeuse',
+ 'Logitech MX Master' => 'logitech-mx-master',
+ 'Logitech MX Master 2S' => 'logitech-mx-master-2s',
+ 'Logitech Tastaturen' => 'logitech-tastaturen',
+ 'Logitech Z333' => 'logitech-z333',
+ 'Logitech Z337' => 'logitech-z337',
+ 'Logitech Z906' => 'logitech-z906',
+ 'Luftbefeuchter' => 'luftbefeuchter',
+ 'Luftentfeuchter' => 'luftentfeuchter',
+ 'Luftmatratzen' => 'luftmatratzen',
+ 'Luftreiniger' => 'luftreiniger',
+ 'Luigi&#039;s Mansion' => 'luigis-mansion',
+ 'Luigi&#039;s Mansion 3' => 'luigis-mansion-3',
+ 'Lustiges Taschenbuch' => 'lustiges-taschenbuch',
+ 'M.2 SSD' => 'm2-ssd',
+ 'MacBook' => 'macbook',
+ 'MacBook Air' => 'macbook-air',
+ 'MacBook Pro' => 'macbook-pro',
+ 'MacBook Pro 13' => 'macbook-pro-13',
+ 'MacBook Pro 15' => 'macbook-pro-15',
+ 'MacBook Pro 16' => 'macbook-pro-16',
+ 'Mac mini' => 'mac-mini',
+ 'Mac Software' => 'mac-software',
+ 'Madden NFL' => 'madden-nfl',
+ 'Magazine' => 'magazine',
+ 'Magnat' => 'magnat',
+ 'Magnum Eis' => 'magnum-eis',
+ 'Mähroboter' => 'maehroboter',
+ 'Mainboards' => 'mainboards',
+ 'Make Up Adventskalender' => 'make-up-adventskalender',
+ 'Makita' => 'makita',
+ 'Makita Akkuschrauber' => 'makita-akkuschrauber',
+ 'Malerwerkzeuge' => 'malerpinsel',
+ 'Mangas' => 'mangas',
+ 'Marantz' => 'marantz',
+ 'Mario Kart' => 'mario-kart',
+ 'Mario Kart 8 Deluxe' => 'mario-kart-8-deluxe',
+ 'Marken' => 'marken',
+ 'Marvel' => 'marvel',
+ 'Marvel&#039;s Spider-Man: Miles Morales' => 'marvels-spider-man-miles-morales',
+ 'Mass Effect' => 'mass-effect',
+ 'Mass Effect: Andromeda' => 'mass-effect-andromeda',
+ 'Massivholzmöbel' => 'massivholzmoebel',
+ 'Mastercard' => 'mastercard',
+ 'Matratzen' => 'matratzen',
+ 'Maxi Cosi' => 'maxi-cosi',
+ 'Mazda' => 'mazda',
+ 'Medion' => 'medion',
+ 'Mercedes-Benz' => 'mercedes-benz',
+ 'Mesh WLAN Router' => 'mesh-wlan-router',
+ 'Metabo' => 'metabo',
+ 'Metro (Spiel)' => 'metro',
+ 'Metro Exodus' => 'metro-exodus',
+ 'Michael Kors' => 'michael-kors',
+ 'microSD' => 'microsd',
+ 'microSDHC' => 'microsdhc',
+ 'microSDXC' => 'microsdxc',
+ 'Microsoft Flight Simulator' => 'microsoft-flight-simulator',
+ 'Microsoft Software' => 'microsoft-software',
+ 'Microsoft Surface Notebooks' => 'microsoft-surface-notebooks',
+ 'Microsoft Surface Pro 4' => 'surface-pro-4',
+ 'Microsoft Surface Pro 6' => 'surface-pro-6',
+ 'Microsoft Surface Pro 7' => 'microsoft-surface-pro-7',
+ 'Microsoft Surface Tablets' => 'microsoft-surface',
+ 'Miele' => 'miele',
+ 'Miele Geschirrspüler' => 'miele-geschirrspueler',
+ 'Miele Staubsauger' => 'miele-staubsauger',
+ 'Miele Waschmaschinen' => 'miele-waschmaschine',
+ 'Mietwagen' => 'mietwagen',
+ 'Mikrofone' => 'mikrofone',
+ 'Mikrowellen' => 'mikrowelle',
+ 'Milchaufschäumer' => 'milchaufschaeumer',
+ 'Milka' => 'milka',
+ 'Minecraft' => 'minecraft',
+ 'Mineralwasser' => 'mineralwasser',
+ 'Minions' => 'minions',
+ 'Mini PCs' => 'mini-pc',
+ 'Mitsubishi' => 'mitsubishi',
+ 'Mittelerde' => 'middle-earth',
+ 'Mittelerde: Mordors Schatten' => 'mittelerde-mordors-schatten',
+ 'Mittelerde: Schatten des Krieges' => 'mittelerde-schatten-des-krieges',
+ 'Mixer &amp; Rührer' => 'mixer',
+ 'Möbel' => 'moebel-deko',
+ 'Modellbau' => 'modellbau',
+ 'Modern wohnen' => 'modern-wohnen',
+ 'Monitore' => 'monitor',
+ 'Monkey 47' => 'monkey-47',
+ 'Monopoly' => 'monopoly',
+ 'Monster Hunter' => 'monster-hunter',
+ 'Monster Hunter: World' => 'monster-hunter-world',
+ 'Mortal Kombat' => 'mortal-kombat',
+ 'Mortal Kombat 11' => 'mortal-kombat-11',
+ 'Motorola' => 'motorola',
+ 'Motorola Smartphones' => 'motorola-smartphones',
+ 'Motorradbekleidung' => 'motorradbekleidung',
+ 'Motorradhelm' => 'motorradhelm',
+ 'Motorrad Zubehör' => 'motorrad',
+ 'Moto Z' => 'moto-z',
+ 'Mountainbikes' => 'mountainbikes',
+ 'MSI' => 'msi',
+ 'Mülleimer' => 'muelleimer',
+ 'Multifunktionsdrucker' => 'multifunktionsdrucker',
+ 'Multiroom Speaker' => 'multiroom',
+ 'Mund- &amp; Zahnpflege' => 'mund-zahnpflege',
+ 'Mundschutzmasken' => 'mundschutzmasken',
+ 'Museums-Tickets' => 'museum',
+ 'Musical Tickets' => 'musical',
+ 'Musik' => 'musik',
+ 'Musik Apps' => 'musik-apps',
+ 'Musikinstrumente' => 'musikinstrumente',
+ 'Musik Streaming' => 'musik-streaming',
+ 'Müsli' => 'muesli',
+ 'Mustang' => 'mustang',
+ 'Mützen' => 'muetzen',
+ 'Nachtwäsche' => 'nachtwaesche',
+ 'Nähbedarf' => 'naehen',
+ 'Nähmaschinen' => 'naehmaschine',
+ 'Nahrungsergänzungsmittel' => 'nahrungsergaenzungsmittel',
+ 'Nahverkehr' => 'nahverkehr',
+ 'Naketano' => 'naketano',
+ 'NAS' => 'nas',
+ 'Nassrasierer' => 'rasierer',
+ 'Navigationsgeräte' => 'navigationsgeraete',
+ 'Neato' => 'neato',
+ 'Neato Robotics Botvac D7 Connected' => 'neato-botvac-d7',
+ 'Need for Speed' => 'need-for-speed',
+ 'Need for Speed Heat' => 'need-for-speed-heat',
+ 'Need for Speed Payback' => 'need-for-speed-payback',
+ 'Nerf' => 'nerf',
+ 'Nescafé' => 'nescafe',
+ 'Nespresso' => 'nespresso',
+ 'Nespresso Kaffeemaschinen' => 'nespresso-kaffeemaschinen',
+ 'Netflix' => 'netflix',
+ 'NETGEAR' => 'netgear',
+ 'NETGEAR Nighthawk' => 'netgear-nighthawk',
+ 'NETGEAR Orbi' => 'netgear-orbi',
+ 'NETGEAR Router' => 'netgear-router',
+ 'Netzteile' => 'netzteile',
+ 'Netzwerk' => 'netzwerk',
+ 'New Balance' => 'new-balance',
+ 'Nike' => 'nike',
+ 'Nike Air Force 1' => 'nike-air-force',
+ 'Nike Air Max' => 'nike-air-max',
+ 'Nike Air Max 270' => 'nike-air-max-270',
+ 'Nike Air Max 720' => 'nike-air-max-720',
+ 'Nike Air Max Thea' => 'nike-air-max-thea',
+ 'Nike Air Presto' => 'nike-presto',
+ 'Nike Free' => 'nike-free',
+ 'Nike Huarache' => 'nike-huarache',
+ 'Nike Roshe Run' => 'nike-roshe-run',
+ 'Nike Schuhe' => 'nike-schuhe',
+ 'Nikon' => 'nikon',
+ 'Nikon DSLR' => 'nikon-dslr',
+ 'Ni No Kuni' => 'ni-no-kuni',
+ 'Ni No Kuni: Der Fluch der Weißen Königin' => 'ni-no-kuni-der-fluch-der-weissen-koenigin',
+ 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-ii',
+ 'Nintendo' => 'nintendo',
+ 'Nintendo 2DS Konsolen' => 'nintendo-2ds',
+ 'Nintendo 3DS Konsolen' => 'nintendo-3ds',
+ 'Nintendo 3DS Spiele' => 'nintendo-3ds-spiele',
+ 'Nintendo 3DS Zubehör' => 'nintendo-3ds-zubehoer',
+ 'Nintendo Classic Mini NES Konsolen' => 'nintendo-classic-mini-nes',
+ 'Nintendo Classic Mini SNES Konsolen' => 'nintendo-classic-mini-snes',
+ 'Nintendo eShop Guthaben' => 'nintendo-eshop-guthaben',
+ 'Nintendo Switch Controller' => 'nintendo-switch-controller',
+ 'Nintendo Switch Konsolen' => 'nintendo-switch',
+ 'Nintendo Switch Lite Konsolen' => 'nintendo-switch-lite',
+ 'Nintendo Switch Pro Controller' => 'nintendo-switch-pro-controller',
+ 'Nintendo Switch Spiele' => 'nintendo-switch-spiele',
+ 'Nintendo Switch Zubehör' => 'nintendo-switch-zubehoer',
+ 'Nintendo Zubehör' => 'nintendo-zubehoer',
+ 'Nissan' => 'nissan',
+ 'Nivea' => 'nivea',
+ 'Nokia' => 'nokia',
+ 'Nokia Handys' => 'nokia-handys',
+ 'Nudeln' => 'nudeln',
+ 'Nuki Smart Locks' => 'nuki-smart-lock',
+ 'Nüsse' => 'nuesse',
+ 'Nutella' => 'nutella',
+ 'Nvidia' => 'nvidia',
+ 'Nvidia GeForce' => 'nvidia-geforce',
+ 'Nvidia SHIELD TV' => 'nvidia-shield',
+ 'o2' => 'o2-netz',
+ 'Objektive' => 'objektiv',
+ 'Obst' => 'obst',
+ 'Obst &amp; Gemüse' => 'obst-gemuese',
+ 'Oculus Quest' => 'oculus-quest',
+ 'Oculus Rift' => 'oculus-rift',
+ 'Office Programme' => 'office-programme',
+ 'OLED Fernseher' => 'oled-fernseher',
+ 'Olympus' => 'olympus',
+ 'On-Ear Kopfhörer' => 'on-ear-kopfhoerer',
+ 'OnePlus 3' => 'oneplus-3',
+ 'OnePlus 5' => 'oneplus-5',
+ 'OnePlus 6' => 'oneplus-6',
+ 'OnePlus 7' => 'oneplus-7',
+ 'OnePlus 7 Pro' => 'oneplus-7-pro',
+ 'OnePlus 7T' => 'oneplus-7t',
+ 'OnePlus 7T Pro' => 'oneplus-7t-pro',
+ 'OnePlus 8' => 'oneplus-8',
+ 'OnePlus 8 Pro' => 'one-plus-8-pro',
+ 'OnePlus 8T' => 'oneplus-8t',
+ 'OnePlus Nord' => 'oneplus-nord',
+ 'OnePlus Smartphones' => 'oneplus-smartphones',
+ 'Onkyo' => 'onkyo',
+ 'Opel' => 'opel',
+ 'OPPO Find X2 Lite' => 'oppo-find-x2-lite',
+ 'OPPO Find X2 Neo' => 'oppo-find-x2-neo',
+ 'OPPO Find X2 Pro' => 'oppo-find-x2-pro',
+ 'OPPO Reno2' => 'oppo-reno2',
+ 'OPPO Reno2 Z' => 'oppo-reno2-z',
+ 'OPPO Reno4 5G' => 'oppo-reno4-5g',
+ 'OPPO Reno4 Pro 5G' => 'oppo-reno4-pro-5g',
+ 'OPPO Reno4 Z 5G' => 'oppo-reno4-z-5g',
+ 'OPPO Smartphones' => 'oppo-smartphones',
+ 'Oral-B' => 'oral-b',
+ 'Oral-B Elektrische Zahnbürsten' => 'oral-b-elektrische-zahnbuersten',
+ 'Origin' => 'origin',
+ 'Osram' => 'osram',
+ 'Osram Smart+' => 'osram-smart-plus',
+ 'Osterdeko' => 'osterdeko',
+ 'Outdoor &amp; Camping' => 'outdoor',
+ 'Outdoorbekleidung' => 'outdoorbekleidung',
+ 'Outdoorjacken' => 'outdoorjacken',
+ 'Outdoor Spielzeuge' => 'outdoor-spielzeug',
+ 'Over-Ear Kopfhörer' => 'over-ear-kopfhoerer',
+ 'Pampers' => 'pampers',
+ 'Panama Jack' => 'panama-jack',
+ 'Panasonic' => 'panasonic',
+ 'Panasonic Fernseher' => 'panasonic-fernseher',
+ 'Panasonic Kameras' => 'panasonic-kameras',
+ 'Panasonic Lumix' => 'panasonic-lumix',
+ 'Paper Mario: The Origami King' => 'paper-mario-the-origami-king',
+ 'Papiertapete' => 'papiertapete',
+ 'Parfum' => 'parfum',
+ 'Parfum Damen' => 'parfum-damen',
+ 'Parfum Herren' => 'parfum-herren',
+ 'Pauschalreisen' => 'pauschalreise',
+ 'Pavillons' => 'pavillons',
+ 'Paw Patrol' => 'paw-patrol',
+ 'PAYBACK' => 'payback',
+ 'Payday' => 'payday',
+ 'Payday 2' => 'payday-2',
+ 'paydirekt' => 'paydirekt',
+ 'PC Gaming Systeme' => 'pc-gaming-systeme',
+ 'PC Gaming Zubehör' => 'pc-gaming-zubehoer',
+ 'PC Gehäuse' => 'pc-gehaeuse',
+ 'PC Komponenten' => 'hardware',
+ 'PC Lautsprecher' => 'pc-lautsprecher',
+ 'PC Mäuse' => 'pc-maus',
+ 'PC Spiele' => 'pc-spiele',
+ 'PC Zubehör' => 'pc-zubehoer',
+ 'Pendelleuchten' => 'pendelleuchten',
+ 'Pentax' => 'pentax',
+ 'Pepe Jeans' => 'pepe-jeans',
+ 'Peppa Wutz' => 'peppa-wutz',
+ 'PepperBonus' => 'pepperbonus',
+ 'Pestos' => 'pestos',
+ 'Peugeot' => 'peugeot',
+ 'Pfannen' => 'pfannen',
+ 'Pflanzen' => 'pflanzen',
+ 'Philips' => 'philips',
+ 'Philips Fernseher' => 'philips-fernseher',
+ 'Philips Hue' => 'philips-hue',
+ 'Philips Hue E14' => 'philips-hue-e14',
+ 'Philips Hue E27' => 'philips-hue-e27',
+ 'Philips Hue Go' => 'philips-hue-go',
+ 'Philips Hue GU10' => 'philips-hue-gu10',
+ 'Philips Hue LightStrip' => 'philips-hue-lightstrip',
+ 'Philips Hue Play Gradient LightStrip' => 'philips-hue-play-gradient-lightstrip',
+ 'Philips Hue Play HDMI Sync Box' => 'philips-hue-play-hdmi-sync-box',
+ 'Philips Hue Play Lightbar' => 'philips-hue-play',
+ 'Philips OneBlade' => 'philips-oneblade',
+ 'Philips Rasierer' => 'philips-rasierer',
+ 'Philips Sonicare' => 'philips-sonicare',
+ 'Philips Staubsauger' => 'philips-staubsauger',
+ 'Philips Wecker' => 'philips-wecker',
+ 'Photoshop' => 'photoshop',
+ 'Pioneer' => 'pioneer',
+ 'Pizza' => 'pizza',
+ 'Plattenspieler' => 'plattenspieler',
+ 'Playboy' => 'playboy',
+ 'Playerunknown&#039;s Battlegrounds' => 'playerunknowns-battlegrounds',
+ 'PLAYMOBIL' => 'playmobil',
+ 'PLAYMOBIL Adventskalender' => 'playmobil-adventskalender',
+ 'PlayStation' => 'playstation',
+ 'PlayStation 4 Controller' => 'playstation-4-controller',
+ 'PlayStation 4 Konsolen' => 'playstation-4',
+ 'PlayStation 4 Pro Konsolen' => 'playstation-4-pro',
+ 'PlayStation 4 Spiele' => 'playstation-4-spiele',
+ 'PlayStation 5 Konsolen' => 'playstation-5',
+ 'PlayStation 5 Spiele' => 'playstation-5-spiele',
+ 'PlayStation Classic Konsolen' => 'playstation-classic',
+ 'PlayStation Now' => 'playstation-now',
+ 'PlayStation Plus' => 'playstation-plus',
+ 'PlayStation Zubehör' => 'playstation-zubehoer',
+ 'Plüschtiere' => 'plueschtiere',
+ 'Plus Size Mode' => 'plus-size-mode',
+ 'POCO F2 Pro' => 'poco-f2-pro',
+ 'POCO X3' => 'poco-x3',
+ 'Pokémon' => 'pokemon',
+ 'Pokémon: Let&#039;s Go' => 'pokemon-lets-go',
+ 'Pokémon Schwert und Schild' => 'pokemon-schwert-schild',
+ 'Pokémon Tekken' => 'pokemon-tekken',
+ 'Pokémon Ultrasonne &amp; Ultramond' => 'pokemon-ultrasonne-ultramond',
+ 'Poloshirts' => 'poloshirts',
+ 'Polsterbetten' => 'polsterbetten',
+ 'Polyrattan Möbel' => 'polyrattan',
+ 'Pools' => 'pools',
+ 'Powerbanks' => 'powerbanks',
+ 'Powerbeats Pro' => 'powerbeats',
+ 'Preisfehler' => 'preisfehler',
+ 'Prepaid-Tarife' => 'prepaid-tarife',
+ 'Prime Gaming' => 'twitch-prime',
+ 'Pro Evolution Soccer' => 'pro-evolution-soccer',
+ 'Pro Evolution Soccer 2018' => 'pes-2018',
+ 'Pro Evolution Soccer 2019' => 'pes-2019',
+ 'Pro Evolution Soccer 2020' => 'pes-2020',
+ 'Proteine' => 'whey-proteine',
+ 'Prozessoren' => 'prozessoren',
+ 'PSN Guthaben' => 'psn-guthaben',
+ 'Puky' => 'puky',
+ 'Pullover' => 'pullover',
+ 'PUMA' => 'puma',
+ 'Pumps' => 'pumps',
+ 'Puppen' => 'puppen',
+ 'Puppenhäuser' => 'puppenhaeuser',
+ 'Puzzles' => 'puzzle',
+ 'Qeridoo' => 'qeridoo',
+ 'Qeridoo Fahrradanhänger' => 'qeridoo-fahrradanhaenger',
+ 'Qeridoo KidGoo 2' => 'qeridoo-kidgoo-2',
+ 'Qeridoo Sportrex 2' => 'qeridoo-sportrex-2',
+ 'Quiksilver' => 'quiksilver',
+ 'Raclettes' => 'raclettes',
+ 'Radios' => 'radios',
+ 'Radsport' => 'radsport',
+ 'Rasenmäher' => 'rasenmaeher',
+ 'Rasentrimmer' => 'rasentrimmer',
+ 'Rasierklingen' => 'rasierklingen',
+ 'Raspberry Pi' => 'raspberry-pi',
+ 'Rasur, Enthaarung &amp; Trimmen' => 'rasur-enthaarung',
+ 'Rauchmelder' => 'rauchmelder',
+ 'Ravensburger' => 'ravensburger',
+ 'Ray-Ban' => 'ray-ban',
+ 'Razer DeathAdder' => 'razer-deathadder',
+ 'RC Autos' => 'rc-autos',
+ 'Red Bull' => 'red-bull',
+ 'Red Dead Redemption' => 'red-dead-redemption',
+ 'Red Dead Redemption 2' => 'red-dead-redemption-2',
+ 'Reebok' => 'reebok',
+ 'Regale' => 'regale',
+ 'Reifen' => 'reifen',
+ 'Reinigungsmittel' => 'reinigungsmittel',
+ 'Reise Apps' => 'reise-apps',
+ 'Reisen' => 'reisen',
+ 'Reiskocher' => 'reiskocher',
+ 'Remington' => 'remington',
+ 'Renault' => 'renault',
+ 'Rennräder' => 'rennraeder',
+ 'Repeater' => 'repeater',
+ 'Resident Evil' => 'resident-evil',
+ 'Resident Evil 2' => 'resident-evil-2',
+ 'Resident Evil 7' => 'resident-evil-7',
+ 'Restaurant' => 'restaurant',
+ 'Retro Stil' => 'retro-stil',
+ 'Rimowa' => 'rimowa',
+ 'Ring Fit Adventure' => 'ring-fit-adventure',
+ 'Rituals' => 'rituals',
+ 'Rituals Adventskalender' => 'rituals-adventskalender',
+ 'Roborock' => 'xiaomi-roborock',
+ 'Roborock S5 Max' => 'roborock-s5-max',
+ 'Roborock S6' => 'roborock-s6',
+ 'Roborock S6 MaxV' => 'roborock-s6-maxv',
+ 'ROCCAT' => 'roccat',
+ 'ROCCAT Tyon' => 'roccat-tyon',
+ 'Röcke' => 'roecke',
+ 'Rocket League' => 'rocket-league',
+ 'Roidmi Staubsauger' => 'roidmi-staubsauger',
+ 'Rollei' => 'rollei',
+ 'Rösle' => 'roesle',
+ 'Router' => 'router',
+ 'Roxy' => 'roxy',
+ 'RTX 2060' => 'rtx-2060',
+ 'RTX 2070' => 'rtx-2070',
+ 'RTX 2080' => 'rtx-2080',
+ 'RTX 2080 Ti' => 'rtx-2080-ti',
+ 'RTX 3070' => 'rtx-3070',
+ 'RTX 3080' => 'rtx-3080',
+ 'RTX 3090' => 'rtx-3090',
+ 'Rucksäcke' => 'rucksaecke',
+ 'Russell Hobbs' => 'russell-hobbs',
+ 'RX 480' => 'rx-480',
+ 'RX 570' => 'rx-570',
+ 'RX 580' => 'rx-580',
+ 'RX 590' => 'rx-590',
+ 'RX 5700 XT' => 'rx-5700-xt',
+ 'RX 6800' => 'rx-6800',
+ 'RX 6800 XT' => 'rx-6800-xt',
+ 'RX 6900 XT' => 'rx-6900-xt',
+ 'RX Vega 56' => 'rx-vega-56',
+ 'RX Vega 64' => 'rx-vega-64',
+ 'Sägen' => 'saegen',
+ 'Salomon' => 'salomon',
+ 'Samsonite' => 'samsonite',
+ 'Samsung' => 'samsung',
+ 'Samsung Fernseher' => 'samsung-fernseher',
+ 'Samsung Galaxy A7' => 'samsung-galaxy-a7',
+ 'Samsung Galaxy A8' => 'samsung-galaxy-a8',
+ 'Samsung Galaxy A51' => 'samsung-galaxy-a51',
+ 'Samsung Galaxy A71' => 'samsung-galaxy-a71',
+ 'Samsung Galaxy Buds' => 'samsung-galaxy-buds',
+ 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus',
+ 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live',
+ 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro',
+ 'Samsung Galaxy Note9' => 'samsung-galaxy-note-9',
+ 'Samsung Galaxy Note20' => 'samsung-galaxy-note20',
+ 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note20-ultra',
+ 'Samsung Galaxy S7' => 'samsung-galaxy-s7',
+ 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge',
+ 'Samsung Galaxy S8' => 'samsung-galaxy-s8',
+ 'Samsung Galaxy S8+' => 'samsung-galaxy-s8-plus',
+ 'Samsung Galaxy S9' => 'samsung-galaxy-s9',
+ 'Samsung Galaxy S9+' => 'samsung-galaxy-s9-plus',
+ 'Samsung Galaxy S10' => 'samsung-galaxy-s10',
+ 'Samsung Galaxy S10+' => 'samsung-galaxy-s10-plus',
+ 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e',
+ 'Samsung Galaxy S20' => 'samsung-galaxy-s20',
+ 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe',
+ 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra',
+ 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus',
+ 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g',
+ 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g',
+ 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g',
+ 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4',
+ 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6',
+ 'Samsung Galaxy Watch' => 'samsung-galaxy-watch',
+ 'Samsung Galaxy Watch Active2' => 'samsung-galaxy-watch-active-2',
+ 'Samsung Gear' => 'samsung-gear',
+ 'Samsung Gear S3' => 'samsung-gear-s3',
+ 'Samsung Gear VR' => 'samsung-gear-vr',
+ 'Samsung Kopfhörer' => 'samsung-kopfhoerer',
+ 'Samsung Kühlschränke' => 'samsung-kuehlschrank',
+ 'Samsung Monitore' => 'samsung-monitor',
+ 'Samsung QLED Fernseher' => 'samsung-qled-fernseher',
+ 'Samsung Smartphones' => 'samsung-smartphone',
+ 'Samsung SSD' => 'samsung-ssd',
+ 'Samsung Tablets' => 'samsung-tablet',
+ 'Samsung The Frame Fernseher' => 'samsung-the-frame-fernseher',
+ 'Samsung Waschmaschinen' => 'samsung-waschmaschine',
+ 'Sandalen' => 'sandalen',
+ 'SanDisk' => 'sandisk',
+ 'SanDisk SSD' => 'sandisk-ssd',
+ 'Sanitär &amp; Armaturen' => 'sanitaer-armaturen',
+ 'Saucen' => 'saucen',
+ 'Saugroboter' => 'saugroboter',
+ 'Scanner' => 'scanner',
+ 'Schallplatten' => 'schallplatten',
+ 'Scheppach' => 'scheppach',
+ 'Schlafsäcke' => 'schlafsack',
+ 'Schlafsofas' => 'schlafsofas',
+ 'Schlafzimmer' => 'schlafzimmer',
+ 'Schlagschrauber' => 'schlagschrauber',
+ 'Schlauchboote' => 'schlauchboote',
+ 'Schleich' => 'schleich',
+ 'Schlitten' => 'schlitten',
+ 'Schmuck' => 'schmuck',
+ 'Schneefräsen' => 'schneefraesen',
+ 'Schnellkochtöpfe' => 'schnellkochtoepfe',
+ 'Schnürhalbschuhe' => 'schnuerhalbschuhe',
+ 'Schokolade' => 'schokolade',
+ 'Schraubendreher' => 'schraubendreher',
+ 'Schreibgeräte' => 'schreibgeraete',
+ 'Schreibtische' => 'schreibtisch',
+ 'Schuhe' => 'schuhe',
+ 'Schuhschränke' => 'schuhschraenke',
+ 'Schulbedarf' => 'schulbedarf',
+ 'Schulranzen' => 'schulranzen',
+ 'Schutzfolien' => 'schutzfolien',
+ 'Schwangerschaft' => 'schwangerschaft',
+ 'Schwerlastregale' => 'schwerlastregale',
+ 'Scooter' => 'scooter',
+ 'Scotch Whisky' => 'scotch-whisky',
+ 'SDHC Speicherkarten' => 'sdhc-speicherkarten',
+ 'SD Karten' => 'sd-karten',
+ 'Seagate' => 'seagate',
+ 'Sea of Thieves' => 'sea-of-thieves',
+ 'Seat' => 'seat',
+ 'Sega Mega Drive Mini Konsolen' => 'sega-mega-drive-mini',
+ 'Seidensticker' => 'seidensticker',
+ 'Sekiro: Shadows Die Twice' => 'sekiro',
+ 'Senf' => 'senf',
+ 'Sennheiser' => 'sennheiser',
+ 'Senseo' => 'senseo',
+ 'Service-Verträge' => 'service-vertraege',
+ 'Sessel' => 'sessel',
+ 'Sextoys' => 'sextoys',
+ 'Shadow of the Tomb Raider' => 'shadow-of-the-tomb-raider',
+ 'Shampoo' => 'shampoo',
+ 'Sharkoon' => 'sharkoon',
+ 'Sharp' => 'sharp',
+ 'Shenmue' => 'shenmue',
+ 'Shenmue I &amp; II' => 'shenmue-i-ii',
+ 'Shenmue III' => 'shenmue-iii',
+ 'Shishas' => 'shishas',
+ 'Shishas &amp; Zubehör' => 'shishas-zubehoer',
+ 'Shoop' => 'shoop',
+ 'Shops: Erfahrungen' => 'shops',
+ 'Shorts' => 'shorts',
+ 'Sicherheitstechnik' => 'sicherheitstechnik',
+ 'Side-by-Side-Kühlschränke' => 'side-by-side-kuehlschrank',
+ 'Sid Meier&#039;s Civilization VI' => 'sid-meiers-civilization-vi',
+ 'Sid Meier’s Civilization' => 'sid-meiers-civilization',
+ 'Siemens' => 'siemens',
+ 'Siemens Geschirrspüler' => 'siemens-geschirrspueler',
+ 'Siemens Kühlschränke' => 'siemens-kuehlschrank',
+ 'Siemens Waschmaschinen' => 'siemens-waschmaschine',
+ 'Silit' => 'silit',
+ 'Skandi Stil' => 'skandi-stil',
+ 'Skateboards' => 'skateboard',
+ 'Skaten' => 'skaten',
+ 'Ski &amp; Snowboard' => 'snowboard',
+ 'Skoda' => 'skoda',
+ 'Sky' => 'sky',
+ 'Sky Ticket' => 'sky-ticket',
+ 'Smarte Beleuchtung' => 'smarte-beleuchtung',
+ 'Smarte Wecker' => 'smarte-wecker',
+ 'Smart Home' => 'smart-home',
+ 'Smart Home Steckdosen' => 'smart-home-steckdosen',
+ 'Smart Locks' => 'smart-lock',
+ 'Smartphones' => 'smartphone',
+ 'Smartphones unter 200€' => 'smartphones-unter-200-euro',
+ 'Smart Speaker' => 'smart-speaker',
+ 'Smart Tech &amp; Gadgets' => 'smart-tech',
+ 'Smartwatches' => 'smartwatch',
+ 'Smoothie Maker' => 'smoothie-maker',
+ 'Snacks &amp; Knabberzeug' => 'snacks-knabberzeug',
+ 'Sneakers' => 'sneaker',
+ 'Socken' => 'socken',
+ 'SodaStream' => 'sodastream',
+ 'Sofas' => 'sofa',
+ 'Sofortbildkameras' => 'sofortbildkameras',
+ 'Softdrinks' => 'softdrinks',
+ 'Software' => 'software',
+ 'Software &amp; Apps' => 'apps-software',
+ 'Solarleuchten' => 'solarleuchten',
+ 'Somat' => 'somat',
+ 'Sommerreifen' => 'sommerreifen',
+ 'Sonnenbrillen' => 'sonnenbrillen',
+ 'Sonnencreme' => 'sonnencreme',
+ 'Sonnenpflege' => 'sonnenpflege',
+ 'Sonnenschirme' => 'sonnenschirme',
+ 'Sonoff' => 'sonoff',
+ 'Sonos' => 'sonos',
+ 'Sonos Beam' => 'sonos-beam',
+ 'Sonos Move' => 'sonos-move',
+ 'Sonos One' => 'sonos-one',
+ 'Sonos PLAY:1' => 'sonos-play-1',
+ 'Sonos PLAY:3' => 'sonos-play-3',
+ 'Sonos Play:5 (Five)' => 'sonos-play-5',
+ 'Sonos Playbar' => 'sonos-playbar',
+ 'Sonos Playbase' => 'sonos-playbase',
+ 'Sonstiges' => 'diverses',
+ 'Sony' => 'sony',
+ 'Sony Alpha 7' => 'sony-alpha-7',
+ 'Sony Alpha 7 II' => 'sony-alpha-7-ii',
+ 'Sony Alpha 7 III' => 'sony-alpha-7-iii',
+ 'Sony Alpha 6000' => 'sony-alpha-6000',
+ 'Sony Alpha 6300' => 'sony-alpha-6300',
+ 'Sony Alpha 6400' => 'sony-alpha-6400',
+ 'Sony Alpha 6500' => 'sony-alpha-6500',
+ 'Sony DualSense Wireless-Controller' => 'playstation-5-controller',
+ 'Sony Fernseher' => 'sony-fernseher',
+ 'Sony Kameras' => 'sony-kameras',
+ 'Sony Kopfhörer' => 'sony-kopfhoerer',
+ 'Sony PlayStation VR' => 'sony-playstation-vr',
+ 'Sony PULSE 3D Wireless Headset' => 'sony-pulse-3d-wireless-headset',
+ 'Sony WF-1000XM3' => 'sony-wf-1000xm3',
+ 'Sony WH-1000XM3' => 'sony-wh-1000xm3',
+ 'Sony WH-1000XM4' => 'sony-wh-1000xm4',
+ 'Sony Xperia' => 'sony-xperia',
+ 'Sony Xperia X' => 'sony-xperia-x',
+ 'Sony Xperia XA' => 'sony-xperia-xa',
+ 'Sony Xperia XZ' => 'sony-xperia-xz',
+ 'Soundbar' => 'soundbar',
+ 'Soundbase' => 'soundbase',
+ 'Soundkarten' => 'soundkarten',
+ 'South Park: Die rektakuläre Zerreißprobe' => 'south-park-die-rektakulaere-zerreissprobe',
+ 'Spartipps' => 'spartipps',
+ 'Speicherkarten' => 'speicherkarten',
+ 'Speichermedien' => 'speichermedien',
+ 'Speiseöle' => 'speiseoele',
+ 'Spiegelreflexkameras' => 'spiegelreflexkamera',
+ 'Spiele &amp; Brettspiele' => 'spiele-brettspiele',
+ 'Spiele Apps' => 'spiele-apps',
+ 'Spielekonsolen' => 'spielekonsolen',
+ 'Spielfiguren &amp; Spielsets' => 'spielfiguren-spielsets',
+ 'Spielzeuge' => 'spielzeug',
+ 'Spirituosen' => 'spirituosen',
+ 'Sport &amp; Outdoor' => 'sport',
+ 'Sportbekleidung' => 'sportbekleidung',
+ 'Sport Bild' => 'sport-bild',
+ 'Sportnahrung' => 'sportlernahrung',
+ 'Sporttasche' => 'sporttasche',
+ 'Spotify' => 'spotify',
+ 'Spülmaschinentabs' => 'spuelmaschinentabs',
+ 'Spyro Reignited Trilogy' => 'spyro-reignited-trilogy',
+ 'SSD' => 'ssd',
+ 'Stabmixer' => 'stabmixer',
+ 'Städtereisen' => 'staedtereise',
+ 'Standmixer' => 'standmixer',
+ 'Star Trek' => 'star-trek',
+ 'Star Wars' => 'star-wars',
+ 'Star Wars: Battlefront 2' => 'star-wars-battlefront-2',
+ 'Star Wars: Squadrons' => 'star-wars-squadrons',
+ 'Star Wars Battlefront' => 'star-wars-battlefront',
+ 'Star Wars Jedi: Fallen Order' => 'star-wars-jedi-fallen-order',
+ 'Staubsauger' => 'staubsauger',
+ 'Staubsaugerbeutel' => 'staubsaugerbeutel',
+ 'Staubsauger ohne Beutel' => 'staubsauger-ohne-beutel',
+ 'Steam' => 'steam',
+ 'Steckschlüssel' => 'steckschluessel',
+ 'SteelSeries' => 'steelseries',
+ 'Stehlampen' => 'stehlampen',
+ 'Steiff' => 'steiff',
+ 'Stern (Magazin)' => 'stern-magazin',
+ 'Stichsägen' => 'stichsaegen',
+ 'Stiefel' => 'stiefel',
+ 'Stiefeletten' => 'stiefeletten',
+ 'Stiftung Warentest' => 'stiftung-warentest-magazin',
+ 'Streaming-Dienste' => 'streaming-dienste',
+ 'Streaming Lautsprecher' => 'streaming-lautsprecher',
+ 'Strom &amp; Gas' => 'strom-gas',
+ 'Stromtarif' => 'stromtarif',
+ 'Studentenrabatte' => 'studentenrabatte',
+ 'Stühle' => 'stuehle',
+ 'Subwoofer' => 'subwoofer',
+ 'SUP Boards' => 'sup-boards',
+ 'Superdry' => 'superdry',
+ 'Super Mario' => 'super-mario',
+ 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars',
+ 'Super Mario Maker 2' => 'super-mario-maker-2',
+ 'Super Mario Odyssey' => 'super-mario-odyssey',
+ 'Super Mario Party' => 'super-mario-party',
+ 'Supermarkt' => 'supermarkt',
+ 'Super Smash Bros. Ultimate' => 'super-smash-bros-ultimate',
+ 'Süßigkeiten' => 'suessigkeiten',
+ 'Synology' => 'synology',
+ 'Syoss' => 'syoss',
+ 'Systemkameras' => 'systemkamera',
+ 'T-Shirts' => 't-shirts',
+ 'Tablets' => 'tablet',
+ 'Tablet Zubehör' => 'tablet-zubehoer',
+ 'tado° Smartes Heizkörper-Thermostat' => 'tado-smartes-thermostat',
+ 'Tamaris' => 'tamaris',
+ 'Tangle Teezer' => 'tangle-teezer',
+ 'Tanqueray' => 'tanqueray',
+ 'Tapeten' => 'tapeten',
+ 'Taschen' => 'taschen',
+ 'Taschenlampen' => 'taschenlampen',
+ 'Taschentücher' => 'taschentuecher',
+ 'Tassimo' => 'tassimo',
+ 'Tassimo Kaffeemaschinen' => 'tassimo-kaffeemaschinen',
+ 'Tastaturen' => 'tastatur',
+ 'TCL Fernseher' => 'tcl-fernseher',
+ 'Team Sonic Racing' => 'team-sonic-racing',
+ 'Teamsport' => 'teamsport',
+ 'Tee' => 'tee',
+ 'Tefal' => 'tefal',
+ 'Tefal OptiGrills' => 'tefal-optigrill',
+ 'Tefal Pfannen' => 'tefal-pfannen',
+ 'Tekken' => 'tekken',
+ 'Tekken 7' => 'tekken-7',
+ 'Telefon- &amp; Internet-Verträge' => 'telefon-internet',
+ 'Telefone &amp; Zubehör' => 'handy-smartphone',
+ 'Telekom' => 'telekom-net',
+ 'Telekom Magenta' => 'telekom-magenta',
+ 'Telekom SmartHome' => 'telekom-smarthome',
+ 'Teppiche' => 'teppiche',
+ 'Tesla' => 'tesla',
+ 'Tetris' => 'tetris',
+ 'Teufel' => 'teufel',
+ 'The Elder Scrolls' => 'the-elder-scrolls',
+ 'The Elder Scrolls V: Skyrim' => 'skyrim',
+ 'The Evil Within' => 'the-evil-within',
+ 'The Evil Within 2' => 'the-evil-within-2',
+ 'The Last of Us' => 'the-last-of-us',
+ 'The Last of Us Part II' => 'the-last-of-us-part-ii',
+ 'The Legend of Zelda' => 'the-legend-of-zelda',
+ 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild',
+ 'The Legend of Zelda: Link&#039;s Awakening' => 'zelda-links-awakening',
+ 'The Legend of Zelda: Skyward Sword HD' => 'zelda-skyward-sword-hd',
+ 'The North Face' => 'the-north-face',
+ 'The Outer Worlds' => 'the-outer-worlds',
+ 'Thermosflaschen' => 'thermosflaschen',
+ 'Thermoskannen' => 'thermoskanne',
+ 'The Witcher' => 'the-witcher',
+ 'The Witcher 3' => 'the-witcher-3',
+ 'Thule' => 'thule',
+ 'Thule Chariot Fahrradanhänger' => 'thule-chariot-fahrradanhaenger',
+ 'Thule Dachboxen' => 'thule-dachboxen',
+ 'Thule Fahrradträger' => 'thule-fahrradtraeger',
+ 'Tickets &amp; Shows' => 'erlebnisse',
+ 'Tiefkühlkost' => 'tiefkuehkost',
+ 'Timberland' => 'timberland',
+ 'Tintenstrahldrucker' => 'tintenstrahldrucker',
+ 'Tischlampen' => 'tischlampen',
+ 'Tischtennis' => 'tischtennis',
+ 'Tischtennisplatten' => 'tischtennisplatten',
+ 'Tischtennisschläger' => 'tischtennisschlaeger',
+ 'Toaster' => 'toaster',
+ 'Toilettenpapier' => 'toilettenpapier',
+ 'tolino' => 'tolino',
+ 'Tomb Raider' => 'tomb-raider',
+ 'Tom Clancy&#039;s' => 'tom-clancys',
+ 'Tom Clancy&#039;s: Ghost Recon Wildlands' => 'tom-clancys-ghost-recon-wildlands',
+ 'Tom Clancy&#039;s Ghost Recon Breakpoint' => 'tom-clancys-ghost-recon-breakpoint',
+ 'Tom Clancy&#039;s The Division 2' => 'tom-clancy-the-division-2',
+ 'Tommy Hilfiger' => 'tommy-hilfiger',
+ 'TOM TAILOR' => 'tom-tailor',
+ 'Toner' => 'toner',
+ 'Tonic Water' => 'tonic-water',
+ 'Toniebox' => 'toniebox',
+ 'Tonies Figuren' => 'tonie-figuren',
+ 'Töpfe' => 'toepfe',
+ 'Töpfe &amp; Pfannen' => 'kochen',
+ 'Toplader' => 'toplader',
+ 'Toshiba' => 'toshiba',
+ 'Total War' => 'total-war',
+ 'Toyota' => 'toyota',
+ 'TP-Link' => 'tp-link',
+ 'TP-Link Router' => 'tp-link-router',
+ 'Trampoline' => 'trampolin',
+ 'TREKSTOR' => 'trekstor',
+ 'Trockner' => 'trockner',
+ 'Tropical Islands' => 'tropical-island',
+ 'Tropico' => 'tropico',
+ 'Tropico 5' => 'tropico-5',
+ 'Tropico 6' => 'tropico-6',
+ 'TV &amp; Video' => 'tv-video',
+ 'TV Boxen' => 'tv-box',
+ 'TV Spielfilm' => 'tv-spielfilm',
+ 'TV Wandhalterungen' => 'tv-wandhalterung',
+ 'TV Zubehör' => 'tv-zubehoer',
+ 'Übergangsjacken' => 'uebergangsjacken',
+ 'Überwachungskamera' => 'ueberwachungskamera',
+ 'UE BLAST' => 'ue-blast',
+ 'UE BOOM' => 'ue-boom',
+ 'UE BOOM 2' => 'ue-boom-2',
+ 'UE BOOM 3' => 'ue-boom-3',
+ 'UE MEGABLAST' => 'ue-megablast',
+ 'UE MEGABOOM' => 'ue-megaboom',
+ 'UE MEGABOOM 3' => 'ue-megaboom-3',
+ 'UE WONDERBOOM' => 'ue-wonderboom',
+ 'UE WONDERBOOM 2' => 'ue-wonderboom-2',
+ 'UGG' => 'ugg',
+ 'Uhren' => 'uhren',
+ 'Umstandsmode' => 'umstandsmode',
+ 'Uncharted' => 'uncharted',
+ 'Uncharted 4' => 'uncharted-4',
+ 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy',
+ 'Under Armour' => 'under-armour',
+ 'Universalfernbedienungen' => 'universalfernbedienungen',
+ 'Unterwäsche' => 'unterwaesche',
+ 'Uplay' => 'uplay',
+ 'Urban Sport' => 'urban-sport',
+ 'Urlaub' => 'urlaub',
+ 'USB Sticks' => 'usb-stick',
+ 'Vakuumierer' => 'vakuumierer',
+ 'Vans' => 'vans',
+ 'Vans Old Skool' => 'vans-old-skool',
+ 'Vans Schuhe' => 'vans-schuhe',
+ 'Vaude' => 'vaude',
+ 'Ventilatoren' => 'ventilator',
+ 'Verbandskästen' => 'verbandskaesten',
+ 'Versicherung' => 'versicherung',
+ 'Versicherung &amp; Finanzen' => 'vertraege-finanzen',
+ 'Videobearbeitungsprogramme' => 'videobearbeitungsprogramme',
+ 'Video Player' => 'video-player',
+ 'Videospiele' => 'videospiele',
+ 'Video Streaming' => 'video-streaming',
+ 'Vileda' => 'vileda',
+ 'Villeroy &amp; Boch' => 'villeroy-boch',
+ 'Virenschutz' => 'virenschutz',
+ 'VISA' => 'visa',
+ 'Vliestapeten' => 'vliestapete',
+ 'Vodafone' => 'vodafone-netz',
+ 'Vodka' => 'vodka',
+ 'Volvo' => 'volvo',
+ 'Vorratsdosen' => 'vorratsdosen',
+ 'Vorstellungsrunde' => 'vorstellungsrunde',
+ 'VPN' => 'vpn',
+ 'VPS' => 'vps',
+ 'VR Brillen' => 'vr-brille',
+ 'VR Spiele' => 'vr-spiele',
+ 'VTech' => 'vtech',
+ 'VW' => 'vw',
+ 'Waffeleisen' => 'waffeleisen',
+ 'Wandbilder' => 'wandtattoos',
+ 'Wanderrucksäcke' => 'wanderrucksack',
+ 'Wanderschuhe' => 'wanderschuhe',
+ 'Wandersport' => 'hiking',
+ 'Wandfarben' => 'wandfarben',
+ 'Wandlampen' => 'wandlampen',
+ 'Wäscheständer' => 'waeschestaender',
+ 'Waschmaschinen' => 'waschmaschinen',
+ 'Waschmittel' => 'waschmittel',
+ 'Waschtrockner' => 'waschtrockner',
+ 'Wasserfilter' => 'wasserfilter',
+ 'Wasserkocher' => 'wasserkocher',
+ 'Wasserkühlung' => 'wasserkuehlung',
+ 'Wasserspielzeuge' => 'wasserspielzeug',
+ 'Wassersport' => 'wassersport',
+ 'Watch Dogs' => 'watch-dogs',
+ 'Watch Dogs 2' => 'watch-dogs-2',
+ 'Watch Dogs: Legion' => 'watch-dogs-legion',
+ 'WC Sitze' => 'wc-sitze',
+ 'WD-40' => 'wd-40',
+ 'Wearables' => 'wearable',
+ 'Webcams' => 'webcam',
+ 'Weber Gasgrills' => 'weber-gasgrill',
+ 'Weber Grills' => 'weber-grill',
+ 'Weihnachtsbäume' => 'weihnachtsbaum',
+ 'Weihnachtsbeleuchtung' => 'weihnachtsbeleuchtung',
+ 'Weihnachtsdeko' => 'weihnachtsdeko',
+ 'Weihnachtspullover' => 'weihnachtspullover',
+ 'Wein' => 'wein',
+ 'Wellensteyn' => 'wellensteyn',
+ 'Wellness &amp; Gesundheit' => 'wellness-massagen',
+ 'Wera' => 'wera',
+ 'Werkstatt &amp; Service' => 'werkstatt-service',
+ 'Werkstatteinrichtungen' => 'werkstatteinrichtungen',
+ 'Werkzeuge' => 'werkzeug',
+ 'Werkzeugkoffer' => 'werkzeugkoffer',
+ 'Wesco Mülleimer' => 'wesco-muelleimer',
+ 'Western Digital' => 'western-digital',
+ 'Wetterstationen' => 'wetterstationen',
+ 'Whirlpools' => 'whirlpools',
+ 'Whisky' => 'whisky',
+ 'Wiko' => 'wiko',
+ 'Wilkinson Sword Rasierer' => 'wilkinson-sword',
+ 'Windeln' => 'windeln',
+ 'Winkelschleifer' => 'winkelschleifer',
+ 'Winterdeko' => 'winterdeko',
+ 'Winterjacken' => 'winterjacken',
+ 'Winterreifen' => 'winterreifen',
+ 'Winterstiefel' => 'winterstiefel',
+ 'Wireless Charger' => 'wireless-charger',
+ 'Wirtschaftswoche' => 'wirtschaftswoche',
+ 'WMF' => 'wmf',
+ 'WMF Besteck' => 'wmf-besteck',
+ 'WMF Topfset' => 'wmf-topfset',
+ 'Wohnzimmermöbel' => 'wohnzimmer',
+ 'Wolfenstein' => 'wolfenstein',
+ 'Wolfenstein II: The New Colossus' => 'wolfenstein-2-the-new-colossus',
+ 'Womanizer' => 'womanizer',
+ 'World of Warcraft' => 'world-of-warcraft',
+ 'Wrangler' => 'wrangler',
+ 'X570 Mainboard' => 'x570-mainboard',
+ 'Xbox' => 'xbox',
+ 'Xbox Controller' => 'xbox-controller',
+ 'Xbox Elite Wireless Controller' => 'xbox-one-elite-controller',
+ 'Xbox Elite Wireless Controller 2' => 'xbox-one-elite-controller-2',
+ 'Xbox Game Pass' => 'xbox-game-pass',
+ 'Xbox Game Pass Ultimate' => 'xbox-game-pass-ultimate',
+ 'Xbox Guthaben' => 'xbox-guthaben',
+ 'Xbox Live Gold' => 'xbox-live',
+ 'Xbox One Controller' => 'xbox-one-controller',
+ 'Xbox One S Konsolen' => 'xbox-one-s',
+ 'Xbox One Spiele' => 'xbox-one-spiele',
+ 'Xbox One X Konsolen' => 'xbox-one-x',
+ 'Xbox Series S Konsolen' => 'xbox-series-s',
+ 'Xbox Series X Controller' => 'xbox-series-x-controller',
+ 'Xbox Series X Konsolen' => 'xbox-series-x',
+ 'Xbox Series X Spiele' => 'xbox-series-x-spiele',
+ 'Xbox Wireless Headset' => 'xbox-wireless-headset',
+ 'Xbox Zubehör' => 'xbox-zubehoer',
+ 'Xiaomi' => 'xiaomi',
+ 'Xiaomi Air Laptop' => 'xiaomi-air',
+ 'Xiaomi E-Scooter' => 'xiaomi-e-scooter',
+ 'Xiaomi Fernseher' => 'xiaomi-fernseher',
+ 'Xiaomi Kopfhörer' => 'xiaomi-kopfhoerer',
+ 'Xiaomi Mi 5S' => 'xiaomi-mi-5',
+ 'Xiaomi Mi 6' => 'xiaomi-mi-6',
+ 'Xiaomi Mi 8' => 'xiaomi-mi-8',
+ 'Xiaomi Mi 8 Lite' => 'xiaomi-mi-8-lite',
+ 'Xiaomi Mi 8 Pro' => 'xiaomi-mi-8-pro',
+ 'Xiaomi Mi 9' => 'xiaomi-mi-9',
+ 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite',
+ 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se',
+ 'Xiaomi Mi 9T' => 'xiaomi-mi-9t',
+ 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro',
+ 'Xiaomi Mi 10' => 'xiaomi-mi-10',
+ 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite',
+ 'Xiaomi Mi 10 Pro' => 'xiaomi-mi-10-pro',
+ 'Xiaomi Mi 11' => 'xiaomi-mi-11',
+ 'Xiaomi Mi A1' => 'xiaomi-mi-a1',
+ 'Xiaomi Mi A2' => 'xiaomi-mi-a2',
+ 'Xiaomi Mi AirDots' => 'xiaomi-mi-airdots',
+ 'Xiaomi Mi AirDots Pro' => 'xiaomi-airdots-pro',
+ 'Xiaomi Mi Band' => 'xiaomi-mi-band',
+ 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4',
+ 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5',
+ 'Xiaomi Mi Electric Scooter 1S' => 'xiaomi-mi-scooter-1s',
+ 'Xiaomi Mi Electric Scooter M365' => 'xiaomi-mi-electric-scooter-m365',
+ 'Xiaomi Mi Electric Scooter Pro 2' => 'xiaomi-mi-electric-scooter-pro-2',
+ 'Xiaomi Mi Mix' => 'xiaomi-mi-mix',
+ 'Xiaomi Mi Mix 3' => 'xiaomi-mi-mix-3',
+ 'Xiaomi Mi Note' => 'xiaomi-mi-note',
+ 'Xiaomi Mi Note 10' => 'xiaomi-mi-note-10',
+ 'Xiaomi Mi Note 10 Lite' => 'xiaomi-mi-note-10-lite',
+ 'Xiaomi Mi Note 10 Pro' => 'xiaomi-mi-note-10-pro',
+ 'Xiaomi Mi TV 4S' => 'xiaomi-mi-smart-tv-4s',
+ 'Xiaomi Mi TV Stick' => 'xiaomi-mi-tv-stick',
+ 'Xiaomi Pocophone F1' => 'xiaomi-pocophone-f1',
+ 'Xiaomi Redmi 9' => 'xiaomi-redmi-9',
+ 'Xiaomi Redmi 9A' => 'xiaomi-redmi-9a',
+ 'Xiaomi Redmi AirDots' => 'xiaomi-redmi-airdots',
+ 'Xiaomi Redmi Note 4' => 'xiaomi-redmi-note-4',
+ 'Xiaomi Redmi Note 5' => 'xiaomi-redmi-note-5',
+ 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8',
+ 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro',
+ 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9',
+ 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro',
+ 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s',
+ 'Xiaomi Redmi Note 10' => 'xiaomi-redmi-note-10',
+ 'Xiaomi Redmi Note 10 Pro' => 'xiaomi-redmi-note-10-pro',
+ 'Xiaomi Smart Home' => 'xiaomi-smart-home',
+ 'Xiaomi Smartphones' => 'xiaomi-smartphones',
+ 'Xiaomi YouPin' => 'xiaomi-youpin',
+ 'XMG' => 'xmg',
+ 'Yamaha' => 'yamaha',
+ 'Yeelight' => 'xiaomi-yeelight',
+ 'Yoga' => 'yoga',
+ 'Yogamatten' => 'yogamatten',
+ 'Yoshi&#039;s Crafted World' => 'yoshis-crafted-world',
+ 'Zahnbürsten' => 'zahnbuersten',
+ 'Zahnpasta' => 'zahnpasta',
+ 'Zahnzusatzversicherung' => 'zahnzusatzversicherung',
+ 'Zeitschriften' => 'zeitschriften-magazine',
+ 'Zelte' => 'zelte',
+ 'Zirkel' => 'zirkel',
+ 'Zoo-Tickets' => 'zoo',
+ 'Zotac' => 'zotac',
+ 'ZTE Smartphones' => 'zte-smartphones',
+ 'ZWILLING' => 'zwilling',
+ 'ZWILLING Besteck' => 'zwilling-besteck',
+ ]
+ ],
+ 'order' => [
+ 'name' => 'sortieren nach',
+ 'type' => 'list',
+ 'title' => 'Sortierung der deals',
+ 'values' => [
+ 'Vom heißesten zum kältesten Deal' => '-hot',
+ 'Vom jüngsten Deal zum ältesten' => '-new',
+ ]
+ ],
+ ],
+ 'Überwachung Diskussion' => [
+ 'url' => [
+ 'name' => 'URL der Diskussion',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'URL-Diskussion zu überwachen: https://www.mydealz.de/diskussion/title-123',
+ 'exampleValue' => 'https://www.mydealz.de/diskussion/anleitung-wie-schreibe-ich-einen-deal-1658317',
+ ],
+ 'only_with_url' => [
+ 'name' => 'Kommentare ohne URL ausschließen',
+ 'type' => 'checkbox',
+ 'title' => 'Kommentare, die keine URL enthalten, im Feed ausschließen',
+ 'defaultValue' => false,
+ ]
+ ]
+ ];
+ public $lang = [
+ 'bridge-uri' => self::URI,
+ 'bridge-name' => self::NAME,
+ 'context-keyword' => 'Suche nach Stichworten',
+ 'context-group' => 'Deals pro Gruppen',
+ 'context-talk' => 'Überwachung Diskussion',
+ 'uri-group' => 'gruppe/',
+ 'request-error' => 'Could not request mydeals',
+ 'thread-error' => 'Die ID der Diskussion kann nicht ermittelt werden. Überprüfen Sie die eingegebene URL',
+ 'no-results' => 'Ups, wir konnten keine Deals zu',
+ 'relative-date-indicator' => [
+ 'vor',
+ 'seit'
+ ],
+ 'price' => 'Preis',
+ 'shipping' => 'Versand',
+ 'origin' => 'Ursprung',
+ 'discount' => 'Rabatte',
+ 'title-keyword' => 'Suche',
+ 'title-group' => 'Gruppe',
+ 'title-talk' => 'Überwachung Diskussion',
+ 'local-months' => [
+ 'Jan',
+ 'Feb',
+ 'Mär',
+ 'Apr',
+ 'Mai',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Okt',
+ 'Nov',
+ 'Dez',
+ '.'
+ ],
+ 'local-time-relative' => [
+ 'eingestellt vor ',
+ 'm',
+ 'h,',
+ 'day',
+ 'days',
+ 'month',
+ 'year',
+ 'and '
+ ],
+ 'date-prefixes' => [
+ 'eingestellt am ',
+ 'lokal ',
+ 'aktualisiert ',
+ ],
+ 'relative-date-alt-prefixes' => [
+ 'aktualisiert vor ',
+ 'kommentiert vor ',
+ 'heiß seit '
+ ],
+ 'relative-date-ignore-suffix' => [
+ '/von.*$/'
+ ],
+ 'localdeal' => [
+ 'Lokal ',
+ 'Läuft bis '
+ ]
+ ];
}
diff --git a/bridges/N26Bridge.php b/bridges/N26Bridge.php
index 5ca78224..8865600d 100644
--- a/bridges/N26Bridge.php
+++ b/bridges/N26Bridge.php
@@ -2,42 +2,42 @@
class N26Bridge extends BridgeAbstract
{
- const MAINTAINER = 'quentinus95';
- const NAME = 'N26 Blog';
- const URI = 'https://n26.com';
- const CACHE_TIMEOUT = 1800;
- const DESCRIPTION = 'Returns recent blog posts from N26.';
+ const MAINTAINER = 'quentinus95';
+ const NAME = 'N26 Blog';
+ const URI = 'https://n26.com';
+ const CACHE_TIMEOUT = 1800;
+ const DESCRIPTION = 'Returns recent blog posts from N26.';
- public function collectData()
- {
- $limit = 5;
- $url = 'https://n26.com/en-eu/blog/all';
- $html = getSimpleHTMLDOM($url);
+ public function collectData()
+ {
+ $limit = 5;
+ $url = 'https://n26.com/en-eu/blog/all';
+ $html = getSimpleHTMLDOM($url);
- $articles = $html->find('div[class="bl bm"]');
+ $articles = $html->find('div[class="bl bm"]');
- foreach($articles as $article) {
- $item = array();
+ foreach ($articles as $article) {
+ $item = [];
- $itemUrl = self::URI . $article->find('a', 1)->href;
- $item['uri'] = $itemUrl;
+ $itemUrl = self::URI . $article->find('a', 1)->href;
+ $item['uri'] = $itemUrl;
- $item['title'] = $article->find('a', 1)->plaintext;
+ $item['title'] = $article->find('a', 1)->plaintext;
- $fullArticle = getSimpleHTMLDOM($item['uri']);
+ $fullArticle = getSimpleHTMLDOM($item['uri']);
- $createdAt = $fullArticle->find('time', 0);
- $item['timestamp'] = strtotime($createdAt->plaintext);
+ $createdAt = $fullArticle->find('time', 0);
+ $item['timestamp'] = strtotime($createdAt->plaintext);
- $this->items[] = $item;
- if (count($this->items) >= $limit) {
- break;
- }
- }
- }
+ $this->items[] = $item;
+ if (count($this->items) >= $limit) {
+ break;
+ }
+ }
+ }
- public function getIcon()
- {
- return 'https://n26.com/favicon.ico';
- }
+ public function getIcon()
+ {
+ return 'https://n26.com/favicon.ico';
+ }
}
diff --git a/bridges/NFLRUSBridge.php b/bridges/NFLRUSBridge.php
index c9a92379..18e067a8 100644
--- a/bridges/NFLRUSBridge.php
+++ b/bridges/NFLRUSBridge.php
@@ -1,27 +1,28 @@
<?php
-class NFLRUSBridge extends BridgeAbstract {
+class NFLRUSBridge extends BridgeAbstract
+{
+ const NAME = 'NFLRUS';
+ const URI = 'http://nflrus.ru/';
+ const DESCRIPTION = 'Returns the recent articles published on nflrus.ru';
+ const MAINTAINER = 'Maxim Shpak';
- const NAME = 'NFLRUS';
- const URI = 'http://nflrus.ru/';
- const DESCRIPTION = 'Returns the recent articles published on nflrus.ru';
- const MAINTAINER = 'Maxim Shpak';
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
+ $html = defaultLinkTo($html, self::URI);
- public function collectData() {
- $html = getSimpleHTMLDOM(self::URI);
- $html = defaultLinkTo($html, self::URI);
+ $articles = $html->find('.big-post_content-col');
- $articles = $html->find('.big-post_content-col');
+ foreach ($articles as $article) {
+ $item = [];
- foreach($articles as $article) {
- $item = array();
+ $url = $article->find('.big-post_title.card-title a', 0);
- $url = $article->find('.big-post_title.card-title a', 0);
-
- $item['uri'] = $url->href;
- $item['title'] = $url->plaintext;
- $item['content'] = $article->find('div', 0)->innertext;
- $this->items[] = $item;
- }
- }
+ $item['uri'] = $url->href;
+ $item['title'] = $url->plaintext;
+ $item['content'] = $article->find('div', 0)->innertext;
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/NYTBridge.php b/bridges/NYTBridge.php
index 15fded3a..87404c4d 100644
--- a/bridges/NYTBridge.php
+++ b/bridges/NYTBridge.php
@@ -1,37 +1,41 @@
<?php
-class NYTBridge extends FeedExpander {
- const MAINTAINER = 'IceWreck';
- const NAME = 'New York Times Bridge';
- const URI = 'https://www.nytimes.com/';
- const CACHE_TIMEOUT = 900; // 15 minutes
- const DESCRIPTION = 'RSS feed for the New York Times';
-
- public function collectData(){
- $this->collectExpandableDatas('https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', 40);
- }
-
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- $article = '';
-
- // $articlePage gets the entire page's contents
- $articlePage = getSimpleHTMLDOM($newsItem->link);
-
- // handle subtitle
- $subtitle = $articlePage->find('p.css-w6ymp8', 0);
- if ($subtitle != null) {
- $article .= '<strong>' . $subtitle->plaintext . '</strong>';
- }
-
- // figure contain's the main article image
- $article .= $articlePage->find('figure', 0) . '<br />';
-
- // section.meteredContent has the actual article
- foreach($articlePage->find('section.meteredContent p') as $element)
- $article .= '' . $element . '';
-
- $item['content'] = $article;
- return $item;
- }
+class NYTBridge extends FeedExpander
+{
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'New York Times Bridge';
+ const URI = 'https://www.nytimes.com/';
+ const CACHE_TIMEOUT = 900; // 15 minutes
+ const DESCRIPTION = 'RSS feed for the New York Times';
+
+ public function collectData()
+ {
+ $this->collectExpandableDatas('https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', 40);
+ }
+
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ $article = '';
+
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+
+ // handle subtitle
+ $subtitle = $articlePage->find('p.css-w6ymp8', 0);
+ if ($subtitle != null) {
+ $article .= '<strong>' . $subtitle->plaintext . '</strong>';
+ }
+
+ // figure contain's the main article image
+ $article .= $articlePage->find('figure', 0) . '<br />';
+
+ // section.meteredContent has the actual article
+ foreach ($articlePage->find('section.meteredContent p') as $element) {
+ $article .= '' . $element . '';
+ }
+
+ $item['content'] = $article;
+ return $item;
+ }
}
diff --git a/bridges/NasaApodBridge.php b/bridges/NasaApodBridge.php
index f23f6da9..5aff63aa 100644
--- a/bridges/NasaApodBridge.php
+++ b/bridges/NasaApodBridge.php
@@ -1,46 +1,47 @@
<?php
-class NasaApodBridge extends BridgeAbstract {
-
- const MAINTAINER = 'corenting';
- const NAME = 'NASA APOD Bridge';
- const URI = 'https://apod.nasa.gov/apod/';
- const CACHE_TIMEOUT = 43200; // 12h
- const DESCRIPTION = 'Returns the 3 latest NASA APOD pictures and explanations';
-
- public function collectData(){
-
- $html = getSimpleHTMLDOM(self::URI . 'archivepix.html');
-
- // Start at 1 to skip the "APOD Full Archive" on top of the page
- for($i = 1; $i < 4; $i++) {
- $item = array();
-
- $uri_page = $html->find('a', $i + 3)->href;
- $uri = self::URI . $uri_page;
- $item['uri'] = $uri;
-
- $picture_html = getSimpleHTMLDOM($uri);
- $picture_html_string = $picture_html->innertext;
-
- //Extract image and explanation
- $image_wrapper = $picture_html->find('a', 1);
- $image_path = $image_wrapper->href;
- $img_placeholder = $image_wrapper->find('img', 0);
- $img_alt = $img_placeholder->alt;
- $img_style = $img_placeholder->style;
- $image_uri = self::URI . $image_path;
- $new_img_placeholder = "<img src=\"$image_uri\" alt=\"$img_alt\" style=\"$img_style\">";
- $media = "<a href=\"$image_uri\">$new_img_placeholder</a>";
- $explanation = $picture_html->find('p', 2)->innertext;
-
- //Extract date from the picture page
- $date = explode(' ', $picture_html->find('p', 1)->innertext);
- $item['timestamp'] = strtotime($date[4] . $date[3] . $date[2]);
-
- //Other informations
- $item['content'] = $media . '<br />' . $explanation;
- $item['title'] = $picture_html->find('b', 0)->innertext;
- $this->items[] = $item;
- }
- }
+
+class NasaApodBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'corenting';
+ const NAME = 'NASA APOD Bridge';
+ const URI = 'https://apod.nasa.gov/apod/';
+ const CACHE_TIMEOUT = 43200; // 12h
+ const DESCRIPTION = 'Returns the 3 latest NASA APOD pictures and explanations';
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI . 'archivepix.html');
+
+ // Start at 1 to skip the "APOD Full Archive" on top of the page
+ for ($i = 1; $i < 4; $i++) {
+ $item = [];
+
+ $uri_page = $html->find('a', $i + 3)->href;
+ $uri = self::URI . $uri_page;
+ $item['uri'] = $uri;
+
+ $picture_html = getSimpleHTMLDOM($uri);
+ $picture_html_string = $picture_html->innertext;
+
+ //Extract image and explanation
+ $image_wrapper = $picture_html->find('a', 1);
+ $image_path = $image_wrapper->href;
+ $img_placeholder = $image_wrapper->find('img', 0);
+ $img_alt = $img_placeholder->alt;
+ $img_style = $img_placeholder->style;
+ $image_uri = self::URI . $image_path;
+ $new_img_placeholder = "<img src=\"$image_uri\" alt=\"$img_alt\" style=\"$img_style\">";
+ $media = "<a href=\"$image_uri\">$new_img_placeholder</a>";
+ $explanation = $picture_html->find('p', 2)->innertext;
+
+ //Extract date from the picture page
+ $date = explode(' ', $picture_html->find('p', 1)->innertext);
+ $item['timestamp'] = strtotime($date[4] . $date[3] . $date[2]);
+
+ //Other informations
+ $item['content'] = $media . '<br />' . $explanation;
+ $item['title'] = $picture_html->find('b', 0)->innertext;
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/NationalGeographicBridge.php b/bridges/NationalGeographicBridge.php
index e5273a8e..a7bb947a 100644
--- a/bridges/NationalGeographicBridge.php
+++ b/bridges/NationalGeographicBridge.php
@@ -1,334 +1,350 @@
<?php
-class NationalGeographicBridge extends BridgeAbstract {
-
- const CONTEXT_BY_TOPIC = 'By Topic';
- const PARAMETER_TOPIC = 'topic';
- const PARAMETER_FULL_ARTICLE = 'full';
- const TOPIC_MAGAZINE = 'Magazine';
- const TOPIC_LATEST_STORIES = 'Latest Stories';
- const CACHE_TIMEOUT = 900; //15 min
-
- const NAME = 'National Geographic';
- const URI = 'https://www.nationalgeographic.com/';
- const DESCRIPTION = 'Fetches the latest articles from the National Geographic Magazine';
- const MAINTAINER = 'csisoap';
- const PARAMETERS = array(
- self::CONTEXT_BY_TOPIC => array(
- self::PARAMETER_TOPIC => array(
- 'name' => 'Topic',
- 'type' => 'list',
- 'values' => array(
- self::TOPIC_MAGAZINE => 'magazine',
- self::TOPIC_LATEST_STORIES => 'latest-stories'
- ),
- 'title' => 'Select your topic',
- 'defaultValue' => 'Magazine'
- )
- ),
- 'global' => array(
- self::PARAMETER_FULL_ARTICLE => array(
- 'name' => 'Full Article',
- 'type' => 'checkbox',
- 'title' => 'Enable to load full articles and other infos (takes longer)'
- )
- )
- );
-
- private $topicName = '';
- const CONTEXT = 'eyJjb250ZW50VHlwZSI6IlVuaXNvbkh1YiIsInZhcmlhYmxlcyI6eyJsb2NhdG9yIjoiL3BhZ2VzL3
+
+class NationalGeographicBridge extends BridgeAbstract
+{
+ const CONTEXT_BY_TOPIC = 'By Topic';
+ const PARAMETER_TOPIC = 'topic';
+ const PARAMETER_FULL_ARTICLE = 'full';
+ const TOPIC_MAGAZINE = 'Magazine';
+ const TOPIC_LATEST_STORIES = 'Latest Stories';
+ const CACHE_TIMEOUT = 900; //15 min
+
+ const NAME = 'National Geographic';
+ const URI = 'https://www.nationalgeographic.com/';
+ const DESCRIPTION = 'Fetches the latest articles from the National Geographic Magazine';
+ const MAINTAINER = 'csisoap';
+ const PARAMETERS = [
+ self::CONTEXT_BY_TOPIC => [
+ self::PARAMETER_TOPIC => [
+ 'name' => 'Topic',
+ 'type' => 'list',
+ 'values' => [
+ self::TOPIC_MAGAZINE => 'magazine',
+ self::TOPIC_LATEST_STORIES => 'latest-stories'
+ ],
+ 'title' => 'Select your topic',
+ 'defaultValue' => 'Magazine'
+ ]
+ ],
+ 'global' => [
+ self::PARAMETER_FULL_ARTICLE => [
+ 'name' => 'Full Article',
+ 'type' => 'checkbox',
+ 'title' => 'Enable to load full articles and other infos (takes longer)'
+ ]
+ ]
+ ];
+
+ private $topicName = '';
+ const CONTEXT = 'eyJjb250ZW50VHlwZSI6IlVuaXNvbkh1YiIsInZhcmlhYmxlcyI6eyJsb2NhdG9yIjoiL3BhZ2VzL3
RvcGljL2xhdGVzdC1zdG9yaWVzIiwicG9ydGZvbGlvIjoibmF0Z2VvIiwicXVlcn
lUeXBlIjoiTE9DQVRPUiJ9LCJtb2R1bGVJZCI6bnVsbH0';
- const LATEST_STORIES_ID = array(
- '1df278bb-0e3d-4a67-a0ce-8fae48392822-f2-m1'
- );
- const MAGAZINE_ID = array(
- '94d87d74-f41a-4a32-9acd-b591ba2df288-f2-m1',
- '94d87d74-f41a-4a32-9acd-b591ba2df288-f5-m2',
- );
-
- public function getURI() {
- switch ($this->queriedContext) {
- case self::CONTEXT_BY_TOPIC:
- return self::URI . $this->getInput(self::PARAMETER_TOPIC);
- default:
- return parent::getURI();
- }
- }
-
- private function getAPIURL($id) {
- $context = preg_replace('/\s*/m', '', self::CONTEXT);
- $url = 'https://www.nationalgeographic.com/proxy/hub?context='
- . $context . '&id=' . $id
- . '&moduleType=InfiniteFeedModule&_xhr=pageContent';
- return $url;
- }
-
- public function collectData() {
- $this->topicName = $this->getTopicName($this->getInput(self::PARAMETER_TOPIC));
- switch($this->topicName) {
- case self::TOPIC_MAGAZINE:
- return $this->collectMagazine();
- case self::TOPIC_LATEST_STORIES:
- return $this->collectLatestStories();
- default:
- returnServerError('Unknown topic: "' . $this->topicName . '"');
- }
- }
-
- public function getName() {
- switch ($this->queriedContext) {
- case self::CONTEXT_BY_TOPIC:
- return static::NAME . ': ' . $this->topicName;
- default:
- return parent::getName();
- }
- }
-
- private function getTopicName($topic) {
- return array_search($topic, static::PARAMETERS[self::CONTEXT_BY_TOPIC][self::PARAMETER_TOPIC]['values']);
- }
-
- private function collectMagazine() {
- $stories = array();
-
- foreach(self::MAGAZINE_ID as $id) {
- $uri = $this->getAPIURL($id);
-
- $json_raw = getContents($uri);
-
- $json = json_decode($json_raw, true)['tiles'];
- $stories = array_merge($json, $stories);
- }
-
- foreach($stories as $story) {
- $this->addStory($story);
- }
- }
-
- private function collectLatestStories() {
- $stories = array();
-
- foreach(self::LATEST_STORIES_ID as $id) {
- $uri = $this->getAPIURL($id);
-
- $json_raw = getContents($uri);
-
- $json = json_decode($json_raw, true)['tiles'];
- $stories = array_merge($stories, $json);
- }
-
- foreach($stories as $story) {
- $this->addStory($story);
- }
- }
-
- private function addStory($story) {
- $title = 'Unknown title';
- $content = '';
- $story_type = '';
- $uri = '';
-
- foreach($story['ctas'] as $component) {
- $uri = $component['url'];
- $story_type = $component['icon'];
- }
-
- $item = array();
- if(isset($story['description'])) {
- $content = '<p>' . $story['description'] . '</p>';
- }
- $title = $story['title'];
- $item['uri'] = $uri;
- $item['title'] = $story['title'];
-
- // if full article is requested!
- if ($this->getInput(self::PARAMETER_FULL_ARTICLE)) {
- if($story_type != 'interactive') {
- /* Nat Geo doesn't provided much info about interactive page
- * and it requires JS to load the interactive.
- */
- $article_data = $this->getFullArticle($item['uri']);
- $item['timestamp'] = $article_data['published_date'];
- $item['author'] = $article_data['authors'];
- $item['content'] = $content . $article_data['content'];
- } else {
- $item['content'] = $content;
- }
- } else
- $item['content'] = $content;
-
- $image = $story['img'];
- $item['enclosures'][] = $image['src'];
-
- $tags = $story['tags'];
- foreach($tags as $tag) {
- $tag_name = $tag['name'];
- $item['categories'][] = $tag_name;
- }
-
- $this->items[] = $item;
- }
-
- private function filterArticleData($data) {
- $article_module = array_filter(
- $data, function ($item) {
- if(isset($item['id']) && $item['id'] == 'natgeo-template1-frame-1') {
- return true;
- }
- }
- );
-
- $article_data = array_reduce(
- $article_module,
- function (array $carry, array $item) {
- $module = $item['mods'];
- return array_merge(
- $carry,
- array_filter(
- $module, function ($data) {
- return $data['id'] == 'natgeo-template1-frame-1-module-1';
- }
- )
- );
- },
- array()
- );
-
- return $article_data[0];
- }
-
- private function handleImages($image_module, $image_type) {
- $image_alt = '';
- $image_credit = '';
- $image_src = '';
- $image_caption = '';
- $caption = '';
- switch($image_type) {
- case 'image':
- case 'imagegroup':
- $image = $image_module['image'];
- $image_src = $image['src'];
- if(isset($image_module['alt'])) {
- $image_alt = $image_module['alt'];
- } elseif(isset($image['altText'])) {
- $image_alt = $image['altText'];
- }
- if(isset($image['crdt'])) {
- $image_credit = $image['crdt'];
- }
- $caption = (isset($image_module['caption']) ? $image_module['caption'] : '');
- break;
- case 'photogallery':
- $image_credit = (isset($image_module['caption']['credit']) ? $image_module['caption']['credit'] : '');
- $caption = $image_module['caption']['text'];
- $image_src = $image_module['img']['src'];
- $image_alt = $image_module['img']['altText'];
- break;
- case 'video':
- $image_credit = (isset($image_module['credit']) ? $image_module['credit'] : '');
- $description = (isset($image_module['description']) ? $image_module['description'] : '');
- $caption = $description . ' Video can be watched on the article\'s page';
- $image = $image_module['image'];
- $image_alt = $image['altText'];
- $image_src = $image['src'];
- }
-
- $image_caption = $caption . ' ' . $image_credit
- . '. Notes: Some image may have copyrighted on it.';
- $wrapper = <<<EOD
+ const LATEST_STORIES_ID = [
+ '1df278bb-0e3d-4a67-a0ce-8fae48392822-f2-m1'
+ ];
+ const MAGAZINE_ID = [
+ '94d87d74-f41a-4a32-9acd-b591ba2df288-f2-m1',
+ '94d87d74-f41a-4a32-9acd-b591ba2df288-f5-m2',
+ ];
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case self::CONTEXT_BY_TOPIC:
+ return self::URI . $this->getInput(self::PARAMETER_TOPIC);
+ default:
+ return parent::getURI();
+ }
+ }
+
+ private function getAPIURL($id)
+ {
+ $context = preg_replace('/\s*/m', '', self::CONTEXT);
+ $url = 'https://www.nationalgeographic.com/proxy/hub?context='
+ . $context . '&id=' . $id
+ . '&moduleType=InfiniteFeedModule&_xhr=pageContent';
+ return $url;
+ }
+
+ public function collectData()
+ {
+ $this->topicName = $this->getTopicName($this->getInput(self::PARAMETER_TOPIC));
+ switch ($this->topicName) {
+ case self::TOPIC_MAGAZINE:
+ return $this->collectMagazine();
+ case self::TOPIC_LATEST_STORIES:
+ return $this->collectLatestStories();
+ default:
+ returnServerError('Unknown topic: "' . $this->topicName . '"');
+ }
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case self::CONTEXT_BY_TOPIC:
+ return static::NAME . ': ' . $this->topicName;
+ default:
+ return parent::getName();
+ }
+ }
+
+ private function getTopicName($topic)
+ {
+ return array_search($topic, static::PARAMETERS[self::CONTEXT_BY_TOPIC][self::PARAMETER_TOPIC]['values']);
+ }
+
+ private function collectMagazine()
+ {
+ $stories = [];
+
+ foreach (self::MAGAZINE_ID as $id) {
+ $uri = $this->getAPIURL($id);
+
+ $json_raw = getContents($uri);
+
+ $json = json_decode($json_raw, true)['tiles'];
+ $stories = array_merge($json, $stories);
+ }
+
+ foreach ($stories as $story) {
+ $this->addStory($story);
+ }
+ }
+
+ private function collectLatestStories()
+ {
+ $stories = [];
+
+ foreach (self::LATEST_STORIES_ID as $id) {
+ $uri = $this->getAPIURL($id);
+
+ $json_raw = getContents($uri);
+
+ $json = json_decode($json_raw, true)['tiles'];
+ $stories = array_merge($stories, $json);
+ }
+
+ foreach ($stories as $story) {
+ $this->addStory($story);
+ }
+ }
+
+ private function addStory($story)
+ {
+ $title = 'Unknown title';
+ $content = '';
+ $story_type = '';
+ $uri = '';
+
+ foreach ($story['ctas'] as $component) {
+ $uri = $component['url'];
+ $story_type = $component['icon'];
+ }
+
+ $item = [];
+ if (isset($story['description'])) {
+ $content = '<p>' . $story['description'] . '</p>';
+ }
+ $title = $story['title'];
+ $item['uri'] = $uri;
+ $item['title'] = $story['title'];
+
+ // if full article is requested!
+ if ($this->getInput(self::PARAMETER_FULL_ARTICLE)) {
+ if ($story_type != 'interactive') {
+ /* Nat Geo doesn't provided much info about interactive page
+ * and it requires JS to load the interactive.
+ */
+ $article_data = $this->getFullArticle($item['uri']);
+ $item['timestamp'] = $article_data['published_date'];
+ $item['author'] = $article_data['authors'];
+ $item['content'] = $content . $article_data['content'];
+ } else {
+ $item['content'] = $content;
+ }
+ } else {
+ $item['content'] = $content;
+ }
+
+ $image = $story['img'];
+ $item['enclosures'][] = $image['src'];
+
+ $tags = $story['tags'];
+ foreach ($tags as $tag) {
+ $tag_name = $tag['name'];
+ $item['categories'][] = $tag_name;
+ }
+
+ $this->items[] = $item;
+ }
+
+ private function filterArticleData($data)
+ {
+ $article_module = array_filter(
+ $data,
+ function ($item) {
+ if (isset($item['id']) && $item['id'] == 'natgeo-template1-frame-1') {
+ return true;
+ }
+ }
+ );
+
+ $article_data = array_reduce(
+ $article_module,
+ function (array $carry, array $item) {
+ $module = $item['mods'];
+ return array_merge(
+ $carry,
+ array_filter(
+ $module,
+ function ($data) {
+ return $data['id'] == 'natgeo-template1-frame-1-module-1';
+ }
+ )
+ );
+ },
+ []
+ );
+
+ return $article_data[0];
+ }
+
+ private function handleImages($image_module, $image_type)
+ {
+ $image_alt = '';
+ $image_credit = '';
+ $image_src = '';
+ $image_caption = '';
+ $caption = '';
+ switch ($image_type) {
+ case 'image':
+ case 'imagegroup':
+ $image = $image_module['image'];
+ $image_src = $image['src'];
+ if (isset($image_module['alt'])) {
+ $image_alt = $image_module['alt'];
+ } elseif (isset($image['altText'])) {
+ $image_alt = $image['altText'];
+ }
+ if (isset($image['crdt'])) {
+ $image_credit = $image['crdt'];
+ }
+ $caption = (isset($image_module['caption']) ? $image_module['caption'] : '');
+ break;
+ case 'photogallery':
+ $image_credit = (isset($image_module['caption']['credit']) ? $image_module['caption']['credit'] : '');
+ $caption = $image_module['caption']['text'];
+ $image_src = $image_module['img']['src'];
+ $image_alt = $image_module['img']['altText'];
+ break;
+ case 'video':
+ $image_credit = (isset($image_module['credit']) ? $image_module['credit'] : '');
+ $description = (isset($image_module['description']) ? $image_module['description'] : '');
+ $caption = $description . ' Video can be watched on the article\'s page';
+ $image = $image_module['image'];
+ $image_alt = $image['altText'];
+ $image_src = $image['src'];
+ }
+
+ $image_caption = $caption . ' ' . $image_credit
+ . '. Notes: Some image may have copyrighted on it.';
+ $wrapper = <<<EOD
<figure>
<img src="{$image_src}" alt="{$image_alt}">
<figcaption>$image_caption</figcaption>
</figure>
EOD;
- return $wrapper;
- }
-
- private function getFullArticle($uri) {
- $html = getContents($uri);
-
- $scriptRegex = '/window\[\'__natgeo__\'\]=(.*);<\/script>/';
-
- preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0);
-
- $json = json_decode($matches[1][0], true);
-
- $unfiltered_data = $json['page']['content']['article']['frms'];
- $filtered_data = $this->filterArticleData($unfiltered_data);
-
- $article = $filtered_data['edgs'][0];
-
- $contributors = $article['cntrbGrp'];
- $authors = array();
- if(count($contributors) > 0) {
- $authors = $contributors[0]['contributors'];
- }
-
- $authors_name = '';
- $counter = 0;
- foreach($authors as $author) {
- $counter++;
- if($counter == count($authors)) {
- $authors_name .= $author['displayName'];
- } else {
- $authors_name .= $author['displayName'] . ', ';
- }
- }
-
- $published_date = $article['pbDt'];
- $article_body = $article['bdy'];
- $content = '';
-
- foreach($article_body as $body) {
- switch($body['type']) {
- case 'p':
- $content .= '<p>' . $body['cntnt']['mrkup'] . '</p>';
- break;
- case 'h2':
- $content .= '<h2>' . $body['cntnt']['mrkup'] . '</h2>';
- break;
- case 'inline':
- $module = $body['cntnt'];
- if(empty($module))
- continue 2;
- switch($module['cmsType']) {
- case 'image':
- $content .= $this->handleImages($module, $module['cmsType']);
- break;
- case 'imagegroup':
- $images = $module['images'];
- foreach($images as $image) {
- $content .= $this->handleImages($image, $module['cmsType']);
- }
- break;
- case 'editorsNote':
- $content .= $module['note'];
- break;
- case 'listicle':
- $content .= '<h2>' . $module['title'] . '</h2>';
- if(isset($module['image'])) {
- $content .= $this->handleImages($module['image'], $module['image']['cmsType']);
- }
- $content .= '<p>' . (isset($module['text']) ? $module['text'] : '') . '</p>';
- break;
- case 'photogallery':
- $gallery = $body['cntnt']['media'];
- foreach($gallery as $image) {
- $content .= $this->handleImages($image, $module['cmsType']);
- }
- break;
- case 'video':
- $content .= $this->handleImages($module, $module['cmsType']);
- break;
- case 'pullquote':
- $quote = $module['quote'];
- $author_name = '';
- $authors = (isset($module['byLineProps']['authors']) ? $module['byLineProps']['authors'] : array());
- foreach($authors as $author) {
- $author_desc = (isset($author['authorDesc']) ? $author['authorDesc'] : '');
- $author_name .= $author['displayName'] . ', ' . $author_desc;
- }
- $content .= <<<EOD
+ return $wrapper;
+ }
+
+ private function getFullArticle($uri)
+ {
+ $html = getContents($uri);
+
+ $scriptRegex = '/window\[\'__natgeo__\'\]=(.*);<\/script>/';
+
+ preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0);
+
+ $json = json_decode($matches[1][0], true);
+
+ $unfiltered_data = $json['page']['content']['article']['frms'];
+ $filtered_data = $this->filterArticleData($unfiltered_data);
+
+ $article = $filtered_data['edgs'][0];
+
+ $contributors = $article['cntrbGrp'];
+ $authors = [];
+ if (count($contributors) > 0) {
+ $authors = $contributors[0]['contributors'];
+ }
+
+ $authors_name = '';
+ $counter = 0;
+ foreach ($authors as $author) {
+ $counter++;
+ if ($counter == count($authors)) {
+ $authors_name .= $author['displayName'];
+ } else {
+ $authors_name .= $author['displayName'] . ', ';
+ }
+ }
+
+ $published_date = $article['pbDt'];
+ $article_body = $article['bdy'];
+ $content = '';
+
+ foreach ($article_body as $body) {
+ switch ($body['type']) {
+ case 'p':
+ $content .= '<p>' . $body['cntnt']['mrkup'] . '</p>';
+ break;
+ case 'h2':
+ $content .= '<h2>' . $body['cntnt']['mrkup'] . '</h2>';
+ break;
+ case 'inline':
+ $module = $body['cntnt'];
+ if (empty($module)) {
+ continue 2;
+ }
+ switch ($module['cmsType']) {
+ case 'image':
+ $content .= $this->handleImages($module, $module['cmsType']);
+ break;
+ case 'imagegroup':
+ $images = $module['images'];
+ foreach ($images as $image) {
+ $content .= $this->handleImages($image, $module['cmsType']);
+ }
+ break;
+ case 'editorsNote':
+ $content .= $module['note'];
+ break;
+ case 'listicle':
+ $content .= '<h2>' . $module['title'] . '</h2>';
+ if (isset($module['image'])) {
+ $content .= $this->handleImages($module['image'], $module['image']['cmsType']);
+ }
+ $content .= '<p>' . (isset($module['text']) ? $module['text'] : '') . '</p>';
+ break;
+ case 'photogallery':
+ $gallery = $body['cntnt']['media'];
+ foreach ($gallery as $image) {
+ $content .= $this->handleImages($image, $module['cmsType']);
+ }
+ break;
+ case 'video':
+ $content .= $this->handleImages($module, $module['cmsType']);
+ break;
+ case 'pullquote':
+ $quote = $module['quote'];
+ $author_name = '';
+ $authors = (isset($module['byLineProps']['authors']) ? $module['byLineProps']['authors'] : []);
+ foreach ($authors as $author) {
+ $author_desc = (isset($author['authorDesc']) ? $author['authorDesc'] : '');
+ $author_name .= $author['displayName'] . ', ' . $author_desc;
+ }
+ $content .= <<<EOD
<figure>
<blockquote>
<p>$quote</p>
@@ -336,19 +352,19 @@ EOD;
<figcaption>$author_name</figcaption>
</figure>
EOD;
- break;
- }
- break;
- case 'ul':
- $content .= $body['cntnt']['mrkup'] . '<hr>';
- break;
- }
- }
-
- return array(
- 'content' => $content,
- 'published_date' => $published_date,
- 'authors' => $authors_name
- );
- }
+ break;
+ }
+ break;
+ case 'ul':
+ $content .= $body['cntnt']['mrkup'] . '<hr>';
+ break;
+ }
+ }
+
+ return [
+ 'content' => $content,
+ 'published_date' => $published_date,
+ 'authors' => $authors_name
+ ];
+ }
}
diff --git a/bridges/NewOnNetflixBridge.php b/bridges/NewOnNetflixBridge.php
index 094038a8..43278fd9 100644
--- a/bridges/NewOnNetflixBridge.php
+++ b/bridges/NewOnNetflixBridge.php
@@ -1,58 +1,60 @@
<?php
-class NewOnNetflixBridge extends BridgeAbstract {
- const NAME = 'NewOnNetflix removals bridge';
- const URI = 'https://www.newonnetflix.info';
- const DESCRIPTION = 'Upcoming removals from Netflix (NewOnNetflix already provides additions as RSS)';
- const MAINTAINER = 'jdesgats';
- const PARAMETERS = array(array(
- 'country' => array(
- 'name' => 'Country',
- 'type' => 'list',
- 'values' => array(
- 'Australia/New Zealand' => 'anz',
- 'Canada' => 'can',
- 'United Kingdom' => 'uk',
- 'United States' => 'usa',
- ),
- 'defaultValue' => 'uk',
- )
- ));
- const CACHE_TIMEOUT = 3600 * 24;
+class NewOnNetflixBridge extends BridgeAbstract
+{
+ const NAME = 'NewOnNetflix removals bridge';
+ const URI = 'https://www.newonnetflix.info';
+ const DESCRIPTION = 'Upcoming removals from Netflix (NewOnNetflix already provides additions as RSS)';
+ const MAINTAINER = 'jdesgats';
+ const PARAMETERS = [[
+ 'country' => [
+ 'name' => 'Country',
+ 'type' => 'list',
+ 'values' => [
+ 'Australia/New Zealand' => 'anz',
+ 'Canada' => 'can',
+ 'United Kingdom' => 'uk',
+ 'United States' => 'usa',
+ ],
+ 'defaultValue' => 'uk',
+ ]
+ ]];
+ const CACHE_TIMEOUT = 3600 * 24;
- public function collectData() {
- $baseURI = 'https://' . $this->getInput('country') . '.newonnetflix.info';
- $html = getSimpleHTMLDOMCached($baseURI . '/lastchance', self::CACHE_TIMEOUT);
+ public function collectData()
+ {
+ $baseURI = 'https://' . $this->getInput('country') . '.newonnetflix.info';
+ $html = getSimpleHTMLDOMCached($baseURI . '/lastchance', self::CACHE_TIMEOUT);
- foreach($html->find('article.oldpost') as $element) {
- $title = $element->find('a.infopop[title]', 0);
- $img = $element->find('img[lazy_src]', 0);
- $date = $element->find('span[title]', 0);
+ foreach ($html->find('article.oldpost') as $element) {
+ $title = $element->find('a.infopop[title]', 0);
+ $img = $element->find('img[lazy_src]', 0);
+ $date = $element->find('span[title]', 0);
- // format sholud be 'dd/mm/yy - dd/mm/yy'
- // (the added date might be "unknown")
- $fromTo = array();
- if (preg_match('/^\s*(.*?)\s*-\s*(.*?)\s*$/', $date->title, $fromTo)) {
- $from = $fromTo[1];
- $to = $fromTo[2];
- } else {
- $from = 'unknown';
- $to = 'unknown';
- }
- $summary = <<<EOD
+ // format sholud be 'dd/mm/yy - dd/mm/yy'
+ // (the added date might be "unknown")
+ $fromTo = [];
+ if (preg_match('/^\s*(.*?)\s*-\s*(.*?)\s*$/', $date->title, $fromTo)) {
+ $from = $fromTo[1];
+ $to = $fromTo[2];
+ } else {
+ $from = 'unknown';
+ $to = 'unknown';
+ }
+ $summary = <<<EOD
<img src="{$img->lazy_src}" loading="lazy">
<div>{$title->title}</div>
<div><strong>Added on:</strong>$from</div>
<div><strong>Removed on:</strong>$to</div>
EOD;
- $item = array();
- $item['uri'] = $baseURI . $title->href;
- $item['title'] = $to . ' - ' . $title->plaintext;
- $item['content'] = $summary;
- // some movies are added and removed multiple times
- $item['uid'] = $title->href . '-' . $to;
- $this->items[] = $item;
- }
- }
+ $item = [];
+ $item['uri'] = $baseURI . $title->href;
+ $item['title'] = $to . ' - ' . $title->plaintext;
+ $item['content'] = $summary;
+ // some movies are added and removed multiple times
+ $item['uid'] = $title->href . '-' . $to;
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/NewgroundsBridge.php b/bridges/NewgroundsBridge.php
index f84ad8c0..fe956573 100644
--- a/bridges/NewgroundsBridge.php
+++ b/bridges/NewgroundsBridge.php
@@ -1,53 +1,54 @@
<?php
+
declare(strict_types=1);
class NewgroundsBridge extends BridgeAbstract
{
- const NAME = 'Newgrounds';
- const URI = 'https://www.newgrounds.com';
- const DESCRIPTION = 'Get the latest art from a given user';
- const MAINTAINER = 'KamaleiZestri';
- const PARAMETERS = [
- 'User' => [
- 'username' => [
- 'name' => 'Username',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'TomFulp'
- ]
- ]
- ];
+ const NAME = 'Newgrounds';
+ const URI = 'https://www.newgrounds.com';
+ const DESCRIPTION = 'Get the latest art from a given user';
+ const MAINTAINER = 'KamaleiZestri';
+ const PARAMETERS = [
+ 'User' => [
+ 'username' => [
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'TomFulp'
+ ]
+ ]
+ ];
- public function collectData()
- {
- $username = $this->getInput('username');
- if (!preg_match('/^\w+$/', $username)) {
- throw new \Exception('Illegal username');
- }
+ public function collectData()
+ {
+ $username = $this->getInput('username');
+ if (!preg_match('/^\w+$/', $username)) {
+ throw new \Exception('Illegal username');
+ }
- $html = getSimpleHTMLDOM($this->getURI());
+ $html = getSimpleHTMLDOM($this->getURI());
- $posts = $html->find('.item-portalitem-art-medium');
+ $posts = $html->find('.item-portalitem-art-medium');
- foreach ($posts as $post) {
- $item = [];
+ foreach ($posts as $post) {
+ $item = [];
- $item['author'] = $username;
- $item['uri'] = $post->href;
+ $item['author'] = $username;
+ $item['uri'] = $post->href;
- $titleOrRestricted = $post->find('h4')[0]->innertext;
+ $titleOrRestricted = $post->find('h4')[0]->innertext;
- // Newgrounds doesn't show public previews for NSFW content.
- if ($titleOrRestricted === 'Restricted Content: Sign in to view!') {
- $item['title'] = 'NSFW: ' . $item['uri'];
- $item['content'] = <<<EOD
+ // Newgrounds doesn't show public previews for NSFW content.
+ if ($titleOrRestricted === 'Restricted Content: Sign in to view!') {
+ $item['title'] = 'NSFW: ' . $item['uri'];
+ $item['content'] = <<<EOD
<a href="{$item['uri']}">
{$item['title']}
</a>
EOD;
- } else {
- $item['title'] = $titleOrRestricted;
- $item['content'] = <<<EOD
+ } else {
+ $item['title'] = $titleOrRestricted;
+ $item['content'] = <<<EOD
<a href="{$item['uri']}">
<img
style="align:top; width:270px; border:1px solid black;"
@@ -56,25 +57,25 @@ EOD;
title="{$item['title']}" />
</a>
EOD;
- }
+ }
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
- public function getName()
- {
- if ($this->getInput('username')) {
- return sprintf('%s - %s', $this->getInput('username'), self::NAME);
- }
- return parent::getName();
- }
+ public function getName()
+ {
+ if ($this->getInput('username')) {
+ return sprintf('%s - %s', $this->getInput('username'), self::NAME);
+ }
+ return parent::getName();
+ }
- public function getURI()
- {
- if ($this->getInput('username')) {
- return sprintf('https://%s.newgrounds.com/art', $this->getInput('username'));
- }
- return parent::getURI();
- }
+ public function getURI()
+ {
+ if ($this->getInput('username')) {
+ return sprintf('https://%s.newgrounds.com/art', $this->getInput('username'));
+ }
+ return parent::getURI();
+ }
}
diff --git a/bridges/NextInpactBridge.php b/bridges/NextInpactBridge.php
index 4cac7769..408fd783 100644
--- a/bridges/NextInpactBridge.php
+++ b/bridges/NextInpactBridge.php
@@ -1,187 +1,193 @@
<?php
-class NextInpactBridge extends FeedExpander {
-
- const MAINTAINER = 'qwertygc and ORelio';
- const NAME = 'NextInpact Bridge';
- const URI = 'https://www.nextinpact.com/';
- const URI_HARDWARE = 'https://www.inpact-hardware.com/';
- const DESCRIPTION = 'Returns the newest articles.';
-
- const PARAMETERS = array( array(
- 'feed' => array(
- 'name' => 'Feed',
- 'type' => 'list',
- 'values' => array(
- 'Nos actualités' => array(
- 'Toutes nos publications' => 'news',
- 'Toutes nos publications sauf #LeBrief' => 'nobrief',
- 'Toutes nos publications sauf INpact Hardware' => 'noih',
- 'Seulement les publications INpact Hardware' => 'hardware:news',
- 'Seulement les publications Next INpact' => 'nobrief-noih',
- 'Seulement les publications #LeBrief' => 'lebrief',
- ),
- 'Flux spécifiques' => array(
- 'Le blog' => 'blog',
- 'Les bons plans' => 'bonsplans',
- 'Publications INpact Hardware en accès libre' => 'hardware:acces-libre',
- 'Publications Next INpact en accès libre' => 'acces-libre',
- ),
- 'Flux thématiques' => array(
- 'Tech' => 'category:1',
- 'Logiciel' => 'category:2',
- 'Internet' => 'category:3',
- 'Mobilité' => 'category:4',
- 'Droit' => 'category:5',
- 'Économie' => 'category:6',
- 'Culture numérique' => 'category:7',
- 'Next INpact' => 'category:8',
- )
- )
- ),
- 'filter_premium' => array(
- 'name' => 'Premium',
- 'type' => 'list',
- 'values' => array(
- 'No filter' => '0',
- 'Hide Premium' => '1',
- 'Only Premium' => '2'
- )
- ),
- 'filter_brief' => array(
- 'name' => 'Brief',
- 'type' => 'list',
- 'values' => array(
- 'No filter' => '0',
- 'Hide Brief' => '1',
- 'Only Brief' => '2'
- )
- ),
- 'limit' => self::LIMIT,
- ));
-
- public function collectData(){
- $feed = $this->getInput('feed');
- $base_uri = self::URI;
- $args = '';
-
- if (empty($feed)) {
- // Default to All articles
- $feed = 'news';
- }
-
- if (strpos($feed, 'hardware:') === 0) {
- // Feed hosted on Hardware domain
- $base_uri = self::URI_HARDWARE;
- $feed = str_replace('hardware:', '', $feed);
- }
-
- if (strpos($feed, 'category:') === 0) {
- // Feed with specific category parameter
- $args = '?CategoryIds=' . str_replace('category:', '', $feed);
- $feed = 'params';
- }
-
- $url = sprintf('%srss/%s.xml%s', $base_uri, $feed, $args);
- $limit = $this->getInput('limit') ?? 10;
- $this->collectExpandableDatas($url, $limit);
- }
-
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- $item['content'] = $this->extractContent($item, $item['uri']);
- if (is_null($item['content']))
- return null; //Filtered article
- return $item;
- }
-
- private function extractContent($item, $url){
- $html = getSimpleHTMLDOMCached($url);
- if (!is_object($html))
- return 'Failed to request NextInpact: ' . $url;
-
- // Filter premium and brief articles?
- $brief_selector = 'div.brief-container';
- foreach(array(
- 'filter_premium' => 'p.red-msg',
- 'filter_brief' => $brief_selector
- ) as $param_name => $selector) {
- $param_val = intval($this->getInput($param_name));
- if ($param_val != 0) {
- $element_present = is_object($html->find($selector, 0));
- $element_wanted = ($param_val == 2);
- if ($element_present != $element_wanted) {
- return null; //Filter article
- }
- }
- }
-
- $article_content = $html->find('div.article-content', 0);
- if (!is_object($article_content)) {
- $article_content = $html->find('div.content', 0);
- }
- if (is_object($article_content)) {
-
- // Subtitle
- $subtitle = $html->find('small.subtitle', 0);
- if(!is_object($subtitle) && !is_object($html->find($brief_selector, 0))) {
- $subtitle = $html->find('small', 0);
- }
- if(!is_object($subtitle)) {
- $content_wrapper = $html->find('div.content-wrapper', 0);
- if (is_object($content_wrapper)) {
- $subtitle = $content_wrapper->find('h2.title', 0);
- }
- }
- if(is_object($subtitle) && (!isset($item['title']) || $subtitle->plaintext != $item['title'])) {
- $subtitle = '<p><em>' . trim($subtitle->plaintext) . '</em></p>';
- } else {
- $subtitle = '';
- }
-
- // Image
- $postimg = $html->find('div.article-image, div.image-container', 0);
- if(is_object($postimg)) {
- $postimg = $postimg->find('img', 0);
- if (!empty($postimg->src)) {
- $postimg = $postimg->src;
- } else {
- $postimg = $postimg->srcset; //"url 355w, url 1003w, url 748w"
- $postimg = explode(', ', $postimg); //split by ', ' to get each url separately
- $postimg = end($postimg); //Get last item: "url 748w" which is of largest size
- $postimg = explode(' ', $postimg); //split by ' ' to separate url from res
- $postimg = array_reverse($postimg); //reverse array content to have url last
- $postimg = end($postimg); //Get last item of array: "url"
- }
- $postimg = '<p><img src="' . $postimg . '" alt="-" /></p>';
- } else {
- $postimg = '';
- }
-
- // Paywall
- $paywall = $html->find('div.paywall-restriction', 0);
- if (is_object($paywall) && is_object($paywall->find('p.red-msg', 0))) {
- $paywall = '<p><em>' . $paywall->find('span.head-mention', 0)->innertext . '</em></p>';
- } else {
- $paywall = '';
- }
-
- // Content
- $article_content = $article_content->outertext;
- $article_content = str_replace('>Signaler une erreur</span>', '></span>', $article_content);
-
- // Result
- $text = $subtitle
- . $postimg
- . $article_content
- . $paywall;
-
- } else {
- $text = '<p><em>Failed to retrieve full article content</em></p>';
- if (isset($item['content'])) {
- $text = $item['content'] . $text;
- }
- }
-
- return $text;
- }
+
+class NextInpactBridge extends FeedExpander
+{
+ const MAINTAINER = 'qwertygc and ORelio';
+ const NAME = 'NextInpact Bridge';
+ const URI = 'https://www.nextinpact.com/';
+ const URI_HARDWARE = 'https://www.inpact-hardware.com/';
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ const PARAMETERS = [ [
+ 'feed' => [
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'values' => [
+ 'Nos actualités' => [
+ 'Toutes nos publications' => 'news',
+ 'Toutes nos publications sauf #LeBrief' => 'nobrief',
+ 'Toutes nos publications sauf INpact Hardware' => 'noih',
+ 'Seulement les publications INpact Hardware' => 'hardware:news',
+ 'Seulement les publications Next INpact' => 'nobrief-noih',
+ 'Seulement les publications #LeBrief' => 'lebrief',
+ ],
+ 'Flux spécifiques' => [
+ 'Le blog' => 'blog',
+ 'Les bons plans' => 'bonsplans',
+ 'Publications INpact Hardware en accès libre' => 'hardware:acces-libre',
+ 'Publications Next INpact en accès libre' => 'acces-libre',
+ ],
+ 'Flux thématiques' => [
+ 'Tech' => 'category:1',
+ 'Logiciel' => 'category:2',
+ 'Internet' => 'category:3',
+ 'Mobilité' => 'category:4',
+ 'Droit' => 'category:5',
+ 'Économie' => 'category:6',
+ 'Culture numérique' => 'category:7',
+ 'Next INpact' => 'category:8',
+ ]
+ ]
+ ],
+ 'filter_premium' => [
+ 'name' => 'Premium',
+ 'type' => 'list',
+ 'values' => [
+ 'No filter' => '0',
+ 'Hide Premium' => '1',
+ 'Only Premium' => '2'
+ ]
+ ],
+ 'filter_brief' => [
+ 'name' => 'Brief',
+ 'type' => 'list',
+ 'values' => [
+ 'No filter' => '0',
+ 'Hide Brief' => '1',
+ 'Only Brief' => '2'
+ ]
+ ],
+ 'limit' => self::LIMIT,
+ ]];
+
+ public function collectData()
+ {
+ $feed = $this->getInput('feed');
+ $base_uri = self::URI;
+ $args = '';
+
+ if (empty($feed)) {
+ // Default to All articles
+ $feed = 'news';
+ }
+
+ if (strpos($feed, 'hardware:') === 0) {
+ // Feed hosted on Hardware domain
+ $base_uri = self::URI_HARDWARE;
+ $feed = str_replace('hardware:', '', $feed);
+ }
+
+ if (strpos($feed, 'category:') === 0) {
+ // Feed with specific category parameter
+ $args = '?CategoryIds=' . str_replace('category:', '', $feed);
+ $feed = 'params';
+ }
+
+ $url = sprintf('%srss/%s.xml%s', $base_uri, $feed, $args);
+ $limit = $this->getInput('limit') ?? 10;
+ $this->collectExpandableDatas($url, $limit);
+ }
+
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ $item['content'] = $this->extractContent($item, $item['uri']);
+ if (is_null($item['content'])) {
+ return null; //Filtered article
+ }
+ return $item;
+ }
+
+ private function extractContent($item, $url)
+ {
+ $html = getSimpleHTMLDOMCached($url);
+ if (!is_object($html)) {
+ return 'Failed to request NextInpact: ' . $url;
+ }
+
+ // Filter premium and brief articles?
+ $brief_selector = 'div.brief-container';
+ foreach (
+ [
+ 'filter_premium' => 'p.red-msg',
+ 'filter_brief' => $brief_selector
+ ] as $param_name => $selector
+ ) {
+ $param_val = intval($this->getInput($param_name));
+ if ($param_val != 0) {
+ $element_present = is_object($html->find($selector, 0));
+ $element_wanted = ($param_val == 2);
+ if ($element_present != $element_wanted) {
+ return null; //Filter article
+ }
+ }
+ }
+
+ $article_content = $html->find('div.article-content', 0);
+ if (!is_object($article_content)) {
+ $article_content = $html->find('div.content', 0);
+ }
+ if (is_object($article_content)) {
+ // Subtitle
+ $subtitle = $html->find('small.subtitle', 0);
+ if (!is_object($subtitle) && !is_object($html->find($brief_selector, 0))) {
+ $subtitle = $html->find('small', 0);
+ }
+ if (!is_object($subtitle)) {
+ $content_wrapper = $html->find('div.content-wrapper', 0);
+ if (is_object($content_wrapper)) {
+ $subtitle = $content_wrapper->find('h2.title', 0);
+ }
+ }
+ if (is_object($subtitle) && (!isset($item['title']) || $subtitle->plaintext != $item['title'])) {
+ $subtitle = '<p><em>' . trim($subtitle->plaintext) . '</em></p>';
+ } else {
+ $subtitle = '';
+ }
+
+ // Image
+ $postimg = $html->find('div.article-image, div.image-container', 0);
+ if (is_object($postimg)) {
+ $postimg = $postimg->find('img', 0);
+ if (!empty($postimg->src)) {
+ $postimg = $postimg->src;
+ } else {
+ $postimg = $postimg->srcset; //"url 355w, url 1003w, url 748w"
+ $postimg = explode(', ', $postimg); //split by ', ' to get each url separately
+ $postimg = end($postimg); //Get last item: "url 748w" which is of largest size
+ $postimg = explode(' ', $postimg); //split by ' ' to separate url from res
+ $postimg = array_reverse($postimg); //reverse array content to have url last
+ $postimg = end($postimg); //Get last item of array: "url"
+ }
+ $postimg = '<p><img src="' . $postimg . '" alt="-" /></p>';
+ } else {
+ $postimg = '';
+ }
+
+ // Paywall
+ $paywall = $html->find('div.paywall-restriction', 0);
+ if (is_object($paywall) && is_object($paywall->find('p.red-msg', 0))) {
+ $paywall = '<p><em>' . $paywall->find('span.head-mention', 0)->innertext . '</em></p>';
+ } else {
+ $paywall = '';
+ }
+
+ // Content
+ $article_content = $article_content->outertext;
+ $article_content = str_replace('>Signaler une erreur</span>', '></span>', $article_content);
+
+ // Result
+ $text = $subtitle
+ . $postimg
+ . $article_content
+ . $paywall;
+ } else {
+ $text = '<p><em>Failed to retrieve full article content</em></p>';
+ if (isset($item['content'])) {
+ $text = $item['content'] . $text;
+ }
+ }
+
+ return $text;
+ }
}
diff --git a/bridges/NextgovBridge.php b/bridges/NextgovBridge.php
index bd76ecae..ad17f88e 100644
--- a/bridges/NextgovBridge.php
+++ b/bridges/NextgovBridge.php
@@ -1,67 +1,72 @@
<?php
-class NextgovBridge extends FeedExpander {
- const MAINTAINER = 'ORelio';
- const NAME = 'Nextgov Bridge';
- const URI = 'https://www.nextgov.com/';
- const DESCRIPTION = 'USA Federal technology news, best practices, and web 2.0 tools.';
+class NextgovBridge extends FeedExpander
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = 'Nextgov Bridge';
+ const URI = 'https://www.nextgov.com/';
+ const DESCRIPTION = 'USA Federal technology news, best practices, and web 2.0 tools.';
- const PARAMETERS = array( array(
- 'category' => array(
- 'name' => 'Category',
- 'type' => 'list',
- 'values' => array(
- 'All' => 'all',
- 'Technology News' => 'technology-news',
- 'CIO Briefing' => 'cio-briefing',
- 'Emerging Tech' => 'emerging-tech',
- 'Cybersecurity' => 'cybersecurity',
- 'IT Modernization' => 'it-modernization',
- 'Policy' => 'policy',
- 'Ideas' => 'ideas',
- )
- )
- ));
+ const PARAMETERS = [ [
+ 'category' => [
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => [
+ 'All' => 'all',
+ 'Technology News' => 'technology-news',
+ 'CIO Briefing' => 'cio-briefing',
+ 'Emerging Tech' => 'emerging-tech',
+ 'Cybersecurity' => 'cybersecurity',
+ 'IT Modernization' => 'it-modernization',
+ 'Policy' => 'policy',
+ 'Ideas' => 'ideas',
+ ]
+ ]
+ ]];
- public function collectData(){
- $this->collectExpandableDatas(self::URI . 'rss/' . $this->getInput('category') . '/', 10);
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas(self::URI . 'rss/' . $this->getInput('category') . '/', 10);
+ }
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
- $article_thumbnail = 'https://cdn.nextgov.com/nextgov/images/logo.png';
- $item['content'] = '<p><b>' . $item['content'] . '</b></p>';
+ $article_thumbnail = 'https://cdn.nextgov.com/nextgov/images/logo.png';
+ $item['content'] = '<p><b>' . $item['content'] . '</b></p>';
- $namespaces = $newsItem->getNamespaces(true);
- if(isset($namespaces['media'])) {
- $media = $newsItem->children($namespaces['media']);
- if(isset($media->content)) {
- $attributes = $media->content->attributes();
- $item['content'] = '<p><img src="' . $attributes['url'] . '"></p>' . $item['content'];
- $article_thumbnail = str_replace(
- 'large.jpg',
- 'small.jpg',
- strval($attributes['url'])
- );
- }
- }
+ $namespaces = $newsItem->getNamespaces(true);
+ if (isset($namespaces['media'])) {
+ $media = $newsItem->children($namespaces['media']);
+ if (isset($media->content)) {
+ $attributes = $media->content->attributes();
+ $item['content'] = '<p><img src="' . $attributes['url'] . '"></p>' . $item['content'];
+ $article_thumbnail = str_replace(
+ 'large.jpg',
+ 'small.jpg',
+ strval($attributes['url'])
+ );
+ }
+ }
- $item['enclosures'] = array($article_thumbnail);
- $item['content'] .= $this->extractContent($item['uri']);
- return $item;
- }
+ $item['enclosures'] = [$article_thumbnail];
+ $item['content'] .= $this->extractContent($item['uri']);
+ return $item;
+ }
- private function extractContent($url){
- $article = getSimpleHTMLDOMCached($url);
+ private function extractContent($url)
+ {
+ $article = getSimpleHTMLDOMCached($url);
- if (!is_object($article))
- return 'Could not request Nextgov: ' . $url;
+ if (!is_object($article)) {
+ return 'Could not request Nextgov: ' . $url;
+ }
- $contents = $article->find('div.wysiwyg', 0);
- $contents = $contents->innertext;
- $contents = stripWithDelimiters($contents, '<div class="ad-container">', '</div>');
- $contents = stripWithDelimiters($contents, '<div', '</div>'); //ad outer div
- return trim(stripWithDelimiters($contents, '<script', '</script>'));
- }
+ $contents = $article->find('div.wysiwyg', 0);
+ $contents = $contents->innertext;
+ $contents = stripWithDelimiters($contents, '<div class="ad-container">', '</div>');
+ $contents = stripWithDelimiters($contents, '<div', '</div>'); //ad outer div
+ return trim(stripWithDelimiters($contents, '<script', '</script>'));
+ }
}
diff --git a/bridges/NiceMatinBridge.php b/bridges/NiceMatinBridge.php
index b0af7608..6e622b42 100644
--- a/bridges/NiceMatinBridge.php
+++ b/bridges/NiceMatinBridge.php
@@ -1,32 +1,38 @@
<?php
-class NiceMatinBridge extends FeedExpander {
- const MAINTAINER = 'pit-fgfjiudghdf';
- const NAME = 'NiceMatin';
- const URI = 'https://www.nicematin.com/';
- const DESCRIPTION = 'Returns the 10 newest posts from NiceMatin (full text)';
+class NiceMatinBridge extends FeedExpander
+{
+ const MAINTAINER = 'pit-fgfjiudghdf';
+ const NAME = 'NiceMatin';
+ const URI = 'https://www.nicematin.com/';
+ const DESCRIPTION = 'Returns the 10 newest posts from NiceMatin (full text)';
- public function collectData(){
- $this->collectExpandableDatas(self::URI . 'derniere-minute/rss', 10);
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas(self::URI . 'derniere-minute/rss', 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){
- $html = getSimpleHTMLDOMCached($url);
- if(!$html)
- return 'Could not acquire content from url: ' . $url . '!';
+ private function extractContent($url)
+ {
+ $html = getSimpleHTMLDOMCached($url);
+ if (!$html) {
+ return 'Could not acquire content from url: ' . $url . '!';
+ }
- $content = $html->find('article', 0);
- if(!$content)
- return 'Could not find \'section\'!';
+ $content = $html->find('article', 0);
+ if (!$content) {
+ return 'Could not find \'section\'!';
+ }
- $text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $content->innertext);
- $text = strip_tags($text, '<p><a><img>');
- return $text;
- }
+ $text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $content->innertext);
+ $text = strip_tags($text, '<p><a><img>');
+ return $text;
+ }
}
diff --git a/bridges/NikonDownloadCenterBridge.php b/bridges/NikonDownloadCenterBridge.php
index 88b33c41..143d40f5 100644
--- a/bridges/NikonDownloadCenterBridge.php
+++ b/bridges/NikonDownloadCenterBridge.php
@@ -1,34 +1,39 @@
<?php
-class NikonDownloadCenterBridge extends BridgeAbstract {
- const NAME = 'Nikon Download Center – What\'s New';
- const URI = 'https://downloadcenter.nikonimglib.com/';
- const DESCRIPTION = 'Firmware updates and new software from Nikon.';
- const MAINTAINER = 'sal0max';
- const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours
- public function getURI() {
- $year = date('Y');
- return self::URI . 'en/update/index/' . $year . '.html';
- }
+class NikonDownloadCenterBridge extends BridgeAbstract
+{
+ const NAME = 'Nikon Download Center – What\'s New';
+ const URI = 'https://downloadcenter.nikonimglib.com/';
+ const DESCRIPTION = 'Firmware updates and new software from Nikon.';
+ const MAINTAINER = 'sal0max';
+ const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours
- public function getIcon() {
- return self::URI . 'favicon.ico';
- }
+ public function getURI()
+ {
+ $year = date('Y');
+ return self::URI . 'en/update/index/' . $year . '.html';
+ }
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI());
+ public function getIcon()
+ {
+ return self::URI . 'favicon.ico';
+ }
- foreach ($html->find('dd>ul>li') as $element) {
- $date = $element->find('.date', 0)->plaintext;
- $productType = $element->find('.icon>img', 0)->alt;
- $desc = $element->find('p>a', 0)->plaintext;
- $link = urljoin(self::URI, $element->find('p>a', 0)->href);
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
- $item = array(
- 'title' => $desc,
- 'uri' => $link,
- 'timestamp' => strtotime($date),
- 'content' => <<<EOD
+ foreach ($html->find('dd>ul>li') as $element) {
+ $date = $element->find('.date', 0)->plaintext;
+ $productType = $element->find('.icon>img', 0)->alt;
+ $desc = $element->find('p>a', 0)->plaintext;
+ $link = urljoin(self::URI, $element->find('p>a', 0)->href);
+
+ $item = [
+ 'title' => $desc,
+ 'uri' => $link,
+ 'timestamp' => strtotime($date),
+ 'content' => <<<EOD
<p>
New/updated {$productType}:<br>
<strong><a href="{$link}">{$desc}</a></strong>
@@ -37,8 +42,8 @@ class NikonDownloadCenterBridge extends BridgeAbstract {
{$date}
</p>
EOD
- );
- $this->items[] = $item;
- }
- }
+ ];
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/NineGagBridge.php b/bridges/NineGagBridge.php
index 6b8ef822..61e3975d 100644
--- a/bridges/NineGagBridge.php
+++ b/bridges/NineGagBridge.php
@@ -1,372 +1,388 @@
<?php
-class NineGagBridge extends BridgeAbstract {
- const NAME = '9gag Bridge';
- const URI = 'https://9gag.com/';
- const DESCRIPTION = 'Returns latest quotes from 9gag.';
- const MAINTAINER = 'ZeNairolf';
- const CACHE_TIMEOUT = 3600;
- const PARAMETERS = array(
- 'Popular' => array(
- 'd' => array(
- 'name' => 'Section',
- 'type' => 'list',
- 'values' => array(
- 'Hot' => 'hot',
- 'Trending' => 'trending',
- 'Fresh' => 'fresh',
- ),
- ),
- 'video' => array(
- 'name' => 'Filter Video',
- 'type' => 'list',
- 'values' => array(
- 'NotFiltred' => 'none',
- 'VideoFiltred' => 'without',
- 'VideoOnly' => 'only',
- ),
- ),
- 'p' => array(
- 'name' => 'Pages',
- 'type' => 'number',
- 'defaultValue' => 3,
- ),
- ),
- 'Sections' => array(
- 'g' => array(
- 'name' => 'Section',
- 'type' => 'list',
- 'values' => array(
- 'Among Us' => 'among-us',
- 'Animals' => 'animals',
- 'Anime & Manga' => 'anime-manga',
- 'Anime Waifu' => 'animewaifu',
- 'Anime Wallpaper' => 'animewallpaper',
- 'Apex Legends' => 'apexlegends',
- 'Ask 9GAG' => 'ask9gag',
- 'Awesome' => 'awesome',
- 'Car' => 'car',
- 'Comic & Webtoon' => 'comic-webtoon',
- 'Coronavirus ' => 'coronavirus',
- 'Cosplay' => 'cosplay',
- 'Countryballs' => 'countryballs',
- 'Cozy & Comfy' => 'home-living',
- 'Crappy Design' => 'crappydesign',
- 'Cryptocurrency ' => 'cryptocurrency',
- 'Cyberpunk 2077' => 'cyberpunk2077',
- 'Dark Humor' => 'darkhumor',
- 'Drawing, DIY & Crafts' => 'drawing-diy-crafts',
- 'Fashion & Beauty' => 'rate-my-outfit',
- 'Food & Drinks' => 'food-drinks',
- 'Football' => 'football',
- 'Fortnite' => 'fortnite',
- 'Funny' => 'funny',
- 'Game of Thrones' => 'got',
- 'Gaming' => 'gaming',
- 'GIF' => 'gif',
- 'Girl' => 'girl',
- 'Girl Celebrity' => 'girlcelebrity',
- 'Guy' => 'guy',
- 'History' => 'history',
- 'Horror' => 'horror',
- 'K-Pop' => 'kpop',
- 'Latest News' => 'timely',
- 'League of Legends' => 'leagueoflegends',
- 'LEGO' => 'lego',
- 'Marvel & DC' => 'superhero',
- 'Meme' => 'meme',
- 'Movie & TV' => 'movie-tv',
- 'Music' => 'music',
- 'NBA' => 'basketball',
- 'Overwatch' => 'overwatch',
- 'PC Master Race' => 'pcmr',
- 'Pokémon' => 'pokemon',
- 'Politics ' => 'politics',
- 'PUBG' => 'pubg',
- 'Random ' => 'random',
- 'Relationship' => 'relationship',
- 'Satisfying' => 'satisfying',
- 'Savage' => 'savage',
- 'Science & Tech' => 'science-tech',
- 'Sport ' => 'sport',
- 'Star Wars' => 'starwars',
- 'Teens Can Relate' => 'school',
- 'Travel & Photography' => 'travel-photography',
- 'Video' => 'video',
- 'Wallpaper' => 'wallpaper',
- 'Warhammer' => 'warhammer',
- 'Wholesome' => 'wholesome',
- 'WTF' => 'wtf',
- ),
- ),
- 't' => array(
- 'name' => 'Type',
- 'type' => 'list',
- 'values' => array(
- 'Hot' => 'hot',
- 'Fresh' => 'fresh',
- ),
- ),
- 'video' => array(
- 'name' => 'Filter Video',
- 'type' => 'list',
- 'values' => array(
- 'NotFiltred' => 'none',
- 'VideoFiltred' => 'without',
- 'VideoOnly' => 'only',
- ),
- ),
- 'p' => array(
- 'name' => 'Pages',
- 'type' => 'number',
- 'defaultValue' => 3,
- ),
- ),
- );
-
- const MIN_NBR_PAGE = 1;
- const MAX_NBR_PAGE = 6;
-
- protected $p = null;
-
- public function collectData() {
- $url = sprintf(
- '%sv1/group-posts/group/%s/type/%s?',
- self::URI,
- $this->getGroup(),
- $this->getType()
- );
- $cursor = 'c=10';
- $posts = array();
- for ($i = 0; $i < $this->getPages(); ++$i) {
- $content = getContents($url . $cursor);
- $json = json_decode($content, true);
- $posts = array_merge($posts, $json['data']['posts']);
- $cursor = $json['data']['nextCursor'];
- }
-
- foreach ($posts as $post) {
- $AvoidElement = false;
- switch ($this->getInput('video')) {
- case 'without':
- if ($post['type'] === 'Animated') {
- $AvoidElement = true;
- }
- break;
- case 'only':
- echo $post['type'];
- if ($post['type'] !== 'Animated') {
- $AvoidElement = true;
- }
- break;
- case 'none': default:
- break;
- }
-
- if (!$AvoidElement) {
- $item['uri'] = preg_replace('/^http:/i', 'https:', $post['url']);
- $item['title'] = $post['title'];
- $item['content'] = self::getContent($post);
- $item['categories'] = self::getCategories($post);
- $item['timestamp'] = self::getTimestamp($post);
-
- $this->items[] = $item;
- }
- }
- }
-
- public function getName() {
- if ($this->getInput('d')) {
- $name = sprintf('%s - %s', '9GAG', $this->getParameterKey('d'));
- } elseif ($this->getInput('g')) {
- $name = sprintf('%s - %s', '9GAG', $this->getParameterKey('g'));
- if ($this->getInput('t')) {
- $name = sprintf('%s [%s]', $name, $this->getParameterKey('t'));
- }
- }
- if (!empty($name)) {
- return $name;
- }
-
- return self::NAME;
- }
-
- public function getURI() {
- $uri = $this->getInput('g');
- if ($uri === 'default') {
- $uri = $this->getInput('t');
- }
-
- return self::URI . $uri;
- }
-
- protected function getGroup() {
- if ($this->getInput('d')) {
- return 'default';
- }
-
- return $this->getInput('g');
- }
-
- protected function getType() {
- if ($this->getInput('d')) {
- return $this->getInput('d');
- }
-
- return $this->getInput('t');
- }
-
- protected function getPages() {
- if ($this->p === null) {
- $value = (int) $this->getInput('p');
- $value = ($value < self::MIN_NBR_PAGE) ? self::MIN_NBR_PAGE : $value;
- $value = ($value > self::MAX_NBR_PAGE) ? self::MAX_NBR_PAGE : $value;
-
- $this->p = $value;
- }
-
- return $this->p;
- }
-
- protected function getParameterKey($input = '') {
- $params = $this->getParameters();
- $tab = 'Sections';
- if ($input === 'd') {
- $tab = 'Popular';
- }
- if (!isset($params[$tab][$input])) {
- return '';
- }
-
- return array_search(
- $this->getInput($input),
- $params[$tab][$input]['values']
- );
- }
-
- protected static function getContent($post) {
- if ($post['type'] === 'Animated') {
- $content = self::getAnimated($post);
- } elseif ($post['type'] === 'Article') {
- $content = self::getArticle($post);
- } else {
- $content = self::getPhoto($post);
- }
-
- return $content;
- }
-
- protected static function getPhoto($post) {
- $image = $post['images']['image460'];
- $photo = '<picture>';
- $photo .= sprintf(
- '<source srcset="%s" type="image/webp">',
- $image['webpUrl']
- );
- $photo .= sprintf(
- '<img src="%s" alt="%s" %s>',
- $image['url'],
- $post['title'],
- 'width="500"'
- );
- $photo .= '</picture>';
-
- return $photo;
- }
-
- protected static function getAnimated($post) {
- $poster = $post['images']['image460']['url'];
- $sources = $post['images'];
- $video = sprintf(
- '<video poster="%s" %s>',
- $poster,
- 'preload="auto" loop controls style="min-height: 300px" width="500"'
- );
- $video .= sprintf(
- '<source src="%s" type="video/webm">',
- $sources['image460sv']['vp9Url']
- );
- $video .= sprintf(
- '<source src="%s" type="video/mp4">',
- $sources['image460sv']['h265Url']
- );
- $video .= sprintf(
- '<source src="%s" type="video/mp4">',
- $sources['image460svwm']['url']
- );
- $video .= '</video>';
-
- return $video;
- }
-
- protected static function getArticle($post) {
- $blocks = $post['article']['blocks'];
- $medias = $post['article']['medias'];
- $contents = array();
- foreach ($blocks as $block) {
- if ('Media' === $block['type']) {
- $mediaId = $block['mediaId'];
- $contents[] = self::getContent($medias[$mediaId]);
- } elseif ('RichText' === $block['type']) {
- $contents[] = self::getRichText($block['content']);
- }
- }
-
- $content = join('</div><div>', $contents);
- $content = sprintf(
- '<%1$s>%2$s</%1$s>',
- 'div',
- $content
- );
-
- return $content;
- }
-
- protected static function getRichText($text = '') {
- $text = trim($text);
-
- if (preg_match('/^>\s(?<text>.*)/', $text, $matches)) {
- $text = sprintf(
- '<%1$s>%2$s</%1$s>',
- 'blockquote',
- $matches['text']
- );
- } else {
- $text = sprintf(
- '<%1$s>%2$s</%1$s>',
- 'p',
- $text
- );
- }
-
- return $text;
- }
-
- protected static function getCategories($post) {
- $params = self::PARAMETERS;
- $sections = $params['Sections']['g']['values'];
-
- if(isset($post['sections'])) {
- $postSections = $post['sections'];
- } elseif (isset($post['postSection'])) {
- $postSections = array($post['postSection']);
- } else {
- $postSections = array();
- }
-
- foreach ($postSections as $key => $section) {
- $postSections[$key] = array_search($section, $sections);
- }
-
- return $postSections;
- }
-
- protected static function getTimestamp($post) {
- $url = $post['images']['image460']['url'];
- $headers = get_headers($url, true);
- $date = $headers['Date'];
- $time = strtotime($date);
-
- return $time;
- }
+class NineGagBridge extends BridgeAbstract
+{
+ const NAME = '9gag Bridge';
+ const URI = 'https://9gag.com/';
+ const DESCRIPTION = 'Returns latest quotes from 9gag.';
+ const MAINTAINER = 'ZeNairolf';
+ const CACHE_TIMEOUT = 3600;
+ const PARAMETERS = [
+ 'Popular' => [
+ 'd' => [
+ 'name' => 'Section',
+ 'type' => 'list',
+ 'values' => [
+ 'Hot' => 'hot',
+ 'Trending' => 'trending',
+ 'Fresh' => 'fresh',
+ ],
+ ],
+ 'video' => [
+ 'name' => 'Filter Video',
+ 'type' => 'list',
+ 'values' => [
+ 'NotFiltred' => 'none',
+ 'VideoFiltred' => 'without',
+ 'VideoOnly' => 'only',
+ ],
+ ],
+ 'p' => [
+ 'name' => 'Pages',
+ 'type' => 'number',
+ 'defaultValue' => 3,
+ ],
+ ],
+ 'Sections' => [
+ 'g' => [
+ 'name' => 'Section',
+ 'type' => 'list',
+ 'values' => [
+ 'Among Us' => 'among-us',
+ 'Animals' => 'animals',
+ 'Anime & Manga' => 'anime-manga',
+ 'Anime Waifu' => 'animewaifu',
+ 'Anime Wallpaper' => 'animewallpaper',
+ 'Apex Legends' => 'apexlegends',
+ 'Ask 9GAG' => 'ask9gag',
+ 'Awesome' => 'awesome',
+ 'Car' => 'car',
+ 'Comic & Webtoon' => 'comic-webtoon',
+ 'Coronavirus ' => 'coronavirus',
+ 'Cosplay' => 'cosplay',
+ 'Countryballs' => 'countryballs',
+ 'Cozy & Comfy' => 'home-living',
+ 'Crappy Design' => 'crappydesign',
+ 'Cryptocurrency ' => 'cryptocurrency',
+ 'Cyberpunk 2077' => 'cyberpunk2077',
+ 'Dark Humor' => 'darkhumor',
+ 'Drawing, DIY & Crafts' => 'drawing-diy-crafts',
+ 'Fashion & Beauty' => 'rate-my-outfit',
+ 'Food & Drinks' => 'food-drinks',
+ 'Football' => 'football',
+ 'Fortnite' => 'fortnite',
+ 'Funny' => 'funny',
+ 'Game of Thrones' => 'got',
+ 'Gaming' => 'gaming',
+ 'GIF' => 'gif',
+ 'Girl' => 'girl',
+ 'Girl Celebrity' => 'girlcelebrity',
+ 'Guy' => 'guy',
+ 'History' => 'history',
+ 'Horror' => 'horror',
+ 'K-Pop' => 'kpop',
+ 'Latest News' => 'timely',
+ 'League of Legends' => 'leagueoflegends',
+ 'LEGO' => 'lego',
+ 'Marvel & DC' => 'superhero',
+ 'Meme' => 'meme',
+ 'Movie & TV' => 'movie-tv',
+ 'Music' => 'music',
+ 'NBA' => 'basketball',
+ 'Overwatch' => 'overwatch',
+ 'PC Master Race' => 'pcmr',
+ 'Pokémon' => 'pokemon',
+ 'Politics ' => 'politics',
+ 'PUBG' => 'pubg',
+ 'Random ' => 'random',
+ 'Relationship' => 'relationship',
+ 'Satisfying' => 'satisfying',
+ 'Savage' => 'savage',
+ 'Science & Tech' => 'science-tech',
+ 'Sport ' => 'sport',
+ 'Star Wars' => 'starwars',
+ 'Teens Can Relate' => 'school',
+ 'Travel & Photography' => 'travel-photography',
+ 'Video' => 'video',
+ 'Wallpaper' => 'wallpaper',
+ 'Warhammer' => 'warhammer',
+ 'Wholesome' => 'wholesome',
+ 'WTF' => 'wtf',
+ ],
+ ],
+ 't' => [
+ 'name' => 'Type',
+ 'type' => 'list',
+ 'values' => [
+ 'Hot' => 'hot',
+ 'Fresh' => 'fresh',
+ ],
+ ],
+ 'video' => [
+ 'name' => 'Filter Video',
+ 'type' => 'list',
+ 'values' => [
+ 'NotFiltred' => 'none',
+ 'VideoFiltred' => 'without',
+ 'VideoOnly' => 'only',
+ ],
+ ],
+ 'p' => [
+ 'name' => 'Pages',
+ 'type' => 'number',
+ 'defaultValue' => 3,
+ ],
+ ],
+ ];
+
+ const MIN_NBR_PAGE = 1;
+ const MAX_NBR_PAGE = 6;
+
+ protected $p = null;
+
+ public function collectData()
+ {
+ $url = sprintf(
+ '%sv1/group-posts/group/%s/type/%s?',
+ self::URI,
+ $this->getGroup(),
+ $this->getType()
+ );
+ $cursor = 'c=10';
+ $posts = [];
+ for ($i = 0; $i < $this->getPages(); ++$i) {
+ $content = getContents($url . $cursor);
+ $json = json_decode($content, true);
+ $posts = array_merge($posts, $json['data']['posts']);
+ $cursor = $json['data']['nextCursor'];
+ }
+
+ foreach ($posts as $post) {
+ $AvoidElement = false;
+ switch ($this->getInput('video')) {
+ case 'without':
+ if ($post['type'] === 'Animated') {
+ $AvoidElement = true;
+ }
+ break;
+ case 'only':
+ echo $post['type'];
+ if ($post['type'] !== 'Animated') {
+ $AvoidElement = true;
+ }
+ break;
+ case 'none':
+ default:
+ break;
+ }
+
+ if (!$AvoidElement) {
+ $item['uri'] = preg_replace('/^http:/i', 'https:', $post['url']);
+ $item['title'] = $post['title'];
+ $item['content'] = self::getContent($post);
+ $item['categories'] = self::getCategories($post);
+ $item['timestamp'] = self::getTimestamp($post);
+
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ public function getName()
+ {
+ if ($this->getInput('d')) {
+ $name = sprintf('%s - %s', '9GAG', $this->getParameterKey('d'));
+ } elseif ($this->getInput('g')) {
+ $name = sprintf('%s - %s', '9GAG', $this->getParameterKey('g'));
+ if ($this->getInput('t')) {
+ $name = sprintf('%s [%s]', $name, $this->getParameterKey('t'));
+ }
+ }
+ if (!empty($name)) {
+ return $name;
+ }
+
+ return self::NAME;
+ }
+
+ public function getURI()
+ {
+ $uri = $this->getInput('g');
+ if ($uri === 'default') {
+ $uri = $this->getInput('t');
+ }
+
+ return self::URI . $uri;
+ }
+
+ protected function getGroup()
+ {
+ if ($this->getInput('d')) {
+ return 'default';
+ }
+
+ return $this->getInput('g');
+ }
+
+ protected function getType()
+ {
+ if ($this->getInput('d')) {
+ return $this->getInput('d');
+ }
+
+ return $this->getInput('t');
+ }
+
+ protected function getPages()
+ {
+ if ($this->p === null) {
+ $value = (int) $this->getInput('p');
+ $value = ($value < self::MIN_NBR_PAGE) ? self::MIN_NBR_PAGE : $value;
+ $value = ($value > self::MAX_NBR_PAGE) ? self::MAX_NBR_PAGE : $value;
+
+ $this->p = $value;
+ }
+
+ return $this->p;
+ }
+
+ protected function getParameterKey($input = '')
+ {
+ $params = $this->getParameters();
+ $tab = 'Sections';
+ if ($input === 'd') {
+ $tab = 'Popular';
+ }
+ if (!isset($params[$tab][$input])) {
+ return '';
+ }
+
+ return array_search(
+ $this->getInput($input),
+ $params[$tab][$input]['values']
+ );
+ }
+
+ protected static function getContent($post)
+ {
+ if ($post['type'] === 'Animated') {
+ $content = self::getAnimated($post);
+ } elseif ($post['type'] === 'Article') {
+ $content = self::getArticle($post);
+ } else {
+ $content = self::getPhoto($post);
+ }
+
+ return $content;
+ }
+
+ protected static function getPhoto($post)
+ {
+ $image = $post['images']['image460'];
+ $photo = '<picture>';
+ $photo .= sprintf(
+ '<source srcset="%s" type="image/webp">',
+ $image['webpUrl']
+ );
+ $photo .= sprintf(
+ '<img src="%s" alt="%s" %s>',
+ $image['url'],
+ $post['title'],
+ 'width="500"'
+ );
+ $photo .= '</picture>';
+
+ return $photo;
+ }
+
+ protected static function getAnimated($post)
+ {
+ $poster = $post['images']['image460']['url'];
+ $sources = $post['images'];
+ $video = sprintf(
+ '<video poster="%s" %s>',
+ $poster,
+ 'preload="auto" loop controls style="min-height: 300px" width="500"'
+ );
+ $video .= sprintf(
+ '<source src="%s" type="video/webm">',
+ $sources['image460sv']['vp9Url']
+ );
+ $video .= sprintf(
+ '<source src="%s" type="video/mp4">',
+ $sources['image460sv']['h265Url']
+ );
+ $video .= sprintf(
+ '<source src="%s" type="video/mp4">',
+ $sources['image460svwm']['url']
+ );
+ $video .= '</video>';
+
+ return $video;
+ }
+
+ protected static function getArticle($post)
+ {
+ $blocks = $post['article']['blocks'];
+ $medias = $post['article']['medias'];
+ $contents = [];
+ foreach ($blocks as $block) {
+ if ('Media' === $block['type']) {
+ $mediaId = $block['mediaId'];
+ $contents[] = self::getContent($medias[$mediaId]);
+ } elseif ('RichText' === $block['type']) {
+ $contents[] = self::getRichText($block['content']);
+ }
+ }
+
+ $content = join('</div><div>', $contents);
+ $content = sprintf(
+ '<%1$s>%2$s</%1$s>',
+ 'div',
+ $content
+ );
+
+ return $content;
+ }
+
+ protected static function getRichText($text = '')
+ {
+ $text = trim($text);
+
+ if (preg_match('/^>\s(?<text>.*)/', $text, $matches)) {
+ $text = sprintf(
+ '<%1$s>%2$s</%1$s>',
+ 'blockquote',
+ $matches['text']
+ );
+ } else {
+ $text = sprintf(
+ '<%1$s>%2$s</%1$s>',
+ 'p',
+ $text
+ );
+ }
+
+ return $text;
+ }
+
+ protected static function getCategories($post)
+ {
+ $params = self::PARAMETERS;
+ $sections = $params['Sections']['g']['values'];
+
+ if (isset($post['sections'])) {
+ $postSections = $post['sections'];
+ } elseif (isset($post['postSection'])) {
+ $postSections = [$post['postSection']];
+ } else {
+ $postSections = [];
+ }
+
+ foreach ($postSections as $key => $section) {
+ $postSections[$key] = array_search($section, $sections);
+ }
+
+ return $postSections;
+ }
+
+ protected static function getTimestamp($post)
+ {
+ $url = $post['images']['image460']['url'];
+ $headers = get_headers($url, true);
+ $date = $headers['Date'];
+ $time = strtotime($date);
+
+ return $time;
+ }
}
diff --git a/bridges/NordbayernBridge.php b/bridges/NordbayernBridge.php
index ded9c682..7a80f930 100644
--- a/bridges/NordbayernBridge.php
+++ b/bridges/NordbayernBridge.php
@@ -1,163 +1,177 @@
<?php
-class NordbayernBridge extends BridgeAbstract {
+class NordbayernBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'schabi.org';
+ const NAME = 'Nordbayern';
+ const CACHE_TIMEOUT = 3600;
+ const URI = 'https://www.nordbayern.de';
+ const DESCRIPTION = 'Bridge for Bavarian regional news site nordbayern.de';
+ const PARAMETERS = [ [
+ 'region' => [
+ 'name' => 'region',
+ 'type' => 'list',
+ 'exampleValue' => 'Nürnberg',
+ 'title' => 'Select a region',
+ 'values' => [
+ 'Nürnberg' => 'nuernberg',
+ 'Fürth' => 'fuerth',
+ 'Erlangen' => 'erlangen',
+ 'Altdorf' => 'altdorf',
+ 'Ansbach' => 'ansbach',
+ 'Bad Windsheim' => 'bad-windsheim',
+ 'Bamberg' => 'bamberg',
+ 'Dinkelsbühl/Feuchtwangen' => 'dinkelsbuehl-feuchtwangen',
+ 'Feucht' => 'feucht',
+ 'Forchheim' => 'forchheim',
+ 'Gunzenhausen' => 'gunzenhausen',
+ 'Hersbruck' => 'hersbruck',
+ 'Herzogenaurach' => 'herzogenaurach',
+ 'Hilpoltstein' => 'hilpoltstein',
+ 'Höchstadt' => 'hoechstadt',
+ 'Lauf' => 'lauf',
+ 'Neumarkt' => 'neumarkt',
+ 'Neustadt/Aisch' => 'neustadt-aisch',
+ 'Pegnitz' => 'pegnitz',
+ 'Roth' => 'roth',
+ 'Rothenburg o.d.T.' => 'rothenburg-o-d-t',
+ 'Treuchtlingen' => 'treuchtlingen',
+ 'Weißenburg' => 'weissenburg'
+ ]
+ ],
+ 'policeReports' => [
+ 'name' => 'Police Reports',
+ 'type' => 'checkbox',
+ 'exampleValue' => 'checked',
+ 'title' => 'Include Police Reports',
+ ]
+ ]];
- const MAINTAINER = 'schabi.org';
- const NAME = 'Nordbayern';
- const CACHE_TIMEOUT = 3600;
- const URI = 'https://www.nordbayern.de';
- const DESCRIPTION = 'Bridge for Bavarian regional news site nordbayern.de';
- const PARAMETERS = array( array(
- 'region' => array(
- 'name' => 'region',
- 'type' => 'list',
- 'exampleValue' => 'Nürnberg',
- 'title' => 'Select a region',
- 'values' => array(
- 'Nürnberg' => 'nuernberg',
- 'Fürth' => 'fuerth',
- 'Erlangen' => 'erlangen',
- 'Altdorf' => 'altdorf',
- 'Ansbach' => 'ansbach',
- 'Bad Windsheim' => 'bad-windsheim',
- 'Bamberg' => 'bamberg',
- 'Dinkelsbühl/Feuchtwangen' => 'dinkelsbuehl-feuchtwangen',
- 'Feucht' => 'feucht',
- 'Forchheim' => 'forchheim',
- 'Gunzenhausen' => 'gunzenhausen',
- 'Hersbruck' => 'hersbruck',
- 'Herzogenaurach' => 'herzogenaurach',
- 'Hilpoltstein' => 'hilpoltstein',
- 'Höchstadt' => 'hoechstadt',
- 'Lauf' => 'lauf',
- 'Neumarkt' => 'neumarkt',
- 'Neustadt/Aisch' => 'neustadt-aisch',
- 'Pegnitz' => 'pegnitz',
- 'Roth' => 'roth',
- 'Rothenburg o.d.T.' => 'rothenburg-o-d-t',
- 'Treuchtlingen' => 'treuchtlingen',
- 'Weißenburg' => 'weissenburg'
- )
- ),
- 'policeReports' => array(
- 'name' => 'Police Reports',
- 'type' => 'checkbox',
- 'exampleValue' => 'checked',
- 'title' => 'Include Police Reports',
- )
- ));
+ private function getValidImage($picture)
+ {
+ $img = $picture->find('img', 0);
+ if ($img) {
+ $imgUrl = $img->src;
+ if (!preg_match('#/logo-.*\.png#', $imgUrl)) {
+ return '<br><img src="' . $imgUrl . '">';
+ }
+ }
+ return '';
+ }
- private function getValidImage($picture) {
- $img = $picture->find('img', 0);
- if ($img) {
- $imgUrl = $img->src;
- if(!preg_match('#/logo-.*\.png#', $imgUrl)) {
- return '<br><img src="' . $imgUrl . '">';
- }
- }
- return '';
- }
+ private function getUseFullContent($rawContent)
+ {
+ $content = '';
+ foreach ($rawContent->children as $element) {
+ if (
+ ($element->tag === 'p' || $element->tag === 'h3') &&
+ $element->class !== 'article__teaser'
+ ) {
+ $content .= $element;
+ } elseif ($element->tag === 'main') {
+ $content .= self::getUseFullContent($element->find('article', 0));
+ } elseif ($element->tag === 'header') {
+ $content .= self::getUseFullContent($element);
+ } elseif (
+ $element->tag === 'div' &&
+ !str_contains($element->class, 'article__infobox') &&
+ !str_contains($element->class, 'authorinfo')
+ ) {
+ $content .= self::getUseFullContent($element);
+ } elseif (
+ $element->tag === 'section' &&
+ (str_contains($element->class, 'article__richtext') ||
+ str_contains($element->class, 'article__context'))
+ ) {
+ $content .= self::getUseFullContent($element);
+ } elseif ($element->tag === 'picture') {
+ $content .= self::getValidImage($element);
+ }
+ }
+ return $content;
+ }
- private function getUseFullContent($rawContent) {
- $content = '';
- foreach($rawContent->children as $element) {
- if(($element->tag === 'p' || $element->tag === 'h3') &&
- $element->class !== 'article__teaser') {
- $content .= $element;
- } else if($element->tag === 'main') {
- $content .= self::getUseFullContent($element->find('article', 0));
- } else if($element->tag === 'header') {
- $content .= self::getUseFullContent($element);
- } else if($element->tag === 'div' &&
- !str_contains($element->class, 'article__infobox') &&
- !str_contains($element->class, 'authorinfo')) {
- $content .= self::getUseFullContent($element);
- } else if($element->tag === 'section' &&
- (str_contains($element->class, 'article__richtext') ||
- str_contains($element->class, 'article__context'))) {
- $content .= self::getUseFullContent($element);
- } else if($element->tag === 'picture') {
- $content .= self::getValidImage($element);
- }
- }
- return $content;
- }
+ private function getTeaser($content)
+ {
+ $teaser = $content->find('p[class=article__teaser]', 0);
+ if ($teaser === null) {
+ return '';
+ }
+ $teaser = $teaser->plaintext;
+ $teaser = preg_replace('/[ ]{2,}/', ' ', $teaser);
+ $teaser = '<p class="article__teaser">' . $teaser . '</p>';
+ return $teaser;
+ }
- private function getTeaser($content) {
- $teaser = $content->find('p[class=article__teaser]', 0);
- if($teaser === null) {
- return '';
- }
- $teaser = $teaser->plaintext;
- $teaser = preg_replace('/[ ]{2,}/', ' ', $teaser);
- $teaser = '<p class="article__teaser">' . $teaser . '</p>';
- return $teaser;
- }
+ private function handleArticle($link)
+ {
+ $item = [];
+ $article = getSimpleHTMLDOM($link);
+ defaultLinkTo($article, self::URI);
+ $content = $article->find('article[id=article]', 0);
+ $item['uri'] = $link;
- private function handleArticle($link) {
- $item = array();
- $article = getSimpleHTMLDOM($link);
- defaultLinkTo($article, self::URI);
- $content = $article->find('article[id=article]', 0);
- $item['uri'] = $link;
+ $author = $article->find('.article__author', 1);
+ if ($author !== null) {
+ $item['author'] = trim($author->plaintext);
+ }
- $author = $article->find('.article__author', 1);
- if ($author !== null) {
- $item['author'] = trim($author->plaintext);
- }
+ $createdAt = $article->find('[class=article__release]', 0);
+ if ($createdAt) {
+ $item['timestamp'] = strtotime(str_replace('Uhr', '', $createdAt->plaintext));
+ }
- $createdAt = $article->find('[class=article__release]', 0);
- if ($createdAt) {
- $item['timestamp'] = strtotime(str_replace('Uhr', '', $createdAt->plaintext));
- }
+ if ($article->find('h2', 0) === null) {
+ $item['title'] = $article->find('h3', 0)->innertext;
+ } else {
+ $item['title'] = $article->find('h2', 0)->innertext;
+ }
+ $item['content'] = '';
- if ($article->find('h2', 0) === null) {
- $item['title'] = $article->find('h3', 0)->innertext;
- } else {
- $item['title'] = $article->find('h2', 0)->innertext;
- }
- $item['content'] = '';
+ if ($article->find('section[class*=article__richtext]', 0) === null) {
+ $content = $article->find('div[class*=modul__teaser]', 0)
+ ->find('p', 0);
+ $item['content'] .= $content;
+ } else {
+ $content = $article->find('article', 0);
+ // change order of article teaser in order to show it on top
+ // of the title image. If we didn't do this some rss programs
+ // would show the subtitle of the title image as teaser instead
+ // of the actuall article teaser.
+ $item['content'] .= self::getTeaser($content);
+ $item['content'] .= self::getUseFullContent($content);
+ }
- if ($article->find('section[class*=article__richtext]', 0) === null) {
- $content = $article->find('div[class*=modul__teaser]', 0)
- ->find('p', 0);
- $item['content'] .= $content;
- } else {
- $content = $article->find('article', 0);
- // change order of article teaser in order to show it on top
- // of the title image. If we didn't do this some rss programs
- // would show the subtitle of the title image as teaser instead
- // of the actuall article teaser.
- $item['content'] .= self::getTeaser($content);
- $item['content'] .= self::getUseFullContent($content);
- }
+ // exclude police reports if desired
+ if (
+ $this->getInput('policeReports') ||
+ !str_contains($item['content'], 'Hier geht es zu allen aktuellen Polizeimeldungen.')
+ ) {
+ $this->items[] = $item;
+ }
- // exclude police reports if desired
- if($this->getInput('policeReports') ||
- !str_contains($item['content'], 'Hier geht es zu allen aktuellen Polizeimeldungen.')) {
- $this->items[] = $item;
- }
+ $article->clear();
+ }
- $article->clear();
- }
+ private function handleNewsblock($listSite)
+ {
+ $main = $listSite->find('main', 0);
+ foreach ($main->find('article') as $article) {
+ $url = $article->find('a', 0)->href;
+ $url = urljoin(self::URI, $url);
+ self::handleArticle($url);
+ }
+ }
- private function handleNewsblock($listSite) {
- $main = $listSite->find('main', 0);
- foreach($main->find('article') as $article) {
- $url = $article->find('a', 0)->href;
- $url = urljoin(self::URI, $url);
- self::handleArticle($url);
- }
- }
+ public function collectData()
+ {
+ $region = $this->getInput('region');
+ if ($region === 'rothenburg-o-d-t') {
+ $region = 'rothenburg-ob-der-tauber';
+ }
+ $url = self::URI . '/region/' . $region;
+ $listSite = getSimpleHTMLDOM($url);
- public function collectData() {
- $region = $this->getInput('region');
- if($region === 'rothenburg-o-d-t') {
- $region = 'rothenburg-ob-der-tauber';
- }
- $url = self::URI . '/region/' . $region;
- $listSite = getSimpleHTMLDOM($url);
-
- self::handleNewsblock($listSite);
- }
+ self::handleNewsblock($listSite);
+ }
}
diff --git a/bridges/NotAlwaysBridge.php b/bridges/NotAlwaysBridge.php
index f8e56cd6..33b619ad 100644
--- a/bridges/NotAlwaysBridge.php
+++ b/bridges/NotAlwaysBridge.php
@@ -1,59 +1,64 @@
<?php
-class NotAlwaysBridge extends BridgeAbstract {
-
- const MAINTAINER = 'mozes';
- const NAME = 'Not Always family Bridge';
- const URI = 'https://notalwaysright.com/';
- const DESCRIPTION = 'Returns the latest stories';
- const CACHE_TIMEOUT = 1800; // 30 minutes
-
- const PARAMETERS = array( array(
- 'filter' => array(
- 'type' => 'list',
- 'name' => 'Filter',
- 'values' => array(
- 'All' => '',
- 'Right' => 'right',
- 'Working' => 'working',
- 'Romantic' => 'romantic',
- 'Related' => 'related',
- 'Learning' => 'learning',
- 'Friendly' => 'friendly',
- 'Hopeless' => 'hopeless',
- 'Unfiltered' => 'unfiltered'
- )
- )
- ));
-
- public function getIcon() {
- return self::URI . 'favicon_nar.png';
- }
-
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI());
- foreach($html->find('.post') as $post) {
- #print_r($post);
- $item = array();
- $item['uri'] = $post->find('h1', 0)->find('a', 0)->href;
- $item['content'] = $post;
- $item['title'] = $post->find('h1', 0)->find('a', 0)->innertext;
- $this->items[] = $item;
- }
- }
-
- public function getName(){
- if(!is_null($this->getInput('filter'))) {
- return $this->getInput('filter') . ' - NotAlways Bridge';
- }
-
- return parent::getName();
- }
-
- public function getURI(){
- if(!is_null($this->getInput('filter'))) {
- return self::URI . $this->getInput('filter') . '/';
- }
-
- return parent::getURI();
- }
+
+class NotAlwaysBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'mozes';
+ const NAME = 'Not Always family Bridge';
+ const URI = 'https://notalwaysright.com/';
+ const DESCRIPTION = 'Returns the latest stories';
+ const CACHE_TIMEOUT = 1800; // 30 minutes
+
+ const PARAMETERS = [ [
+ 'filter' => [
+ 'type' => 'list',
+ 'name' => 'Filter',
+ 'values' => [
+ 'All' => '',
+ 'Right' => 'right',
+ 'Working' => 'working',
+ 'Romantic' => 'romantic',
+ 'Related' => 'related',
+ 'Learning' => 'learning',
+ 'Friendly' => 'friendly',
+ 'Hopeless' => 'hopeless',
+ 'Unfiltered' => 'unfiltered'
+ ]
+ ]
+ ]];
+
+ public function getIcon()
+ {
+ return self::URI . 'favicon_nar.png';
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+ foreach ($html->find('.post') as $post) {
+ #print_r($post);
+ $item = [];
+ $item['uri'] = $post->find('h1', 0)->find('a', 0)->href;
+ $item['content'] = $post;
+ $item['title'] = $post->find('h1', 0)->find('a', 0)->innertext;
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('filter'))) {
+ return $this->getInput('filter') . ' - NotAlways Bridge';
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('filter'))) {
+ return self::URI . $this->getInput('filter') . '/';
+ }
+
+ return parent::getURI();
+ }
}
diff --git a/bridges/NovayaGazetaEuropeBridge.php b/bridges/NovayaGazetaEuropeBridge.php
index c7511a31..fb8c0e77 100644
--- a/bridges/NovayaGazetaEuropeBridge.php
+++ b/bridges/NovayaGazetaEuropeBridge.php
@@ -1,141 +1,145 @@
<?php
+
class NovayaGazetaEuropeBridge extends BridgeAbstract
{
+ const MAINTAINER = 'sqrtminusone';
+ const NAME = 'Novaya Gazeta Europe Bridge';
+ const URI = 'https://novayagazeta.eu';
- const MAINTAINER = 'sqrtminusone';
- const NAME = 'Novaya Gazeta Europe Bridge';
- const URI = 'https://novayagazeta.eu';
-
- const CACHE_TIMEOUT = 3600; // 1 hour
- const DESCRIPTION = 'Returns articles from Novaya Gazeta Europe';
+ const CACHE_TIMEOUT = 3600; // 1 hour
+ const DESCRIPTION = 'Returns articles from Novaya Gazeta Europe';
- const PARAMETERS = array(
- '' => array(
- 'language' => array(
- 'name' => 'Language',
- 'type' => 'list',
- 'defaultValue' => 'ru',
- 'values' => array(
- 'Russian' => 'ru',
- 'English' => 'en',
- )
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Maximum number of items to return',
- 'defaultValue' => 20
- )
- )
- );
+ const PARAMETERS = [
+ '' => [
+ 'language' => [
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'defaultValue' => 'ru',
+ 'values' => [
+ 'Russian' => 'ru',
+ 'English' => 'en',
+ ]
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Maximum number of items to return',
+ 'defaultValue' => 20
+ ]
+ ]
+ ];
- public function collectData()
- {
- $url = 'https://novayagazeta.eu/api/v1/get/main';
- if ($this->getInput('language') != 'ru') {
- $url .= '?lang=' . $this->getInput('language');
- }
+ public function collectData()
+ {
+ $url = 'https://novayagazeta.eu/api/v1/get/main';
+ if ($this->getInput('language') != 'ru') {
+ $url .= '?lang=' . $this->getInput('language');
+ }
- $json = getContents($url);
- $data = json_decode($json);
+ $json = getContents($url);
+ $data = json_decode($json);
- foreach ($data->records as $record) {
- foreach ($record->blocks as $block) {
- if (!property_exists($block, 'date')) {
- continue;
- }
- $title = strip_tags($block->title);
- if (!empty($block->subtitle)) {
- $title .= '. ' . strip_tags($block->subtitle);
- }
- $item = array(
- 'uri' => self::URI . '/articles/' . $block->slug,
- 'block' => $block,
- 'title' => $title,
- 'author' => join(', ', array_map(function ($author) {
- return $author->name;
- }, $block->authors)),
- 'timestamp' => $block->date / 1000,
- 'categories' => $block->tags
- );
- $this->items[] = $item;
- }
- }
- usort($this->items, function ($item1, $item2) {
- return $item2['timestamp'] <=> $item1['timestamp'];
- });
- if ($this->getInput('limit') !== null) {
- $this->items = array_slice($this->items, 0, $this->getInput('limit'));
- }
- foreach ($this->items as &$item) {
- $block = $item['block'];
- $body = '';
- if (property_exists($block, 'body') && $block->body !== null) {
- $body = self::convertBody($block);
- } else {
- $record_json = getContents("https://novayagazeta.eu/api/v1/get/record?slug={$block->slug}");
- $record_data = json_decode($record_json);
- $body = self::convertBody($record_data->record);
- }
- $item['content'] = $body;
- unset($item['block']);
- }
- }
+ foreach ($data->records as $record) {
+ foreach ($record->blocks as $block) {
+ if (!property_exists($block, 'date')) {
+ continue;
+ }
+ $title = strip_tags($block->title);
+ if (!empty($block->subtitle)) {
+ $title .= '. ' . strip_tags($block->subtitle);
+ }
+ $item = [
+ 'uri' => self::URI . '/articles/' . $block->slug,
+ 'block' => $block,
+ 'title' => $title,
+ 'author' => join(', ', array_map(function ($author) {
+ return $author->name;
+ }, $block->authors)),
+ 'timestamp' => $block->date / 1000,
+ 'categories' => $block->tags
+ ];
+ $this->items[] = $item;
+ }
+ }
+ usort($this->items, function ($item1, $item2) {
+ return $item2['timestamp'] <=> $item1['timestamp'];
+ });
+ if ($this->getInput('limit') !== null) {
+ $this->items = array_slice($this->items, 0, $this->getInput('limit'));
+ }
+ foreach ($this->items as &$item) {
+ $block = $item['block'];
+ $body = '';
+ if (property_exists($block, 'body') && $block->body !== null) {
+ $body = self::convertBody($block);
+ } else {
+ $record_json = getContents("https://novayagazeta.eu/api/v1/get/record?slug={$block->slug}");
+ $record_data = json_decode($record_json);
+ $body = self::convertBody($record_data->record);
+ }
+ $item['content'] = $body;
+ unset($item['block']);
+ }
+ }
- private function convertBody($data) {
- $body = '';
- if ($data->previewUrl !== null && !$data->isPreviewHidden) {
- $body .= '<figure><img src="' . $data->previewUrl . '"/>';
- if ($data->previewCaption !== null) {
- $body .= '<figcaption>' . $data->previewCaption . '</figcaption>';
- }
- $body .= '</figure>';
- }
- if ($data->lead !== null) {
- $body .= "<p><b>{$data->lead}</b></p>";
- }
- if (!empty($data->body)) {
- foreach ($data->body as $datum) {
- $body .= self::convertElement($datum);
- }
- }
- return $body;
- }
+ private function convertBody($data)
+ {
+ $body = '';
+ if ($data->previewUrl !== null && !$data->isPreviewHidden) {
+ $body .= '<figure><img src="' . $data->previewUrl . '"/>';
+ if ($data->previewCaption !== null) {
+ $body .= '<figcaption>' . $data->previewCaption . '</figcaption>';
+ }
+ $body .= '</figure>';
+ }
+ if ($data->lead !== null) {
+ $body .= "<p><b>{$data->lead}</b></p>";
+ }
+ if (!empty($data->body)) {
+ foreach ($data->body as $datum) {
+ $body .= self::convertElement($datum);
+ }
+ }
+ return $body;
+ }
- private function convertElement($datum) {
- switch ($datum->type) {
- case 'text':
- return $datum->data;
- case 'image/single':
- $alt = strip_tags($datum->data);
- $res = "<figure><img src=\"{$datum->previewUrl}\" alt=\"{$alt}\" />";
- if ($datum->data !== null) {
- $res .= "<figcaption>{$datum->data}</figcaption>";
- }
- $res .= '</figure>';
- return $res;
- case 'text/quote':
- return "<figure><blockquote>{$datum->data}</blockquote></figure><br>";
- case 'embed/native':
- $desc = $datum->link;
- if (property_exists($datum, 'caption')) {
- $desc = $datum->caption;
- }
- return "<p><a link=\"{$datum->link}\">{$desc}</a></p>";
- case 'text/framed':
- $res = '';
- if (property_exists($datum, 'typeDisplay')) {
- $res .= "<p><b>{$datum->typeDisplay}</b></p>";
- }
- $res .= "<p>{$datum->data}</p>";
- if (property_exists($datum, 'attachment')
- && property_exists($datum->attachment, 'type')) {
- $res .= self::convertElement($datum->attachment);
- }
- return $res;
- default:
- return '';
- }
- }
+ private function convertElement($datum)
+ {
+ switch ($datum->type) {
+ case 'text':
+ return $datum->data;
+ case 'image/single':
+ $alt = strip_tags($datum->data);
+ $res = "<figure><img src=\"{$datum->previewUrl}\" alt=\"{$alt}\" />";
+ if ($datum->data !== null) {
+ $res .= "<figcaption>{$datum->data}</figcaption>";
+ }
+ $res .= '</figure>';
+ return $res;
+ case 'text/quote':
+ return "<figure><blockquote>{$datum->data}</blockquote></figure><br>";
+ case 'embed/native':
+ $desc = $datum->link;
+ if (property_exists($datum, 'caption')) {
+ $desc = $datum->caption;
+ }
+ return "<p><a link=\"{$datum->link}\">{$desc}</a></p>";
+ case 'text/framed':
+ $res = '';
+ if (property_exists($datum, 'typeDisplay')) {
+ $res .= "<p><b>{$datum->typeDisplay}</b></p>";
+ }
+ $res .= "<p>{$datum->data}</p>";
+ if (
+ property_exists($datum, 'attachment')
+ && property_exists($datum->attachment, 'type')
+ ) {
+ $res .= self::convertElement($datum->attachment);
+ }
+ return $res;
+ default:
+ return '';
+ }
+ }
}
diff --git a/bridges/NovelUpdatesBridge.php b/bridges/NovelUpdatesBridge.php
index 60d3fa5d..62e5f5b8 100644
--- a/bridges/NovelUpdatesBridge.php
+++ b/bridges/NovelUpdatesBridge.php
@@ -1,68 +1,72 @@
<?php
-class NovelUpdatesBridge extends BridgeAbstract {
- const MAINTAINER = 'albirew';
- const NAME = 'Novel Updates';
- const URI = 'https://www.novelupdates.com/';
- const CACHE_TIMEOUT = 21600; // 6h
- const DESCRIPTION = 'Returns releases from Novel Updates';
- const PARAMETERS = array( array(
- 'n' => array(
- 'name' => 'Novel name as found in the url',
- 'exampleValue' => 'spirit-realm',
- 'required' => true
- )
- ));
+class NovelUpdatesBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'albirew';
+ const NAME = 'Novel Updates';
+ const URI = 'https://www.novelupdates.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns releases from Novel Updates';
+ const PARAMETERS = [ [
+ 'n' => [
+ 'name' => 'Novel name as found in the url',
+ 'exampleValue' => 'spirit-realm',
+ 'required' => true
+ ]
+ ]];
- private $seriesTitle = '';
+ private $seriesTitle = '';
- public function getURI(){
- if(!is_null($this->getInput('n'))) {
- return static::URI . '/series/' . $this->getInput('n') . '/';
- }
+ public function getURI()
+ {
+ if (!is_null($this->getInput('n'))) {
+ return static::URI . '/series/' . $this->getInput('n') . '/';
+ }
- return parent::getURI();
- }
+ return parent::getURI();
+ }
- public function collectData(){
- $fullhtml = getSimpleHTMLDOM($this->getURI());
+ public function collectData()
+ {
+ $fullhtml = getSimpleHTMLDOM($this->getURI());
- $this->seriesTitle = $fullhtml->find('h4.seriestitle', 0)->plaintext;
- // dirty fix for nasty simpledom bug: https://github.com/sebsauvage/rss-bridge/issues/259
- // forcefully removes tbody
- $html = $fullhtml->find('table#myTable', 0)->innertext;
- $html = stristr($html, '<tbody>'); //strip thead
- $html = stristr($html, '<tr>'); //remove tbody
- $html = str_get_html(stristr($html, '</tbody>', true)); //remove last tbody and get back as an array
- foreach($html->find('tr') as $element) {
- $item = array();
- $item['uri'] = $element->find('td', 2)->find('a', 0)->href;
- $item['title'] = $element->find('td', 2)->find('a', 0)->plaintext;
- $item['team'] = $element->find('td', 1)->innertext;
- $item['timestamp'] = strtotime($element->find('td', 0)->plaintext);
- $item['content'] = '<a href="'
- . $item['uri']
- . '">'
- . $this->seriesTitle
- . ' - '
- . $item['title']
- . '</a> by '
- . $item['team']
- . '<br><a href="'
- . $item['uri']
- . '">'
- . $fullhtml->find('div.seriesimg', 0)->innertext
- . '</a>';
+ $this->seriesTitle = $fullhtml->find('h4.seriestitle', 0)->plaintext;
+ // dirty fix for nasty simpledom bug: https://github.com/sebsauvage/rss-bridge/issues/259
+ // forcefully removes tbody
+ $html = $fullhtml->find('table#myTable', 0)->innertext;
+ $html = stristr($html, '<tbody>'); //strip thead
+ $html = stristr($html, '<tr>'); //remove tbody
+ $html = str_get_html(stristr($html, '</tbody>', true)); //remove last tbody and get back as an array
+ foreach ($html->find('tr') as $element) {
+ $item = [];
+ $item['uri'] = $element->find('td', 2)->find('a', 0)->href;
+ $item['title'] = $element->find('td', 2)->find('a', 0)->plaintext;
+ $item['team'] = $element->find('td', 1)->innertext;
+ $item['timestamp'] = strtotime($element->find('td', 0)->plaintext);
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '">'
+ . $this->seriesTitle
+ . ' - '
+ . $item['title']
+ . '</a> by '
+ . $item['team']
+ . '<br><a href="'
+ . $item['uri']
+ . '">'
+ . $fullhtml->find('div.seriesimg', 0)->innertext
+ . '</a>';
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
- public function getName(){
- if(!empty($this->seriesTitle)) {
- return $this->seriesTitle . ' - ' . static::NAME;
- }
+ public function getName()
+ {
+ if (!empty($this->seriesTitle)) {
+ return $this->seriesTitle . ' - ' . static::NAME;
+ }
- return parent::getName();
- }
+ return parent::getName();
+ }
}
diff --git a/bridges/NpciBridge.php b/bridges/NpciBridge.php
index 64dab909..17567778 100644
--- a/bridges/NpciBridge.php
+++ b/bridges/NpciBridge.php
@@ -1,95 +1,99 @@
<?php
-class NpciBridge extends BridgeAbstract {
- const MAINTAINER = 'captn3m0';
- const NAME = 'NCPI Circulars';
- const URI = 'https://npci.org.in';
- const CACHE_TIMEOUT = 3600;
- const DESCRIPTION = 'Returns circulars from National Payments Corporation of India)';
+class NpciBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'captn3m0';
+ const NAME = 'NCPI Circulars';
+ const URI = 'https://npci.org.in';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'Returns circulars from National Payments Corporation of India)';
- const URL_SUFFIX = [
- 'cts' => 'circulars',
- 'upi' => 'circular',
- 'rupay' => 'circulars',
- 'nach' => 'circulars',
- 'imps' => 'circular',
- 'netc-fastag' => 'circulars',
- '99' => 'circular',
- 'nfs' => 'circulars',
- 'aeps' => 'circulars',
- 'bhim-aadhaar' => 'circular',
- 'e-rupi' => 'circular',
- 'Bharat QR' => 'circulars',
- 'bharat-billpay' => 'circulars',
- ];
+ const URL_SUFFIX = [
+ 'cts' => 'circulars',
+ 'upi' => 'circular',
+ 'rupay' => 'circulars',
+ 'nach' => 'circulars',
+ 'imps' => 'circular',
+ 'netc-fastag' => 'circulars',
+ '99' => 'circular',
+ 'nfs' => 'circulars',
+ 'aeps' => 'circulars',
+ 'bhim-aadhaar' => 'circular',
+ 'e-rupi' => 'circular',
+ 'Bharat QR' => 'circulars',
+ 'bharat-billpay' => 'circulars',
+ ];
- const PARAMETERS = [[
- 'product' => [
- 'name' => 'product',
- 'type' => 'list',
- 'values' => [
- 'CTS' => 'cts',
- 'UPI' => 'upi',
- 'RuPay' => 'rupay',
- 'NACH' => 'nach',
- 'IMPS' => 'imps',
- 'NETC FASTag' => 'netc-fastag',
- '*99#' => '99',
- 'NFS' => 'nfs',
- 'AePS' => 'aeps',
- 'BHIM Aadhaar' => 'bhim-aadhaar',
- 'e-RUPI' => 'e-rupi',
- 'Bharat BillPay' => 'bharat-billpay'
- ]
- ]
- ]];
+ const PARAMETERS = [[
+ 'product' => [
+ 'name' => 'product',
+ 'type' => 'list',
+ 'values' => [
+ 'CTS' => 'cts',
+ 'UPI' => 'upi',
+ 'RuPay' => 'rupay',
+ 'NACH' => 'nach',
+ 'IMPS' => 'imps',
+ 'NETC FASTag' => 'netc-fastag',
+ '*99#' => '99',
+ 'NFS' => 'nfs',
+ 'AePS' => 'aeps',
+ 'BHIM Aadhaar' => 'bhim-aadhaar',
+ 'e-RUPI' => 'e-rupi',
+ 'Bharat BillPay' => 'bharat-billpay'
+ ]
+ ]
+ ]];
- public function getName() {
- $product = $this->getInput('product');
- if ($product) {
- $productNameMap = array_flip(self::PARAMETERS[0]['product']['values']);
- $productName = $productNameMap[$product];
- return "NPCI Circulars: $productName";
- }
+ public function getName()
+ {
+ $product = $this->getInput('product');
+ if ($product) {
+ $productNameMap = array_flip(self::PARAMETERS[0]['product']['values']);
+ $productName = $productNameMap[$product];
+ return "NPCI Circulars: $productName";
+ }
- return 'NPCI Circulars';
- }
+ return 'NPCI Circulars';
+ }
- public function getURI(){
- $product = $this->getInput('product');
- return $product ? sprintf('%s/what-we-do/%s/%s', self::URI, $product, self::URL_SUFFIX[$product]) : self::URI;
- }
+ public function getURI()
+ {
+ $product = $this->getInput('product');
+ return $product ? sprintf('%s/what-we-do/%s/%s', self::URI, $product, self::URL_SUFFIX[$product]) : self::URI;
+ }
- public function collectData(){
- $html = getSimpleHTMLDOMCached($this->getURI());
- $year = date('Y');
- $elements = $html->find("div[id=year$year] .pdf-item");
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOMCached($this->getURI());
+ $year = date('Y');
+ $elements = $html->find("div[id=year$year] .pdf-item");
- foreach($elements as $element) {
- $title = $element->find('p', 0)->innertext;
+ foreach ($elements as $element) {
+ $title = $element->find('p', 0)->innertext;
- $link = $element->find('a', 0);
+ $link = $element->find('a', 0);
- $uri = null;
+ $uri = null;
- if ($link) {
- $pdfLink = $link->getAttribute('href');
- $uri = self::URI . str_replace(' ', '+', $pdfLink);
- }
+ if ($link) {
+ $pdfLink = $link->getAttribute('href');
+ $uri = self::URI . str_replace(' ', '+', $pdfLink);
+ }
- $item = [
- 'uri' => $uri,
- 'title' => $title,
- 'content' => $title ,
- 'uid' => sha1($pdfLink),
- 'enclosures' => [
- $uri
- ]
- ];
+ $item = [
+ 'uri' => $uri,
+ 'title' => $title,
+ 'content' => $title ,
+ 'uid' => sha1($pdfLink),
+ 'enclosures' => [
+ $uri
+ ]
+ ];
- $this->items[] = $item;
- }
+ $this->items[] = $item;
+ }
- $this->items = array_slice($this->items, 0, 15);
- }
+ $this->items = array_slice($this->items, 0, 15);
+ }
}
diff --git a/bridges/NyaaTorrentsBridge.php b/bridges/NyaaTorrentsBridge.php
index 1ce1a027..e281b79d 100644
--- a/bridges/NyaaTorrentsBridge.php
+++ b/bridges/NyaaTorrentsBridge.php
@@ -1,107 +1,112 @@
<?php
-class NyaaTorrentsBridge extends FeedExpander {
- const MAINTAINER = 'ORelio';
- const NAME = 'NyaaTorrents';
- const URI = 'https://nyaa.si/';
- const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.';
- const PARAMETERS = array(
- array(
- 'f' => array(
- 'name' => 'Filter',
- 'type' => 'list',
- 'values' => array(
- 'No filter' => '0',
- 'No remakes' => '1',
- 'Trusted only' => '2'
- )
- ),
- 'c' => array(
- 'name' => 'Category',
- 'type' => 'list',
- 'values' => array(
- 'All categories' => '0_0',
- 'Anime' => '1_0',
- 'Anime - AMV' => '1_1',
- 'Anime - English' => '1_2',
- 'Anime - Non-English' => '1_3',
- 'Anime - Raw' => '1_4',
- 'Audio' => '2_0',
- 'Audio - Lossless' => '2_1',
- 'Audio - Lossy' => '2_2',
- 'Literature' => '3_0',
- 'Literature - English' => '3_1',
- 'Literature - Non-English' => '3_2',
- 'Literature - Raw' => '3_3',
- 'Live Action' => '4_0',
- 'Live Action - English' => '4_1',
- 'Live Action - Idol/PV' => '4_2',
- 'Live Action - Non-English' => '4_3',
- 'Live Action - Raw' => '4_4',
- 'Pictures' => '5_0',
- 'Pictures - Graphics' => '5_1',
- 'Pictures - Photos' => '5_2',
- 'Software' => '6_0',
- 'Software - Apps' => '6_1',
- 'Software - Games' => '6_2',
- )
- ),
- 'q' => array(
- 'name' => 'Keyword',
- 'description' => 'Keyword(s)',
- 'type' => 'text'
- ),
- 'u' => array(
- 'name' => 'User',
- 'description' => 'User',
- 'type' => 'text'
- )
- )
- );
+class NyaaTorrentsBridge extends FeedExpander
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = 'NyaaTorrents';
+ const URI = 'https://nyaa.si/';
+ const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.';
+ const PARAMETERS = [
+ [
+ 'f' => [
+ 'name' => 'Filter',
+ 'type' => 'list',
+ 'values' => [
+ 'No filter' => '0',
+ 'No remakes' => '1',
+ 'Trusted only' => '2'
+ ]
+ ],
+ 'c' => [
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => [
+ 'All categories' => '0_0',
+ 'Anime' => '1_0',
+ 'Anime - AMV' => '1_1',
+ 'Anime - English' => '1_2',
+ 'Anime - Non-English' => '1_3',
+ 'Anime - Raw' => '1_4',
+ 'Audio' => '2_0',
+ 'Audio - Lossless' => '2_1',
+ 'Audio - Lossy' => '2_2',
+ 'Literature' => '3_0',
+ 'Literature - English' => '3_1',
+ 'Literature - Non-English' => '3_2',
+ 'Literature - Raw' => '3_3',
+ 'Live Action' => '4_0',
+ 'Live Action - English' => '4_1',
+ 'Live Action - Idol/PV' => '4_2',
+ 'Live Action - Non-English' => '4_3',
+ 'Live Action - Raw' => '4_4',
+ 'Pictures' => '5_0',
+ 'Pictures - Graphics' => '5_1',
+ 'Pictures - Photos' => '5_2',
+ 'Software' => '6_0',
+ 'Software - Apps' => '6_1',
+ 'Software - Games' => '6_2',
+ ]
+ ],
+ 'q' => [
+ 'name' => 'Keyword',
+ 'description' => 'Keyword(s)',
+ 'type' => 'text'
+ ],
+ 'u' => [
+ 'name' => 'User',
+ 'description' => 'User',
+ 'type' => 'text'
+ ]
+ ]
+ ];
- public function getIcon() {
- return self::URI . 'static/favicon.png';
- }
+ public function getIcon()
+ {
+ return self::URI . 'static/favicon.png';
+ }
- public function collectData(){
- $this->collectExpandableDatas(
- self::URI . '?page=rss&s=id&o=desc&'
- . http_build_query(array(
- 'f' => $this->getInput('f'),
- 'c' => $this->getInput('c'),
- 'q' => $this->getInput('q'),
- 'u' => $this->getInput('u')
- )), 20);
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas(
+ self::URI . '?page=rss&s=id&o=desc&'
+ . http_build_query([
+ 'f' => $this->getInput('f'),
+ 'c' => $this->getInput('c'),
+ 'q' => $this->getInput('q'),
+ 'u' => $this->getInput('u')
+ ]),
+ 20
+ );
+ }
- protected function parseItem($newItem){
- $item = parent::parseItem($newItem);
+ protected function parseItem($newItem)
+ {
+ $item = parent::parseItem($newItem);
- //Convert URI from torrent file to web page
- $item['uri'] = str_replace('/download/', '/view/', $item['uri']);
- $item['uri'] = str_replace('.torrent', '', $item['uri']);
+ //Convert URI from torrent file to web page
+ $item['uri'] = str_replace('/download/', '/view/', $item['uri']);
+ $item['uri'] = str_replace('.torrent', '', $item['uri']);
- if ($item_html = getSimpleHTMLDOMCached($item['uri'])) {
+ if ($item_html = getSimpleHTMLDOMCached($item['uri'])) {
+ //Retrieve full description from page contents
+ $item_desc = str_get_html(
+ markdownToHtml(html_entity_decode($item_html->find('#torrent-description', 0)->innertext))
+ );
- //Retrieve full description from page contents
- $item_desc = str_get_html(
- markdownToHtml(html_entity_decode($item_html->find('#torrent-description', 0)->innertext))
- );
+ //Retrieve image for thumbnail or generic logo fallback
+ $item_image = $this->getURI() . 'static/img/avatar/default.png';
+ foreach ($item_desc->find('img') as $img) {
+ if (strpos($img->src, 'prez') === false) {
+ $item_image = $img->src;
+ break;
+ }
+ }
- //Retrieve image for thumbnail or generic logo fallback
- $item_image = $this->getURI() . 'static/img/avatar/default.png';
- foreach ($item_desc->find('img') as $img) {
- if (strpos($img->src, 'prez') === false) {
- $item_image = $img->src;
- break;
- }
- }
+ //Add expanded fields to the current item
+ $item['enclosures'] = [$item_image];
+ $item['content'] = $item_desc;
+ }
- //Add expanded fields to the current item
- $item['enclosures'] = array($item_image);
- $item['content'] = $item_desc;
- }
-
- return $item;
- }
+ return $item;
+ }
}
diff --git a/bridges/OnVaSortirBridge.php b/bridges/OnVaSortirBridge.php
index ed1dcb65..af80dd31 100644
--- a/bridges/OnVaSortirBridge.php
+++ b/bridges/OnVaSortirBridge.php
@@ -1,130 +1,134 @@
<?php
-class OnVaSortirBridge extends FeedExpander {
- const MAINTAINER = 'AntoineTurmel';
- const NAME = 'OnVaSortir';
- const URI = 'https://www.onvasortir.com';
- const DESCRIPTION = 'Returns the newest events from OnVaSortir (full text)';
- const PARAMETERS = array(
- array(
- 'city' => array(
- 'name' => 'City',
- 'type' => 'list',
- 'values' => array(
- 'Agen' => 'Agen',
- 'Ajaccio' => 'Ajaccio',
- 'Albi' => 'Albi',
- 'Amiens' => 'Amiens',
- 'Angers' => 'Angers',
- 'Angoulême' => 'Angouleme',
- 'Annecy' => 'annecy',
- 'Aurillac' => 'aurillac',
- 'Auxerre' => 'auxerre',
- 'Avignon' => 'avignon',
- 'Béziers' => 'Beziers',
- 'Bastia' => 'Bastia',
- 'Beauvais' => 'Beauvais',
- 'Belfort' => 'Belfort',
- 'Bergerac' => 'bergerac',
- 'Besançon' => 'Besancon',
- 'Biarritz' => 'Biarritz',
- 'Blois' => 'Blois',
- 'Bordeaux' => 'bordeaux',
- 'Bourg-en-Bresse' => 'bourg-en-bresse',
- 'Bourges' => 'Bourges',
- 'Brest' => 'Brest',
- 'Brive' => 'brive-la-gaillarde',
- 'Bruxelles' => 'bruxelles',
- 'Caen' => 'Caen',
- 'Calais' => 'Calais',
- 'Carcassonne' => 'Carcassonne',
- 'Châteauroux' => 'Chateauroux',
- 'Chalon-sur-saone' => 'chalon-sur-saone',
- 'Chambéry' => 'chambery',
- 'Chantilly' => 'chantilly',
- 'Charleroi' => 'charleroi',
- 'Charleville-Mézières' => 'Charleville-Mezieres',
- 'Chartres' => 'Chartres',
- 'Cherbourg' => 'Cherbourg',
- 'Cholet' => 'cholet',
- 'Clermont-Ferrand' => 'Clermont-Ferrand',
- 'Compiègne' => 'compiegne',
- 'Dieppe' => 'dieppe',
- 'Dijon' => 'Dijon',
- 'Dunkerque' => 'Dunkerque',
- 'Evreux' => 'evreux',
- 'Fréjus' => 'frejus',
- 'Gap' => 'gap',
- 'Genève' => 'geneve',
- 'Grenoble' => 'Grenoble',
- 'La Roche sur Yon' => 'La-Roche-sur-Yon',
- 'La Rochelle' => 'La-Rochelle',
- 'Lausanne' => 'lausanne',
- 'Laval' => 'Laval',
- 'Le Havre' => 'le-havre',
- 'Le Mans' => 'le-mans',
- 'Liège' => 'liege',
- 'Lille' => 'lille',
- 'Limoges' => 'Limoges',
- 'Lorient' => 'Lorient',
- 'Luxembourg' => 'Luxembourg',
- 'Lyon' => 'lyon',
- 'Marseille' => 'marseille',
- 'Metz' => 'Metz',
- 'Mons' => 'Mons',
- 'Mont de Marsan' => 'mont-de-marsan',
- 'Montauban' => 'Montauban',
- 'Montluçon' => 'montlucon',
- 'Montpellier' => 'montpellier',
- 'Mulhouse' => 'Mulhouse',
- 'Nîmes' => 'nimes',
- 'Namur' => 'Namur',
- 'Nancy' => 'Nancy',
- 'Nantes' => 'nantes',
- 'Nevers' => 'nevers',
- 'Nice' => 'nice',
- 'Niort' => 'niort',
- 'Orléans' => 'orleans',
- 'Périgueux' => 'perigueux',
- 'Paris' => 'paris',
- 'Pau' => 'Pau',
- 'Perpignan' => 'Perpignan',
- 'Poitiers' => 'Poitiers',
- 'Quimper' => 'Quimper',
- 'Reims' => 'Reims',
- 'Rennes' => 'Rennes',
- 'Roanne' => 'roanne',
- 'Rodez' => 'rodez',
- 'Rouen' => 'Rouen',
- 'Saint-Brieuc' => 'Saint-Brieuc',
- 'Saint-Etienne' => 'saint-etienne',
- 'Saint-Malo' => 'saint-malo',
- 'Saint-Nazaire' => 'saint-nazaire',
- 'Saint-Quentin' => 'saint-quentin',
- 'Saintes' => 'saintes',
- 'Strasbourg' => 'Strasbourg',
- 'Tarbes' => 'Tarbes',
- 'Toulon' => 'Toulon',
- 'Toulouse' => 'Toulouse',
- 'Tours' => 'Tours',
- 'Troyes' => 'troyes',
- 'Valence' => 'valence',
- 'Vannes' => 'vannes',
- 'Zurich' => 'zurich',
- )
- )
- )
- );
- protected function parseItem($item){
- $item = parent::parseItem($item);
- $html = getSimpleHTMLDOMCached($item['uri']);
- $text = $html->find('div.corpsMax', 0)->innertext;
- $item['content'] = utf8_encode($text);
- return $item;
- }
+class OnVaSortirBridge extends FeedExpander
+{
+ const MAINTAINER = 'AntoineTurmel';
+ const NAME = 'OnVaSortir';
+ const URI = 'https://www.onvasortir.com';
+ const DESCRIPTION = 'Returns the newest events from OnVaSortir (full text)';
+ const PARAMETERS = [
+ [
+ 'city' => [
+ 'name' => 'City',
+ 'type' => 'list',
+ 'values' => [
+ 'Agen' => 'Agen',
+ 'Ajaccio' => 'Ajaccio',
+ 'Albi' => 'Albi',
+ 'Amiens' => 'Amiens',
+ 'Angers' => 'Angers',
+ 'Angoulême' => 'Angouleme',
+ 'Annecy' => 'annecy',
+ 'Aurillac' => 'aurillac',
+ 'Auxerre' => 'auxerre',
+ 'Avignon' => 'avignon',
+ 'Béziers' => 'Beziers',
+ 'Bastia' => 'Bastia',
+ 'Beauvais' => 'Beauvais',
+ 'Belfort' => 'Belfort',
+ 'Bergerac' => 'bergerac',
+ 'Besançon' => 'Besancon',
+ 'Biarritz' => 'Biarritz',
+ 'Blois' => 'Blois',
+ 'Bordeaux' => 'bordeaux',
+ 'Bourg-en-Bresse' => 'bourg-en-bresse',
+ 'Bourges' => 'Bourges',
+ 'Brest' => 'Brest',
+ 'Brive' => 'brive-la-gaillarde',
+ 'Bruxelles' => 'bruxelles',
+ 'Caen' => 'Caen',
+ 'Calais' => 'Calais',
+ 'Carcassonne' => 'Carcassonne',
+ 'Châteauroux' => 'Chateauroux',
+ 'Chalon-sur-saone' => 'chalon-sur-saone',
+ 'Chambéry' => 'chambery',
+ 'Chantilly' => 'chantilly',
+ 'Charleroi' => 'charleroi',
+ 'Charleville-Mézières' => 'Charleville-Mezieres',
+ 'Chartres' => 'Chartres',
+ 'Cherbourg' => 'Cherbourg',
+ 'Cholet' => 'cholet',
+ 'Clermont-Ferrand' => 'Clermont-Ferrand',
+ 'Compiègne' => 'compiegne',
+ 'Dieppe' => 'dieppe',
+ 'Dijon' => 'Dijon',
+ 'Dunkerque' => 'Dunkerque',
+ 'Evreux' => 'evreux',
+ 'Fréjus' => 'frejus',
+ 'Gap' => 'gap',
+ 'Genève' => 'geneve',
+ 'Grenoble' => 'Grenoble',
+ 'La Roche sur Yon' => 'La-Roche-sur-Yon',
+ 'La Rochelle' => 'La-Rochelle',
+ 'Lausanne' => 'lausanne',
+ 'Laval' => 'Laval',
+ 'Le Havre' => 'le-havre',
+ 'Le Mans' => 'le-mans',
+ 'Liège' => 'liege',
+ 'Lille' => 'lille',
+ 'Limoges' => 'Limoges',
+ 'Lorient' => 'Lorient',
+ 'Luxembourg' => 'Luxembourg',
+ 'Lyon' => 'lyon',
+ 'Marseille' => 'marseille',
+ 'Metz' => 'Metz',
+ 'Mons' => 'Mons',
+ 'Mont de Marsan' => 'mont-de-marsan',
+ 'Montauban' => 'Montauban',
+ 'Montluçon' => 'montlucon',
+ 'Montpellier' => 'montpellier',
+ 'Mulhouse' => 'Mulhouse',
+ 'Nîmes' => 'nimes',
+ 'Namur' => 'Namur',
+ 'Nancy' => 'Nancy',
+ 'Nantes' => 'nantes',
+ 'Nevers' => 'nevers',
+ 'Nice' => 'nice',
+ 'Niort' => 'niort',
+ 'Orléans' => 'orleans',
+ 'Périgueux' => 'perigueux',
+ 'Paris' => 'paris',
+ 'Pau' => 'Pau',
+ 'Perpignan' => 'Perpignan',
+ 'Poitiers' => 'Poitiers',
+ 'Quimper' => 'Quimper',
+ 'Reims' => 'Reims',
+ 'Rennes' => 'Rennes',
+ 'Roanne' => 'roanne',
+ 'Rodez' => 'rodez',
+ 'Rouen' => 'Rouen',
+ 'Saint-Brieuc' => 'Saint-Brieuc',
+ 'Saint-Etienne' => 'saint-etienne',
+ 'Saint-Malo' => 'saint-malo',
+ 'Saint-Nazaire' => 'saint-nazaire',
+ 'Saint-Quentin' => 'saint-quentin',
+ 'Saintes' => 'saintes',
+ 'Strasbourg' => 'Strasbourg',
+ 'Tarbes' => 'Tarbes',
+ 'Toulon' => 'Toulon',
+ 'Toulouse' => 'Toulouse',
+ 'Tours' => 'Tours',
+ 'Troyes' => 'troyes',
+ 'Valence' => 'valence',
+ 'Vannes' => 'vannes',
+ 'Zurich' => 'zurich',
+ ]
+ ]
+ ]
+ ];
- public function collectData(){
- $this->collectExpandableDatas('https://' .
- $this->getInput('city') . '.onvasortir.com/rss.php');
- }
+ protected function parseItem($item)
+ {
+ $item = parent::parseItem($item);
+ $html = getSimpleHTMLDOMCached($item['uri']);
+ $text = $html->find('div.corpsMax', 0)->innertext;
+ $item['content'] = utf8_encode($text);
+ return $item;
+ }
+
+ public function collectData()
+ {
+ $this->collectExpandableDatas('https://' .
+ $this->getInput('city') . '.onvasortir.com/rss.php');
+ }
}
diff --git a/bridges/OneFortuneADayBridge.php b/bridges/OneFortuneADayBridge.php
index 62fe767d..c74f22d0 100644
--- a/bridges/OneFortuneADayBridge.php
+++ b/bridges/OneFortuneADayBridge.php
@@ -1,76 +1,82 @@
<?php
-class OneFortuneADayBridge extends BridgeAbstract {
- const NAME = 'One Fortune a Day';
- const URI = 'https://github.com/fulmeek';
- const DESCRIPTION = 'Get a fortune quote every single day.';
- const MAINTAINER = 'fulmeek';
- const PARAMETERS = array(array(
- 'time' => array(
- 'name' => 'Time in UTC',
- 'type' => 'list',
- 'values' => array(
- '0:00' => 0,
- '1:00' => 1,
- '2:00' => 2,
- '3:00' => 3,
- '4:00' => 4,
- '5:00' => 5,
- '6:00' => 6,
- '7:00' => 7,
- '8:00' => 8,
- '9:00' => 9,
- '10:00' => 10,
- '11:00' => 11,
- '12:00' => 12,
- '13:00' => 13,
- '14:00' => 14,
- '15:00' => 15,
- '16:00' => 16,
- '17:00' => 17,
- '18:00' => 18,
- '19:00' => 19,
- '20:00' => 20,
- '21:00' => 21,
- '22:00' => 22,
- '23:00' => 23,
- ),
- 'defaultValue' => 5
- ),
- 'lucky' => array(
- 'name' => 'Lucky number (optional)',
- 'type' => 'text'
- )
- ));
- const LIMIT_ITEMS = 7;
- const DAY_SECS = 86400;
+class OneFortuneADayBridge extends BridgeAbstract
+{
+ const NAME = 'One Fortune a Day';
+ const URI = 'https://github.com/fulmeek';
+ const DESCRIPTION = 'Get a fortune quote every single day.';
+ const MAINTAINER = 'fulmeek';
+ const PARAMETERS = [[
+ 'time' => [
+ 'name' => 'Time in UTC',
+ 'type' => 'list',
+ 'values' => [
+ '0:00' => 0,
+ '1:00' => 1,
+ '2:00' => 2,
+ '3:00' => 3,
+ '4:00' => 4,
+ '5:00' => 5,
+ '6:00' => 6,
+ '7:00' => 7,
+ '8:00' => 8,
+ '9:00' => 9,
+ '10:00' => 10,
+ '11:00' => 11,
+ '12:00' => 12,
+ '13:00' => 13,
+ '14:00' => 14,
+ '15:00' => 15,
+ '16:00' => 16,
+ '17:00' => 17,
+ '18:00' => 18,
+ '19:00' => 19,
+ '20:00' => 20,
+ '21:00' => 21,
+ '22:00' => 22,
+ '23:00' => 23,
+ ],
+ 'defaultValue' => 5
+ ],
+ 'lucky' => [
+ 'name' => 'Lucky number (optional)',
+ 'type' => 'text'
+ ]
+ ]];
- public function getDescription(){
- return self::DESCRIPTION . '<br/>Set a lucky number to get your personal quotes, like ' . mt_rand();
- }
+ const LIMIT_ITEMS = 7;
+ const DAY_SECS = 86400;
- public function collectData() {
- $time = gmmktime((int)$this->getInput('time'), 0, 0);
- if ($time > time())
- $time -= self::DAY_SECS;
+ public function getDescription()
+ {
+ return self::DESCRIPTION . '<br/>Set a lucky number to get your personal quotes, like ' . mt_rand();
+ }
- for ($i = self::LIMIT_ITEMS; $i > 0; --$i) {
- $seed = gmdate('Ymd', $time) . $this->getInput('lucky');
- $quote = $this->getQuote($seed);
+ public function collectData()
+ {
+ $time = gmmktime((int)$this->getInput('time'), 0, 0);
+ if ($time > time()) {
+ $time -= self::DAY_SECS;
+ }
- $item['title'] = strftime('%A, %x', $time);
- $item['content'] = htmlentities($quote, ENT_QUOTES, 'UTF-8');
- $item['timestamp'] = $time;
- $item['uid'] = hash('sha1', $seed);
+ for ($i = self::LIMIT_ITEMS; $i > 0; --$i) {
+ $seed = gmdate('Ymd', $time) . $this->getInput('lucky');
+ $quote = $this->getQuote($seed);
- $this->items[] = $item;
+ $item['title'] = strftime('%A, %x', $time);
+ $item['content'] = htmlentities($quote, ENT_QUOTES, 'UTF-8');
+ $item['timestamp'] = $time;
+ $item['uid'] = hash('sha1', $seed);
- $time -= self::DAY_SECS;
- }
- }
+ $this->items[] = $item;
- private function getQuote($seed) {
- $quotes = explode('//', <<<QUOTES
+ $time -= self::DAY_SECS;
+ }
+ }
+
+ private function getQuote($seed)
+ {
+ $quotes = explode('//', <<<QUOTES
People are naturally attracted to you.
//You learn from your mistakes... You will learn a lot today.
//If you have something good in your life, don't let it go!
@@ -954,9 +960,9 @@ you never try.
//Working hard will make you live a happy life.
//A pleasant surprise is waiting for you.
QUOTES
- );
+ );
- $i = round(fmod(hexdec(hash('crc32', $seed)), count($quotes)), 0);
- return trim(str_replace(array("\r\n", "\n", "\r"), ' ', $quotes[$i]));
- }
+ $i = round(fmod(hexdec(hash('crc32', $seed)), count($quotes)), 0);
+ return trim(str_replace(["\r\n", "\n", "\r"], ' ', $quotes[$i]));
+ }
}
diff --git a/bridges/OpenlyBridge.php b/bridges/OpenlyBridge.php
index 3395905d..9f54e22a 100644
--- a/bridges/OpenlyBridge.php
+++ b/bridges/OpenlyBridge.php
@@ -1,247 +1,255 @@
<?php
-class OpenlyBridge extends BridgeAbstract {
- const NAME = 'Openly Bridge';
- const URI = 'https://www.openlynews.com/';
- const DESCRIPTION = 'Returns news articles';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(
- 'All News' => array(),
- 'All Opinion' => array(),
- 'By Region' => array(
- 'region' => array(
- 'name' => 'Region',
- 'type' => 'list',
- 'values' => array(
- 'Africa' => 'africa',
- 'Asia Pacific' => 'asia-pacific',
- 'Europe' => 'europe',
- 'Latin America' => 'latin-america',
- 'Middle Easta' => 'middle-east',
- 'North America' => 'north-america'
- )
- ),
- 'content' => array(
- 'name' => 'Content',
- 'type' => 'list',
- 'values' => array(
- 'News' => 'news',
- 'Opinion' => 'people'
- ),
- 'defaultValue' => 'news'
- )
- ),
- 'By Tag' => array(
- 'tag' => array(
- 'name' => 'Tag',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'lgbt-law',
- ),
- 'content' => array(
- 'name' => 'Content',
- 'type' => 'list',
- 'values' => array(
- 'News' => 'news',
- 'Opinion' => 'people'
- ),
- 'defaultValue' => 'news'
- )
- ),
- 'By Author' => array(
- 'profileId' => array(
- 'name' => 'Profile ID',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => '003D000002WZGYRIA5',
- )
- )
- );
-
- const TEST_DETECT_PARAMETERS = array(
- 'https://www.openlynews.com/profile/?id=0033z00002XUTepAAH' => array(
- 'context' => 'By Author', 'profileId' => '0033z00002XUTepAAH'
- ),
- 'https://www.openlynews.com/news/?page=1&theme=lgbt-law' => array(
- 'context' => 'By Tag', 'content' => 'news', 'tag' => 'lgbt-law'
- ),
- 'https://www.openlynews.com/news/?page=1&region=north-america' => array(
- 'context' => 'By Region', 'content' => 'news', 'region' => 'north-america'
- ),
- 'https://www.openlynews.com/news/?theme=lgbt-law' => array(
- 'context' => 'By Tag', 'content' => 'news', 'tag' => 'lgbt-law'
- ),
- 'https://www.openlynews.com/news/?region=north-america' => array(
- 'context' => 'By Region', 'content' => 'news', 'region' => 'north-america'
- )
- );
-
- const CACHE_TIMEOUT = 900; // 15 mins
- const ARTICLE_CACHE_TIMEOUT = 3600; // 1 hour
-
- private $feedTitle = '';
- private $itemLimit = 10;
-
- private $profileUrlRegex = '/openlynews\.com\/profile\/\?id=([a-zA-Z0-9]+)/';
- private $tagUrlRegex = '/openlynews\.com\/([a-z]+)\/\?(?:page=(?:[0-9]+)&)?theme=([\w-]+)/';
- private $regionUrlRegex = '/openlynews\.com\/([a-z]+)\/\?(?:page=(?:[0-9]+)&)?region=([\w-]+)/';
-
- public function detectParameters($url) {
- $params = array();
-
- if(preg_match($this->profileUrlRegex, $url, $matches) > 0) {
- $params['context'] = 'By Author';
- $params['profileId'] = $matches[1];
- return $params;
- }
-
- if(preg_match($this->tagUrlRegex, $url, $matches) > 0) {
- $params['context'] = 'By Tag';
- $params['content'] = $matches[1];
- $params['tag'] = $matches[2];
- return $params;
- }
-
- if(preg_match($this->regionUrlRegex, $url, $matches) > 0) {
- $params['context'] = 'By Region';
- $params['content'] = $matches[1];
- $params['region'] = $matches[2];
- return $params;
- }
-
- return null;
- }
-
- public function collectData() {
- $url = $this->getAjaxURI();
-
- if ($this->queriedContext === 'By Author') {
- $url = $this->getURI();
- }
-
- $html = getSimpleHTMLDOM($url);
- $html = defaultLinkTo($html, $this->getURI());
-
- if ($html->find('h1', 0)) {
- $this->feedTitle = $html->find('h1', 0)->plaintext;
- }
-
- if ($html->find('h2.title-v4', 0)) {
- $html->find('span.tooltiptext', 0)->innertext = '';
- $this->feedTitle = $html->find('a.tooltipitem', 0)->plaintext;
- }
-
- $items = $html->find('div.item');
- $limit = 5;
- foreach(array_slice($items, 0, $limit) as $div) {
- $this->items[] = $this->getArticle($div->find('a', 0)->href);
-
- if (count($this->items) >= $this->itemLimit) {
- break;
- }
- }
- }
-
- public function getURI() {
- switch ($this->queriedContext) {
- case 'All News':
- return self::URI . 'news';
- break;
- case 'All Opinion':
- return self::URI . 'people';
- break;
- case 'By Tag':
- return self::URI . $this->getInput('content') . '/?theme=' . $this->getInput('tag');
- case 'By Region':
- return self::URI . $this->getInput('content') . '/?region=' . $this->getInput('region');
- break;
- case 'By Author':
- return self::URI . 'profile/?id=' . $this->getInput('profileId');
- break;
- default:
- return parent::getURI();
- }
- }
-
- public function getName() {
- switch ($this->queriedContext) {
- case 'All News':
- return 'News - Openly';
- break;
- case 'All Opinion':
- return 'Opinion - Openly';
- break;
- case 'By Tag':
- if (empty($this->feedTitle)) {
- $this->feedTitle = $this->getInput('tag');
- }
-
- if ($this->getInput('content') === 'people') {
- return $this->feedTitle . ' - Opinion - Openly';
- }
-
- return $this->feedTitle . ' - Openly';
- break;
- case 'By Region':
- if (empty($this->feedTitle)) {
- $this->feedTitle = $this->getInput('region');
- }
-
- if ($this->getInput('content') === 'people') {
- return $this->feedTitle . ' - Opinion - Openly';
- }
-
- return $this->feedTitle . ' - Openly';
- break;
- case 'By Author':
- if (empty($this->feedTitle)) {
- $this->feedTitle = $this->getInput('profileId');
- }
-
- return $this->feedTitle . ' - Author - Openly';
- break;
- default:
- return parent::getName();
- }
- }
-
- private function getAjaxURI() {
- $part = '/ajax.html?';
-
- switch ($this->queriedContext) {
- case 'All News':
- return self::URI . 'news' . $part;
- break;
- case 'All Opinion':
- return self::URI . 'people' . $part;
- break;
- case 'By Tag':
- return self::URI . $this->getInput('content') . $part . 'theme=' . $this->getInput('tag');
- break;
- case 'By Region':
- return self::URI . $this->getInput('content') . $part . 'region=' . $this->getInput('region');
- break;
- }
- }
-
- private function getArticle($url) {
- $article = getSimpleHTMLDOMCached($url, self::ARTICLE_CACHE_TIMEOUT);
- $article = defaultLinkTo($article, $this->getURI());
-
- $item = array();
- $item['title'] = $article->find('h1', 0)->plaintext;
- $item['uri'] = $url;
- $item['content'] = $article->find('div.body-text', 0);
- $item['enclosures'][] = $article->find('meta[name="twitter:image"]', 0)->content;
- $item['timestamp'] = $article->find('div.meta.small', 0)->plaintext;
-
- if ($article->find('div.meta a', 0)) {
- $item['author'] = $article->find('div.meta a', 0)->plaintext;
- }
-
- foreach($article->find('div.themes li') as $li) {
- $item['categories'][] = trim(htmlspecialchars($li->plaintext, ENT_QUOTES));
- }
-
- return $item;
- }
+
+class OpenlyBridge extends BridgeAbstract
+{
+ const NAME = 'Openly Bridge';
+ const URI = 'https://www.openlynews.com/';
+ const DESCRIPTION = 'Returns news articles';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [
+ 'All News' => [],
+ 'All Opinion' => [],
+ 'By Region' => [
+ 'region' => [
+ 'name' => 'Region',
+ 'type' => 'list',
+ 'values' => [
+ 'Africa' => 'africa',
+ 'Asia Pacific' => 'asia-pacific',
+ 'Europe' => 'europe',
+ 'Latin America' => 'latin-america',
+ 'Middle Easta' => 'middle-east',
+ 'North America' => 'north-america'
+ ]
+ ],
+ 'content' => [
+ 'name' => 'Content',
+ 'type' => 'list',
+ 'values' => [
+ 'News' => 'news',
+ 'Opinion' => 'people'
+ ],
+ 'defaultValue' => 'news'
+ ]
+ ],
+ 'By Tag' => [
+ 'tag' => [
+ 'name' => 'Tag',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'lgbt-law',
+ ],
+ 'content' => [
+ 'name' => 'Content',
+ 'type' => 'list',
+ 'values' => [
+ 'News' => 'news',
+ 'Opinion' => 'people'
+ ],
+ 'defaultValue' => 'news'
+ ]
+ ],
+ 'By Author' => [
+ 'profileId' => [
+ 'name' => 'Profile ID',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => '003D000002WZGYRIA5',
+ ]
+ ]
+ ];
+
+ const TEST_DETECT_PARAMETERS = [
+ 'https://www.openlynews.com/profile/?id=0033z00002XUTepAAH' => [
+ 'context' => 'By Author', 'profileId' => '0033z00002XUTepAAH'
+ ],
+ 'https://www.openlynews.com/news/?page=1&theme=lgbt-law' => [
+ 'context' => 'By Tag', 'content' => 'news', 'tag' => 'lgbt-law'
+ ],
+ 'https://www.openlynews.com/news/?page=1&region=north-america' => [
+ 'context' => 'By Region', 'content' => 'news', 'region' => 'north-america'
+ ],
+ 'https://www.openlynews.com/news/?theme=lgbt-law' => [
+ 'context' => 'By Tag', 'content' => 'news', 'tag' => 'lgbt-law'
+ ],
+ 'https://www.openlynews.com/news/?region=north-america' => [
+ 'context' => 'By Region', 'content' => 'news', 'region' => 'north-america'
+ ]
+ ];
+
+ const CACHE_TIMEOUT = 900; // 15 mins
+ const ARTICLE_CACHE_TIMEOUT = 3600; // 1 hour
+
+ private $feedTitle = '';
+ private $itemLimit = 10;
+
+ private $profileUrlRegex = '/openlynews\.com\/profile\/\?id=([a-zA-Z0-9]+)/';
+ private $tagUrlRegex = '/openlynews\.com\/([a-z]+)\/\?(?:page=(?:[0-9]+)&)?theme=([\w-]+)/';
+ private $regionUrlRegex = '/openlynews\.com\/([a-z]+)\/\?(?:page=(?:[0-9]+)&)?region=([\w-]+)/';
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ if (preg_match($this->profileUrlRegex, $url, $matches) > 0) {
+ $params['context'] = 'By Author';
+ $params['profileId'] = $matches[1];
+ return $params;
+ }
+
+ if (preg_match($this->tagUrlRegex, $url, $matches) > 0) {
+ $params['context'] = 'By Tag';
+ $params['content'] = $matches[1];
+ $params['tag'] = $matches[2];
+ return $params;
+ }
+
+ if (preg_match($this->regionUrlRegex, $url, $matches) > 0) {
+ $params['context'] = 'By Region';
+ $params['content'] = $matches[1];
+ $params['region'] = $matches[2];
+ return $params;
+ }
+
+ return null;
+ }
+
+ public function collectData()
+ {
+ $url = $this->getAjaxURI();
+
+ if ($this->queriedContext === 'By Author') {
+ $url = $this->getURI();
+ }
+
+ $html = getSimpleHTMLDOM($url);
+ $html = defaultLinkTo($html, $this->getURI());
+
+ if ($html->find('h1', 0)) {
+ $this->feedTitle = $html->find('h1', 0)->plaintext;
+ }
+
+ if ($html->find('h2.title-v4', 0)) {
+ $html->find('span.tooltiptext', 0)->innertext = '';
+ $this->feedTitle = $html->find('a.tooltipitem', 0)->plaintext;
+ }
+
+ $items = $html->find('div.item');
+ $limit = 5;
+ foreach (array_slice($items, 0, $limit) as $div) {
+ $this->items[] = $this->getArticle($div->find('a', 0)->href);
+
+ if (count($this->items) >= $this->itemLimit) {
+ break;
+ }
+ }
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'All News':
+ return self::URI . 'news';
+ break;
+ case 'All Opinion':
+ return self::URI . 'people';
+ break;
+ case 'By Tag':
+ return self::URI . $this->getInput('content') . '/?theme=' . $this->getInput('tag');
+ case 'By Region':
+ return self::URI . $this->getInput('content') . '/?region=' . $this->getInput('region');
+ break;
+ case 'By Author':
+ return self::URI . 'profile/?id=' . $this->getInput('profileId');
+ break;
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'All News':
+ return 'News - Openly';
+ break;
+ case 'All Opinion':
+ return 'Opinion - Openly';
+ break;
+ case 'By Tag':
+ if (empty($this->feedTitle)) {
+ $this->feedTitle = $this->getInput('tag');
+ }
+
+ if ($this->getInput('content') === 'people') {
+ return $this->feedTitle . ' - Opinion - Openly';
+ }
+
+ return $this->feedTitle . ' - Openly';
+ break;
+ case 'By Region':
+ if (empty($this->feedTitle)) {
+ $this->feedTitle = $this->getInput('region');
+ }
+
+ if ($this->getInput('content') === 'people') {
+ return $this->feedTitle . ' - Opinion - Openly';
+ }
+
+ return $this->feedTitle . ' - Openly';
+ break;
+ case 'By Author':
+ if (empty($this->feedTitle)) {
+ $this->feedTitle = $this->getInput('profileId');
+ }
+
+ return $this->feedTitle . ' - Author - Openly';
+ break;
+ default:
+ return parent::getName();
+ }
+ }
+
+ private function getAjaxURI()
+ {
+ $part = '/ajax.html?';
+
+ switch ($this->queriedContext) {
+ case 'All News':
+ return self::URI . 'news' . $part;
+ break;
+ case 'All Opinion':
+ return self::URI . 'people' . $part;
+ break;
+ case 'By Tag':
+ return self::URI . $this->getInput('content') . $part . 'theme=' . $this->getInput('tag');
+ break;
+ case 'By Region':
+ return self::URI . $this->getInput('content') . $part . 'region=' . $this->getInput('region');
+ break;
+ }
+ }
+
+ private function getArticle($url)
+ {
+ $article = getSimpleHTMLDOMCached($url, self::ARTICLE_CACHE_TIMEOUT);
+ $article = defaultLinkTo($article, $this->getURI());
+
+ $item = [];
+ $item['title'] = $article->find('h1', 0)->plaintext;
+ $item['uri'] = $url;
+ $item['content'] = $article->find('div.body-text', 0);
+ $item['enclosures'][] = $article->find('meta[name="twitter:image"]', 0)->content;
+ $item['timestamp'] = $article->find('div.meta.small', 0)->plaintext;
+
+ if ($article->find('div.meta a', 0)) {
+ $item['author'] = $article->find('div.meta a', 0)->plaintext;
+ }
+
+ foreach ($article->find('div.themes li') as $li) {
+ $item['categories'][] = trim(htmlspecialchars($li->plaintext, ENT_QUOTES));
+ }
+
+ return $item;
+ }
}
diff --git a/bridges/OpenwhydBridge.php b/bridges/OpenwhydBridge.php
index 865003c6..431173bc 100644
--- a/bridges/OpenwhydBridge.php
+++ b/bridges/OpenwhydBridge.php
@@ -1,62 +1,66 @@
<?php
-class OpenwhydBridge extends BridgeAbstract {
-
- const MAINTAINER = 'kranack';
- const NAME = 'Openwhyd Bridge';
- const URI = 'https://openwhyd.org';
- const CACHE_TIMEOUT = 600; // 10min
- const DESCRIPTION = 'Returns 10 newest music from user profile';
-
- const PARAMETERS = array( array(
- 'u' => array(
- 'name' => 'username/id',
- 'exampleValue' => '5247f0267e91c862b2b052d0',
- 'required' => true
- )
- ));
-
- private $userName = '';
-
- public function getIcon() {
- return self::URI . '/images/favicon.ico';
- }
-
- public function collectData(){
- $html = '';
- if(strlen(preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))) == 24) {
- // is input the userid ?
- $html = getSimpleHTMLDOM(
- self::URI . '/u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))
- );
- } else { // input may be the username
- $html = getSimpleHTMLDOM(
- self::URI . '/search?q=' . urlencode($this->getInput('u'))
- );
-
- for($j = 0; $j < 5; $j++) {
- if(strtolower($html->find('div.user', $j)->find('a', 0)->plaintext) == strtolower($this->getInput('u'))) {
- $html = getSimpleHTMLDOM(
- self::URI . $html->find('div.user', $j)->find('a', 0)->getAttribute('href')
- );
- break;
- }
- }
- }
- $this->userName = $html->find('div#profileTop', 0)->find('h1', 0)->plaintext;
-
- for($i = 0; $i < 10; $i++) {
- $track = $html->find('div.post', $i);
- $item = array();
- $item['author'] = $track->find('h2', 0)->plaintext;
- $item['title'] = $track->find('h2', 0)->plaintext;
- $item['content'] = $track->find('a.thumb', 0) . '<br/>' . $track->find('h2', 0)->plaintext;
- $item['id'] = self::URI . $track->find('a.no-ajaxy', 0)->getAttribute('href');
- $item['uri'] = self::URI . $track->find('a.no-ajaxy', 0)->getAttribute('href');
- $this->items[] = $item;
- }
- }
-
- public function getName(){
- return (!empty($this->userName) ? $this->userName . ' - ' : '') . 'Openwhyd Bridge';
- }
+
+class OpenwhydBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'kranack';
+ const NAME = 'Openwhyd Bridge';
+ const URI = 'https://openwhyd.org';
+ const CACHE_TIMEOUT = 600; // 10min
+ const DESCRIPTION = 'Returns 10 newest music from user profile';
+
+ const PARAMETERS = [ [
+ 'u' => [
+ 'name' => 'username/id',
+ 'exampleValue' => '5247f0267e91c862b2b052d0',
+ 'required' => true
+ ]
+ ]];
+
+ private $userName = '';
+
+ public function getIcon()
+ {
+ return self::URI . '/images/favicon.ico';
+ }
+
+ public function collectData()
+ {
+ $html = '';
+ if (strlen(preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))) == 24) {
+ // is input the userid ?
+ $html = getSimpleHTMLDOM(
+ self::URI . '/u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))
+ );
+ } else { // input may be the username
+ $html = getSimpleHTMLDOM(
+ self::URI . '/search?q=' . urlencode($this->getInput('u'))
+ );
+
+ for ($j = 0; $j < 5; $j++) {
+ if (strtolower($html->find('div.user', $j)->find('a', 0)->plaintext) == strtolower($this->getInput('u'))) {
+ $html = getSimpleHTMLDOM(
+ self::URI . $html->find('div.user', $j)->find('a', 0)->getAttribute('href')
+ );
+ break;
+ }
+ }
+ }
+ $this->userName = $html->find('div#profileTop', 0)->find('h1', 0)->plaintext;
+
+ for ($i = 0; $i < 10; $i++) {
+ $track = $html->find('div.post', $i);
+ $item = [];
+ $item['author'] = $track->find('h2', 0)->plaintext;
+ $item['title'] = $track->find('h2', 0)->plaintext;
+ $item['content'] = $track->find('a.thumb', 0) . '<br/>' . $track->find('h2', 0)->plaintext;
+ $item['id'] = self::URI . $track->find('a.no-ajaxy', 0)->getAttribute('href');
+ $item['uri'] = self::URI . $track->find('a.no-ajaxy', 0)->getAttribute('href');
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName()
+ {
+ return (!empty($this->userName) ? $this->userName . ' - ' : '') . 'Openwhyd Bridge';
+ }
}
diff --git a/bridges/OpenwrtSecurityBridge.php b/bridges/OpenwrtSecurityBridge.php
index 8cdeec01..bfe4e9dd 100644
--- a/bridges/OpenwrtSecurityBridge.php
+++ b/bridges/OpenwrtSecurityBridge.php
@@ -1,36 +1,40 @@
<?php
-class OpenwrtSecurityBridge extends BridgeAbstract {
- const NAME = 'OpenWrt Security Advisories';
- const URI = 'https://openwrt.org/advisory/start';
- const DESCRIPTION = 'Security Advisories published by openwrt.org';
- const MAINTAINER = 'mschwld';
- const CACHE_TIMEOUT = 3600;
- const WEBROOT = 'https://openwrt.org';
-
- public function collectData() {
- $item = array();
- $html = getSimpleHTMLDOM(self::URI);
-
- $advisories = $html->find('div[class=plugin_nspages]', 0);
-
- foreach($advisories->find('a[class=wikilink1]') as $element) {
- $item = array();
-
- $row = $element->innertext;
-
- $item['title'] = substr($row, 0, strpos($row, ' - '));
- $item['timestamp'] = $this->getDate($element->href);
- $item['uri'] = self::WEBROOT . $element->href;
- $item['uid'] = self::WEBROOT . $element->href;
- $item['content'] = substr($row, strpos($row, ' - ') + 3);
- $item['author'] = 'OpenWrt Project';
-
- $this->items[] = $item;
- }
- }
-
- private function getDate($href) {
- $date = substr($href, -12);
- return $date;
- }
+
+class OpenwrtSecurityBridge extends BridgeAbstract
+{
+ const NAME = 'OpenWrt Security Advisories';
+ const URI = 'https://openwrt.org/advisory/start';
+ const DESCRIPTION = 'Security Advisories published by openwrt.org';
+ const MAINTAINER = 'mschwld';
+ const CACHE_TIMEOUT = 3600;
+ const WEBROOT = 'https://openwrt.org';
+
+ public function collectData()
+ {
+ $item = [];
+ $html = getSimpleHTMLDOM(self::URI);
+
+ $advisories = $html->find('div[class=plugin_nspages]', 0);
+
+ foreach ($advisories->find('a[class=wikilink1]') as $element) {
+ $item = [];
+
+ $row = $element->innertext;
+
+ $item['title'] = substr($row, 0, strpos($row, ' - '));
+ $item['timestamp'] = $this->getDate($element->href);
+ $item['uri'] = self::WEBROOT . $element->href;
+ $item['uid'] = self::WEBROOT . $element->href;
+ $item['content'] = substr($row, strpos($row, ' - ') + 3);
+ $item['author'] = 'OpenWrt Project';
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function getDate($href)
+ {
+ $date = substr($href, -12);
+ return $date;
+ }
}
diff --git a/bridges/OtrkeyFinderBridge.php b/bridges/OtrkeyFinderBridge.php
index 9c5b8a2a..7920ff9a 100644
--- a/bridges/OtrkeyFinderBridge.php
+++ b/bridges/OtrkeyFinderBridge.php
@@ -1,180 +1,197 @@
<?php
-class OtrkeyFinderBridge extends BridgeAbstract {
- const MAINTAINER = 'mibe';
- const NAME = 'OtrkeyFinder';
- const URI = 'https://otrkeyfinder.com';
- const URI_TEMPLATE = 'https://otrkeyfinder.com/en/?search=%s&order=&page=%d';
- const CACHE_TIMEOUT = 3600; // 1h
- const DESCRIPTION = 'Returns the newest .otrkey files matching the search criteria.';
- const PARAMETERS = array(
- array(
- 'searchterm' => array(
- 'name' => 'Search term',
- 'exampleValue' => 'Tatort',
- 'title' => 'The search term is case-insensitive',
- ),
- 'station' => array(
- 'name' => 'Station name',
- 'exampleValue' => 'ARD',
- ),
- 'type' => array(
- 'name' => 'Media type',
- 'type' => 'list',
- 'values' => array(
- 'any' => '',
- 'Detail' => array(
- 'HD' => 'HD.avi',
- 'AC3' => 'HD.ac3',
- 'HD &amp; AC3' => 'HD.',
- 'HQ' => 'HQ.avi',
- 'AVI' => 'g.avi', // 'g.' to exclude HD.avi and HQ.avi (filename always contains 'mpg.')
- 'MP4' => '.mp4',
- ),
- ),
- ),
- 'minTime' => array(
- 'name' => 'Min. running time',
- 'type' => 'number',
- 'title' => 'The minimum running time in minutes. The resolution is 5 minutes.',
- 'exampleValue' => '90',
- 'defaultValue' => '0',
- ),
- 'maxTime' => array(
- 'name' => 'Max. running time',
- 'type' => 'number',
- 'title' => 'The maximum running time in minutes. The resolution is 5 minutes.',
- 'exampleValue' => '120',
- 'defaultValue' => '0',
- ),
- 'pages' => array(
- 'name' => 'Number of pages',
- 'type' => 'number',
- 'title' => 'Specifies the number of pages to fetch. Increase this value if you get an empty feed.',
- 'exampleValue' => '5',
- 'defaultValue' => '5',
- ),
- )
- );
- // Example: Terminator_20.04.13_02-25_sf2_100_TVOON_DE.mpg.avi.otrkey
- // The first group is the running time in minutes
- const FILENAME_REGEX = '/_(\d+)_TVOON_DE\.mpg\..+\.otrkey/';
- // year.month.day_hour-minute with leading zeros
- const TIME_REGEX = '/\d{2}\.\d{2}\.\d{2}_\d{2}-\d{2}/';
- const CONTENT_TEMPLATE = '<ul>%s</ul>';
- const MIRROR_TEMPLATE = '<li><a href="https://otrkeyfinder.com%s">%s</a></li>';
-
- public function collectData() {
- $pages = $this->getInput('pages');
-
- for($page = 1; $page <= $pages; $page++) {
- $uri = $this->buildUri($page);
-
- $html = getSimpleHTMLDOMCached($uri, self::CACHE_TIMEOUT);
-
- $keys = $html->find('div.otrkey');
-
- foreach($keys as $key) {
- $temp = $this->buildItem($key);
-
- if ($temp != null)
- $this->items[] = $temp;
- }
-
- // Sleep for 0.5 seconds to don't hammer the server.
- usleep(500000);
- }
- }
-
- private function buildUri($page) {
- $searchterm = $this->getInput('searchterm');
- $station = $this->getInput('station');
- $type = $this->getInput('type');
-
- // Combine all three parts to a search query by separating them with white space
- $search = implode(' ', array($searchterm, $station, $type));
- $search = trim($search);
- $search = urlencode($search);
-
- return sprintf(self::URI_TEMPLATE, $search, $page);
- }
-
- private function buildItem(simple_html_dom_node $node) {
- $file = $this->getFilename($node);
-
- if ($file == null)
- return null;
-
- $minTime = $this->getInput('minTime');
- $maxTime = $this->getInput('maxTime');
-
- // Do we need to check the running time?
- if ($minTime != 0 || $maxTime != 0) {
- if ($maxTime > 0 && $maxTime < $minTime)
- returnClientError('The minimum running time must be less than the maximum running time.');
-
- preg_match(self::FILENAME_REGEX, $file, $matches);
-
- if (!isset($matches[1]))
- return null;
-
- $time = (integer)$matches[1];
-
- // Check for minimum running time
- if ($minTime > 0 && $minTime > $time)
- return null;
-
- // Check for maximum running time
- if ($maxTime > 0 && $maxTime < $time)
- return null;
- }
-
- $item = array();
- $item['title'] = $file;
-
- // The URI_TEMPLATE for querying the site can be reused here
- $item['uri'] = sprintf(self::URI_TEMPLATE, $file, 1);
-
- $content = $this->buildContent($node);
-
- if ($content != null)
- $item['content'] = $content;
-
- if (preg_match(self::TIME_REGEX, $file, $matches) === 1) {
- $item['timestamp'] = DateTime::createFromFormat(
- 'y.m.d_H-i',
- $matches[0],
- new DateTimeZone('Europe/Berlin')
- )->getTimestamp();
- }
- return $item;
- }
-
- private function getFilename(simple_html_dom_node $node) {
- $file = $node->find('.file', 0);
-
- if ($file == null)
- return null;
-
- // Sometimes there is HTML in the filename - we don't want that.
- // To filter that out, enumerate to the node which contains the text only.
- foreach($file->nodes as $node)
- if ($node->nodetype == HDOM_TYPE_TEXT)
- return trim($node->innertext);
-
- return null;
- }
-
- private function buildContent(simple_html_dom_node $node) {
- $mirrors = $node->find('div.mirror');
- $list = '';
-
- // Build list of available mirrors
- foreach($mirrors as $mirror) {
- $anchor = $mirror->find('a', 0);
- $list .= sprintf(self::MIRROR_TEMPLATE, $anchor->href, $anchor->innertext);
- }
-
- return sprintf(self::CONTENT_TEMPLATE, $list);
- }
+class OtrkeyFinderBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'mibe';
+ const NAME = 'OtrkeyFinder';
+ const URI = 'https://otrkeyfinder.com';
+ const URI_TEMPLATE = 'https://otrkeyfinder.com/en/?search=%s&order=&page=%d';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Returns the newest .otrkey files matching the search criteria.';
+ const PARAMETERS = [
+ [
+ 'searchterm' => [
+ 'name' => 'Search term',
+ 'exampleValue' => 'Tatort',
+ 'title' => 'The search term is case-insensitive',
+ ],
+ 'station' => [
+ 'name' => 'Station name',
+ 'exampleValue' => 'ARD',
+ ],
+ 'type' => [
+ 'name' => 'Media type',
+ 'type' => 'list',
+ 'values' => [
+ 'any' => '',
+ 'Detail' => [
+ 'HD' => 'HD.avi',
+ 'AC3' => 'HD.ac3',
+ 'HD &amp; AC3' => 'HD.',
+ 'HQ' => 'HQ.avi',
+ 'AVI' => 'g.avi', // 'g.' to exclude HD.avi and HQ.avi (filename always contains 'mpg.')
+ 'MP4' => '.mp4',
+ ],
+ ],
+ ],
+ 'minTime' => [
+ 'name' => 'Min. running time',
+ 'type' => 'number',
+ 'title' => 'The minimum running time in minutes. The resolution is 5 minutes.',
+ 'exampleValue' => '90',
+ 'defaultValue' => '0',
+ ],
+ 'maxTime' => [
+ 'name' => 'Max. running time',
+ 'type' => 'number',
+ 'title' => 'The maximum running time in minutes. The resolution is 5 minutes.',
+ 'exampleValue' => '120',
+ 'defaultValue' => '0',
+ ],
+ 'pages' => [
+ 'name' => 'Number of pages',
+ 'type' => 'number',
+ 'title' => 'Specifies the number of pages to fetch. Increase this value if you get an empty feed.',
+ 'exampleValue' => '5',
+ 'defaultValue' => '5',
+ ],
+ ]
+ ];
+ // Example: Terminator_20.04.13_02-25_sf2_100_TVOON_DE.mpg.avi.otrkey
+ // The first group is the running time in minutes
+ const FILENAME_REGEX = '/_(\d+)_TVOON_DE\.mpg\..+\.otrkey/';
+ // year.month.day_hour-minute with leading zeros
+ const TIME_REGEX = '/\d{2}\.\d{2}\.\d{2}_\d{2}-\d{2}/';
+ const CONTENT_TEMPLATE = '<ul>%s</ul>';
+ const MIRROR_TEMPLATE = '<li><a href="https://otrkeyfinder.com%s">%s</a></li>';
+
+ public function collectData()
+ {
+ $pages = $this->getInput('pages');
+
+ for ($page = 1; $page <= $pages; $page++) {
+ $uri = $this->buildUri($page);
+
+ $html = getSimpleHTMLDOMCached($uri, self::CACHE_TIMEOUT);
+
+ $keys = $html->find('div.otrkey');
+
+ foreach ($keys as $key) {
+ $temp = $this->buildItem($key);
+
+ if ($temp != null) {
+ $this->items[] = $temp;
+ }
+ }
+
+ // Sleep for 0.5 seconds to don't hammer the server.
+ usleep(500000);
+ }
+ }
+
+ private function buildUri($page)
+ {
+ $searchterm = $this->getInput('searchterm');
+ $station = $this->getInput('station');
+ $type = $this->getInput('type');
+
+ // Combine all three parts to a search query by separating them with white space
+ $search = implode(' ', [$searchterm, $station, $type]);
+ $search = trim($search);
+ $search = urlencode($search);
+
+ return sprintf(self::URI_TEMPLATE, $search, $page);
+ }
+
+ private function buildItem(simple_html_dom_node $node)
+ {
+ $file = $this->getFilename($node);
+
+ if ($file == null) {
+ return null;
+ }
+
+ $minTime = $this->getInput('minTime');
+ $maxTime = $this->getInput('maxTime');
+
+ // Do we need to check the running time?
+ if ($minTime != 0 || $maxTime != 0) {
+ if ($maxTime > 0 && $maxTime < $minTime) {
+ returnClientError('The minimum running time must be less than the maximum running time.');
+ }
+
+ preg_match(self::FILENAME_REGEX, $file, $matches);
+
+ if (!isset($matches[1])) {
+ return null;
+ }
+
+ $time = (int)$matches[1];
+
+ // Check for minimum running time
+ if ($minTime > 0 && $minTime > $time) {
+ return null;
+ }
+
+ // Check for maximum running time
+ if ($maxTime > 0 && $maxTime < $time) {
+ return null;
+ }
+ }
+
+ $item = [];
+ $item['title'] = $file;
+
+ // The URI_TEMPLATE for querying the site can be reused here
+ $item['uri'] = sprintf(self::URI_TEMPLATE, $file, 1);
+
+ $content = $this->buildContent($node);
+
+ if ($content != null) {
+ $item['content'] = $content;
+ }
+
+ if (preg_match(self::TIME_REGEX, $file, $matches) === 1) {
+ $item['timestamp'] = DateTime::createFromFormat(
+ 'y.m.d_H-i',
+ $matches[0],
+ new DateTimeZone('Europe/Berlin')
+ )->getTimestamp();
+ }
+
+ return $item;
+ }
+
+ private function getFilename(simple_html_dom_node $node)
+ {
+ $file = $node->find('.file', 0);
+
+ if ($file == null) {
+ return null;
+ }
+
+ // Sometimes there is HTML in the filename - we don't want that.
+ // To filter that out, enumerate to the node which contains the text only.
+ foreach ($file->nodes as $node) {
+ if ($node->nodetype == HDOM_TYPE_TEXT) {
+ return trim($node->innertext);
+ }
+ }
+
+ return null;
+ }
+
+ private function buildContent(simple_html_dom_node $node)
+ {
+ $mirrors = $node->find('div.mirror');
+ $list = '';
+
+ // Build list of available mirrors
+ foreach ($mirrors as $mirror) {
+ $anchor = $mirror->find('a', 0);
+ $list .= sprintf(self::MIRROR_TEMPLATE, $anchor->href, $anchor->innertext);
+ }
+
+ return sprintf(self::CONTENT_TEMPLATE, $list);
+ }
}
diff --git a/bridges/PCGWNewsBridge.php b/bridges/PCGWNewsBridge.php
index 92b80fdc..4b3a7c76 100644
--- a/bridges/PCGWNewsBridge.php
+++ b/bridges/PCGWNewsBridge.php
@@ -1,34 +1,38 @@
<?php
-class PCGWNewsBridge extends FeedExpander {
- const MAINTAINER = 'somini';
- const NAME = 'PCGamingWiki News';
- const BASE_URI = 'https://www.pcgamingwiki.com';
- const URI = self::BASE_URI . '/wiki/PCGamingWiki:News';
- const DESCRIPTION = 'PCGW News Feed';
- public function getIcon() {
- return 'https://static.pcgamingwiki.com/favicons/pcgamingwiki.png';
- }
+class PCGWNewsBridge extends FeedExpander
+{
+ const MAINTAINER = 'somini';
+ const NAME = 'PCGamingWiki News';
+ const BASE_URI = 'https://www.pcgamingwiki.com';
+ const URI = self::BASE_URI . '/wiki/PCGamingWiki:News';
+ const DESCRIPTION = 'PCGW News Feed';
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI());
+ public function getIcon()
+ {
+ return 'https://static.pcgamingwiki.com/favicons/pcgamingwiki.png';
+ }
- $now = strtotime('now');
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
- foreach($html->find('.mw-parser-output .news_li') as $element) {
- $item = array();
+ $now = strtotime('now');
- $date_string = $element->find('b', 0)->innertext;
- $date = strtotime($date_string);
- if ($date > $now) {
- $date = strtotime($date_string . ' - 1 year');
- }
- $item['title'] = self::NAME . ' for ' . date('Y-m-d', $date);
- $item['content'] = $element;
- $item['uri'] = $this->getURI();
- $item['timestamp'] = $date;
+ foreach ($html->find('.mw-parser-output .news_li') as $element) {
+ $item = [];
- $this->items[] = $item;
- }
- }
+ $date_string = $element->find('b', 0)->innertext;
+ $date = strtotime($date_string);
+ if ($date > $now) {
+ $date = strtotime($date_string . ' - 1 year');
+ }
+ $item['title'] = self::NAME . ' for ' . date('Y-m-d', $date);
+ $item['content'] = $element;
+ $item['uri'] = $this->getURI();
+ $item['timestamp'] = $date;
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/PanacheDigitalGamesBridge.php b/bridges/PanacheDigitalGamesBridge.php
index ba9640ef..61164381 100644
--- a/bridges/PanacheDigitalGamesBridge.php
+++ b/bridges/PanacheDigitalGamesBridge.php
@@ -1,45 +1,50 @@
<?php
-class PanacheDigitalGamesBridge extends BridgeAbstract {
- const NAME = 'Panache Digital Games';
- const URI = 'https://www.panachedigitalgames.com';
- const DESCRIPTION = 'Panache Digital Games News Blog';
- const MAINTAINER = 'somini';
- const PARAMETERS = array(
- );
-
- public function getIcon() {
- return 'https://www.panachedigitalgames.com/favicon-32x32.png';
- }
-
- public function getURI() {
- return self::URI . '/en/news/';
- }
-
- public function collectData() {
- $articles = self::getURI();
- $html = getSimpleHTMLDOMCached($articles);
-
- foreach($html->find('.news-item') as $element) {
- $item = array();
-
- $title = $element->find('.news-item-texts-title', 0);
- $link = $element->find('.news-item-texts a', 0);
- $timestamp = $element->find('.news-item-texts-date', 0);
-
- $item['title'] = $title->plaintext;
- $item['uri'] = self::URI . $link->href;
- $item['timestamp'] = strtotime($timestamp->plaintext);
-
- $image_html = $element->find('.news-item-thumbnail-image', 0);
- if ($image_html) {
- $image_strings = explode('\'', $image_html);
- /* Debug::log('S: ' . count($image_strings) . '||' . implode('_ _', $image_strings)); */
- if (count($image_strings) == 4) {
- $item['content'] = '<img src="' . $image_strings[1] . '" />';
- }
- }
-
- $this->items[] = $item;
- }
- }
+
+class PanacheDigitalGamesBridge extends BridgeAbstract
+{
+ const NAME = 'Panache Digital Games';
+ const URI = 'https://www.panachedigitalgames.com';
+ const DESCRIPTION = 'Panache Digital Games News Blog';
+ const MAINTAINER = 'somini';
+ const PARAMETERS = [
+ ];
+
+ public function getIcon()
+ {
+ return 'https://www.panachedigitalgames.com/favicon-32x32.png';
+ }
+
+ public function getURI()
+ {
+ return self::URI . '/en/news/';
+ }
+
+ public function collectData()
+ {
+ $articles = self::getURI();
+ $html = getSimpleHTMLDOMCached($articles);
+
+ foreach ($html->find('.news-item') as $element) {
+ $item = [];
+
+ $title = $element->find('.news-item-texts-title', 0);
+ $link = $element->find('.news-item-texts a', 0);
+ $timestamp = $element->find('.news-item-texts-date', 0);
+
+ $item['title'] = $title->plaintext;
+ $item['uri'] = self::URI . $link->href;
+ $item['timestamp'] = strtotime($timestamp->plaintext);
+
+ $image_html = $element->find('.news-item-thumbnail-image', 0);
+ if ($image_html) {
+ $image_strings = explode('\'', $image_html);
+ /* Debug::log('S: ' . count($image_strings) . '||' . implode('_ _', $image_strings)); */
+ if (count($image_strings) == 4) {
+ $item['content'] = '<img src="' . $image_strings[1] . '" />';
+ }
+ }
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/ParksOnTheAirBridge.php b/bridges/ParksOnTheAirBridge.php
index ceaf78b6..67910f6e 100644
--- a/bridges/ParksOnTheAirBridge.php
+++ b/bridges/ParksOnTheAirBridge.php
@@ -1,27 +1,28 @@
<?php
-class ParksOnTheAirBridge extends BridgeAbstract {
- const MAINTAINER = 's0lesurviv0r';
- const NAME = 'Parks On The Air Spots';
- const URI = 'https://pota.app/#';
- const API_URI = 'https://api.pota.app/spot/activator';
- const CACHE_TIMEOUT = 60; // 1m
- const DESCRIPTION = 'Parks On The Air Activator Spots';
+class ParksOnTheAirBridge extends BridgeAbstract
+{
+ const MAINTAINER = 's0lesurviv0r';
+ const NAME = 'Parks On The Air Spots';
+ const URI = 'https://pota.app/#';
+ const API_URI = 'https://api.pota.app/spot/activator';
+ const CACHE_TIMEOUT = 60; // 1m
+ const DESCRIPTION = 'Parks On The Air Activator Spots';
- public function collectData() {
+ public function collectData()
+ {
+ $header = ['Content-type:application/json'];
+ $opts = [CURLOPT_HTTPGET => 1];
+ $json = getContents(self::API_URI, $header, $opts);
- $header = array('Content-type:application/json');
- $opts = array(CURLOPT_HTTPGET => 1);
- $json = getContents(self::API_URI, $header, $opts);
+ $spots = json_decode($json, true);
- $spots = json_decode($json, true);
+ foreach ($spots as $spot) {
+ $title = $spot['activator'] . ' @ ' . $spot['reference'] . ' ' .
+ $spot['frequency'] . ' kHz';
+ $park_link = self::URI . '/park/' . $spot['reference'];
- foreach ($spots as $spot) {
- $title = $spot['activator'] . ' @ ' . $spot['reference'] . ' ' .
- $spot['frequency'] . ' kHz';
- $park_link = self::URI . '/park/' . $spot['reference'];
-
- $content = <<<EOL
+ $content = <<<EOL
<a href="{$park_link}">
{$spot['reference']}, {$spot['name']}</a><br />
Location: {$spot['locationDesc']}<br />
@@ -30,12 +31,12 @@ Spotter: {$spot['spotter']}<br />
Comments: {$spot['comments']}
EOL;
- $this->items[] = array(
- 'uri' => $park_link,
- 'title' => $title,
- 'content' => $content,
- 'timestamp' => $spot['spotTime']
- );
- }
- }
+ $this->items[] = [
+ 'uri' => $park_link,
+ 'title' => $title,
+ 'content' => $content,
+ 'timestamp' => $spot['spotTime']
+ ];
+ }
+ }
}
diff --git a/bridges/ParlerBridge.php b/bridges/ParlerBridge.php
index 69b4f9a1..97e9dab0 100644
--- a/bridges/ParlerBridge.php
+++ b/bridges/ParlerBridge.php
@@ -2,79 +2,79 @@
final class ParlerBridge extends BridgeAbstract
{
- const NAME = 'Parler.com bridge';
- const URI = 'https://parler.com';
- const DESCRIPTION = 'Fetches the latest posts from a parler user';
- const MAINTAINER = 'dvikan';
- const CACHE_TIMEOUT = 60 * 15; // 15m
- const PARAMETERS = [
- [
- 'user' => [
- 'name' => 'User',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'NigelFarage',
- ],
- 'limit' => self::LIMIT,
- ]
- ];
+ const NAME = 'Parler.com bridge';
+ const URI = 'https://parler.com';
+ const DESCRIPTION = 'Fetches the latest posts from a parler user';
+ const MAINTAINER = 'dvikan';
+ const CACHE_TIMEOUT = 60 * 15; // 15m
+ const PARAMETERS = [
+ [
+ 'user' => [
+ 'name' => 'User',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'NigelFarage',
+ ],
+ 'limit' => self::LIMIT,
+ ]
+ ];
- public function collectData()
- {
- $user = trim($this->getInput('user'));
+ public function collectData()
+ {
+ $user = trim($this->getInput('user'));
- if (preg_match('#^https?://parler\.com/(\w+)#i', $user, $m)) {
- $user = $m[1];
- }
+ if (preg_match('#^https?://parler\.com/(\w+)#i', $user, $m)) {
+ $user = $m[1];
+ }
- $posts = $this->fetchParlerProfileFeed($user);
+ $posts = $this->fetchParlerProfileFeed($user);
- foreach ($posts as $post) {
- // For some reason, the post data is placed inside primary attribute
- $primary = $post->primary;
+ foreach ($posts as $post) {
+ // For some reason, the post data is placed inside primary attribute
+ $primary = $post->primary;
- $item = [
- 'title' => mb_substr($primary->body, 0, 100),
- 'uri' => sprintf('https://parler.com/feed/%s', $primary->uuid),
- 'author' => $primary->username,
- 'uid' => $primary->uuid,
- 'content' => nl2br($primary->full_body),
- ];
+ $item = [
+ 'title' => mb_substr($primary->body, 0, 100),
+ 'uri' => sprintf('https://parler.com/feed/%s', $primary->uuid),
+ 'author' => $primary->username,
+ 'uid' => $primary->uuid,
+ 'content' => nl2br($primary->full_body),
+ ];
- $date = DateTimeImmutable::createFromFormat('m/d/YH:i A', $primary->date_str . $primary->time_str);
- if ($date) {
- $item['timestamp'] = $date->getTimestamp();
- } else {
- Debug::log(sprintf('Unable to parse data from Parler.com: "%s"', $date));
- }
+ $date = DateTimeImmutable::createFromFormat('m/d/YH:i A', $primary->date_str . $primary->time_str);
+ if ($date) {
+ $item['timestamp'] = $date->getTimestamp();
+ } else {
+ Debug::log(sprintf('Unable to parse data from Parler.com: "%s"', $date));
+ }
- if (isset($primary->image)) {
- $item['enclosures'][] = $primary->image;
- $item['content'] .= sprintf('<img loading="lazy" src="%s">', $primary->image);
- }
+ if (isset($primary->image)) {
+ $item['enclosures'][] = $primary->image;
+ $item['content'] .= sprintf('<img loading="lazy" src="%s">', $primary->image);
+ }
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
- private function fetchParlerProfileFeed(string $user): array
- {
- $json = getContents('https://parler.com/open-api/ProfileFeedEndpoint.php', [], [
- CURLOPT_POSTFIELDS => http_build_query([
- 'user' => $user,
- 'page' => '1',
- ]),
- ]);
- $response = json_decode($json);
- if ($response === false) {
- throw new \Exception('Unable to decode json from Parler');
- }
- if ($response->status !== 'ok') {
- throw new \Exception('Did not get OK from Parler');
- }
- if ($response->data === []) {
- throw new \Exception('Unknown Parler username');
- }
- return $response->data;
- }
+ private function fetchParlerProfileFeed(string $user): array
+ {
+ $json = getContents('https://parler.com/open-api/ProfileFeedEndpoint.php', [], [
+ CURLOPT_POSTFIELDS => http_build_query([
+ 'user' => $user,
+ 'page' => '1',
+ ]),
+ ]);
+ $response = json_decode($json);
+ if ($response === false) {
+ throw new \Exception('Unable to decode json from Parler');
+ }
+ if ($response->status !== 'ok') {
+ throw new \Exception('Did not get OK from Parler');
+ }
+ if ($response->data === []) {
+ throw new \Exception('Unknown Parler username');
+ }
+ return $response->data;
+ }
}
diff --git a/bridges/ParuVenduImmoBridge.php b/bridges/ParuVenduImmoBridge.php
index e7a0c02e..f48e36df 100644
--- a/bridges/ParuVenduImmoBridge.php
+++ b/bridges/ParuVenduImmoBridge.php
@@ -1,112 +1,115 @@
<?php
-class ParuVenduImmoBridge extends BridgeAbstract {
-
- const MAINTAINER = 'polo2ro';
- const NAME = 'Paru Vendu Immobilier';
- const URI = 'https://www.paruvendu.fr';
- const CACHE_TIMEOUT = 10800; // 3h
- const DESCRIPTION = 'Returns the ads from the first page of search result.';
-
- const PARAMETERS = array( array(
- 'minarea' => array(
- 'name' => 'Minimal surface m²',
- 'type' => 'number'
- ),
- 'maxprice' => array(
- 'name' => 'Max price',
- 'type' => 'number'
- ),
- 'pa' => array(
- 'name' => 'Country code',
- 'exampleValue' => 'FR'
- ),
- 'lo' => array(
- 'name' => 'department numbers or postal codes, comma-separated'
- )
- ));
-
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI());
-
- $elements = $html->find('#bloc_liste > div.ergov3-annonce a');
-
- foreach($elements as $element) {
-
- if(!$element->title) {
- continue;
- }
-
- $img = '';
- foreach($element->find('span.img img') as $img) {
- if($img->original) {
- $img = '<img src="' . $img->original . '" />';
- }
- }
-
- $description = $element->find('p', 0);
- if ($description) {
- $desc = str_replace("voir l'annonce", '', $description->innertext);
- } else {
- $desc = '';
- }
-
- $priceElement = $element->find('div.ergov3-priceannonce', 0);
- if ($priceElement) {
- $price = $priceElement->innertext;
- } else {
- $price = '';
- }
-
- list($href) = explode('#', $element->href);
-
- $item = array();
- $item['uri'] = self::URI . $href;
- $item['title'] = $element->title;
- $item['content'] = $img . $desc . $price;
- $this->items[] = $item;
- }
- }
-
- public function getURI(){
- $appartment = '&tbApp=1&tbDup=1&tbChb=1&tbLof=1&tbAtl=1&tbPla=1';
- $maison = '&tbMai=1&tbVil=1&tbCha=1&tbPro=1&tbHot=1&tbMou=1&tbFer=1';
- $link = self::URI
- . '/immobilier/annonceimmofo/liste/listeAnnonces?tt=1'
- . $appartment
- . $maison;
-
- if($this->getInput('minarea')) {
- $link .= '&sur0=' . urlencode($this->getInput('minarea'));
- }
-
- if($this->getInput('maxprice')) {
- $link .= '&px1=' . urlencode($this->getInput('maxprice'));
- }
-
- if($this->getInput('pa')) {
- $link .= '&pa=' . urlencode($this->getInput('pa'));
- }
-
- if($this->getInput('lo')) {
- $link .= '&lo=' . urlencode($this->getInput('lo'));
- }
- return $link;
- }
-
- public function getName(){
- if(!is_null($this->getInput('minarea'))) {
- $request = '';
- $minarea = $this->getInput('minarea');
- if(!empty($minarea)) {
- $request .= ' ' . $minarea . ' m2';
- }
- $location = $this->getInput('lo');
- if(!empty($location)) {
- $request .= ' In: ' . $location;
- }
- return 'Paru Vendu Immobilier' . $request;
- }
-
- return parent::getName();
- }
+
+class ParuVenduImmoBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'polo2ro';
+ const NAME = 'Paru Vendu Immobilier';
+ const URI = 'https://www.paruvendu.fr';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the ads from the first page of search result.';
+
+ const PARAMETERS = [ [
+ 'minarea' => [
+ 'name' => 'Minimal surface m²',
+ 'type' => 'number'
+ ],
+ 'maxprice' => [
+ 'name' => 'Max price',
+ 'type' => 'number'
+ ],
+ 'pa' => [
+ 'name' => 'Country code',
+ 'exampleValue' => 'FR'
+ ],
+ 'lo' => [
+ 'name' => 'department numbers or postal codes, comma-separated'
+ ]
+ ]];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $elements = $html->find('#bloc_liste > div.ergov3-annonce a');
+
+ foreach ($elements as $element) {
+ if (!$element->title) {
+ continue;
+ }
+
+ $img = '';
+ foreach ($element->find('span.img img') as $img) {
+ if ($img->original) {
+ $img = '<img src="' . $img->original . '" />';
+ }
+ }
+
+ $description = $element->find('p', 0);
+ if ($description) {
+ $desc = str_replace("voir l'annonce", '', $description->innertext);
+ } else {
+ $desc = '';
+ }
+
+ $priceElement = $element->find('div.ergov3-priceannonce', 0);
+ if ($priceElement) {
+ $price = $priceElement->innertext;
+ } else {
+ $price = '';
+ }
+
+ list($href) = explode('#', $element->href);
+
+ $item = [];
+ $item['uri'] = self::URI . $href;
+ $item['title'] = $element->title;
+ $item['content'] = $img . $desc . $price;
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ $appartment = '&tbApp=1&tbDup=1&tbChb=1&tbLof=1&tbAtl=1&tbPla=1';
+ $maison = '&tbMai=1&tbVil=1&tbCha=1&tbPro=1&tbHot=1&tbMou=1&tbFer=1';
+ $link = self::URI
+ . '/immobilier/annonceimmofo/liste/listeAnnonces?tt=1'
+ . $appartment
+ . $maison;
+
+ if ($this->getInput('minarea')) {
+ $link .= '&sur0=' . urlencode($this->getInput('minarea'));
+ }
+
+ if ($this->getInput('maxprice')) {
+ $link .= '&px1=' . urlencode($this->getInput('maxprice'));
+ }
+
+ if ($this->getInput('pa')) {
+ $link .= '&pa=' . urlencode($this->getInput('pa'));
+ }
+
+ if ($this->getInput('lo')) {
+ $link .= '&lo=' . urlencode($this->getInput('lo'));
+ }
+ return $link;
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('minarea'))) {
+ $request = '';
+ $minarea = $this->getInput('minarea');
+ if (!empty($minarea)) {
+ $request .= ' ' . $minarea . ' m2';
+ }
+ $location = $this->getInput('lo');
+ if (!empty($location)) {
+ $request .= ' In: ' . $location;
+ }
+ return 'Paru Vendu Immobilier' . $request;
+ }
+
+ return parent::getName();
+ }
}
diff --git a/bridges/PatreonBridge.php b/bridges/PatreonBridge.php
index 5f9a4565..a15d0378 100644
--- a/bridges/PatreonBridge.php
+++ b/bridges/PatreonBridge.php
@@ -1,202 +1,217 @@
<?php
-class PatreonBridge extends BridgeAbstract {
- const NAME = 'Patreon Bridge';
- const URI = 'https://www.patreon.com/';
- const CACHE_TIMEOUT = 300; // 5min
- const DESCRIPTION = 'Returns posts by creators on Patreon';
- const MAINTAINER = 'Roliga';
- const PARAMETERS = array( array(
- 'creator' => array(
- 'name' => 'Creator',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'sanityinc',
- 'title' => 'Creator name as seen in their page URL'
- )
- ));
-
- public function collectData(){
- $html = getSimpleHTMLDOMCached($this->getURI(), 86400);
- $regex = '#/api/campaigns/([0-9]+)#';
- if(preg_match($regex, $html->save(), $matches) > 0) {
- $campaign_id = $matches[1];
- } else {
- returnServerError('Could not find campaign ID');
- }
-
- $query = array(
- 'include' => implode(',', array(
- 'user',
- 'attachments',
- 'user_defined_tags',
- //'campaign',
- //'poll.choices',
- //'poll.current_user_responses.user',
- //'poll.current_user_responses.choice',
- //'poll.current_user_responses.poll',
- //'access_rules.tier.null',
- //'images.null',
- //'audio.null'
- )),
- 'fields' => array(
- 'post' => implode(',', array(
- //'change_visibility_at',
- //'comment_count',
- 'content',
- //'current_user_can_delete',
- //'current_user_can_view',
- //'current_user_has_liked',
- //'embed',
- 'image',
- //'is_paid',
- //'like_count',
- //'min_cents_pledged_to_view',
- //'patreon_url',
- //'patron_count',
- //'pledge_url',
- //'post_file',
- //'post_metadata',
- //'post_type',
- 'published_at',
- 'teaser_text',
- //'thumbnail_url',
- 'title',
- //'upgrade_url',
- 'url',
- //'was_posted_by_campaign_owner'
- )),
- 'user' => implode(',', array(
- //'image_url',
- 'full_name',
- //'url'
- ))
- ),
- 'filter' => array(
- 'contains_exclusive_posts' => true,
- 'is_draft' => false,
- 'campaign_id' => $campaign_id
- ),
- 'sort' => '-published_at'
- );
- $posts = $this->apiGet('posts', $query);
-
- foreach($posts->data as $post) {
- $item = array(
- 'uri' => $post->attributes->url,
- 'title' => $post->attributes->title,
- 'timestamp' => $post->attributes->published_at,
- 'content' => '',
- 'uid' => 'patreon.com/' . $post->id
- );
-
- $user = $this->findInclude($posts,
- 'user',
- $post->relationships->user->data->id);
- $item['author'] = $user->full_name;
-
- if(isset($post->attributes->image))
- $item['content'] .= '<p><a href="'
- . $post->attributes->url
- . '"><img src="'
- . $post->attributes->image->thumb_url
- . '" /></a></p>';
-
- if(isset($post->attributes->content)) {
- $item['content'] .= $post->attributes->content;
- } elseif (isset($post->attributes->teaser_text)) {
- $item['content'] .= '<p>'
- . $post->attributes->teaser_text
- . '</p>';
- }
-
- if(isset($post->relationships->user_defined_tags)) {
- $item['categories'] = array();
- foreach($post->relationships->user_defined_tags->data as $tag) {
- $attrs = $this->findInclude($posts, 'post_tag', $tag->id);
- $item['categories'][] = $attrs->value;
- }
- }
-
- if(isset($post->relationships->attachments)) {
- $item['enclosures'] = array();
- foreach($post->relationships->attachments->data as $attachment) {
- $attrs = $this->findInclude($posts, 'attachment', $attachment->id);
- $item['enclosures'][] = $attrs->url;
- }
- }
-
- $this->items[] = $item;
- }
- }
-
- /*
- * Searches the "included" array in an API response and returns attributes
- * for the first match.
- */
- private function findInclude($data, $type, $id) {
- foreach($data->included as $include)
- if($include->type === $type && $include->id === $id)
- return $include->attributes;
- }
-
- private function apiGet($endpoint, $query_data = array()) {
- $query_data['json-api-version'] = 1.0;
- $query_data['json-api-use-default-includes'] = 0;
-
- $url = 'https://www.patreon.com/api/'
- . $endpoint
- . '?'
- . http_build_query($query_data);
-
- /*
- * Accept-Language header and the CURL cipher list are for bypassing the
- * Cloudflare anti-bot protection on the Patreon API. If this ever breaks,
- * here are some other project that also deal with this:
- * https://github.com/mikf/gallery-dl/issues/342
- * https://github.com/daemionfox/patreon-feed/issues/7
- * https://www.patreondevelopers.com/t/api-returning-cloudflare-challenge/2025
- * https://github.com/splitbrain/patreon-rss/issues/4
- */
- $header = array(
- 'Accept-Language: en-US',
- 'Content-Type: application/json'
- );
- $opts = array(
- CURLOPT_SSL_CIPHER_LIST => implode(':', array(
- 'DEFAULT',
- '!DHE-RSA-CHACHA20-POLY1305'
- ))
- );
-
- $data = json_decode(getContents($url, $header, $opts));
-
- return $data;
- }
-
- public function getName(){
- if(!is_null($this->getInput('creator')))
- return $this->getInput('creator') . ' posts';
-
- return parent::getName();
- }
-
- public function getURI(){
- if(!is_null($this->getInput('creator')))
- return self::URI . $this->getInput('creator');
-
- return parent::getURI();
- }
-
- public function detectParameters($url){
- $params = array();
-
- // Matches e.g. https://www.patreon.com/SomeCreator
- $regex = '/^(https?:\/\/)?(www\.)?patreon\.com\/([^\/&?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['creator'] = urldecode($matches[3]);
- return $params;
- }
-
- return null;
- }
+
+class PatreonBridge extends BridgeAbstract
+{
+ const NAME = 'Patreon Bridge';
+ const URI = 'https://www.patreon.com/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns posts by creators on Patreon';
+ const MAINTAINER = 'Roliga';
+ const PARAMETERS = [ [
+ 'creator' => [
+ 'name' => 'Creator',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'sanityinc',
+ 'title' => 'Creator name as seen in their page URL'
+ ]
+ ]];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOMCached($this->getURI(), 86400);
+ $regex = '#/api/campaigns/([0-9]+)#';
+ if (preg_match($regex, $html->save(), $matches) > 0) {
+ $campaign_id = $matches[1];
+ } else {
+ returnServerError('Could not find campaign ID');
+ }
+
+ $query = [
+ 'include' => implode(',', [
+ 'user',
+ 'attachments',
+ 'user_defined_tags',
+ //'campaign',
+ //'poll.choices',
+ //'poll.current_user_responses.user',
+ //'poll.current_user_responses.choice',
+ //'poll.current_user_responses.poll',
+ //'access_rules.tier.null',
+ //'images.null',
+ //'audio.null'
+ ]),
+ 'fields' => [
+ 'post' => implode(',', [
+ //'change_visibility_at',
+ //'comment_count',
+ 'content',
+ //'current_user_can_delete',
+ //'current_user_can_view',
+ //'current_user_has_liked',
+ //'embed',
+ 'image',
+ //'is_paid',
+ //'like_count',
+ //'min_cents_pledged_to_view',
+ //'patreon_url',
+ //'patron_count',
+ //'pledge_url',
+ //'post_file',
+ //'post_metadata',
+ //'post_type',
+ 'published_at',
+ 'teaser_text',
+ //'thumbnail_url',
+ 'title',
+ //'upgrade_url',
+ 'url',
+ //'was_posted_by_campaign_owner'
+ ]),
+ 'user' => implode(',', [
+ //'image_url',
+ 'full_name',
+ //'url'
+ ])
+ ],
+ 'filter' => [
+ 'contains_exclusive_posts' => true,
+ 'is_draft' => false,
+ 'campaign_id' => $campaign_id
+ ],
+ 'sort' => '-published_at'
+ ];
+ $posts = $this->apiGet('posts', $query);
+
+ foreach ($posts->data as $post) {
+ $item = [
+ 'uri' => $post->attributes->url,
+ 'title' => $post->attributes->title,
+ 'timestamp' => $post->attributes->published_at,
+ 'content' => '',
+ 'uid' => 'patreon.com/' . $post->id
+ ];
+
+ $user = $this->findInclude(
+ $posts,
+ 'user',
+ $post->relationships->user->data->id
+ );
+ $item['author'] = $user->full_name;
+
+ if (isset($post->attributes->image)) {
+ $item['content'] .= '<p><a href="'
+ . $post->attributes->url
+ . '"><img src="'
+ . $post->attributes->image->thumb_url
+ . '" /></a></p>';
+ }
+
+ if (isset($post->attributes->content)) {
+ $item['content'] .= $post->attributes->content;
+ } elseif (isset($post->attributes->teaser_text)) {
+ $item['content'] .= '<p>'
+ . $post->attributes->teaser_text
+ . '</p>';
+ }
+
+ if (isset($post->relationships->user_defined_tags)) {
+ $item['categories'] = [];
+ foreach ($post->relationships->user_defined_tags->data as $tag) {
+ $attrs = $this->findInclude($posts, 'post_tag', $tag->id);
+ $item['categories'][] = $attrs->value;
+ }
+ }
+
+ if (isset($post->relationships->attachments)) {
+ $item['enclosures'] = [];
+ foreach ($post->relationships->attachments->data as $attachment) {
+ $attrs = $this->findInclude($posts, 'attachment', $attachment->id);
+ $item['enclosures'][] = $attrs->url;
+ }
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ /*
+ * Searches the "included" array in an API response and returns attributes
+ * for the first match.
+ */
+ private function findInclude($data, $type, $id)
+ {
+ foreach ($data->included as $include) {
+ if ($include->type === $type && $include->id === $id) {
+ return $include->attributes;
+ }
+ }
+ }
+
+ private function apiGet($endpoint, $query_data = [])
+ {
+ $query_data['json-api-version'] = 1.0;
+ $query_data['json-api-use-default-includes'] = 0;
+
+ $url = 'https://www.patreon.com/api/'
+ . $endpoint
+ . '?'
+ . http_build_query($query_data);
+
+ /*
+ * Accept-Language header and the CURL cipher list are for bypassing the
+ * Cloudflare anti-bot protection on the Patreon API. If this ever breaks,
+ * here are some other project that also deal with this:
+ * https://github.com/mikf/gallery-dl/issues/342
+ * https://github.com/daemionfox/patreon-feed/issues/7
+ * https://www.patreondevelopers.com/t/api-returning-cloudflare-challenge/2025
+ * https://github.com/splitbrain/patreon-rss/issues/4
+ */
+ $header = [
+ 'Accept-Language: en-US',
+ 'Content-Type: application/json'
+ ];
+ $opts = [
+ CURLOPT_SSL_CIPHER_LIST => implode(':', [
+ 'DEFAULT',
+ '!DHE-RSA-CHACHA20-POLY1305'
+ ])
+ ];
+
+ $data = json_decode(getContents($url, $header, $opts));
+
+ return $data;
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('creator'))) {
+ return $this->getInput('creator') . ' posts';
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('creator'))) {
+ return self::URI . $this->getInput('creator');
+ }
+
+ return parent::getURI();
+ }
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ // Matches e.g. https://www.patreon.com/SomeCreator
+ $regex = '/^(https?:\/\/)?(www\.)?patreon\.com\/([^\/&?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['creator'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ return null;
+ }
}
diff --git a/bridges/PcGamerBridge.php b/bridges/PcGamerBridge.php
index 95261d9c..5ea15e8c 100644
--- a/bridges/PcGamerBridge.php
+++ b/bridges/PcGamerBridge.php
@@ -1,41 +1,42 @@
<?php
+
class PcGamerBridge extends BridgeAbstract
{
- const NAME = 'PC Gamer';
- const URI = 'https://www.pcgamer.com/';
- const DESCRIPTION = 'PC Gamer is your source for exclusive reviews, demos,
+ const NAME = 'PC Gamer';
+ const URI = 'https://www.pcgamer.com/';
+ const DESCRIPTION = 'PC Gamer is your source for exclusive reviews, demos,
updates and news on all your favorite PC gaming franchises.';
- const MAINTAINER = 'IceWreck, mdemoss';
+ const MAINTAINER = 'IceWreck, mdemoss';
- const PARAMETERS = [
- [
- 'limit' => self::LIMIT,
- ]
- ];
+ const PARAMETERS = [
+ [
+ 'limit' => self::LIMIT,
+ ]
+ ];
- public function collectData()
- {
- $html = getSimpleHTMLDOMCached($this->getURI(), 300);
- $stories = $html->find('a.article-link');
- $limit = $this->getInput('limit') ?? 10;
- foreach (array_slice($stories, 0, $limit) as $element) {
- $item = array();
- $item['uri'] = $element->href;
- $articleHtml = getSimpleHTMLDOMCached($item['uri']);
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOMCached($this->getURI(), 300);
+ $stories = $html->find('a.article-link');
+ $limit = $this->getInput('limit') ?? 10;
+ foreach (array_slice($stories, 0, $limit) as $element) {
+ $item = [];
+ $item['uri'] = $element->href;
+ $articleHtml = getSimpleHTMLDOMCached($item['uri']);
- // Relying on meta tags ought to be more reliable.
- $item['title'] = $articleHtml->find('meta[name=parsely-title]', 0)->content;
- $item['content'] = html_entity_decode($articleHtml->find('meta[name=description]', 0)->content);
- $item['author'] = $articleHtml->find('meta[name=parsely-author]', 0)->content;
- $item['enclosures'][] = $articleHtml->find('meta[name=parsely-image-url]', 0)->content;
- /* I don't know why every article has two extra tags, but because
- one matches another common tag, "guide," it needs to be removed. */
- $item['categories'] = array_diff(
- explode(',', $articleHtml->find('meta[name=parsely-tags]', 0)->content),
- array('van_buying_guide_progressive', 'serversidehawk')
- );
- $item['timestamp'] = strtotime($articleHtml->find('meta[name=pub_date]', 0)->content);
- $this->items[] = $item;
- }
- }
+ // Relying on meta tags ought to be more reliable.
+ $item['title'] = $articleHtml->find('meta[name=parsely-title]', 0)->content;
+ $item['content'] = html_entity_decode($articleHtml->find('meta[name=description]', 0)->content);
+ $item['author'] = $articleHtml->find('meta[name=parsely-author]', 0)->content;
+ $item['enclosures'][] = $articleHtml->find('meta[name=parsely-image-url]', 0)->content;
+ /* I don't know why every article has two extra tags, but because
+ one matches another common tag, "guide," it needs to be removed. */
+ $item['categories'] = array_diff(
+ explode(',', $articleHtml->find('meta[name=parsely-tags]', 0)->content),
+ ['van_buying_guide_progressive', 'serversidehawk']
+ );
+ $item['timestamp'] = strtotime($articleHtml->find('meta[name=pub_date]', 0)->content);
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/PepperBridgeAbstract.php b/bridges/PepperBridgeAbstract.php
index e6e44acd..123638c5 100644
--- a/bridges/PepperBridgeAbstract.php
+++ b/bridges/PepperBridgeAbstract.php
@@ -1,175 +1,178 @@
<?php
-class PepperBridgeAbstract extends BridgeAbstract {
-
- const CACHE_TIMEOUT = 3600;
-
- public function collectData(){
- switch($this->queriedContext) {
- case $this->i8n('context-keyword'):
- return $this->collectDataKeywords();
- break;
- case $this->i8n('context-group'):
- return $this->collectDataGroup();
- break;
- case $this->i8n('context-talk'):
- return $this->collectDataTalk();
- break;
- }
- }
-
- /**
- * Get the Deal data from the choosen group in the choosed order
- */
- protected function collectDataGroup()
- {
- $url = $this->getGroupURI();
- $this->collectDeals($url);
- }
-
- /**
- * Get the Deal data from the choosen keywords and parameters
- */
- protected function collectDataKeywords()
- {
- /* Even if the original website uses POST with the search page, GET works too */
- $url = $this->getSearchURI();
- $this->collectDeals($url);
- }
-
- /**
- * Get the Deal data using the given URL
- */
- protected function collectDeals($url){
- $html = getSimpleHTMLDOM($url);
- $list = $html->find('article[id]');
-
- // Deal Image Link CSS Selector
- $selectorImageLink = implode(
- ' ', /* Notice this is a space! */
- array(
- 'cept-thread-image-link',
- 'imgFrame',
- 'imgFrame--noBorder',
- 'thread-listImgCell',
- )
- );
-
- // Deal Link CSS Selector
- $selectorLink = implode(
- ' ', /* Notice this is a space! */
- array(
- 'cept-tt',
- 'thread-link',
- 'linkPlain',
- )
- );
-
- // Deal Hotness CSS Selector
- $selectorHot = implode(
- ' ', /* Notice this is a space! */
- array(
- 'cept-vote-box',
- 'vote-box'
- )
- );
-
- // Deal Description CSS Selector
- $selectorDescription = implode(
- ' ', /* Notice this is a space! */
- array(
- 'overflow--wrap-break'
- )
- );
-
- // Deal Date CSS Selector
- $selectorDate = implode(
- ' ', /* Notice this is a space! */
- array(
- 'size--all-s',
- 'flex',
- 'boxAlign-jc--all-fe'
- )
- );
-
- // If there is no results, we don't parse the content because it display some random deals
- $noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0);
- if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) {
- $this->items = array();
- } else {
- foreach ($list as $deal) {
- $item = array();
- $item['uri'] = $this->getDealURI($deal);
- $item['title'] = $this->getTitle($deal);
- $item['author'] = $deal->find('span.thread-username', 0)->plaintext;
-
- $item['content'] = '<table><tr><td><a href="'
- . $item['uri']
- . '"><img src="'
- . $this->getImage($deal)
- . '"/></td><td>'
- . $this->getHTMLTitle($item)
- . $this->getPrice($deal)
- . $this->getDiscount($deal)
- . $this->getShipsFrom($deal)
- . $this->getShippingCost($deal)
- . $this->getSource($deal)
- . $deal->find('div[class*=' . $selectorDescription . ']', 0)->innertext
- . '</td><td>'
- . $deal->find('div[class*=' . $selectorHot . ']', 0)
- ->find('span', 1)->outertext
- . '</td></table>';
-
- // Check if a clock icon is displayed on the deal
- $clocks = $deal->find('svg[class*=icon--clock]');
- if($clocks !== null && count($clocks) > 0) {
- // Get the last clock, corresponding to the deal posting date
- $clock = end($clocks);
-
- // Find the text corresponding to the clock
- $spanDateDiv = $clock->parent()->find('span[class=hide--toW3]', 0);
- $itemDate = $spanDateDiv->plaintext;
- // In case of a Local deal, there is no date, but we can use
- // this case for other reason (like date not in the last field)
- if ($this->contains($itemDate, $this->i8n('localdeal'))) {
- $item['timestamp'] = time();
- } else if ($this->contains($itemDate, $this->i8n('relative-date-indicator'))) {
- $item['timestamp'] = $this->relativeDateToTimestamp($itemDate);
- } else {
- $item['timestamp'] = $this->parseDate($itemDate);
- }
- }
- $this->items[] = $item;
- }
- }
- }
-
- /**
- * Get the Talk lastest comments
- */
- protected function collectDataTalk(){
- $threadURL = $this->getInput('url');
- $onlyWithUrl = $this->getInput('only_with_url');
-
- // Get Thread ID from url passed in parameter
- $threadSearch = preg_match('/-([0-9]{1,20})$/', $threadURL, $matches);
-
- // Show an error message if we can't find the thread ID in the URL sent by the user
- if($threadSearch !== 1) {
- returnClientError($this->i8n('thread-error'));
- }
- $threadID = $matches[1];
-
- $url = $this->i8n('bridge-uri') . 'graphql';
-
- // Get Cookies header to do the query
- $cookies = $this->getCookies($url);
-
- // GraphQL String
- // This was extracted from https://www.dealabs.com/assets/js/modern/common_211b99.js
- // This string was extracted during a Website visit, and minified using this neat tool :
- // https://codepen.io/dangodev/pen/Baoqmoy
- $graphqlString = <<<'HEREDOC'
+class PepperBridgeAbstract extends BridgeAbstract
+{
+ const CACHE_TIMEOUT = 3600;
+
+ public function collectData()
+ {
+ switch ($this->queriedContext) {
+ case $this->i8n('context-keyword'):
+ return $this->collectDataKeywords();
+ break;
+ case $this->i8n('context-group'):
+ return $this->collectDataGroup();
+ break;
+ case $this->i8n('context-talk'):
+ return $this->collectDataTalk();
+ break;
+ }
+ }
+
+ /**
+ * Get the Deal data from the choosen group in the choosed order
+ */
+ protected function collectDataGroup()
+ {
+ $url = $this->getGroupURI();
+ $this->collectDeals($url);
+ }
+
+ /**
+ * Get the Deal data from the choosen keywords and parameters
+ */
+ protected function collectDataKeywords()
+ {
+ /* Even if the original website uses POST with the search page, GET works too */
+ $url = $this->getSearchURI();
+ $this->collectDeals($url);
+ }
+
+ /**
+ * Get the Deal data using the given URL
+ */
+ protected function collectDeals($url)
+ {
+ $html = getSimpleHTMLDOM($url);
+ $list = $html->find('article[id]');
+
+ // Deal Image Link CSS Selector
+ $selectorImageLink = implode(
+ ' ', /* Notice this is a space! */
+ [
+ 'cept-thread-image-link',
+ 'imgFrame',
+ 'imgFrame--noBorder',
+ 'thread-listImgCell',
+ ]
+ );
+
+ // Deal Link CSS Selector
+ $selectorLink = implode(
+ ' ', /* Notice this is a space! */
+ [
+ 'cept-tt',
+ 'thread-link',
+ 'linkPlain',
+ ]
+ );
+
+ // Deal Hotness CSS Selector
+ $selectorHot = implode(
+ ' ', /* Notice this is a space! */
+ [
+ 'cept-vote-box',
+ 'vote-box'
+ ]
+ );
+
+ // Deal Description CSS Selector
+ $selectorDescription = implode(
+ ' ', /* Notice this is a space! */
+ [
+ 'overflow--wrap-break'
+ ]
+ );
+
+ // Deal Date CSS Selector
+ $selectorDate = implode(
+ ' ', /* Notice this is a space! */
+ [
+ 'size--all-s',
+ 'flex',
+ 'boxAlign-jc--all-fe'
+ ]
+ );
+
+ // If there is no results, we don't parse the content because it display some random deals
+ $noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0);
+ if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) {
+ $this->items = [];
+ } else {
+ foreach ($list as $deal) {
+ $item = [];
+ $item['uri'] = $this->getDealURI($deal);
+ $item['title'] = $this->getTitle($deal);
+ $item['author'] = $deal->find('span.thread-username', 0)->plaintext;
+
+ $item['content'] = '<table><tr><td><a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $this->getImage($deal)
+ . '"/></td><td>'
+ . $this->getHTMLTitle($item)
+ . $this->getPrice($deal)
+ . $this->getDiscount($deal)
+ . $this->getShipsFrom($deal)
+ . $this->getShippingCost($deal)
+ . $this->getSource($deal)
+ . $deal->find('div[class*=' . $selectorDescription . ']', 0)->innertext
+ . '</td><td>'
+ . $deal->find('div[class*=' . $selectorHot . ']', 0)
+ ->find('span', 1)->outertext
+ . '</td></table>';
+
+ // Check if a clock icon is displayed on the deal
+ $clocks = $deal->find('svg[class*=icon--clock]');
+ if ($clocks !== null && count($clocks) > 0) {
+ // Get the last clock, corresponding to the deal posting date
+ $clock = end($clocks);
+
+ // Find the text corresponding to the clock
+ $spanDateDiv = $clock->parent()->find('span[class=hide--toW3]', 0);
+ $itemDate = $spanDateDiv->plaintext;
+ // In case of a Local deal, there is no date, but we can use
+ // this case for other reason (like date not in the last field)
+ if ($this->contains($itemDate, $this->i8n('localdeal'))) {
+ $item['timestamp'] = time();
+ } elseif ($this->contains($itemDate, $this->i8n('relative-date-indicator'))) {
+ $item['timestamp'] = $this->relativeDateToTimestamp($itemDate);
+ } else {
+ $item['timestamp'] = $this->parseDate($itemDate);
+ }
+ }
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ /**
+ * Get the Talk lastest comments
+ */
+ protected function collectDataTalk()
+ {
+ $threadURL = $this->getInput('url');
+ $onlyWithUrl = $this->getInput('only_with_url');
+
+ // Get Thread ID from url passed in parameter
+ $threadSearch = preg_match('/-([0-9]{1,20})$/', $threadURL, $matches);
+
+ // Show an error message if we can't find the thread ID in the URL sent by the user
+ if ($threadSearch !== 1) {
+ returnClientError($this->i8n('thread-error'));
+ }
+ $threadID = $matches[1];
+
+ $url = $this->i8n('bridge-uri') . 'graphql';
+
+ // Get Cookies header to do the query
+ $cookies = $this->getCookies($url);
+
+ // GraphQL String
+ // This was extracted from https://www.dealabs.com/assets/js/modern/common_211b99.js
+ // This string was extracted during a Website visit, and minified using this neat tool :
+ // https://codepen.io/dangodev/pen/Baoqmoy
+ $graphqlString = <<<'HEREDOC'
query comments($filter:CommentFilter!,$limit:Int,$page:Int){comments(filter:$filter,limit:$limit,page:$page){
items{...commentFields}pagination{...paginationFields}}}fragment commentFields on Comment{commentId threadId url
preparedHtmlContent user{...userMediumAvatarFields...userNameFields...userPersonaFields bestBadge{...badgeFields}}
@@ -182,501 +185,509 @@ fragment badgeLevelFields on BadgeLevel{key name description}fragment pagination
next previous size order}
HEREDOC;
- // Construct the JSON object to send to the Website
- $queryArray = array (
- 'query' => $graphqlString,
- 'variables' => array (
- 'filter' => array (
- 'threadId' => array (
- 'eq' => $threadID,
- ),
- 'order' => array (
- 'direction' => 'Descending',
- ),
-
- ),
- 'page' => 1,
- ),
- );
- $queryJSON = json_encode($queryArray);
-
- // HTTP headers
- $header = array(
- 'Content-Type: application/json',
- 'Accept: application/json, text/plain, */*',
- 'X-Pepper-Txn: threads.show',
- 'X-Request-Type: application/vnd.pepper.v1+json',
- 'X-Requested-With: XMLHttpRequest',
- $cookies,
- );
- // CURL Options
- $opts = array(
- CURLOPT_POST => 1,
- CURLOPT_POSTFIELDS => $queryJSON
- );
- $json = getContents($url, $header, $opts);
- $objects = json_decode($json);
- foreach($objects->data->comments->items as $comment) {
- $item = array();
- $item['uri'] = $comment->url;
- $item['title'] = $comment->user->username . ' - ' . $comment->createdAt;
- $item['author'] = $comment->user->username;
- $item['content'] = $comment->preparedHtmlContent;
- $item['uid'] = $comment->commentId;
- // Timestamp handling needs a new parsing function
- if($onlyWithUrl == true) {
- // Count Links and Quote Links
- $content = str_get_html($item['content']);
- $countLinks = count($content->find('a[href]'));
- $countQuoteLinks = count($content->find('a[href][class=userHtml-quote-source]'));
- // Only add element if there are Links ans more links tant Quote links
- if($countLinks > 0 && $countLinks > $countQuoteLinks) {
- $this->items[] = $item;
- }
- } else {
- $this->items[] = $item;
- }
- }
- }
-
- /**
- * Extract the cookies obtained from the URL
- * @return array the array containing the cookies set by the URL
- */
- private function getCookies($url)
- {
- $ch = curl_init($url);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
- // get headers too with this line
- curl_setopt($ch, CURLOPT_HEADER, 1);
- $result = curl_exec($ch);
- // get cookie
- // multi-cookie variant contributed by @Combuster in comments
- preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $result, $matches);
- $cookies = array();
- foreach($matches[1] as $item) {
- parse_str($item, $cookie);
- $cookies = array_merge($cookies, $cookie);
- }
- $header = 'Cookie: ';
- foreach($cookies as $name => $content) {
- $header .= $name . '=' . $content . '; ';
- }
- return $header;
- }
-
- /**
- * Check if the string $str contains any of the string of the array $arr
- * @return boolean true if the string matched anything otherwise false
- */
- private function contains($str, array $arr)
- {
- foreach ($arr as $a) {
- if (stripos($str, $a) !== false) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Get the Price from a Deal if it exists
- * @return string String of the deal price
- */
- private function getPrice($deal)
- {
- if ($deal->find(
- 'span[class*=thread-price]', 0) != null) {
- return '<div>' . $this->i8n('price') . ' : '
- . $deal->find(
- 'span[class*=thread-price]', 0
- )->plaintext
- . '</div>';
- } else {
- return '';
- }
- }
-
- /**
- * Get the Title from a Deal if it exists
- * @return string String of the deal title
- */
- private function getTitle($deal)
- {
-
- $titleRoot = $deal->find('div[class*=threadGrid-title]', 0);
- $titleA = $titleRoot->find('a[class*=thread-link]', 0);
- $titleFirstChild = $titleRoot->first_child();
- if($titleA !== null) {
- $title = $titleA->plaintext;
- } else {
- // In some case, expired deals have a different format
- $title = $titleRoot->find('span', 0)->plaintext;
- }
-
- return $title;
-
- }
-
- /**
- * Get the Title from a Talk if it exists
- * @return string String of the Talk title
- */
- private function getTalkTitle()
- {
- $html = getSimpleHTMLDOMCached($this->getInput('url'));
- $title = $html->find('h1[class=thread-title]', 0)->plaintext;
- return $title;
-
- }
-
- /**
- * Get the HTML Title code from an item
- * @return string String of the deal title
- */
- private function getHTMLTitle($item)
- {
- if($item['uri'] == '') {
- $html = '<h2>' . $item['title'] . '</h2>';
- } else {
- $html = '<h2><a href="' . $item['uri'] . '">'
- . $item['title'] . '</a></h2>';
- }
-
- return $html;
-
- }
-
- /**
- * Get the URI from a Deal if it exists
- * @return string String of the deal URI
- */
- private function getDealURI($deal)
- {
-
- $uriA = $deal->find('div[class*=threadGrid-title]', 0)->find('a[class*=thread-link]', 0);
- if($uriA === null) {
- $uri = '';
- } else {
- $uri = $uriA->href;
- }
-
- return $uri;
-
- }
-
- /**
- * Get the Shipping costs from a Deal if it exists
- * @return string String of the deal shipping Cost
- */
- private function getShippingCost($deal)
- {
- if ($deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0) != null) {
- if ($deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0)->children(1) != null) {
- return '<div>' . $this->i8n('shipping') . ' : '
- . $deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0)->children(1)->innertext
- . '</div>';
- } else {
- return '<div>' . $this->i8n('shipping') . ' : '
- . $deal->find('span[class*=text--color-greyShade flex--inline]', 0)->innertext
- . '</div>';
- }
- } else {
- return '';
- }
- }
-
- /**
- * Get the source of a Deal if it exists
- * @return string String of the deal source
- */
- private function getSource($deal)
- {
- if ($deal->find('a[class*=text--color-greyShade]', 0) != null) {
- return '<div>' . $this->i8n('origin') . ' : '
- . $deal->find('a[class*=text--color-greyShade]', 0)->outertext
- . '</div>';
- } else {
- return '';
- }
- }
-
- /**
- * Get the original Price and discout from a Deal if it exists
- * @return string String of the deal original price and discount
- */
- private function getDiscount($deal)
- {
- if ($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) {
- $discountHtml = $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0);
- if ($discountHtml != null) {
- $discount = $discountHtml->plaintext;
- } else {
- $discount = '';
- }
- return '<div>' . $this->i8n('discount') . ' : <span style="text-decoration: line-through;">'
- . $deal->find(
- 'span[class*=mute--text text--lineThrough]', 0
- )->plaintext
- . '</span>&nbsp;'
- . $discount
- . '</div>';
- } else {
- return '';
- }
- }
-
- /**
- * Get the Picture URL from a Deal if it exists
- * @return string String of the deal Picture URL
- */
- private function getImage($deal)
- {
- $selectorLazy = implode(
- ' ', /* Notice this is a space! */
- array(
- 'thread-image',
- 'width--all-auto',
- 'height--all-auto',
- 'imgFrame-img',
- 'img--dummy',
- 'js-lazy-img'
- )
- );
-
- $selectorPlain = implode(
- ' ', /* Notice this is a space! */
- array(
- 'thread-image',
- 'width--all-auto',
- 'height--all-auto',
- 'imgFrame-img',
- )
- );
- if ($deal->find('img[class=' . $selectorLazy . ']', 0) != null) {
- return json_decode(
- html_entity_decode(
- $deal->find('img[class=' . $selectorLazy . ']', 0)
- ->getAttribute('data-lazy-img')))->{'src'};
- } else {
- return $deal->find('img[class*=' . $selectorPlain . ']', 0 )->src;
- }
- }
-
- /**
- * Get the originating country from a Deal if it exists
- * @return string String of the deal originating country
- */
- private function getShipsFrom($deal)
- {
- $selector = implode(
- ' ', /* Notice this is a space! */
- array(
- 'hide--toW2',
- 'metaRibbon',
- )
- );
- if ($deal->find('span[class*=' . $selector . ']', 0) != null) {
- return '<div>'
- . $deal->find('span[class*=' . $selector . ']', 0)->children(2)->plaintext
- . '</div>';
- } else {
- return '';
- }
- }
-
- /**
- * Transforms a local date into a timestamp
- * @return int timestamp of the input date
- */
- private function parseDate($string)
- {
- $month_local = $this->i8n('local-months');
- $month_en = array(
- 'January',
- 'February',
- 'March',
- 'April',
- 'May',
- 'June',
- 'July',
- 'August',
- 'September',
- 'October',
- 'November',
- 'December'
- );
-
- // A date can be prfixed with some words, we remove theme
- $string = $this->removeDatePrefixes($string);
- // We translate the local months name in the english one
- $date_str = trim(str_replace($month_local, $month_en, $string));
-
- // If the date does not contain any year, we add the current year
- if (!preg_match('/[0-9]{4}/', $string)) {
- $date_str .= ' ' . date('Y');
- }
-
- // Add the Hour and minutes
- $date_str .= ' 00:00';
- $date = DateTime::createFromFormat('j F Y H:i', $date_str);
- // In some case, the date is not recognized : as a workaround the actual date is taken
- if($date === false) {
- $date = new DateTime();
- }
- return $date->getTimestamp();
- }
-
- /**
- * Remove the prefix of a date if it has one
- * @return the date without prefiux
- */
- private function removeDatePrefixes($string)
- {
- $string = str_replace($this->i8n('date-prefixes'), array(), $string);
- return $string;
- }
-
- /**
- * Remove the suffix of a relative date if it has one
- * @return the relative date without suffixes
- */
- private function removeRelativeDateSuffixes($string)
- {
- if (count($this->i8n('relative-date-ignore-suffix')) > 0) {
- $string = preg_replace($this->i8n('relative-date-ignore-suffix'), '', $string);
- }
- return $string;
- }
-
- /**
- * Transforms a relative local date into a timestamp
- * @return int timestamp of the input date
- */
- private function relativeDateToTimestamp($str) {
- $date = new DateTime();
-
- // In case of update date, replace it by the regular relative date first word
- $str = str_replace($this->i8n('relative-date-alt-prefixes'), $this->i8n('local-time-relative')[0], $str);
-
- $str = $this->removeRelativeDateSuffixes($str);
-
- $search = $this->i8n('local-time-relative');
-
- $replace = array(
- '-',
- 'minute',
- 'hour',
- 'day',
- 'month',
- 'year',
- ''
- );
- $date->modify(str_replace($search, $replace, $str));
- return $date->getTimestamp();
- }
-
- /**
- * Returns the RSS Feed title according to the parameters
- * @return string the RSS feed Tiyle
- */
- public function getName(){
- switch($this->queriedContext) {
- case $this->i8n('context-keyword'):
- return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-keyword') . ' : ' . $this->getInput('q');
- break;
- case $this->i8n('context-group'):
- $values = $this->getParameters()[$this->i8n('context-group')]['group']['values'];
- $group = array_search($this->getInput('group'), $values);
- return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-group') . ' : ' . $group;
- break;
- case $this->i8n('context-talk'):
- return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-talk') . ' : ' . $this->getTalkTitle();
- break;
- default: // Return default value
- return static::NAME;
- }
- }
-
- /**
- * Returns the RSS Feed URI according to the parameters
- * @return string the RSS feed Title
- */
- public function getURI(){
- switch($this->queriedContext) {
- case $this->i8n('context-keyword'):
- return $this->getSearchURI();
- break;
- case $this->i8n('context-group'):
- return $this->getGroupURI();
- break;
- case $this->i8n('context-talk'):
- return $this->getTalkURI();
- break;
- default: // Return default value
- return static::URI;
- }
- }
-
- /**
- * Returns the RSS Feed URI for a keyword Feed
- * @return string the RSS feed URI
- */
- private function getSearchURI(){
- $q = $this->getInput('q');
- $hide_expired = $this->getInput('hide_expired');
- $hide_local = $this->getInput('hide_local');
- $priceFrom = $this->getInput('priceFrom');
- $priceTo = $this->getInput('priceTo');
- $url = $this->i8n('bridge-uri')
- . 'search/advanced?q='
- . urlencode($q)
- . '&hide_expired=' . $hide_expired
- . '&hide_local=' . $hide_local
- . '&priceFrom=' . $priceFrom
- . '&priceTo=' . $priceTo
- /* Some default parameters
- * search_fields : Search in Titres & Descriptions & Codes
- * sort_by : Sort the search by new deals
- * time_frame : Search will not be on a limited timeframe
- */
- . '&search_fields[]=1&search_fields[]=2&search_fields[]=3&sort_by=new&time_frame=0';
- return $url;
- }
-
- /**
- * Returns the RSS Feed URI for a group Feed
- * @return string the RSS feed URI
- */
- private function getGroupURI(){
- $group = $this->getInput('group');
- $order = $this->getInput('order');
-
- $url = $this->i8n('bridge-uri')
- . $this->i8n('uri-group') . $group . $order;
- return $url;
- }
-
- /**
- * Returns the RSS Feed URI for a Talk Feed
- * @return string the RSS feed URI
- */
- private function getTalkURI(){
- $url = $this->getInput('url');
- return $url;
- }
-
- /**
- * This is some "localisation" function that returns the needed content using
- * the "$lang" class variable in the local class
- * @return various the local content needed
- */
- protected function i8n($key)
- {
- if (array_key_exists($key, $this->lang)) {
- return $this->lang[$key];
- } else {
- return null;
- }
- }
+ // Construct the JSON object to send to the Website
+ $queryArray = [
+ 'query' => $graphqlString,
+ 'variables' => [
+ 'filter' => [
+ 'threadId' => [
+ 'eq' => $threadID,
+ ],
+ 'order' => [
+ 'direction' => 'Descending',
+ ],
+
+ ],
+ 'page' => 1,
+ ],
+ ];
+ $queryJSON = json_encode($queryArray);
+
+ // HTTP headers
+ $header = [
+ 'Content-Type: application/json',
+ 'Accept: application/json, text/plain, */*',
+ 'X-Pepper-Txn: threads.show',
+ 'X-Request-Type: application/vnd.pepper.v1+json',
+ 'X-Requested-With: XMLHttpRequest',
+ $cookies,
+ ];
+ // CURL Options
+ $opts = [
+ CURLOPT_POST => 1,
+ CURLOPT_POSTFIELDS => $queryJSON
+ ];
+ $json = getContents($url, $header, $opts);
+ $objects = json_decode($json);
+ foreach ($objects->data->comments->items as $comment) {
+ $item = [];
+ $item['uri'] = $comment->url;
+ $item['title'] = $comment->user->username . ' - ' . $comment->createdAt;
+ $item['author'] = $comment->user->username;
+ $item['content'] = $comment->preparedHtmlContent;
+ $item['uid'] = $comment->commentId;
+ // Timestamp handling needs a new parsing function
+ if ($onlyWithUrl == true) {
+ // Count Links and Quote Links
+ $content = str_get_html($item['content']);
+ $countLinks = count($content->find('a[href]'));
+ $countQuoteLinks = count($content->find('a[href][class=userHtml-quote-source]'));
+ // Only add element if there are Links ans more links tant Quote links
+ if ($countLinks > 0 && $countLinks > $countQuoteLinks) {
+ $this->items[] = $item;
+ }
+ } else {
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ /**
+ * Extract the cookies obtained from the URL
+ * @return array the array containing the cookies set by the URL
+ */
+ private function getCookies($url)
+ {
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ // get headers too with this line
+ curl_setopt($ch, CURLOPT_HEADER, 1);
+ $result = curl_exec($ch);
+ // get cookie
+ // multi-cookie variant contributed by @Combuster in comments
+ preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $result, $matches);
+ $cookies = [];
+ foreach ($matches[1] as $item) {
+ parse_str($item, $cookie);
+ $cookies = array_merge($cookies, $cookie);
+ }
+ $header = 'Cookie: ';
+ foreach ($cookies as $name => $content) {
+ $header .= $name . '=' . $content . '; ';
+ }
+ return $header;
+ }
+
+ /**
+ * Check if the string $str contains any of the string of the array $arr
+ * @return boolean true if the string matched anything otherwise false
+ */
+ private function contains($str, array $arr)
+ {
+ foreach ($arr as $a) {
+ if (stripos($str, $a) !== false) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the Price from a Deal if it exists
+ * @return string String of the deal price
+ */
+ private function getPrice($deal)
+ {
+ if (
+ $deal->find(
+ 'span[class*=thread-price]',
+ 0
+ ) != null
+ ) {
+ return '<div>' . $this->i8n('price') . ' : '
+ . $deal->find(
+ 'span[class*=thread-price]',
+ 0
+ )->plaintext
+ . '</div>';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get the Title from a Deal if it exists
+ * @return string String of the deal title
+ */
+ private function getTitle($deal)
+ {
+ $titleRoot = $deal->find('div[class*=threadGrid-title]', 0);
+ $titleA = $titleRoot->find('a[class*=thread-link]', 0);
+ $titleFirstChild = $titleRoot->first_child();
+ if ($titleA !== null) {
+ $title = $titleA->plaintext;
+ } else {
+ // In some case, expired deals have a different format
+ $title = $titleRoot->find('span', 0)->plaintext;
+ }
+
+ return $title;
+ }
+
+ /**
+ * Get the Title from a Talk if it exists
+ * @return string String of the Talk title
+ */
+ private function getTalkTitle()
+ {
+ $html = getSimpleHTMLDOMCached($this->getInput('url'));
+ $title = $html->find('h1[class=thread-title]', 0)->plaintext;
+ return $title;
+ }
+
+ /**
+ * Get the HTML Title code from an item
+ * @return string String of the deal title
+ */
+ private function getHTMLTitle($item)
+ {
+ if ($item['uri'] == '') {
+ $html = '<h2>' . $item['title'] . '</h2>';
+ } else {
+ $html = '<h2><a href="' . $item['uri'] . '">'
+ . $item['title'] . '</a></h2>';
+ }
+
+ return $html;
+ }
+
+ /**
+ * Get the URI from a Deal if it exists
+ * @return string String of the deal URI
+ */
+ private function getDealURI($deal)
+ {
+ $uriA = $deal->find('div[class*=threadGrid-title]', 0)->find('a[class*=thread-link]', 0);
+ if ($uriA === null) {
+ $uri = '';
+ } else {
+ $uri = $uriA->href;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Get the Shipping costs from a Deal if it exists
+ * @return string String of the deal shipping Cost
+ */
+ private function getShippingCost($deal)
+ {
+ if ($deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0) != null) {
+ if ($deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0)->children(1) != null) {
+ return '<div>' . $this->i8n('shipping') . ' : '
+ . $deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0)->children(1)->innertext
+ . '</div>';
+ } else {
+ return '<div>' . $this->i8n('shipping') . ' : '
+ . $deal->find('span[class*=text--color-greyShade flex--inline]', 0)->innertext
+ . '</div>';
+ }
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get the source of a Deal if it exists
+ * @return string String of the deal source
+ */
+ private function getSource($deal)
+ {
+ if ($deal->find('a[class*=text--color-greyShade]', 0) != null) {
+ return '<div>' . $this->i8n('origin') . ' : '
+ . $deal->find('a[class*=text--color-greyShade]', 0)->outertext
+ . '</div>';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get the original Price and discout from a Deal if it exists
+ * @return string String of the deal original price and discount
+ */
+ private function getDiscount($deal)
+ {
+ if ($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) {
+ $discountHtml = $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0);
+ if ($discountHtml != null) {
+ $discount = $discountHtml->plaintext;
+ } else {
+ $discount = '';
+ }
+ return '<div>' . $this->i8n('discount') . ' : <span style="text-decoration: line-through;">'
+ . $deal->find(
+ 'span[class*=mute--text text--lineThrough]',
+ 0
+ )->plaintext
+ . '</span>&nbsp;'
+ . $discount
+ . '</div>';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get the Picture URL from a Deal if it exists
+ * @return string String of the deal Picture URL
+ */
+ private function getImage($deal)
+ {
+ $selectorLazy = implode(
+ ' ', /* Notice this is a space! */
+ [
+ 'thread-image',
+ 'width--all-auto',
+ 'height--all-auto',
+ 'imgFrame-img',
+ 'img--dummy',
+ 'js-lazy-img'
+ ]
+ );
+
+ $selectorPlain = implode(
+ ' ', /* Notice this is a space! */
+ [
+ 'thread-image',
+ 'width--all-auto',
+ 'height--all-auto',
+ 'imgFrame-img',
+ ]
+ );
+ if ($deal->find('img[class=' . $selectorLazy . ']', 0) != null) {
+ return json_decode(
+ html_entity_decode(
+ $deal->find('img[class=' . $selectorLazy . ']', 0)
+ ->getAttribute('data-lazy-img')
+ )
+ )->{'src'};
+ } else {
+ return $deal->find('img[class*=' . $selectorPlain . ']', 0)->src;
+ }
+ }
+
+ /**
+ * Get the originating country from a Deal if it exists
+ * @return string String of the deal originating country
+ */
+ private function getShipsFrom($deal)
+ {
+ $selector = implode(
+ ' ', /* Notice this is a space! */
+ [
+ 'hide--toW2',
+ 'metaRibbon',
+ ]
+ );
+ if ($deal->find('span[class*=' . $selector . ']', 0) != null) {
+ return '<div>'
+ . $deal->find('span[class*=' . $selector . ']', 0)->children(2)->plaintext
+ . '</div>';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Transforms a local date into a timestamp
+ * @return int timestamp of the input date
+ */
+ private function parseDate($string)
+ {
+ $month_local = $this->i8n('local-months');
+ $month_en = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+ ];
+
+ // A date can be prfixed with some words, we remove theme
+ $string = $this->removeDatePrefixes($string);
+ // We translate the local months name in the english one
+ $date_str = trim(str_replace($month_local, $month_en, $string));
+
+ // If the date does not contain any year, we add the current year
+ if (!preg_match('/[0-9]{4}/', $string)) {
+ $date_str .= ' ' . date('Y');
+ }
+
+ // Add the Hour and minutes
+ $date_str .= ' 00:00';
+ $date = DateTime::createFromFormat('j F Y H:i', $date_str);
+ // In some case, the date is not recognized : as a workaround the actual date is taken
+ if ($date === false) {
+ $date = new DateTime();
+ }
+ return $date->getTimestamp();
+ }
+
+ /**
+ * Remove the prefix of a date if it has one
+ * @return the date without prefiux
+ */
+ private function removeDatePrefixes($string)
+ {
+ $string = str_replace($this->i8n('date-prefixes'), [], $string);
+ return $string;
+ }
+
+ /**
+ * Remove the suffix of a relative date if it has one
+ * @return the relative date without suffixes
+ */
+ private function removeRelativeDateSuffixes($string)
+ {
+ if (count($this->i8n('relative-date-ignore-suffix')) > 0) {
+ $string = preg_replace($this->i8n('relative-date-ignore-suffix'), '', $string);
+ }
+ return $string;
+ }
+
+ /**
+ * Transforms a relative local date into a timestamp
+ * @return int timestamp of the input date
+ */
+ private function relativeDateToTimestamp($str)
+ {
+ $date = new DateTime();
+
+ // In case of update date, replace it by the regular relative date first word
+ $str = str_replace($this->i8n('relative-date-alt-prefixes'), $this->i8n('local-time-relative')[0], $str);
+
+ $str = $this->removeRelativeDateSuffixes($str);
+
+ $search = $this->i8n('local-time-relative');
+
+ $replace = [
+ '-',
+ 'minute',
+ 'hour',
+ 'day',
+ 'month',
+ 'year',
+ ''
+ ];
+ $date->modify(str_replace($search, $replace, $str));
+ return $date->getTimestamp();
+ }
+
+ /**
+ * Returns the RSS Feed title according to the parameters
+ * @return string the RSS feed Tiyle
+ */
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case $this->i8n('context-keyword'):
+ return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-keyword') . ' : ' . $this->getInput('q');
+ break;
+ case $this->i8n('context-group'):
+ $values = $this->getParameters()[$this->i8n('context-group')]['group']['values'];
+ $group = array_search($this->getInput('group'), $values);
+ return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-group') . ' : ' . $group;
+ break;
+ case $this->i8n('context-talk'):
+ return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-talk') . ' : ' . $this->getTalkTitle();
+ break;
+ default: // Return default value
+ return static::NAME;
+ }
+ }
+
+ /**
+ * Returns the RSS Feed URI according to the parameters
+ * @return string the RSS feed Title
+ */
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case $this->i8n('context-keyword'):
+ return $this->getSearchURI();
+ break;
+ case $this->i8n('context-group'):
+ return $this->getGroupURI();
+ break;
+ case $this->i8n('context-talk'):
+ return $this->getTalkURI();
+ break;
+ default: // Return default value
+ return static::URI;
+ }
+ }
+
+ /**
+ * Returns the RSS Feed URI for a keyword Feed
+ * @return string the RSS feed URI
+ */
+ private function getSearchURI()
+ {
+ $q = $this->getInput('q');
+ $hide_expired = $this->getInput('hide_expired');
+ $hide_local = $this->getInput('hide_local');
+ $priceFrom = $this->getInput('priceFrom');
+ $priceTo = $this->getInput('priceTo');
+ $url = $this->i8n('bridge-uri')
+ . 'search/advanced?q='
+ . urlencode($q)
+ . '&hide_expired=' . $hide_expired
+ . '&hide_local=' . $hide_local
+ . '&priceFrom=' . $priceFrom
+ . '&priceTo=' . $priceTo
+ /* Some default parameters
+ * search_fields : Search in Titres & Descriptions & Codes
+ * sort_by : Sort the search by new deals
+ * time_frame : Search will not be on a limited timeframe
+ */
+ . '&search_fields[]=1&search_fields[]=2&search_fields[]=3&sort_by=new&time_frame=0';
+ return $url;
+ }
+
+ /**
+ * Returns the RSS Feed URI for a group Feed
+ * @return string the RSS feed URI
+ */
+ private function getGroupURI()
+ {
+ $group = $this->getInput('group');
+ $order = $this->getInput('order');
+
+ $url = $this->i8n('bridge-uri')
+ . $this->i8n('uri-group') . $group . $order;
+ return $url;
+ }
+
+ /**
+ * Returns the RSS Feed URI for a Talk Feed
+ * @return string the RSS feed URI
+ */
+ private function getTalkURI()
+ {
+ $url = $this->getInput('url');
+ return $url;
+ }
+
+ /**
+ * This is some "localisation" function that returns the needed content using
+ * the "$lang" class variable in the local class
+ * @return various the local content needed
+ */
+ protected function i8n($key)
+ {
+ if (array_key_exists($key, $this->lang)) {
+ return $this->lang[$key];
+ } else {
+ return null;
+ }
+ }
}
diff --git a/bridges/PhoronixBridge.php b/bridges/PhoronixBridge.php
index 76d35f5d..620fda66 100644
--- a/bridges/PhoronixBridge.php
+++ b/bridges/PhoronixBridge.php
@@ -1,65 +1,69 @@
<?php
-class PhoronixBridge extends FeedExpander {
- const MAINTAINER = 'IceWreck';
- const NAME = 'Phoronix Bridge';
- const URI = 'https://www.phoronix.com';
- const CACHE_TIMEOUT = 3600;
- const DESCRIPTION = 'RSS feed for Linux news website Phoronix';
- const PARAMETERS = array(array(
- 'n' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Maximum number of items to return',
- 'defaultValue' => 10
- ),
- 'svgAsImg' => array(
- 'name' => 'SVG in "image" tag',
- 'type' => 'checkbox',
- 'title' => 'Some benchmarks are exported as SVG with "object" tag,
+class PhoronixBridge extends FeedExpander
+{
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'Phoronix Bridge';
+ const URI = 'https://www.phoronix.com';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'RSS feed for Linux news website Phoronix';
+ const PARAMETERS = [[
+ 'n' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Maximum number of items to return',
+ 'defaultValue' => 10
+ ],
+ 'svgAsImg' => [
+ 'name' => 'SVG in "image" tag',
+ 'type' => 'checkbox',
+ 'title' => 'Some benchmarks are exported as SVG with "object" tag,
but some RSS readers don\'t support this. "img" tag are supported by most browsers',
- 'defaultValue' => false
- ),
- ));
+ 'defaultValue' => false
+ ],
+ ]];
- public function collectData(){
- $this->collectExpandableDatas('https://www.phoronix.com/rss.php', $this->getInput('n'));
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas('https://www.phoronix.com/rss.php', $this->getInput('n'));
+ }
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- // $articlePage gets the entire page's contents
- $articlePage = getSimpleHTMLDOM($newsItem->link);
- $articlePage = defaultLinkTo($articlePage, $this->getURI());
- // Extract final link. From Facebook's like plugin.
- parse_str(parse_url($articlePage->find('iframe[src^=//www.facebook.com/plugins]', 0), PHP_URL_QUERY), $facebookQuery);
- if (array_key_exists('href', $facebookQuery)) {
- $newsItem->link = $facebookQuery['href'];
- }
- $item['content'] = $this->extractContent($articlePage);
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+ $articlePage = defaultLinkTo($articlePage, $this->getURI());
+ // Extract final link. From Facebook's like plugin.
+ parse_str(parse_url($articlePage->find('iframe[src^=//www.facebook.com/plugins]', 0), PHP_URL_QUERY), $facebookQuery);
+ if (array_key_exists('href', $facebookQuery)) {
+ $newsItem->link = $facebookQuery['href'];
+ }
+ $item['content'] = $this->extractContent($articlePage);
- $pages = $articlePage->find('.pagination a[!title]');
- foreach ($pages as $page) {
- $pageURI = urljoin($newsItem->link, html_entity_decode($page->href));
- $page = getSimpleHTMLDOM($pageURI);
- $item['content'] .= $this->extractContent($page);
- }
- return $item;
- }
+ $pages = $articlePage->find('.pagination a[!title]');
+ foreach ($pages as $page) {
+ $pageURI = urljoin($newsItem->link, html_entity_decode($page->href));
+ $page = getSimpleHTMLDOM($pageURI);
+ $item['content'] .= $this->extractContent($page);
+ }
+ return $item;
+ }
- private function extractContent($page){
- $content = $page->find('.content', 0);
- $objects = $content->find('script[src^=//openbenchmarking.org]');
- foreach ($objects as $object) {
- $objectSrc = preg_replace('/p=0/', 'p=2', $object->src);
- if ($this->getInput('svgAsImg')) {
- $object->outertext = '<a href="' . $objectSrc . '"><img src="' . $objectSrc . '"/></a>';
- } else {
- $object->outertext = '<object data="' . $objectSrc . '" type="image/svg+xml"></object>';
- }
- }
- $content = stripWithDelimiters($content, '<script', '</script>');
- return $content;
- }
+ private function extractContent($page)
+ {
+ $content = $page->find('.content', 0);
+ $objects = $content->find('script[src^=//openbenchmarking.org]');
+ foreach ($objects as $object) {
+ $objectSrc = preg_replace('/p=0/', 'p=2', $object->src);
+ if ($this->getInput('svgAsImg')) {
+ $object->outertext = '<a href="' . $objectSrc . '"><img src="' . $objectSrc . '"/></a>';
+ } else {
+ $object->outertext = '<object data="' . $objectSrc . '" type="image/svg+xml"></object>';
+ }
+ }
+ $content = stripWithDelimiters($content, '<script', '</script>');
+ return $content;
+ }
}
diff --git a/bridges/PicalaBridge.php b/bridges/PicalaBridge.php
index 46e2edbb..35f73d0a 100644
--- a/bridges/PicalaBridge.php
+++ b/bridges/PicalaBridge.php
@@ -1,69 +1,75 @@
<?php
-class PicalaBridge extends BridgeAbstract {
- const TYPES = array(
- 'Actualités' => 'actualites',
- 'Économie' => 'economie',
- 'Tests' => 'tests',
- 'Pratique' => 'pratique',
- );
- const NAME = 'Picala Bridge';
- const URI = 'https://www.picala.fr';
- const DESCRIPTION = 'Dernière nouvelles du média indépendant sur le vélo électrique';
- const MAINTAINER = 'Chouchen';
- const PARAMETERS = array(
- array(
- 'type' => array(
- 'name' => 'Type',
- 'type' => 'list',
- 'values' => self::TYPES,
- ),
- ),
- );
+class PicalaBridge extends BridgeAbstract
+{
+ const TYPES = [
+ 'Actualités' => 'actualites',
+ 'Économie' => 'economie',
+ 'Tests' => 'tests',
+ 'Pratique' => 'pratique',
+ ];
+ const NAME = 'Picala Bridge';
+ const URI = 'https://www.picala.fr';
+ const DESCRIPTION = 'Dernière nouvelles du média indépendant sur le vélo électrique';
+ const MAINTAINER = 'Chouchen';
+ const PARAMETERS = [
+ [
+ 'type' => [
+ 'name' => 'Type',
+ 'type' => 'list',
+ 'values' => self::TYPES,
+ ],
+ ],
+ ];
- public function getURI() {
- if(!is_null($this->getInput('type'))) {
- return sprintf('%s/%s', static::URI, $this->getInput('type'));
- }
+ public function getURI()
+ {
+ if (!is_null($this->getInput('type'))) {
+ return sprintf('%s/%s', static::URI, $this->getInput('type'));
+ }
- return parent::getURI();
- }
+ return parent::getURI();
+ }
- public function getIcon() {
- return 'https://picala-static.s3.amazonaws.com/static/img/favicon/favicon-32x32.png';
- }
+ public function getIcon()
+ {
+ return 'https://picala-static.s3.amazonaws.com/static/img/favicon/favicon-32x32.png';
+ }
- public function getDescription() {
- if(!is_null($this->getInput('type'))) {
- return sprintf('%s - %s', static::DESCRIPTION, array_search($this->getInput('type'), self::TYPES));
- }
+ public function getDescription()
+ {
+ if (!is_null($this->getInput('type'))) {
+ return sprintf('%s - %s', static::DESCRIPTION, array_search($this->getInput('type'), self::TYPES));
+ }
- return parent::getDescription();
- }
+ return parent::getDescription();
+ }
- public function getName() {
- if(!is_null($this->getInput('type'))) {
- return sprintf('%s - %s', static::NAME, array_search($this->getInput('type'), self::TYPES));
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('type'))) {
+ return sprintf('%s - %s', static::NAME, array_search($this->getInput('type'), self::TYPES));
+ }
- return parent::getName();
- }
+ return parent::getName();
+ }
- public function collectData() {
- $fullhtml = getSimpleHTMLDOM($this->getURI());
- foreach($fullhtml->find('.list-container-category a') as $article) {
- $srcsets = explode(',', $article->find('img', 0)->getAttribute('srcset'));
- $image = explode(' ', trim(array_shift($srcsets)))[0];
+ public function collectData()
+ {
+ $fullhtml = getSimpleHTMLDOM($this->getURI());
+ foreach ($fullhtml->find('.list-container-category a') as $article) {
+ $srcsets = explode(',', $article->find('img', 0)->getAttribute('srcset'));
+ $image = explode(' ', trim(array_shift($srcsets)))[0];
- $item = array();
- $item['uri'] = self::URI . $article->href;
- $item['title'] = $article->find('h2', 0)->plaintext;
- $item['content'] = sprintf(
- '<img src="%s" /><br>%s',
- $image,
- $article->find('.teaser__text', 0)->plaintext
- );
- $this->items[] = $item;
- }
- }
+ $item = [];
+ $item['uri'] = self::URI . $article->href;
+ $item['title'] = $article->find('h2', 0)->plaintext;
+ $item['content'] = sprintf(
+ '<img src="%s" /><br>%s',
+ $image,
+ $article->find('.teaser__text', 0)->plaintext
+ );
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/PickyWallpapersBridge.php b/bridges/PickyWallpapersBridge.php
index 2c4f0be3..29fdc1aa 100644
--- a/bridges/PickyWallpapersBridge.php
+++ b/bridges/PickyWallpapersBridge.php
@@ -1,101 +1,106 @@
<?php
-class PickyWallpapersBridge extends BridgeAbstract {
- const MAINTAINER = 'nel50n';
- const NAME = 'PickyWallpapers Bridge';
- const URI = 'https://www.pickywallpapers.com/';
- const CACHE_TIMEOUT = 43200; // 12h
- const DESCRIPTION = 'Returns the latests wallpapers from PickyWallpapers';
+class PickyWallpapersBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'nel50n';
+ const NAME = 'PickyWallpapers Bridge';
+ const URI = 'https://www.pickywallpapers.com/';
+ const CACHE_TIMEOUT = 43200; // 12h
+ const DESCRIPTION = 'Returns the latests wallpapers from PickyWallpapers';
- const PARAMETERS = array( array(
- 'c' => array(
- 'name' => 'category',
- 'exampleValue' => 'funny',
- 'required' => true
- ),
- 's' => array(
- 'name' => 'subcategory'
- ),
- 'm' => array(
- 'name' => 'Max number of wallpapers',
- 'defaultValue' => 12,
- 'type' => 'number'
- ),
- 'r' => array(
- 'name' => 'resolution',
- 'exampleValue' => '1920x1200, 1680x1050,…',
- 'defaultValue' => '1920x1200',
- 'pattern' => '[0-9]{3,4}x[0-9]{3,4}'
- )
- ));
+ const PARAMETERS = [ [
+ 'c' => [
+ 'name' => 'category',
+ 'exampleValue' => 'funny',
+ 'required' => true
+ ],
+ 's' => [
+ 'name' => 'subcategory'
+ ],
+ 'm' => [
+ 'name' => 'Max number of wallpapers',
+ 'defaultValue' => 12,
+ 'type' => 'number'
+ ],
+ 'r' => [
+ 'name' => 'resolution',
+ 'exampleValue' => '1920x1200, 1680x1050,…',
+ 'defaultValue' => '1920x1200',
+ 'pattern' => '[0-9]{3,4}x[0-9]{3,4}'
+ ]
+ ]];
- public function collectData(){
- $lastpage = 1;
- $num = 0;
- $max = $this->getInput('m');
- $resolution = $this->getInput('r'); // Wide wallpaper default
+ public function collectData()
+ {
+ $lastpage = 1;
+ $num = 0;
+ $max = $this->getInput('m');
+ $resolution = $this->getInput('r'); // Wide wallpaper default
- for($page = 1; $page <= $lastpage; $page++) {
- $html = getSimpleHTMLDOM($this->getURI() . '/page-' . $page . '/');
+ for ($page = 1; $page <= $lastpage; $page++) {
+ $html = getSimpleHTMLDOM($this->getURI() . '/page-' . $page . '/');
- if($page === 1) {
- preg_match('/page-(\d+)\/$/', $html->find('.pages li a', -2)->href, $matches);
- $lastpage = min($matches[1], ceil($max / 12));
- }
+ if ($page === 1) {
+ preg_match('/page-(\d+)\/$/', $html->find('.pages li a', -2)->href, $matches);
+ $lastpage = min($matches[1], ceil($max / 12));
+ }
- foreach($html->find('.items li img') as $element) {
- $item = array();
- $item['uri'] = str_replace('www', 'wallpaper', self::URI)
- . '/'
- . $resolution
- . '/'
- . basename($element->src);
+ foreach ($html->find('.items li img') as $element) {
+ $item = [];
+ $item['uri'] = str_replace('www', 'wallpaper', self::URI)
+ . '/'
+ . $resolution
+ . '/'
+ . basename($element->src);
- $item['timestamp'] = time();
- $item['title'] = $element->alt;
- $item['content'] = $item['title']
- . '<br><a href="'
- . $item['uri']
- . '">'
- . $element
- . '</a>';
+ $item['timestamp'] = time();
+ $item['title'] = $element->alt;
+ $item['content'] = $item['title']
+ . '<br><a href="'
+ . $item['uri']
+ . '">'
+ . $element
+ . '</a>';
- $this->items[] = $item;
+ $this->items[] = $item;
- $num++;
- if ($num >= $max)
- break 2;
- }
- }
- }
+ $num++;
+ if ($num >= $max) {
+ break 2;
+ }
+ }
+ }
+ }
- public function getURI(){
- if(!is_null($this->getInput('s')) && !is_null($this->getInput('r')) && !is_null($this->getInput('c'))) {
- $subcategory = $this->getInput('s');
- $link = self::URI
- . $this->getInput('r')
- . '/'
- . $this->getInput('c')
- . '/'
- . $subcategory;
+ public function getURI()
+ {
+ if (!is_null($this->getInput('s')) && !is_null($this->getInput('r')) && !is_null($this->getInput('c'))) {
+ $subcategory = $this->getInput('s');
+ $link = self::URI
+ . $this->getInput('r')
+ . '/'
+ . $this->getInput('c')
+ . '/'
+ . $subcategory;
- return $link;
- }
+ return $link;
+ }
- return parent::getURI();
- }
+ return parent::getURI();
+ }
- public function getName(){
- if(!is_null($this->getInput('s'))) {
- $subcategory = $this->getInput('s');
- return 'PickyWallpapers - '
- . $this->getInput('c')
- . ($subcategory ? ' > ' . $subcategory : '')
- . ' ['
- . $this->getInput('r')
- . ']';
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('s'))) {
+ $subcategory = $this->getInput('s');
+ return 'PickyWallpapers - '
+ . $this->getInput('c')
+ . ($subcategory ? ' > ' . $subcategory : '')
+ . ' ['
+ . $this->getInput('r')
+ . ']';
+ }
- return parent::getName();
- }
+ return parent::getName();
+ }
}
diff --git a/bridges/PicukiBridge.php b/bridges/PicukiBridge.php
index 3c2df739..9f9acf6b 100644
--- a/bridges/PicukiBridge.php
+++ b/bridges/PicukiBridge.php
@@ -1,103 +1,104 @@
<?php
+
class PicukiBridge extends BridgeAbstract
{
- const MAINTAINER = 'marcus-at-localhost';
- const NAME = 'Picuki Bridge';
- const URI = 'https://www.picuki.com/';
- const CACHE_TIMEOUT = 3600; // 1h
- const DESCRIPTION = 'Returns Picuki posts by user and by hashtag';
-
- const PARAMETERS = array(
- 'Username' => array(
- 'u' => array(
- 'name' => 'username',
- 'exampleValue' => 'aesoprockwins',
- 'required' => true,
- ),
- ),
- 'Hashtag' => array(
- 'h' => array(
- 'name' => 'hashtag',
- 'exampleValue' => 'beautifulday',
- 'required' => true,
- ),
- )
- );
-
- public function getURI()
- {
- if (!is_null($this->getInput('u'))) {
- return urljoin(self::URI, '/profile/' . $this->getInput('u'));
- }
-
- if (!is_null($this->getInput('h'))) {
- return urljoin(self::URI, '/tag/' . trim($this->getInput('h'), '#'));
- }
-
- return parent::getURI();
- }
-
- public function collectData()
- {
- $html = getSimpleHTMLDOM($this->getURI());
-
- foreach ($html->find('.box-photos .box-photo') as $element) {
- // skip ad items
- if (in_array('adv', explode(' ', $element->class))) {
- continue;
- }
-
- $url = urljoin(self::URI, $element->find('a', 0)->href);
-
- $author = trim($element->find('.user-nickname', 0)->plaintext);
-
- $date = date_create();
- $relativeDate = str_replace(' ago', '', $element->find('.time', 0)->plaintext);
- date_sub($date, date_interval_create_from_date_string($relativeDate));
-
- $description = trim($element->find('.photo-description', 0)->plaintext);
-
- $isVideo = (bool) $element->find('.video-icon', 0);
- $videoNote = $isVideo ? '<p><i>(video)</i></p>' : '';
-
- $imageUrl = $element->find('.post-image', 0)->src;
-
- // the last path segment needs to be encoded, because it contains special characters like + or |
- $imageUrlParts = explode('/', $imageUrl);
- $imageUrlParts[count($imageUrlParts) - 1] = urlencode($imageUrlParts[count($imageUrlParts) - 1]);
- $imageUrl = implode('/', $imageUrlParts);
-
- // add fake file extension for it to be recognized as image/jpeg instead of application/octet-stream
- $imageUrl = $imageUrl . '#.jpg';
-
- $this->items[] = array(
- 'uri' => $url,
- 'author' => $author,
- 'timestamp' => date_format($date, 'r'),
- 'title' => strlen($description) > 60 ? mb_substr($description, 0, 57) . '...' : $description,
- 'thumbnail' => $imageUrl,
- 'enclosures' => [$imageUrl],
- 'content' => <<<HTML
+ const MAINTAINER = 'marcus-at-localhost';
+ const NAME = 'Picuki Bridge';
+ const URI = 'https://www.picuki.com/';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Returns Picuki posts by user and by hashtag';
+
+ const PARAMETERS = [
+ 'Username' => [
+ 'u' => [
+ 'name' => 'username',
+ 'exampleValue' => 'aesoprockwins',
+ 'required' => true,
+ ],
+ ],
+ 'Hashtag' => [
+ 'h' => [
+ 'name' => 'hashtag',
+ 'exampleValue' => 'beautifulday',
+ 'required' => true,
+ ],
+ ]
+ ];
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('u'))) {
+ return urljoin(self::URI, '/profile/' . $this->getInput('u'));
+ }
+
+ if (!is_null($this->getInput('h'))) {
+ return urljoin(self::URI, '/tag/' . trim($this->getInput('h'), '#'));
+ }
+
+ return parent::getURI();
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ foreach ($html->find('.box-photos .box-photo') as $element) {
+ // skip ad items
+ if (in_array('adv', explode(' ', $element->class))) {
+ continue;
+ }
+
+ $url = urljoin(self::URI, $element->find('a', 0)->href);
+
+ $author = trim($element->find('.user-nickname', 0)->plaintext);
+
+ $date = date_create();
+ $relativeDate = str_replace(' ago', '', $element->find('.time', 0)->plaintext);
+ date_sub($date, date_interval_create_from_date_string($relativeDate));
+
+ $description = trim($element->find('.photo-description', 0)->plaintext);
+
+ $isVideo = (bool) $element->find('.video-icon', 0);
+ $videoNote = $isVideo ? '<p><i>(video)</i></p>' : '';
+
+ $imageUrl = $element->find('.post-image', 0)->src;
+
+ // the last path segment needs to be encoded, because it contains special characters like + or |
+ $imageUrlParts = explode('/', $imageUrl);
+ $imageUrlParts[count($imageUrlParts) - 1] = urlencode($imageUrlParts[count($imageUrlParts) - 1]);
+ $imageUrl = implode('/', $imageUrlParts);
+
+ // add fake file extension for it to be recognized as image/jpeg instead of application/octet-stream
+ $imageUrl = $imageUrl . '#.jpg';
+
+ $this->items[] = [
+ 'uri' => $url,
+ 'author' => $author,
+ 'timestamp' => date_format($date, 'r'),
+ 'title' => strlen($description) > 60 ? mb_substr($description, 0, 57) . '...' : $description,
+ 'thumbnail' => $imageUrl,
+ 'enclosures' => [$imageUrl],
+ 'content' => <<<HTML
<a href="{$url}">
<img loading="lazy" src="{$imageUrl}" />
</a>
{$videoNote}
<p>{$description}<p>
HTML
- );
- }
- }
-
- public function getName()
- {
- if (!is_null($this->getInput('u'))) {
- return $this->getInput('u') . ' - Picuki Bridge';
- }
-
- if (!is_null($this->getInput('h'))) {
- return $this->getInput('h') . ' - Picuki Bridge';
- }
-
- return parent::getName();
- }
+ ];
+ }
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('u'))) {
+ return $this->getInput('u') . ' - Picuki Bridge';
+ }
+
+ if (!is_null($this->getInput('h'))) {
+ return $this->getInput('h') . ' - Picuki Bridge';
+ }
+
+ return parent::getName();
+ }
}
diff --git a/bridges/PikabuBridge.php b/bridges/PikabuBridge.php
index 671d7a15..237f2a94 100644
--- a/bridges/PikabuBridge.php
+++ b/bridges/PikabuBridge.php
@@ -1,150 +1,158 @@
<?php
-class PikabuBridge extends BridgeAbstract {
-
- const NAME = 'Пикабу';
- const URI = 'https://pikabu.ru';
- const DESCRIPTION = 'Выводит посты по тегу, сообществу или пользователю';
- const MAINTAINER = 'em92';
-
- const PARAMETERS_FILTER = array(
- 'name' => 'Фильтр',
- 'type' => 'list',
- 'values' => array(
- 'Горячее' => 'hot',
- 'Свежее' => 'new',
- ),
- 'defaultValue' => 'hot'
- );
-
- const PARAMETERS = array(
- 'По тегу' => array(
- 'tag' => array(
- 'name' => 'Тег',
- 'exampleValue' => 'it',
- 'required' => true
- ),
- 'filter' => self::PARAMETERS_FILTER
- ),
- 'По сообществу' => array(
- 'community' => array(
- 'name' => 'Сообщество',
- 'exampleValue' => 'linux',
- 'required' => true
- ),
- 'filter' => self::PARAMETERS_FILTER
- ),
- 'По пользователю' => array(
- 'user' => array(
- 'name' => 'Пользователь',
- 'exampleValue' => 'admin',
- 'required' => true
- )
- )
- );
-
- protected $title = null;
-
- public function getURI() {
- if ($this->getInput('tag')) {
- return self::URI . '/tag/' . rawurlencode($this->getInput('tag')) . '/' . rawurlencode($this->getInput('filter'));
- } else if ($this->getInput('user')) {
- return self::URI . '/@' . rawurlencode($this->getInput('user'));
- } else if ($this->getInput('community')) {
- $uri = self::URI . '/community/' . rawurlencode($this->getInput('community'));
- if ($this->getInput('filter') != 'hot') {
- $uri .= '/' . rawurlencode($this->getInput('filter'));
- }
- return $uri;
- } else {
- return parent::getURI();
- }
- }
-
- public function getIcon() {
- return 'https://cs.pikabu.ru/assets/favicon.ico';
- }
-
- public function getName() {
- if (is_null($this->title)) {
- return parent::getName();
- } else {
- return $this->title . ' - ' . parent::getName();
- }
- }
-
- public function collectData(){
- $link = $this->getURI();
-
- $text_html = getContents($link);
- $text_html = iconv('windows-1251', 'utf-8', $text_html);
- $html = str_get_html($text_html);
-
- $this->title = $html->find('title', 0)->innertext;
-
- foreach($html->find('article.story') as $post) {
- $time = $post->find('time.story__datetime', 0);
- if (is_null($time)) continue;
-
- $el_to_remove_selectors = array(
- '.story__read-more',
- 'script',
- 'svg.story-image__stretch',
- );
-
- foreach($el_to_remove_selectors as $el_to_remove_selector) {
- foreach($post->find($el_to_remove_selector) as $el) {
- $el->outertext = '';
- }
- }
-
- foreach($post->find('[data-type=gifx]') as $el) {
- $src = $el->getAttribute('data-source');
- $el->outertext = '<img src="' . $src . '">';
- }
-
- foreach($post->find('img') as $img) {
- $src = $img->getAttribute('src');
- if (!$src) {
- $src = $img->getAttribute('data-src');
- if (!$src) {
- continue;
- }
- }
- $img->outertext = '<img src="' . $src . '">';
-
- // it is assumed, that img's parents are links to post itself
- // we don't need them
- $img->parent()->outertext = $img->outertext;
- }
-
- $categories = array();
- foreach($post->find('.tags__tag') as $tag) {
- if ($tag->getAttribute('data-tag')) {
- $categories[] = $tag->innertext;
- }
- }
-
- $title_element = $post->find('.story__title-link', 0);
-
- $title = $title_element->plaintext;
- $community_link = $post->find('.story__community-link', 0);
- // adding special marker for "Maybe News" section
- // these posts are fake
- if (!is_null($community_link) && $community_link->getAttribute('href') == '/community/maybenews') {
- $title = '[' . trim($community_link->plaintext) . '] ' . $title;
- }
-
- $item = array();
- $item['categories'] = $categories;
- $item['author'] = $post->find('.user__nick', 0)->innertext;
- $item['title'] = $title;
- $item['content'] = strip_tags(
- backgroundToImg($post->find('.story__content-inner', 0)->innertext),
- '<br><p><img><a><s>
- ');
- $item['uri'] = $title_element->href;
- $item['timestamp'] = strtotime($time->getAttribute('datetime'));
- $this->items[] = $item;
- }
- }
+
+class PikabuBridge extends BridgeAbstract
+{
+ const NAME = 'Пикабу';
+ const URI = 'https://pikabu.ru';
+ const DESCRIPTION = 'Выводит посты по тегу, сообществу или пользователю';
+ const MAINTAINER = 'em92';
+
+ const PARAMETERS_FILTER = [
+ 'name' => 'Фильтр',
+ 'type' => 'list',
+ 'values' => [
+ 'Горячее' => 'hot',
+ 'Свежее' => 'new',
+ ],
+ 'defaultValue' => 'hot'
+ ];
+
+ const PARAMETERS = [
+ 'По тегу' => [
+ 'tag' => [
+ 'name' => 'Тег',
+ 'exampleValue' => 'it',
+ 'required' => true
+ ],
+ 'filter' => self::PARAMETERS_FILTER
+ ],
+ 'По сообществу' => [
+ 'community' => [
+ 'name' => 'Сообщество',
+ 'exampleValue' => 'linux',
+ 'required' => true
+ ],
+ 'filter' => self::PARAMETERS_FILTER
+ ],
+ 'По пользователю' => [
+ 'user' => [
+ 'name' => 'Пользователь',
+ 'exampleValue' => 'admin',
+ 'required' => true
+ ]
+ ]
+ ];
+
+ protected $title = null;
+
+ public function getURI()
+ {
+ if ($this->getInput('tag')) {
+ return self::URI . '/tag/' . rawurlencode($this->getInput('tag')) . '/' . rawurlencode($this->getInput('filter'));
+ } elseif ($this->getInput('user')) {
+ return self::URI . '/@' . rawurlencode($this->getInput('user'));
+ } elseif ($this->getInput('community')) {
+ $uri = self::URI . '/community/' . rawurlencode($this->getInput('community'));
+ if ($this->getInput('filter') != 'hot') {
+ $uri .= '/' . rawurlencode($this->getInput('filter'));
+ }
+ return $uri;
+ } else {
+ return parent::getURI();
+ }
+ }
+
+ public function getIcon()
+ {
+ return 'https://cs.pikabu.ru/assets/favicon.ico';
+ }
+
+ public function getName()
+ {
+ if (is_null($this->title)) {
+ return parent::getName();
+ } else {
+ return $this->title . ' - ' . parent::getName();
+ }
+ }
+
+ public function collectData()
+ {
+ $link = $this->getURI();
+
+ $text_html = getContents($link);
+ $text_html = iconv('windows-1251', 'utf-8', $text_html);
+ $html = str_get_html($text_html);
+
+ $this->title = $html->find('title', 0)->innertext;
+
+ foreach ($html->find('article.story') as $post) {
+ $time = $post->find('time.story__datetime', 0);
+ if (is_null($time)) {
+ continue;
+ }
+
+ $el_to_remove_selectors = [
+ '.story__read-more',
+ 'script',
+ 'svg.story-image__stretch',
+ ];
+
+ foreach ($el_to_remove_selectors as $el_to_remove_selector) {
+ foreach ($post->find($el_to_remove_selector) as $el) {
+ $el->outertext = '';
+ }
+ }
+
+ foreach ($post->find('[data-type=gifx]') as $el) {
+ $src = $el->getAttribute('data-source');
+ $el->outertext = '<img src="' . $src . '">';
+ }
+
+ foreach ($post->find('img') as $img) {
+ $src = $img->getAttribute('src');
+ if (!$src) {
+ $src = $img->getAttribute('data-src');
+ if (!$src) {
+ continue;
+ }
+ }
+ $img->outertext = '<img src="' . $src . '">';
+
+ // it is assumed, that img's parents are links to post itself
+ // we don't need them
+ $img->parent()->outertext = $img->outertext;
+ }
+
+ $categories = [];
+ foreach ($post->find('.tags__tag') as $tag) {
+ if ($tag->getAttribute('data-tag')) {
+ $categories[] = $tag->innertext;
+ }
+ }
+
+ $title_element = $post->find('.story__title-link', 0);
+
+ $title = $title_element->plaintext;
+ $community_link = $post->find('.story__community-link', 0);
+ // adding special marker for "Maybe News" section
+ // these posts are fake
+ if (!is_null($community_link) && $community_link->getAttribute('href') == '/community/maybenews') {
+ $title = '[' . trim($community_link->plaintext) . '] ' . $title;
+ }
+
+ $item = [];
+ $item['categories'] = $categories;
+ $item['author'] = $post->find('.user__nick', 0)->innertext;
+ $item['title'] = $title;
+ $item['content'] = strip_tags(
+ backgroundToImg($post->find('.story__content-inner', 0)->innertext),
+ '<br><p><img><a><s>
+ '
+ );
+ $item['uri'] = $title_element->href;
+ $item['timestamp'] = strtotime($time->getAttribute('datetime'));
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/PillowfortBridge.php b/bridges/PillowfortBridge.php
index 527cc1c7..07cdbdd8 100644
--- a/bridges/PillowfortBridge.php
+++ b/bridges/PillowfortBridge.php
@@ -1,98 +1,109 @@
<?php
-class PillowfortBridge extends BridgeAbstract {
- const NAME = 'Pillowfort';
- const URI = 'https://www.pillowfort.social';
- const DESCRIPTION = 'Returns recent posts from a user';
- const MAINTAINER = 'KamaleiZestri';
- const PARAMETERS = array(array(
- 'username' => array(
- 'name' => 'Username',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'Staff'
- ),
- 'noava' => array(
- 'name' => 'Hide avatar',
- 'type' => 'checkbox',
- 'title' => 'Check to hide user avatars.'
- ),
- 'noreblog' => array(
- 'name' => 'Hide reblogs',
- 'type' => 'checkbox',
- 'title' => 'Check to only show original posts.'
- ),
- 'noretags' => array(
- 'name' => 'Prefer original tags',
- 'type' => 'checkbox',
- 'title' => 'Check to use tags from original post(if available) instead of reblog\'s tags'
- ),
- 'image' => array(
- 'name' => 'Select image type',
- 'type' => 'list',
- 'title' => 'Decides how the image is displayed, if at all.',
- 'values' => array(
- 'None' => 'None',
- 'Small' => 'Small',
- 'Full' => 'Full'
- ),
- 'defaultValue' => 'Full'
- )
- ));
-
- /**
- * The Pillowfort bridge.
- *
- * Pillowfort pages are dynamically generated from a json file
- * which holds the last 20 or so posts from the given user.
- * This bridge uses that json file and HTML/CSS similar
- * to the Twitter bridge for formatting.
- */
- public function collectData() {
- $jsonSite = getContents($this->getJSONURI());
-
- $jsonFile = json_decode($jsonSite, true);
- $posts = $jsonFile['posts'];
-
- foreach($posts as $post) {
- $item = $this->getItemFromPost($post);
-
- //empty when 'noreblogs' is checked and current post is a reblog.
- if(!empty($item))
- $this->items[] = $item;
- }
- }
-
- public function getName() {
- $name = $this -> getUsername();
- if($name != '')
- return $name . ' - ' . self::NAME;
- else
- return parent::getName();
- }
-
- public function getURI() {
- $name = $this -> getUsername();
- if($name != '')
- return self::URI . '/' . $name;
- else
- return parent::getURI();
- }
-
- protected function getJSONURI() {
- return $this -> getURI() . '/json/?p=1';
- }
-
- protected function getUsername() {
- return $this -> getInput('username');
- }
-
- protected function genAvatarText($author, $avatar_url, $title) {
- $noava = $this -> getInput('noava');
-
- if($noava)
- return '';
- else
- return <<<EOD
+
+class PillowfortBridge extends BridgeAbstract
+{
+ const NAME = 'Pillowfort';
+ const URI = 'https://www.pillowfort.social';
+ const DESCRIPTION = 'Returns recent posts from a user';
+ const MAINTAINER = 'KamaleiZestri';
+ const PARAMETERS = [[
+ 'username' => [
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'Staff'
+ ],
+ 'noava' => [
+ 'name' => 'Hide avatar',
+ 'type' => 'checkbox',
+ 'title' => 'Check to hide user avatars.'
+ ],
+ 'noreblog' => [
+ 'name' => 'Hide reblogs',
+ 'type' => 'checkbox',
+ 'title' => 'Check to only show original posts.'
+ ],
+ 'noretags' => [
+ 'name' => 'Prefer original tags',
+ 'type' => 'checkbox',
+ 'title' => 'Check to use tags from original post(if available) instead of reblog\'s tags'
+ ],
+ 'image' => [
+ 'name' => 'Select image type',
+ 'type' => 'list',
+ 'title' => 'Decides how the image is displayed, if at all.',
+ 'values' => [
+ 'None' => 'None',
+ 'Small' => 'Small',
+ 'Full' => 'Full'
+ ],
+ 'defaultValue' => 'Full'
+ ]
+ ]];
+
+ /**
+ * The Pillowfort bridge.
+ *
+ * Pillowfort pages are dynamically generated from a json file
+ * which holds the last 20 or so posts from the given user.
+ * This bridge uses that json file and HTML/CSS similar
+ * to the Twitter bridge for formatting.
+ */
+ public function collectData()
+ {
+ $jsonSite = getContents($this->getJSONURI());
+
+ $jsonFile = json_decode($jsonSite, true);
+ $posts = $jsonFile['posts'];
+
+ foreach ($posts as $post) {
+ $item = $this->getItemFromPost($post);
+
+ //empty when 'noreblogs' is checked and current post is a reblog.
+ if (!empty($item)) {
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ public function getName()
+ {
+ $name = $this -> getUsername();
+ if ($name != '') {
+ return $name . ' - ' . self::NAME;
+ } else {
+ return parent::getName();
+ }
+ }
+
+ public function getURI()
+ {
+ $name = $this -> getUsername();
+ if ($name != '') {
+ return self::URI . '/' . $name;
+ } else {
+ return parent::getURI();
+ }
+ }
+
+ protected function getJSONURI()
+ {
+ return $this -> getURI() . '/json/?p=1';
+ }
+
+ protected function getUsername()
+ {
+ return $this -> getInput('username');
+ }
+
+ protected function genAvatarText($author, $avatar_url, $title)
+ {
+ $noava = $this -> getInput('noava');
+
+ if ($noava) {
+ return '';
+ } else {
+ return <<<EOD
<a href="{self::URI}/posts/{$author}">
<img
style="align:top; width:75px; border:1px solid black;"
@@ -101,30 +112,32 @@ class PillowfortBridge extends BridgeAbstract {
title="{$title}" />
</a>
EOD;
- }
+ }
+ }
- protected function genImagesText ($media) {
- $dimensions = $this -> getInput('image');
- $text = '';
+ protected function genImagesText($media)
+ {
+ $dimensions = $this -> getInput('image');
+ $text = '';
- //preg_replace used for images with spaces in the url
+ //preg_replace used for images with spaces in the url
- switch($dimensions) {
- case 'None':
- foreach($media as $image) {
- $imageURL = preg_replace('[ ]', '%20', $image['url']);
- $text .= <<<EOD
+ switch ($dimensions) {
+ case 'None':
+ foreach ($media as $image) {
+ $imageURL = preg_replace('[ ]', '%20', $image['url']);
+ $text .= <<<EOD
<a href="{$imageURL}">
{$imageURL}
</a>
EOD;
- }
- break;
+ }
+ break;
- case 'Small':
- foreach($media as $image) {
- $imageURL = preg_replace('[ ]', '%20', $image['small_image_url']);
- $text .= <<<EOD
+ case 'Small':
+ foreach ($media as $image) {
+ $imageURL = preg_replace('[ ]', '%20', $image['small_image_url']);
+ $text .= <<<EOD
<a href="{$imageURL}">
<img
style="align:top; max-width:558px; border:1px solid black;"
@@ -132,13 +145,13 @@ EOD;
/>
</a>
EOD;
- }
- break;
+ }
+ break;
- case 'Full':
- foreach($media as $image) {
- $imageURL = preg_replace('[ ]', '%20', $image['url']);
- $text .= <<<EOD
+ case 'Full':
+ foreach ($media as $image) {
+ $imageURL = preg_replace('[ ]', '%20', $image['url']);
+ $text .= <<<EOD
<a href="{$imageURL}">
<img
style="align:top; max-width:558px; border:1px solid black;"
@@ -146,66 +159,74 @@ EOD;
/>
</a>
EOD;
- }
- break;
-
- default:
- break;
- }
-
- return $text;
- }
-
- protected function getItemFromPost($post) {
- //check if its a reblog.
- if($post['original_post_id'] == null)
- $embPost = false;
- else
- $embPost = true;
-
- if($this -> getInput('noreblog') && $embPost)
- return array();
-
- $item = array();
-
- $item['uid'] = $post['id'];
- $item['timestamp'] = strtotime($post['created_at']);
-
- if($embPost) {
- $item['uri'] = self::URI . '/posts/' . $post['original_post']['id'];
- $item['author'] = $post['original_username'];
- if($post['original_post']['title'] != '')
- $item['title'] = $post['original_post']['title'];
- else
- $item['title'] = '[NO TITLE]';
- } else {
- $item['uri'] = self::URI . '/posts/' . $post['id'];
- $item['author'] = $post['username'];
- if($post['title'] != '')
- $item['title'] = $post['title'];
- else
- $item['title'] = '[NO TITLE]';
- }
-
- /**
- * 4 cases if it is a reblog.
- * 1: reblog has tags, original has tags. defer to option.
- * 2: reblog has tags, original has no tags. use reblog tags.
- * 3: reblog has no tags, original has tags. use original tags.
- * 4: reblog has no tags, original has no tags. use reblog tags not that it matters.
- */
- $item['categories'] = $post['tags'];
- if($embPost) {
- if($this -> getInput('noretags') || ($post['tags'] == null ))
- $item['categories'] = $post['original_post']['tag_list'];
- }
-
- $avatarText = $this -> genAvatarText($item['author'],
- $post['avatar_url'],
- $item['title']);
- $imagesText = $this -> genImagesText($post['media']);
-
- $item['content'] = <<<EOD
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return $text;
+ }
+
+ protected function getItemFromPost($post)
+ {
+ //check if its a reblog.
+ if ($post['original_post_id'] == null) {
+ $embPost = false;
+ } else {
+ $embPost = true;
+ }
+
+ if ($this -> getInput('noreblog') && $embPost) {
+ return [];
+ }
+
+ $item = [];
+
+ $item['uid'] = $post['id'];
+ $item['timestamp'] = strtotime($post['created_at']);
+
+ if ($embPost) {
+ $item['uri'] = self::URI . '/posts/' . $post['original_post']['id'];
+ $item['author'] = $post['original_username'];
+ if ($post['original_post']['title'] != '') {
+ $item['title'] = $post['original_post']['title'];
+ } else {
+ $item['title'] = '[NO TITLE]';
+ }
+ } else {
+ $item['uri'] = self::URI . '/posts/' . $post['id'];
+ $item['author'] = $post['username'];
+ if ($post['title'] != '') {
+ $item['title'] = $post['title'];
+ } else {
+ $item['title'] = '[NO TITLE]';
+ }
+ }
+
+ /**
+ * 4 cases if it is a reblog.
+ * 1: reblog has tags, original has tags. defer to option.
+ * 2: reblog has tags, original has no tags. use reblog tags.
+ * 3: reblog has no tags, original has tags. use original tags.
+ * 4: reblog has no tags, original has no tags. use reblog tags not that it matters.
+ */
+ $item['categories'] = $post['tags'];
+ if ($embPost) {
+ if ($this -> getInput('noretags') || ($post['tags'] == null )) {
+ $item['categories'] = $post['original_post']['tag_list'];
+ }
+ }
+
+ $avatarText = $this -> genAvatarText(
+ $item['author'],
+ $post['avatar_url'],
+ $item['title']
+ );
+ $imagesText = $this -> genImagesText($post['media']);
+
+ $item['content'] = <<<EOD
<div style="display: inline-block; vertical-align: top;">
{$avatarText}
</div>
@@ -217,6 +238,6 @@ EOD;
</div>
EOD;
- return $item;
- }
+ return $item;
+ }
}
diff --git a/bridges/PinterestBridge.php b/bridges/PinterestBridge.php
index 1f8f86cd..fc5b1c19 100644
--- a/bridges/PinterestBridge.php
+++ b/bridges/PinterestBridge.php
@@ -1,63 +1,64 @@
<?php
-class PinterestBridge extends FeedExpander {
-
- const MAINTAINER = 'pauder';
- const NAME = 'Pinterest Bridge';
- const URI = 'https://www.pinterest.com';
- const DESCRIPTION = 'Returns the newest images on a board';
-
- const PARAMETERS = array(
- 'By username and board' => array(
- 'u' => array(
- 'name' => 'username',
- 'exampleValue' => 'VIGOIndustries',
- 'required' => true
- ),
- 'b' => array(
- 'name' => 'board',
- 'exampleValue' => 'bathroom-remodels',
- 'required' => true
- )
- )
- );
-
- public function getIcon() {
- return 'https://s.pinimg.com/webapp/style/images/favicon-9f8f9adf.png';
- }
-
- public function collectData() {
- $this->collectExpandableDatas($this->getURI() . '.rss');
- $this->fixLowRes();
- }
-
- private function fixLowRes() {
-
- $newitems = array();
- $pattern = '/https\:\/\/i\.pinimg\.com\/[a-zA-Z0-9]*x\//';
- foreach($this->items as $item) {
-
- $item['content'] = preg_replace($pattern, 'https://i.pinimg.com/originals/', $item['content']);
- $newitems[] = $item;
- }
- $this->items = $newitems;
-
- }
-
- public function getURI() {
-
- if ($this->queriedContext === 'By username and board') {
- return self::URI . '/' . urlencode($this->getInput('u')) . '/' . urlencode($this->getInput('b'));
- }
-
- return parent::getURI();
- }
-
- public function getName() {
-
- if ($this->queriedContext === 'By username and board') {
- return $this->getInput('u') . ' - ' . $this->getInput('b') . ' - ' . self::NAME;
- }
-
- return parent::getName();
- }
+
+class PinterestBridge extends FeedExpander
+{
+ const MAINTAINER = 'pauder';
+ const NAME = 'Pinterest Bridge';
+ const URI = 'https://www.pinterest.com';
+ const DESCRIPTION = 'Returns the newest images on a board';
+
+ const PARAMETERS = [
+ 'By username and board' => [
+ 'u' => [
+ 'name' => 'username',
+ 'exampleValue' => 'VIGOIndustries',
+ 'required' => true
+ ],
+ 'b' => [
+ 'name' => 'board',
+ 'exampleValue' => 'bathroom-remodels',
+ 'required' => true
+ ]
+ ]
+ ];
+
+ public function getIcon()
+ {
+ return 'https://s.pinimg.com/webapp/style/images/favicon-9f8f9adf.png';
+ }
+
+ public function collectData()
+ {
+ $this->collectExpandableDatas($this->getURI() . '.rss');
+ $this->fixLowRes();
+ }
+
+ private function fixLowRes()
+ {
+ $newitems = [];
+ $pattern = '/https\:\/\/i\.pinimg\.com\/[a-zA-Z0-9]*x\//';
+ foreach ($this->items as $item) {
+ $item['content'] = preg_replace($pattern, 'https://i.pinimg.com/originals/', $item['content']);
+ $newitems[] = $item;
+ }
+ $this->items = $newitems;
+ }
+
+ public function getURI()
+ {
+ if ($this->queriedContext === 'By username and board') {
+ return self::URI . '/' . urlencode($this->getInput('u')) . '/' . urlencode($this->getInput('b'));
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if ($this->queriedContext === 'By username and board') {
+ return $this->getInput('u') . ' - ' . $this->getInput('b') . ' - ' . self::NAME;
+ }
+
+ return parent::getName();
+ }
}
diff --git a/bridges/PirateCommunityBridge.php b/bridges/PirateCommunityBridge.php
index ce861015..a1a9d8f5 100644
--- a/bridges/PirateCommunityBridge.php
+++ b/bridges/PirateCommunityBridge.php
@@ -1,88 +1,102 @@
<?php
-class PirateCommunityBridge extends BridgeAbstract {
- const NAME = 'Pirate-Community Bridge';
- const URI = 'https://raymanpc.com/';
- const CACHE_TIMEOUT = 300; // 5min
- const DESCRIPTION = 'Returns replies to topics';
- const MAINTAINER = 'Roliga';
- const PARAMETERS = array( array(
- 't' => array(
- 'name' => 'Topic ID',
- 'type' => 'number',
- 'exampleValue' => '12651',
- 'title' => 'Topic ID from topic URL. If the URL contains t=12 the ID is 12.',
- 'required' => true
- )));
-
- private $feedName = '';
-
- public function detectParameters($url){
- $parsed_url = parse_url($url);
-
- if($parsed_url['host'] !== 'raymanpc.com')
- return null;
-
- parse_str($parsed_url['query'], $parsed_query);
-
- if($parsed_url['path'] === '/forum/viewtopic.php'
- && array_key_exists('t', $parsed_query)) {
- return array('t' => $parsed_query['t']);
- }
-
- return null;
- }
-
- public function getName() {
- if(!empty($this->feedName))
- return $this->feedName;
-
- return parent::getName();
- }
-
- public function getURI(){
- if(!is_null($this->getInput('t'))) {
- return self::URI
- . 'forum/viewtopic.php?t='
- . $this->getInput('t')
- . '&sd=d'; // sort posts decending by ate so first page has latest posts
- }
-
- return parent::getURI();
- }
-
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI());
-
- $this->feedName = $html->find('head title', 0)->plaintext;
-
- foreach($html->find('.post') as $reply) {
- $item = array();
-
- $item['uri'] = $this->getURI()
- . $reply->find('h3 a', 0)->getAttribute('href');
-
- $item['title'] = $reply->find('h3 a', 0)->plaintext;
-
- $author_html = $reply->find('.author', 0);
- // author_html contains the timestamp as text directly inside it,
- // so delete all other child elements
- foreach($author_html->children as $child)
- $child->outertext = '';
- // Timestamps are always in UTC+1
- $item['timestamp'] = trim($author_html->innertext) . ' +01:00';
-
- $item['author'] = $reply
- ->find('.username, .username-coloured', 0)
- ->plaintext;
-
- $item['content'] = defaultLinkTo($reply->find('.content', 0)->innertext,
- $this->getURI());
-
- $item['enclosures'] = array();
- foreach($reply->find('.attachbox img.postimage') as $img)
- $item['enclosures'][] = urljoin($this->getURI(), $img->src);
-
- $this->items[] = $item;
- }
- }
+
+class PirateCommunityBridge extends BridgeAbstract
+{
+ const NAME = 'Pirate-Community Bridge';
+ const URI = 'https://raymanpc.com/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns replies to topics';
+ const MAINTAINER = 'Roliga';
+ const PARAMETERS = [ [
+ 't' => [
+ 'name' => 'Topic ID',
+ 'type' => 'number',
+ 'exampleValue' => '12651',
+ 'title' => 'Topic ID from topic URL. If the URL contains t=12 the ID is 12.',
+ 'required' => true
+ ]]];
+
+ private $feedName = '';
+
+ public function detectParameters($url)
+ {
+ $parsed_url = parse_url($url);
+
+ if ($parsed_url['host'] !== 'raymanpc.com') {
+ return null;
+ }
+
+ parse_str($parsed_url['query'], $parsed_query);
+
+ if (
+ $parsed_url['path'] === '/forum/viewtopic.php'
+ && array_key_exists('t', $parsed_query)
+ ) {
+ return ['t' => $parsed_query['t']];
+ }
+
+ return null;
+ }
+
+ public function getName()
+ {
+ if (!empty($this->feedName)) {
+ return $this->feedName;
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('t'))) {
+ return self::URI
+ . 'forum/viewtopic.php?t='
+ . $this->getInput('t')
+ . '&sd=d'; // sort posts decending by ate so first page has latest posts
+ }
+
+ return parent::getURI();
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $this->feedName = $html->find('head title', 0)->plaintext;
+
+ foreach ($html->find('.post') as $reply) {
+ $item = [];
+
+ $item['uri'] = $this->getURI()
+ . $reply->find('h3 a', 0)->getAttribute('href');
+
+ $item['title'] = $reply->find('h3 a', 0)->plaintext;
+
+ $author_html = $reply->find('.author', 0);
+ // author_html contains the timestamp as text directly inside it,
+ // so delete all other child elements
+ foreach ($author_html->children as $child) {
+ $child->outertext = '';
+ }
+ // Timestamps are always in UTC+1
+ $item['timestamp'] = trim($author_html->innertext) . ' +01:00';
+
+ $item['author'] = $reply
+ ->find('.username, .username-coloured', 0)
+ ->plaintext;
+
+ $item['content'] = defaultLinkTo(
+ $reply->find('.content', 0)->innertext,
+ $this->getURI()
+ );
+
+ $item['enclosures'] = [];
+ foreach ($reply->find('.attachbox img.postimage') as $img) {
+ $item['enclosures'][] = urljoin($this->getURI(), $img->src);
+ }
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/PixivBridge.php b/bridges/PixivBridge.php
index 6a3fd573..55e00984 100644
--- a/bridges/PixivBridge.php
+++ b/bridges/PixivBridge.php
@@ -1,215 +1,229 @@
<?php
-class PixivBridge extends BridgeAbstract {
-
- // Good resource on API return values (Ex: illustType):
- // https://hackage.haskell.org/package/pixiv-0.1.0/docs/Web-Pixiv-Types.html
- const NAME = 'Pixiv Bridge';
- const URI = 'https://www.pixiv.net/';
- const DESCRIPTION = 'Returns the tag search from pixiv.net';
-
-
- const PARAMETERS = array(
- 'global' => array(
- 'posts' => array(
- 'name' => 'Post Limit',
- 'type' => 'number',
- 'defaultValue' => '10'
- ),
- 'fullsize' => array(
- 'name' => 'Full-size Image',
- 'type' => 'checkbox'
- ),
- 'mode' => array(
- 'name' => 'Post Type',
- 'type' => 'list',
- 'values' => array('All Works' => 'all',
- 'Illustrations' => 'illustrations/',
- 'Manga' => 'manga/',
- 'Novels' => 'novels/')
- ),
- ),
- 'Tag' => array(
- 'tag' => array(
- 'name' => 'Query to search',
- 'exampleValue' => 'オリジナル',
- 'required' => true
- )
- ),
- 'User' => array(
- 'userid' => array(
- 'name' => 'User ID from profile URL',
- 'exampleValue' => '11',
- 'required' => true
- )
- )
- );
-
- // maps from URLs to json keys by context
- const JSON_KEY_MAP = array(
- 'Tag' => array(
- 'illustrations/' => 'illust',
- 'manga/' => 'manga',
- 'novels/' => 'novel'
- ),
- 'User' => array(
- 'illustrations/' => 'illusts',
- 'manga/' => 'manga',
- 'novels/' => 'novels'
- )
- );
-
- // Hold the username for getName()
- private $username = null;
-
- public function getName() {
- switch($this->queriedContext) {
- case 'Tag':
- $context = 'Tag';
- $query = $this->getInput('tag');
- break;
- case 'User':
- $context = 'User';
- $query = $this->username ?? $this->getInput('userid');
- break;
- default:
- return parent::getName();
- }
- $mode = array_search($this->getInput('mode'),
- self::PARAMETERS['global']['mode']['values']);
- return "Pixiv ${mode} from ${context} ${query}";
- }
-
- public function getURI() {
- switch($this->queriedContext) {
- case 'Tag':
- $uri = static::URI . 'tags/' . urlencode($this->getInput('tag') ?? '');
- break;
- case 'User':
- $uri = static::URI . 'users/' . $this->getInput('userid');
- break;
- default:
- return parent::getURI();
- }
- if ($this->getInput('mode') != 'all') {
- $uri = $uri . '/' . $this->getInput('mode');
- }
- return $uri;
- }
-
- private function getSearchURI($mode) {
- switch($this->queriedContext) {
- case 'Tag':
- $query = urlencode($this->getInput('tag'));
- $uri = static::URI . 'ajax/search/top/' . $query;
- break;
- case 'User':
- $uri = static::URI . 'ajax/user/' . $this->getInput('userid')
- . '/profile/top';
- break;
- default:
- returnClientError('Invalid Context');
- }
- return $uri;
- }
-
- private function getDataFromJSON($json, $json_key) {
- $json = $json['body'][$json_key];
- // Tags context contains subkey
- if ($this->queriedContext == 'Tag') {
- $json = $json['data'];
- }
- return $json;
- }
-
- private function collectWorksArray() {
- $content = getContents($this->getSearchURI($this->getInput('mode')));
- $content = json_decode($content, true);
- if ($this->getInput('mode') == 'all') {
- $total = array();
- foreach(self::JSON_KEY_MAP[$this->queriedContext] as $mode => $json_key) {
- $current = $this->getDataFromJSON($content, $json_key);
- $total = array_merge($total, $current);
- }
- $content = $total;
- } else {
- $json_key = self::JSON_KEY_MAP[$this->queriedContext][$this->getInput('mode')];
- $content = $this->getDataFromJSON($content, $json_key);
- }
- return $content;
- }
-
- public function collectData() {
- $content = $this->collectWorksArray();
-
- $content = array_filter($content, function($v, $k) {
- return !array_key_exists('isAdContainer', $v);
- }, ARRAY_FILTER_USE_BOTH);
- // Sort by updateDate to get newest works
- usort($content, function($a, $b) {
- return $b['updateDate'] <=> $a['updateDate'];
- });
- $content = array_slice($content, 0, $this->getInput('posts'));
-
- foreach($content as $result) {
- // Store username for getName()
- if (!$this->username)
- $this->username = $result['userName'];
-
- $item = array();
- $item['uid'] = $result['id'];
- $subpath = array_key_exists('illustType', $result) ? 'artworks/' : 'novel/show.php?id=';
- $item['uri'] = static::URI . $subpath . $result['id'];
- $item['title'] = $result['title'];
- $item['author'] = $result['userName'];
- $item['timestamp'] = $result['updateDate'];
- $item['categories'] = $result['tags'];
- $cached_image = $this->cacheImage($result['url'], $result['id'],
- array_key_exists('illustType', $result));
- $item['content'] = "<img src='" . $cached_image . "' />";
-
- // Additional content items
- if (array_key_exists('pageCount', $result)) {
- $item['content'] .= '<br>Page Count: ' . $result['pageCount'];
- } else {
- $item['content'] .= '<br>Word Count: ' . $result['wordCount'];
- }
-
- $this->items[] = $item;
- }
- }
-
- private function cacheImage($url, $illustId, $isImage) {
- $illustId = preg_replace('/[^0-9]/', '', $illustId);
- $thumbnailurl = $url;
-
- $path = PATH_CACHE . 'pixiv_img/';
- if(!is_dir($path))
- mkdir($path, 0755, true);
-
- $path .= $illustId;
- if ($this->getInput('fullsize')) {
- $path .= '_fullsize';
- }
- $path .= '.jpg';
-
- if(!is_file($path)) {
-
- // Get fullsize URL
- if ($isImage && $this->getInput('fullsize')) {
- $ajax_uri = static::URI . 'ajax/illust/' . $illustId;
- $imagejson = json_decode(getContents($ajax_uri), true);
- $url = $imagejson['body']['urls']['original'];
- }
-
- $headers = array('Referer: ' . static::URI);
- try {
- $illust = getContents($url, $headers);
- } catch (Exception $e) {
- $illust = getContents($thumbnailurl, $headers); // Original thumbnail
- }
- file_put_contents($path, $illust);
- }
-
- return 'cache/pixiv_img/' . preg_replace('/.*\//', '', $path);
- }
+
+class PixivBridge extends BridgeAbstract
+{
+ // Good resource on API return values (Ex: illustType):
+ // https://hackage.haskell.org/package/pixiv-0.1.0/docs/Web-Pixiv-Types.html
+ const NAME = 'Pixiv Bridge';
+ const URI = 'https://www.pixiv.net/';
+ const DESCRIPTION = 'Returns the tag search from pixiv.net';
+
+
+ const PARAMETERS = [
+ 'global' => [
+ 'posts' => [
+ 'name' => 'Post Limit',
+ 'type' => 'number',
+ 'defaultValue' => '10'
+ ],
+ 'fullsize' => [
+ 'name' => 'Full-size Image',
+ 'type' => 'checkbox'
+ ],
+ 'mode' => [
+ 'name' => 'Post Type',
+ 'type' => 'list',
+ 'values' => ['All Works' => 'all',
+ 'Illustrations' => 'illustrations/',
+ 'Manga' => 'manga/',
+ 'Novels' => 'novels/']
+ ],
+ ],
+ 'Tag' => [
+ 'tag' => [
+ 'name' => 'Query to search',
+ 'exampleValue' => 'オリジナル',
+ 'required' => true
+ ]
+ ],
+ 'User' => [
+ 'userid' => [
+ 'name' => 'User ID from profile URL',
+ 'exampleValue' => '11',
+ 'required' => true
+ ]
+ ]
+ ];
+
+ // maps from URLs to json keys by context
+ const JSON_KEY_MAP = [
+ 'Tag' => [
+ 'illustrations/' => 'illust',
+ 'manga/' => 'manga',
+ 'novels/' => 'novel'
+ ],
+ 'User' => [
+ 'illustrations/' => 'illusts',
+ 'manga/' => 'manga',
+ 'novels/' => 'novels'
+ ]
+ ];
+
+ // Hold the username for getName()
+ private $username = null;
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Tag':
+ $context = 'Tag';
+ $query = $this->getInput('tag');
+ break;
+ case 'User':
+ $context = 'User';
+ $query = $this->username ?? $this->getInput('userid');
+ break;
+ default:
+ return parent::getName();
+ }
+ $mode = array_search(
+ $this->getInput('mode'),
+ self::PARAMETERS['global']['mode']['values']
+ );
+ return "Pixiv ${mode} from ${context} ${query}";
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Tag':
+ $uri = static::URI . 'tags/' . urlencode($this->getInput('tag') ?? '');
+ break;
+ case 'User':
+ $uri = static::URI . 'users/' . $this->getInput('userid');
+ break;
+ default:
+ return parent::getURI();
+ }
+ if ($this->getInput('mode') != 'all') {
+ $uri = $uri . '/' . $this->getInput('mode');
+ }
+ return $uri;
+ }
+
+ private function getSearchURI($mode)
+ {
+ switch ($this->queriedContext) {
+ case 'Tag':
+ $query = urlencode($this->getInput('tag'));
+ $uri = static::URI . 'ajax/search/top/' . $query;
+ break;
+ case 'User':
+ $uri = static::URI . 'ajax/user/' . $this->getInput('userid')
+ . '/profile/top';
+ break;
+ default:
+ returnClientError('Invalid Context');
+ }
+ return $uri;
+ }
+
+ private function getDataFromJSON($json, $json_key)
+ {
+ $json = $json['body'][$json_key];
+ // Tags context contains subkey
+ if ($this->queriedContext == 'Tag') {
+ $json = $json['data'];
+ }
+ return $json;
+ }
+
+ private function collectWorksArray()
+ {
+ $content = getContents($this->getSearchURI($this->getInput('mode')));
+ $content = json_decode($content, true);
+ if ($this->getInput('mode') == 'all') {
+ $total = [];
+ foreach (self::JSON_KEY_MAP[$this->queriedContext] as $mode => $json_key) {
+ $current = $this->getDataFromJSON($content, $json_key);
+ $total = array_merge($total, $current);
+ }
+ $content = $total;
+ } else {
+ $json_key = self::JSON_KEY_MAP[$this->queriedContext][$this->getInput('mode')];
+ $content = $this->getDataFromJSON($content, $json_key);
+ }
+ return $content;
+ }
+
+ public function collectData()
+ {
+ $content = $this->collectWorksArray();
+
+ $content = array_filter($content, function ($v, $k) {
+ return !array_key_exists('isAdContainer', $v);
+ }, ARRAY_FILTER_USE_BOTH);
+ // Sort by updateDate to get newest works
+ usort($content, function ($a, $b) {
+ return $b['updateDate'] <=> $a['updateDate'];
+ });
+ $content = array_slice($content, 0, $this->getInput('posts'));
+
+ foreach ($content as $result) {
+ // Store username for getName()
+ if (!$this->username) {
+ $this->username = $result['userName'];
+ }
+
+ $item = [];
+ $item['uid'] = $result['id'];
+ $subpath = array_key_exists('illustType', $result) ? 'artworks/' : 'novel/show.php?id=';
+ $item['uri'] = static::URI . $subpath . $result['id'];
+ $item['title'] = $result['title'];
+ $item['author'] = $result['userName'];
+ $item['timestamp'] = $result['updateDate'];
+ $item['categories'] = $result['tags'];
+ $cached_image = $this->cacheImage(
+ $result['url'],
+ $result['id'],
+ array_key_exists('illustType', $result)
+ );
+ $item['content'] = "<img src='" . $cached_image . "' />";
+
+ // Additional content items
+ if (array_key_exists('pageCount', $result)) {
+ $item['content'] .= '<br>Page Count: ' . $result['pageCount'];
+ } else {
+ $item['content'] .= '<br>Word Count: ' . $result['wordCount'];
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function cacheImage($url, $illustId, $isImage)
+ {
+ $illustId = preg_replace('/[^0-9]/', '', $illustId);
+ $thumbnailurl = $url;
+
+ $path = PATH_CACHE . 'pixiv_img/';
+ if (!is_dir($path)) {
+ mkdir($path, 0755, true);
+ }
+
+ $path .= $illustId;
+ if ($this->getInput('fullsize')) {
+ $path .= '_fullsize';
+ }
+ $path .= '.jpg';
+
+ if (!is_file($path)) {
+ // Get fullsize URL
+ if ($isImage && $this->getInput('fullsize')) {
+ $ajax_uri = static::URI . 'ajax/illust/' . $illustId;
+ $imagejson = json_decode(getContents($ajax_uri), true);
+ $url = $imagejson['body']['urls']['original'];
+ }
+
+ $headers = ['Referer: ' . static::URI];
+ try {
+ $illust = getContents($url, $headers);
+ } catch (Exception $e) {
+ $illust = getContents($thumbnailurl, $headers); // Original thumbnail
+ }
+ file_put_contents($path, $illust);
+ }
+
+ return 'cache/pixiv_img/' . preg_replace('/.*\//', '', $path);
+ }
}
diff --git a/bridges/PlantUMLReleasesBridge.php b/bridges/PlantUMLReleasesBridge.php
index fbf9211b..bc1cca20 100644
--- a/bridges/PlantUMLReleasesBridge.php
+++ b/bridges/PlantUMLReleasesBridge.php
@@ -5,42 +5,45 @@
* @author nicolas-delsaux
*
*/
-class PlantUMLReleasesBridge extends BridgeAbstract {
- const MAINTAINER = 'Riduidel';
- const NAME = 'PlantUML Releases';
- const AUTHOR = 'PlantUML team';
- const URI = 'https://plantuml.com/changes';
+class PlantUMLReleasesBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Riduidel';
+ const NAME = 'PlantUML Releases';
+ const AUTHOR = 'PlantUML team';
+ const URI = 'https://plantuml.com/changes';
- const CACHE_TIMEOUT = 7200; // 2h
- const DESCRIPTION = 'PlantUML releases bridge, showing for each release the changelog';
- const ITEM_LIMIT = 10;
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'PlantUML releases bridge, showing for each release the changelog';
+ const ITEM_LIMIT = 10;
- public function getURI() {
- return self::URI;
- }
+ public function getURI()
+ {
+ return self::URI;
+ }
- public function collectData() {
- $html = defaultLinkTo(getSimpleHTMLDOM($this->getURI()), self::URI);
+ public function collectData()
+ {
+ $html = defaultLinkTo(getSimpleHTMLDOM($this->getURI()), self::URI);
- $num_items = 0;
- $main = $html->find('div[id=root]', 0);
- foreach ($main->find('h2') as $release) {
- // Limit to $ITEM_LIMIT number of results
- if ($num_items++ >= self::ITEM_LIMIT) {
- break;
- }
- $item = array();
- $item['author'] = self::AUTHOR;
- $release_text = $release->innertext;
- if (preg_match('/(.+) \((.*)\)/', $release_text, $matches)) {
- $item['title'] = $matches[1];
- $item['timestamp'] = preg_replace('/(\d+) (\w{3})\w*, (\d+)/', '${1} ${2} ${3}', $matches[2]);
- } else {
- $item['title'] = $release_text;
- }
- $item['uri'] = $this->getURI();
- $item['content'] = $release->next_sibling();
- $this->items[] = $item;
- }
- }
+ $num_items = 0;
+ $main = $html->find('div[id=root]', 0);
+ foreach ($main->find('h2') as $release) {
+ // Limit to $ITEM_LIMIT number of results
+ if ($num_items++ >= self::ITEM_LIMIT) {
+ break;
+ }
+ $item = [];
+ $item['author'] = self::AUTHOR;
+ $release_text = $release->innertext;
+ if (preg_match('/(.+) \((.*)\)/', $release_text, $matches)) {
+ $item['title'] = $matches[1];
+ $item['timestamp'] = preg_replace('/(\d+) (\w{3})\w*, (\d+)/', '${1} ${2} ${3}', $matches[2]);
+ } else {
+ $item['title'] = $release_text;
+ }
+ $item['uri'] = $this->getURI();
+ $item['content'] = $release->next_sibling();
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/PokemonTVBridge.php b/bridges/PokemonTVBridge.php
index 4a34e58e..2c6b8bdc 100644
--- a/bridges/PokemonTVBridge.php
+++ b/bridges/PokemonTVBridge.php
@@ -1,143 +1,147 @@
<?php
-class PokemonTVBridge extends BridgeAbstract {
- const NAME = 'PokemonTV Bridge';
- const URI = 'https://www.pokemon.com/';
- const DESCRIPTION = 'Returns latest episodes from PokemonTV';
- const MAINTAINER = 'Bockiii';
- const CACHE_TIMEOUT = 3600;
- const PARAMETERS = array( array(
- 'language' => array(
- 'name' => 'Language',
- 'type' => 'list',
- 'title' => 'Select your language',
- 'values' => array(
- 'Danish' => 'dk',
- 'Dutch' => 'nl',
- 'English (UK)' => 'uk',
- 'English (US)' => 'us',
- 'Finish' => 'fi',
- 'French' => 'fr',
- 'German' => 'de',
- 'Italian' => 'it',
- 'Latin America' => 'el',
- 'Norwegian' => 'no',
- 'Portoguese' => 'br',
- 'Russian' => 'ru',
- 'Spanish' => 'es',
- 'Swedish' => 'se'
- ),
- 'defaultValue' => 'English (US)'
- ),
- 'filtername' => array(
- 'name' => 'Series Name Filter',
- 'exampleValue' => 'Ultra',
- 'required' => false
- ),
- 'filterseason' => array(
- 'name' => 'Series Season Filter',
- 'exampleValue' => '22',
- 'required' => false
- )
- ));
+class PokemonTVBridge extends BridgeAbstract
+{
+ const NAME = 'PokemonTV Bridge';
+ const URI = 'https://www.pokemon.com/';
+ const DESCRIPTION = 'Returns latest episodes from PokemonTV';
+ const MAINTAINER = 'Bockiii';
+ const CACHE_TIMEOUT = 3600;
+ const PARAMETERS = [ [
+ 'language' => [
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'title' => 'Select your language',
+ 'values' => [
+ 'Danish' => 'dk',
+ 'Dutch' => 'nl',
+ 'English (UK)' => 'uk',
+ 'English (US)' => 'us',
+ 'Finish' => 'fi',
+ 'French' => 'fr',
+ 'German' => 'de',
+ 'Italian' => 'it',
+ 'Latin America' => 'el',
+ 'Norwegian' => 'no',
+ 'Portoguese' => 'br',
+ 'Russian' => 'ru',
+ 'Spanish' => 'es',
+ 'Swedish' => 'se'
+ ],
+ 'defaultValue' => 'English (US)'
+ ],
+ 'filtername' => [
+ 'name' => 'Series Name Filter',
+ 'exampleValue' => 'Ultra',
+ 'required' => false
+ ],
+ 'filterseason' => [
+ 'name' => 'Series Season Filter',
+ 'exampleValue' => '22',
+ 'required' => false
+ ]
+ ]];
- public function collectData(){
- $link = 'https://www.pokemon.com/api/pokemontv/v2/channels/' . $this->getInput('language');
+ public function collectData()
+ {
+ $link = 'https://www.pokemon.com/api/pokemontv/v2/channels/' . $this->getInput('language');
- $html = getSimpleHTMLDOM($link);
- $parsed_json = json_decode($html);
+ $html = getSimpleHTMLDOM($link);
+ $parsed_json = json_decode($html);
- $filtername = $this->getInput('filtername');
- $filterseason = $this->getInput('filterseason');
+ $filtername = $this->getInput('filtername');
+ $filterseason = $this->getInput('filterseason');
- foreach($parsed_json as $element) {
- if(strlen($filtername) >= 1) {
- if (!(stristr($element->{'channel_name'}, $filtername) !== false)) {
- continue;
- }
- }
- foreach($element->{'media'} as $mediaelement) {
- if(strlen($filterseason) >= 1) {
- if ($mediaelement->{'season'} != $filterseason) {
- continue;
- }
- }
- switch($element->{'media_type'}) {
- case 'movie':
- $itemtitle = $element->{'channel_name'};
- break;
- case 'episode':
- $season = str_pad($mediaelement->{'season'}, 2, '0', STR_PAD_LEFT);
- $episode = str_pad($mediaelement->{'episode'}, 2, '0', STR_PAD_LEFT);
- $itemtitle = $element->{'channel_name'} . ' - S' . $season . 'E' . $episode;
- break;
- }
- $streamurl = 'https://watch.pokemon.com/' . $this->getCountryCode() . '/#/player?id=' . $mediaelement->{'id'};
- $item = array();
- $item['uri'] = $streamurl;
- $item['title'] = $itemtitle;
- $item['timestamp'] = $mediaelement->{'last_modified'};
- $item['content'] = '<h1>' . $itemtitle . ' ' . $mediaelement->{'title'}
- . '</h1><br><br><a href="'
- . $streamurl
- . '"><img src="'
- . $mediaelement->{'images'}->{'medium'}
- . '" /></a><br><br>'
- . $mediaelement->{'description'}
- . '<br><br><a href="' . $mediaelement->{'offline_url'} . '">Download</a>';
- $this->items[] = $item;
- }
- }
- }
+ foreach ($parsed_json as $element) {
+ if (strlen($filtername) >= 1) {
+ if (!(stristr($element->{'channel_name'}, $filtername) !== false)) {
+ continue;
+ }
+ }
+ foreach ($element->{'media'} as $mediaelement) {
+ if (strlen($filterseason) >= 1) {
+ if ($mediaelement->{'season'} != $filterseason) {
+ continue;
+ }
+ }
+ switch ($element->{'media_type'}) {
+ case 'movie':
+ $itemtitle = $element->{'channel_name'};
+ break;
+ case 'episode':
+ $season = str_pad($mediaelement->{'season'}, 2, '0', STR_PAD_LEFT);
+ $episode = str_pad($mediaelement->{'episode'}, 2, '0', STR_PAD_LEFT);
+ $itemtitle = $element->{'channel_name'} . ' - S' . $season . 'E' . $episode;
+ break;
+ }
+ $streamurl = 'https://watch.pokemon.com/' . $this->getCountryCode() . '/#/player?id=' . $mediaelement->{'id'};
+ $item = [];
+ $item['uri'] = $streamurl;
+ $item['title'] = $itemtitle;
+ $item['timestamp'] = $mediaelement->{'last_modified'};
+ $item['content'] = '<h1>' . $itemtitle . ' ' . $mediaelement->{'title'}
+ . '</h1><br><br><a href="'
+ . $streamurl
+ . '"><img src="'
+ . $mediaelement->{'images'}->{'medium'}
+ . '" /></a><br><br>'
+ . $mediaelement->{'description'}
+ . '<br><br><a href="' . $mediaelement->{'offline_url'} . '">Download</a>';
+ $this->items[] = $item;
+ }
+ }
+ }
- private function getCountryCode() {
- switch($this->getInput('language')) {
- case 'us':
- return 'en-us';
- break;
- case 'de':
- return 'de-de';
- break;
- case 'fr':
- return 'fr-fr';
- break;
- case 'es':
- return 'es-es';
- break;
- case 'el':
- return 'es-xl';
- break;
- case 'it':
- return 'it-it';
- break;
- case 'dk':
- return 'da-dk';
- break;
- case 'fi':
- return 'fi-fi';
- break;
- case 'br':
- return 'pt-br';
- break;
- case 'uk':
- return 'en-gb';
- break;
- case 'ru':
- return 'ru-ru';
- break;
- case 'nl':
- return 'nl-nl';
- break;
- case 'no':
- return 'nb-no';
- break;
- case 'se':
- return 'sv-se';
- break;
- }
- }
+ private function getCountryCode()
+ {
+ switch ($this->getInput('language')) {
+ case 'us':
+ return 'en-us';
+ break;
+ case 'de':
+ return 'de-de';
+ break;
+ case 'fr':
+ return 'fr-fr';
+ break;
+ case 'es':
+ return 'es-es';
+ break;
+ case 'el':
+ return 'es-xl';
+ break;
+ case 'it':
+ return 'it-it';
+ break;
+ case 'dk':
+ return 'da-dk';
+ break;
+ case 'fi':
+ return 'fi-fi';
+ break;
+ case 'br':
+ return 'pt-br';
+ break;
+ case 'uk':
+ return 'en-gb';
+ break;
+ case 'ru':
+ return 'ru-ru';
+ break;
+ case 'nl':
+ return 'nl-nl';
+ break;
+ case 'no':
+ return 'nb-no';
+ break;
+ case 'se':
+ return 'sv-se';
+ break;
+ }
+ }
- public function getIcon() {
- return 'https://assets.pokemon.com/static2/_ui/img/favicon.ico';
- }
+ public function getIcon()
+ {
+ return 'https://assets.pokemon.com/static2/_ui/img/favicon.ico';
+ }
}
diff --git a/bridges/PornhubBridge.php b/bridges/PornhubBridge.php
index 40ad3bdd..c15db064 100644
--- a/bridges/PornhubBridge.php
+++ b/bridges/PornhubBridge.php
@@ -1,99 +1,102 @@
<?php
-class PornhubBridge extends BridgeAbstract {
-
- const MAINTAINER = 'Mitsukarenai';
- const NAME = 'Pornhub';
- const URI = 'https://www.pornhub.com/';
- const CACHE_TIMEOUT = 3600; // 1h
- const DESCRIPTION = 'Returns videos from specified user,model,pornstar';
-
- const PARAMETERS = array(array(
- 'q' => array(
- 'name' => 'User name',
- 'exampleValue' => 'asa-akira',
- 'required' => true,
- ),
- 'type' => array(
- 'name' => 'User type',
- 'type' => 'list',
- 'values' => array(
- 'user' => 'users',
- 'model' => 'model',
- 'pornstar' => 'pornstar',
- ),
- 'defaultValue' => 'pornstar',
- ),
- 'sort' => array(
- 'name' => 'Sort by',
- 'type' => 'list',
- 'values' => array(
- 'Most recent' => '?',
- 'Most views' => '?o=mv',
- 'Top rated' => '?o=tr',
- 'Longest' => '?o=lg',
- ),
- 'defaultValue' => '?',
- ),
- 'show_images' => array(
- 'name' => 'Show thumbnails',
- 'type' => 'checkbox',
- ),
- ));
-
- public function getName(){
- if(!is_null($this->getInput('type')) && !is_null($this->getInput('q'))) {
- return 'PornHub ' . $this->getInput('type') . ':' . $this->getInput('q');
- }
-
- return parent::getName();
- }
-
- public function collectData() {
-
- $uri = 'https://www.pornhub.com/' . $this->getInput('type') . '/';
- switch($this->getInput('type')) { // select proper permalink format per user type...
- case 'model':
- $uri .= urlencode($this->getInput('q')) . '/videos' . $this->getInput('sort'); break;
- case 'users':
- $uri .= urlencode($this->getInput('q')) . '/videos/public' . $this->getInput('sort'); break;
- case 'pornstar':
- $uri .= urlencode($this->getInput('q')) . '/videos/upload' . $this->getInput('sort'); break;
- }
-
- $show_images = $this->getInput('show_images');
-
- $html = getSimpleHTMLDOM($uri);
-
- foreach($html->find('div.videoUList ul.videos li.videoblock') as $element) {
-
- $item = array();
-
- $item['author'] = $this->getInput('q');
-
- // Title
- $title = $element->find('a', 0)->getAttribute('title');
- if (is_null($title)) {
- continue;
- }
- $item['title'] = $title;
-
- // Url
- $url = $element->find('a', 0)->href;
- $item['uri'] = 'https://www.pornhub.com' . $url;
-
- // Content
- $image = $element->find('img', 0)->getAttribute('data-src');
- if($show_images === true) {
- $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $image . '"></a>';
- }
-
- // date hack, guess upload YYYYMMDD from thumbnail URL (format: https://ci.phncdn.com/videos/201907/25/--- )
- $uploaded = explode('/', $image);
- $uploaded = strtotime($uploaded[4] . $uploaded[5]);
- $item['timestamp'] = $uploaded;
-
- $this->items[] = $item;
- }
- }
+class PornhubBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Mitsukarenai';
+ const NAME = 'Pornhub';
+ const URI = 'https://www.pornhub.com/';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Returns videos from specified user,model,pornstar';
+
+ const PARAMETERS = [[
+ 'q' => [
+ 'name' => 'User name',
+ 'exampleValue' => 'asa-akira',
+ 'required' => true,
+ ],
+ 'type' => [
+ 'name' => 'User type',
+ 'type' => 'list',
+ 'values' => [
+ 'user' => 'users',
+ 'model' => 'model',
+ 'pornstar' => 'pornstar',
+ ],
+ 'defaultValue' => 'pornstar',
+ ],
+ 'sort' => [
+ 'name' => 'Sort by',
+ 'type' => 'list',
+ 'values' => [
+ 'Most recent' => '?',
+ 'Most views' => '?o=mv',
+ 'Top rated' => '?o=tr',
+ 'Longest' => '?o=lg',
+ ],
+ 'defaultValue' => '?',
+ ],
+ 'show_images' => [
+ 'name' => 'Show thumbnails',
+ 'type' => 'checkbox',
+ ],
+ ]];
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('type')) && !is_null($this->getInput('q'))) {
+ return 'PornHub ' . $this->getInput('type') . ':' . $this->getInput('q');
+ }
+
+ return parent::getName();
+ }
+
+ public function collectData()
+ {
+ $uri = 'https://www.pornhub.com/' . $this->getInput('type') . '/';
+ switch ($this->getInput('type')) { // select proper permalink format per user type...
+ case 'model':
+ $uri .= urlencode($this->getInput('q')) . '/videos' . $this->getInput('sort');
+ break;
+ case 'users':
+ $uri .= urlencode($this->getInput('q')) . '/videos/public' . $this->getInput('sort');
+ break;
+ case 'pornstar':
+ $uri .= urlencode($this->getInput('q')) . '/videos/upload' . $this->getInput('sort');
+ break;
+ }
+
+ $show_images = $this->getInput('show_images');
+
+ $html = getSimpleHTMLDOM($uri);
+
+ foreach ($html->find('div.videoUList ul.videos li.videoblock') as $element) {
+ $item = [];
+
+ $item['author'] = $this->getInput('q');
+
+ // Title
+ $title = $element->find('a', 0)->getAttribute('title');
+ if (is_null($title)) {
+ continue;
+ }
+ $item['title'] = $title;
+
+ // Url
+ $url = $element->find('a', 0)->href;
+ $item['uri'] = 'https://www.pornhub.com' . $url;
+
+ // Content
+ $image = $element->find('img', 0)->getAttribute('data-src');
+ if ($show_images === true) {
+ $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $image . '"></a>';
+ }
+
+ // date hack, guess upload YYYYMMDD from thumbnail URL (format: https://ci.phncdn.com/videos/201907/25/--- )
+ $uploaded = explode('/', $image);
+ $uploaded = strtotime($uploaded[4] . $uploaded[5]);
+ $item['timestamp'] = $uploaded;
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/PresidenciaPTBridge.php b/bridges/PresidenciaPTBridge.php
index e7b016ea..a0baa57f 100644
--- a/bridges/PresidenciaPTBridge.php
+++ b/bridges/PresidenciaPTBridge.php
@@ -1,77 +1,86 @@
<?php
-class PresidenciaPTBridge extends BridgeAbstract {
- const NAME = 'Presidência da República Portuguesa';
- const URI = 'https://www.presidencia.pt';
- const DESCRIPTION = 'Presidência da República Portuguesa';
- const MAINTAINER = 'somini';
- const PARAMETERS = array(
- 'Section' => array(
- '/atualidade/noticias' => array(
- 'name' => 'Notícias',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked',
- ),
- '/atualidade/mensagens' => array(
- 'name' => 'Mensagens',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked',
- ),
- '/atualidade/atividade-legislativa' => array(
- 'name' => 'Atividade Legislativa',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked',
- ),
- '/atualidade/notas-informativas' => array(
- 'name' => 'Notas Informativas',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked',
- )
- )
- );
- const PT_MONTH_NAMES = array(
- 'janeiro',
- 'fevereiro',
- 'março',
- 'abril',
- 'maio',
- 'junho',
- 'julho',
- 'agosto',
- 'setembro',
- 'outubro',
- 'novembro',
- 'dezembro');
+class PresidenciaPTBridge extends BridgeAbstract
+{
+ const NAME = 'Presidência da República Portuguesa';
+ const URI = 'https://www.presidencia.pt';
+ const DESCRIPTION = 'Presidência da República Portuguesa';
+ const MAINTAINER = 'somini';
+ const PARAMETERS = [
+ 'Section' => [
+ '/atualidade/noticias' => [
+ 'name' => 'Notícias',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked',
+ ],
+ '/atualidade/mensagens' => [
+ 'name' => 'Mensagens',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked',
+ ],
+ '/atualidade/atividade-legislativa' => [
+ 'name' => 'Atividade Legislativa',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked',
+ ],
+ '/atualidade/notas-informativas' => [
+ 'name' => 'Notas Informativas',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked',
+ ]
+ ]
+ ];
- public function getIcon(){
- return 'https://www.presidencia.pt/Theme/favicon/apple-touch-icon.png';
- }
+ const PT_MONTH_NAMES = [
+ 'janeiro',
+ 'fevereiro',
+ 'março',
+ 'abril',
+ 'maio',
+ 'junho',
+ 'julho',
+ 'agosto',
+ 'setembro',
+ 'outubro',
+ 'novembro',
+ 'dezembro'];
- public function collectData() {
- foreach(array_keys($this->getParameters()['Section']) as $k) {
- Debug::log('Key: ' . var_export($k, true));
- if($this->getInput($k)) {
- $html = getSimpleHTMLDOMCached($this->getURI() . $k);
+ public function getIcon()
+ {
+ return 'https://www.presidencia.pt/Theme/favicon/apple-touch-icon.png';
+ }
- foreach($html->find('#atualidade-list article.card-block') as $element) {
- $item = array();
+ public function collectData()
+ {
+ foreach (array_keys($this->getParameters()['Section']) as $k) {
+ Debug::log('Key: ' . var_export($k, true));
+ if ($this->getInput($k)) {
+ $html = getSimpleHTMLDOMCached($this->getURI() . $k);
- $link = $element->find('a', 0);
- $etitle = $element->find('.content-box h2', 0);
- $edts = $element->find('p', 1);
- $edt = html_entity_decode($edts->innertext, ENT_HTML5);
+ foreach ($html->find('#atualidade-list article.card-block') as $element) {
+ $item = [];
- $item['title'] = strip_tags($etitle->innertext);
- $item['uri'] = self::URI . $link->href;
- $item['description'] = $element;
- $item['timestamp'] = str_ireplace(
- array_map(function($name) { return ' de ' . $name . ' de '; }, self::PT_MONTH_NAMES),
- array_map(function($num) { return sprintf('-%02d-', $num); }, range(1, sizeof(self::PT_MONTH_NAMES))),
- $edt);
+ $link = $element->find('a', 0);
+ $etitle = $element->find('.content-box h2', 0);
+ $edts = $element->find('p', 1);
+ $edt = html_entity_decode($edts->innertext, ENT_HTML5);
- $this->items[] = $item;
- }
- }
- }
- }
+ $item['title'] = strip_tags($etitle->innertext);
+ $item['uri'] = self::URI . $link->href;
+ $item['description'] = $element;
+ $item['timestamp'] = str_ireplace(
+ array_map(function ($name) {
+ return ' de ' . $name . ' de ';
+ }, self::PT_MONTH_NAMES),
+ array_map(function ($num) {
+ return sprintf('-%02d-', $num);
+ }, range(1, sizeof(self::PT_MONTH_NAMES))),
+ $edt
+ );
+
+ $this->items[] = $item;
+ }
+ }
+ }
+ }
}
diff --git a/bridges/RaceDepartmentBridge.php b/bridges/RaceDepartmentBridge.php
index b8b6e6fb..c33ee67a 100644
--- a/bridges/RaceDepartmentBridge.php
+++ b/bridges/RaceDepartmentBridge.php
@@ -1,41 +1,44 @@
<?php
-class RaceDepartmentBridge extends FeedExpander {
- const NAME = 'RaceDepartment News';
- const URI = 'https://racedepartment.com/';
- const DESCRIPTION = 'Get the latest (sim)racing news from RaceDepartment.';
- const MAINTAINER = 't0stiman';
- public function collectData() {
- $this->collectExpandableDatas('https://www.racedepartment.com/ams/index.rss', 10);
- }
-
- protected function parseItem($feedItem) {
- $item = parent::parseRss2Item($feedItem);
-
- //fetch page
- $articlePage = getSimpleHTMLDOMCached($feedItem->link);
-
- $coverImage = $articlePage->find('img.js-articleCoverImage', 0);
- #relative url -> absolute url
- $coverImage = str_replace('src="/', 'src="' . $this->getURI() . '/', $coverImage);
- $article = $articlePage->find('article.articleBody-main > div.bbWrapper', 0);
- $item['content'] = str_get_html($coverImage . $article);
-
- //convert iframes to links. meant for embedded videos.
- foreach($item['content']->find('iframe') as $found) {
-
- $iframeUrl = $found->getAttribute('src');
-
- if ($iframeUrl) {
- $found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>';
- }
- }
-
- $item['categories'] = array();
- foreach($articlePage->find('a.tagItem') as $tag) {
- array_push($item['categories'], $tag->innertext);
- }
-
- return $item;
- }
+class RaceDepartmentBridge extends FeedExpander
+{
+ const NAME = 'RaceDepartment News';
+ const URI = 'https://racedepartment.com/';
+ const DESCRIPTION = 'Get the latest (sim)racing news from RaceDepartment.';
+ const MAINTAINER = 't0stiman';
+
+ public function collectData()
+ {
+ $this->collectExpandableDatas('https://www.racedepartment.com/ams/index.rss', 10);
+ }
+
+ protected function parseItem($feedItem)
+ {
+ $item = parent::parseRss2Item($feedItem);
+
+ //fetch page
+ $articlePage = getSimpleHTMLDOMCached($feedItem->link);
+
+ $coverImage = $articlePage->find('img.js-articleCoverImage', 0);
+ #relative url -> absolute url
+ $coverImage = str_replace('src="/', 'src="' . $this->getURI() . '/', $coverImage);
+ $article = $articlePage->find('article.articleBody-main > div.bbWrapper', 0);
+ $item['content'] = str_get_html($coverImage . $article);
+
+ //convert iframes to links. meant for embedded videos.
+ foreach ($item['content']->find('iframe') as $found) {
+ $iframeUrl = $found->getAttribute('src');
+
+ if ($iframeUrl) {
+ $found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>';
+ }
+ }
+
+ $item['categories'] = [];
+ foreach ($articlePage->find('a.tagItem') as $tag) {
+ array_push($item['categories'], $tag->innertext);
+ }
+
+ return $item;
+ }
}
diff --git a/bridges/RadioMelodieBridge.php b/bridges/RadioMelodieBridge.php
index 6b392394..a402fe45 100644
--- a/bridges/RadioMelodieBridge.php
+++ b/bridges/RadioMelodieBridge.php
@@ -1,196 +1,197 @@
<?php
-class RadioMelodieBridge extends BridgeAbstract {
- const NAME = 'Radio Melodie Actu';
- const URI = 'https://www.radiomelodie.com';
- const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie';
- const MAINTAINER = 'sysadminstory';
-
- public function getIcon() {
- return self::URI . '/img/favicon.png';
- }
-
- public function collectData(){
- $html = getSimpleHTMLDOM(self::URI . '/actu/');
- $list = $html->find('div[class=listArticles]', 0)->children();
-
- foreach($list as $element) {
- if($element->tag == 'a') {
- $articleURL = self::URI . $element->href;
- $article = getSimpleHTMLDOM($articleURL);
- $this->rewriteAudioPlayers($article);
- // Reload the modified content
- $article = str_get_html($article->save());
- $textDOM = $article->find('article', 0);
-
- // Initialise arrays
- $item = array();
- $audio = array();
- $picture = array();
-
- // Get the Main picture URL
- $picture[] = self::URI . $article->find('figure[class=photoviewer]', 0)->find('img', 0)->src;
- $audioHTML = $article->find('audio');
-
- // Add the audio element to the enclosure
- foreach($audioHTML as $audioElement) {
- $audioURL = $audioElement->src;
- $audio[] = $audioURL;
- }
-
- // Rewrite pictures URL
- $imgs = $textDOM->find('img[src^="http://www.radiomelodie.com/image.php]');
- foreach($imgs as $img) {
- $img->src = $this->rewriteImage($img->src);
- $article->save();
- }
-
- // Remove Google Ads
- $ads = $article->find('div[class=adInline]');
- foreach($ads as $ad) {
- $ad->outertext = '';
- $article->save();
- }
-
- // Extract the author
- $author = $article->find('div[class=author]', 0)->children(1)->children(0)->plaintext;
-
- // Handle date to timestamp
- $dateHTML = $article->find('div[class=author]', 0)->children(1)->plaintext;
-
- preg_match('/([a-z]{4,10}[ ]{1,2}[0-9]{1,2} [\p{L}]{3,10} [0-9]{4} à [0-9]{2}:[0-9]{2})/mus', $dateHTML, $matches);
- $dateText = $matches[1];
-
- $timestamp = $this->parseDate($dateText);
-
- $item['enclosures'] = array_merge($picture, $audio);
- $item['author'] = $author;
- $item['uri'] = $articleURL;
- $item['title'] = $article->find('meta[property=og:title]', 0)->content;
- if($timestamp !== false) {
- $item['timestamp'] = $timestamp;
- }
-
- // Remove the share article part
- $textDOM->find('div[class=share]', 0)->outertext = '';
-
- // Rewrite relative Links
- $textDOM = defaultLinkTo($textDOM, self::URI . '/');
-
- $article->save();
- $text = $textDOM->innertext;
- $item['content'] = '<h1>' . $item['title'] . '</h1>' . $dateText . '<br/>' . $text;
- $this->items[] = $item;
- }
- }
- }
-
- /*
- * Function to rewrite image URL to use the real Image URL and not the resized one (which is very slow)
- */
- private function rewriteImage($url)
- {
- $parts = explode('?', $url);
- parse_str(html_entity_decode($parts[1]), $params);
- return self::URI . '/' . $params['image'];
-
- }
-
- /*
- * Function to rewrite Audio Players to use the <audio> tag and not the javascript audio player
- */
- private function rewriteAudioPlayers($html)
- {
- // Find all audio Players
- $audioPlayers = $html->find('div[class=audioPlayer]');
-
- foreach($audioPlayers as $audioPlayer) {
- // Get the javascript content below the player
- $js = $audioPlayer->next_sibling();
-
- // Extract the audio file URL
- preg_match('/wavesurfer[0-9]+.load\(\'(.*)\'\)/m', $js->innertext, $urls);
-
- // Create the plain HTML <audio> content to play this audio file
- $content = '<audio style="width: 100%" src="' . $urls[1] . '" controls ></audio>';
-
- // Replace the <script> tag by the <audio> tag
- $js->outertext = $content;
- // Remove the initial Audio Player
- $audioPlayer->outertext = '';
- }
-
- }
-
- /*
- * Function to parse the article date
- */
- private function parseDate($date_fr)
- {
- // French date texts
- $search_fr = array(
- 'janvier',
- 'février',
- 'mars',
- 'avril',
- 'mai',
- 'juin',
- 'juillet',
- 'août',
- 'septembre',
- 'octobre',
- 'novembre',
- 'décembre',
- 'lundi',
- 'mardi',
- 'mercredi',
- 'jeudi',
- 'vendredi',
- 'samedi',
- 'dimanche'
- );
-
- // English replacement date text
- $replace_en = array(
- 'january',
- 'february',
- 'march',
- 'april',
- 'may',
- 'june',
- 'july',
- 'august',
- 'september',
- 'october',
- 'november',
- 'december',
- 'monday',
- 'tuesday',
- 'wednesday',
- 'thursday',
- 'friday',
- 'saturday',
- 'sunday'
- );
-
- $dateFormat = 'l j F Y \à H:i';
-
- // Convert the date from French to English
- $date_en = str_replace($search_fr, $replace_en, $date_fr);
-
- // Parse the date and convert it to an array
- $date_array = date_parse_from_format($dateFormat, $date_en);
-
- // Convert the array to a unix timestamp
- $timestamp = mktime(
- $date_array['hour'],
- $date_array['minute'],
- $date_array['second'],
- $date_array['month'],
- $date_array['day'],
- $date_array['year']
- );
-
- return $timestamp;
-
- }
+
+class RadioMelodieBridge extends BridgeAbstract
+{
+ const NAME = 'Radio Melodie Actu';
+ const URI = 'https://www.radiomelodie.com';
+ const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie';
+ const MAINTAINER = 'sysadminstory';
+
+ public function getIcon()
+ {
+ return self::URI . '/img/favicon.png';
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI . '/actu/');
+ $list = $html->find('div[class=listArticles]', 0)->children();
+
+ foreach ($list as $element) {
+ if ($element->tag == 'a') {
+ $articleURL = self::URI . $element->href;
+ $article = getSimpleHTMLDOM($articleURL);
+ $this->rewriteAudioPlayers($article);
+ // Reload the modified content
+ $article = str_get_html($article->save());
+ $textDOM = $article->find('article', 0);
+
+ // Initialise arrays
+ $item = [];
+ $audio = [];
+ $picture = [];
+
+ // Get the Main picture URL
+ $picture[] = self::URI . $article->find('figure[class=photoviewer]', 0)->find('img', 0)->src;
+ $audioHTML = $article->find('audio');
+
+ // Add the audio element to the enclosure
+ foreach ($audioHTML as $audioElement) {
+ $audioURL = $audioElement->src;
+ $audio[] = $audioURL;
+ }
+
+ // Rewrite pictures URL
+ $imgs = $textDOM->find('img[src^="http://www.radiomelodie.com/image.php]');
+ foreach ($imgs as $img) {
+ $img->src = $this->rewriteImage($img->src);
+ $article->save();
+ }
+
+ // Remove Google Ads
+ $ads = $article->find('div[class=adInline]');
+ foreach ($ads as $ad) {
+ $ad->outertext = '';
+ $article->save();
+ }
+
+ // Extract the author
+ $author = $article->find('div[class=author]', 0)->children(1)->children(0)->plaintext;
+
+ // Handle date to timestamp
+ $dateHTML = $article->find('div[class=author]', 0)->children(1)->plaintext;
+
+ preg_match('/([a-z]{4,10}[ ]{1,2}[0-9]{1,2} [\p{L}]{3,10} [0-9]{4} à [0-9]{2}:[0-9]{2})/mus', $dateHTML, $matches);
+ $dateText = $matches[1];
+
+ $timestamp = $this->parseDate($dateText);
+
+ $item['enclosures'] = array_merge($picture, $audio);
+ $item['author'] = $author;
+ $item['uri'] = $articleURL;
+ $item['title'] = $article->find('meta[property=og:title]', 0)->content;
+ if ($timestamp !== false) {
+ $item['timestamp'] = $timestamp;
+ }
+
+ // Remove the share article part
+ $textDOM->find('div[class=share]', 0)->outertext = '';
+
+ // Rewrite relative Links
+ $textDOM = defaultLinkTo($textDOM, self::URI . '/');
+
+ $article->save();
+ $text = $textDOM->innertext;
+ $item['content'] = '<h1>' . $item['title'] . '</h1>' . $dateText . '<br/>' . $text;
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ /*
+ * Function to rewrite image URL to use the real Image URL and not the resized one (which is very slow)
+ */
+ private function rewriteImage($url)
+ {
+ $parts = explode('?', $url);
+ parse_str(html_entity_decode($parts[1]), $params);
+ return self::URI . '/' . $params['image'];
+ }
+
+ /*
+ * Function to rewrite Audio Players to use the <audio> tag and not the javascript audio player
+ */
+ private function rewriteAudioPlayers($html)
+ {
+ // Find all audio Players
+ $audioPlayers = $html->find('div[class=audioPlayer]');
+
+ foreach ($audioPlayers as $audioPlayer) {
+ // Get the javascript content below the player
+ $js = $audioPlayer->next_sibling();
+
+ // Extract the audio file URL
+ preg_match('/wavesurfer[0-9]+.load\(\'(.*)\'\)/m', $js->innertext, $urls);
+
+ // Create the plain HTML <audio> content to play this audio file
+ $content = '<audio style="width: 100%" src="' . $urls[1] . '" controls ></audio>';
+
+ // Replace the <script> tag by the <audio> tag
+ $js->outertext = $content;
+ // Remove the initial Audio Player
+ $audioPlayer->outertext = '';
+ }
+ }
+
+ /*
+ * Function to parse the article date
+ */
+ private function parseDate($date_fr)
+ {
+ // French date texts
+ $search_fr = [
+ 'janvier',
+ 'février',
+ 'mars',
+ 'avril',
+ 'mai',
+ 'juin',
+ 'juillet',
+ 'août',
+ 'septembre',
+ 'octobre',
+ 'novembre',
+ 'décembre',
+ 'lundi',
+ 'mardi',
+ 'mercredi',
+ 'jeudi',
+ 'vendredi',
+ 'samedi',
+ 'dimanche'
+ ];
+
+ // English replacement date text
+ $replace_en = [
+ 'january',
+ 'february',
+ 'march',
+ 'april',
+ 'may',
+ 'june',
+ 'july',
+ 'august',
+ 'september',
+ 'october',
+ 'november',
+ 'december',
+ 'monday',
+ 'tuesday',
+ 'wednesday',
+ 'thursday',
+ 'friday',
+ 'saturday',
+ 'sunday'
+ ];
+
+ $dateFormat = 'l j F Y \à H:i';
+
+ // Convert the date from French to English
+ $date_en = str_replace($search_fr, $replace_en, $date_fr);
+
+ // Parse the date and convert it to an array
+ $date_array = date_parse_from_format($dateFormat, $date_en);
+
+ // Convert the array to a unix timestamp
+ $timestamp = mktime(
+ $date_array['hour'],
+ $date_array['minute'],
+ $date_array['second'],
+ $date_array['month'],
+ $date_array['day'],
+ $date_array['year']
+ );
+
+ return $timestamp;
+ }
}
diff --git a/bridges/RainbowSixSiegeBridge.php b/bridges/RainbowSixSiegeBridge.php
index 2d0762bd..73e2bdc4 100644
--- a/bridges/RainbowSixSiegeBridge.php
+++ b/bridges/RainbowSixSiegeBridge.php
@@ -1,48 +1,51 @@
<?php
-class RainbowSixSiegeBridge extends BridgeAbstract {
-
- const MAINTAINER = 'corenting';
- const NAME = 'Rainbow Six Siege News';
- const URI = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege/news-updates';
- const CACHE_TIMEOUT = 7200; // 2h
- const DESCRIPTION = 'Latest news about Rainbow Six Siege';
-
- // API key to call Ubisoft API, extracted from the React frontend
- const NIMBUS_API_KEY = '3u0FfSBUaTSew-2NVfAOSYWevVQHWtY9q3VM8Xx9Lto';
-
- public function getIcon() {
- return 'https://static-dm.akamaized.net/siege/prod/favicon.ico';
- }
-
- public function collectData(){
- $dlUrl = 'https://nimbus.ubisoft.com/api/v1/items?categoriesFilter=all';
- $dlUrl = $dlUrl . '&limit=6&mediaFilter=all&skip=0&startIndex=0&tags=BR-rainbow-six%20GA-siege';
- $dlUrl = $dlUrl . '&locale=en-us&fallbackLocale=en-us&environment=master';
- $jsonString = getContents($dlUrl, array(
- 'Authorization: ' . self::NIMBUS_API_KEY
- ));
-
- $json = json_decode($jsonString, true);
- $json = $json['items'];
-
- // Start at index 2 to remove highlighted articles
- for($i = 0; $i < count($json); $i++) {
- $jsonItem = $json[$i];
-
- $uri = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege';
- $uri = $uri . $jsonItem['button']['buttonUrl'];
-
- $thumbnail = '<img src="' . $jsonItem['thumbnail']['url'] . '" alt="Thumbnail">';
- $content = $thumbnail . '<br />' . markdownToHtml($jsonItem['content']);
-
- $item = array();
- $item['uri'] = $uri;
- $item['id'] = $jsonItem['id'];
- $item['title'] = $jsonItem['title'];
- $item['content'] = $content;
- $item['timestamp'] = strtotime($jsonItem['date']);
-
- $this->items[] = $item;
- }
- }
+
+class RainbowSixSiegeBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'corenting';
+ const NAME = 'Rainbow Six Siege News';
+ const URI = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege/news-updates';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Latest news about Rainbow Six Siege';
+
+ // API key to call Ubisoft API, extracted from the React frontend
+ const NIMBUS_API_KEY = '3u0FfSBUaTSew-2NVfAOSYWevVQHWtY9q3VM8Xx9Lto';
+
+ public function getIcon()
+ {
+ return 'https://static-dm.akamaized.net/siege/prod/favicon.ico';
+ }
+
+ public function collectData()
+ {
+ $dlUrl = 'https://nimbus.ubisoft.com/api/v1/items?categoriesFilter=all';
+ $dlUrl = $dlUrl . '&limit=6&mediaFilter=all&skip=0&startIndex=0&tags=BR-rainbow-six%20GA-siege';
+ $dlUrl = $dlUrl . '&locale=en-us&fallbackLocale=en-us&environment=master';
+ $jsonString = getContents($dlUrl, [
+ 'Authorization: ' . self::NIMBUS_API_KEY
+ ]);
+
+ $json = json_decode($jsonString, true);
+ $json = $json['items'];
+
+ // Start at index 2 to remove highlighted articles
+ for ($i = 0; $i < count($json); $i++) {
+ $jsonItem = $json[$i];
+
+ $uri = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege';
+ $uri = $uri . $jsonItem['button']['buttonUrl'];
+
+ $thumbnail = '<img src="' . $jsonItem['thumbnail']['url'] . '" alt="Thumbnail">';
+ $content = $thumbnail . '<br />' . markdownToHtml($jsonItem['content']);
+
+ $item = [];
+ $item['uri'] = $uri;
+ $item['id'] = $jsonItem['id'];
+ $item['title'] = $jsonItem['title'];
+ $item['content'] = $content;
+ $item['timestamp'] = strtotime($jsonItem['date']);
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/RedditBridge.php b/bridges/RedditBridge.php
index 538d810b..1a643283 100644
--- a/bridges/RedditBridge.php
+++ b/bridges/RedditBridge.php
@@ -1,289 +1,290 @@
<?php
-class RedditBridge extends BridgeAbstract {
-
- const MAINTAINER = 'dawidsowa';
- const NAME = 'Reddit Bridge';
- const URI = 'https://www.reddit.com';
- const DESCRIPTION = 'Return hot submissions from Reddit';
-
- const PARAMETERS = array(
- 'global' => array(
- 'score' => array(
- 'name' => 'Minimal score',
- 'required' => false,
- 'type' => 'number',
- 'exampleValue' => 100,
- 'title' => 'Filter out posts with lower score'
- ),
- 'd' => array(
- 'name' => 'Sort By',
- 'type' => 'list',
- 'title' => 'Sort by new, hot, top or relevancy',
- 'values' => array(
- 'Hot' => 'hot',
- 'Relevance' => 'relevance',
- 'New' => 'new',
- 'Top' => 'top'
- ),
- 'defaultValue' => 'Hot'
- ),
- 'search' => array(
- 'name' => 'Keyword search',
- 'required' => false,
- 'exampleValue' => 'cats, dogs',
- 'title' => 'Keyword search, separated by commas'
- )
- ),
- 'single' => array(
- 'r' => array(
- 'name' => 'SubReddit',
- 'required' => true,
- 'exampleValue' => 'selfhosted',
- 'title' => 'SubReddit name'
- )
- ),
- 'multi' => array(
- 'rs' => array(
- 'name' => 'SubReddits',
- 'required' => true,
- 'exampleValue' => 'selfhosted, php',
- 'title' => 'SubReddit names, separated by commas'
- )
- ),
- 'user' => array(
- 'u' => array(
- 'name' => 'User',
- 'required' => true,
- 'exampleValue' => 'shwikibot',
- 'title' => 'User name'
- ),
- 'comments' => array(
- 'type' => 'checkbox',
- 'name' => 'Comments',
- 'title' => 'Whether to return comments',
- 'defaultValue' => false
- )
- )
- );
-
- public function detectParameters($url) {
- $parsed_url = parse_url($url);
-
- if ($parsed_url['host'] != 'www.reddit.com' && $parsed_url['host'] != 'old.reddit.com') return null;
-
- $path = explode('/', $parsed_url['path']);
-
- if ($path[1] == 'r') {
- return array(
- 'r' => $path[2]
- );
- } elseif ($path[1] == 'user') {
- return array(
- 'u' => $path[2]
- );
- } else {
- return null;
- }
- }
-
- public function getIcon() {
- return 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-96x96.png';
- }
-
- public function getName() {
- if ($this->queriedContext == 'single') {
- return 'Reddit r/' . $this->getInput('r');
- } elseif ($this->queriedContext == 'user') {
- return 'Reddit u/' . $this->getInput('u');
- } else {
- return self::NAME;
- }
- }
-
- public function collectData() {
-
- $user = false;
- $comments = false;
- $section = $this->getInput('d');
-
- switch ($this->queriedContext) {
- case 'single':
- $subreddits[] = $this->getInput('r');
- break;
- case 'multi':
- $subreddits = explode(',', $this->getInput('rs'));
- break;
- case 'user':
- $subreddits[] = $this->getInput('u');
- $user = true;
- $comments = $this->getInput('comments');
- break;
- }
-
- if(!($this->getInput('search') === '')) {
- $keywords = $this->getInput('search');
- $keywords = str_replace(array(',', ' '), '%20', $keywords);
- $keywords = $keywords . '%20';
- } else {
- $keywords = '';
- }
-
- foreach ($subreddits as $subreddit) {
- $name = trim($subreddit);
- $values = getContents(self::URI
- . '/search.json?q='
- . $keywords
- . ($user ? 'author%3A' : 'subreddit%3A')
- . $name
- . '&sort='
- . $this->getInput('d')
- . '&include_over_18=on');
- $decodedValues = json_decode($values);
-
- foreach ($decodedValues->data->children as $post) {
- if ($post->kind == 't1' && !$comments) {
- continue;
- }
-
- $data = $post->data;
-
- if ($data->score < $this->getInput('score')) {
- continue;
- }
-
- $item = array();
- $item['author'] = $data->author;
- $item['uid'] = $data->id;
- $item['timestamp'] = $data->created_utc;
- $item['uri'] = $this->encodePermalink($data->permalink);
-
- $item['categories'] = array();
-
- if ($post->kind == 't1') {
- $item['title'] = 'Comment: ' . $data->link_title;
- } else {
- $item['title'] = $data->title;
-
- $item['categories'][] = $data->link_flair_text;
- $item['categories'][] = $data->pinned ? 'Pinned' : null;
- $item['categories'][] = $data->spoiler ? 'Spoiler' : null;
- }
-
- $item['categories'][] = $data->over_18 ? 'NSFW' : null;
- $item['categories'] = array_filter($item['categories']);
-
- if ($post->kind == 't1') {
- // Comment
-
- $item['content']
- = htmlspecialchars_decode($data->body_html);
-
- } elseif ($data->is_self) {
- // Text post
-
- $item['content']
- = htmlspecialchars_decode($data->selftext_html);
-
- } elseif (isset($data->post_hint) ? $data->post_hint == 'link' : false) {
- // Link with preview
-
- if (isset($data->media)) {
- // Reddit embeds content for some sites (e.g. Twitter)
- $embed = htmlspecialchars_decode(
- $data->media->oembed->html
- );
- } else {
- $embed = '';
- }
-
- $item['content'] = $this->template(
- $data->url,
- $data->thumbnail,
- $data->domain
- ) . $embed;
-
- } elseif (isset($data->post_hint) ? $data->post_hint == 'image' : false) {
- // Single image
-
- $item['content'] = $this->link(
- $this->encodePermalink($data->permalink),
- '<img src="' . $data->url . '" />'
- );
-
- } elseif (isset($data->is_gallery) ? $data->is_gallery : false) {
- // Multiple images
-
- $images = array();
- foreach ($data->gallery_data->items as $media) {
- $id = $media->media_id;
- $type = $data->media_metadata->$id->m == 'image/gif' ? 'gif' : 'u';
- $src = $data->media_metadata->$id->s->$type;
- $images[] = '<figure><img src="' . $src . '"/></figure><br>';
- }
-
- $item['content'] = implode('', $images);
-
- } elseif ($data->is_video) {
- // Video
-
- // Higher index -> Higher resolution
- end($data->preview->images[0]->resolutions);
- $index = key($data->preview->images[0]->resolutions);
-
- $item['content'] = $this->template(
- $data->url,
- $data->preview->images[0]->resolutions[$index]->url,
- 'Video'
- );
-
- } elseif (isset($data->media) ? $data->media->type == 'youtube.com' : false) {
- // Youtube link
-
- $item['content'] = $this->template(
- $data->url,
- $data->media->oembed->thumbnail_url,
- 'YouTube');
-
- } elseif (explode('.', $data->domain)[0] == 'self') {
- // Crossposted text post
- // TODO (optionally?) Fetch content of the original post.
-
- $item['content'] = $this->link(
- $this->encodePermalink($data->permalink),
- 'Crossposted from r/'
- . explode('.', $data->domain)[1]
- );
-
- } else {
- // Link WITHOUT preview
-
- $item['content'] = $this->link($data->url, $data->domain);
- }
-
- $this->items[] = $item;
- }
- }
- // Sort the order to put the latest posts first, even for mixed subreddits
- usort($this->items, function($a, $b) {
- return $a['timestamp'] < $b['timestamp'];
- });
- }
-
- private function encodePermalink($link) {
- return self::URI . implode(
- '/',
- array_map('urlencode', explode('/', $link))
- );
- }
-
- private function template($href, $src, $caption) {
- return '<a href="' . $href . '"><figure><figcaption>'
- . $caption . '</figcaption><img src="'
- . $src . '"/></figure></a>';
- }
-
- private function link($href, $text) {
- return '<a href="' . $href . '">' . $text . '</a>';
- }
+class RedditBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'dawidsowa';
+ const NAME = 'Reddit Bridge';
+ const URI = 'https://www.reddit.com';
+ const DESCRIPTION = 'Return hot submissions from Reddit';
+
+ const PARAMETERS = [
+ 'global' => [
+ 'score' => [
+ 'name' => 'Minimal score',
+ 'required' => false,
+ 'type' => 'number',
+ 'exampleValue' => 100,
+ 'title' => 'Filter out posts with lower score'
+ ],
+ 'd' => [
+ 'name' => 'Sort By',
+ 'type' => 'list',
+ 'title' => 'Sort by new, hot, top or relevancy',
+ 'values' => [
+ 'Hot' => 'hot',
+ 'Relevance' => 'relevance',
+ 'New' => 'new',
+ 'Top' => 'top'
+ ],
+ 'defaultValue' => 'Hot'
+ ],
+ 'search' => [
+ 'name' => 'Keyword search',
+ 'required' => false,
+ 'exampleValue' => 'cats, dogs',
+ 'title' => 'Keyword search, separated by commas'
+ ]
+ ],
+ 'single' => [
+ 'r' => [
+ 'name' => 'SubReddit',
+ 'required' => true,
+ 'exampleValue' => 'selfhosted',
+ 'title' => 'SubReddit name'
+ ]
+ ],
+ 'multi' => [
+ 'rs' => [
+ 'name' => 'SubReddits',
+ 'required' => true,
+ 'exampleValue' => 'selfhosted, php',
+ 'title' => 'SubReddit names, separated by commas'
+ ]
+ ],
+ 'user' => [
+ 'u' => [
+ 'name' => 'User',
+ 'required' => true,
+ 'exampleValue' => 'shwikibot',
+ 'title' => 'User name'
+ ],
+ 'comments' => [
+ 'type' => 'checkbox',
+ 'name' => 'Comments',
+ 'title' => 'Whether to return comments',
+ 'defaultValue' => false
+ ]
+ ]
+ ];
+
+ public function detectParameters($url)
+ {
+ $parsed_url = parse_url($url);
+
+ if ($parsed_url['host'] != 'www.reddit.com' && $parsed_url['host'] != 'old.reddit.com') {
+ return null;
+ }
+
+ $path = explode('/', $parsed_url['path']);
+
+ if ($path[1] == 'r') {
+ return [
+ 'r' => $path[2]
+ ];
+ } elseif ($path[1] == 'user') {
+ return [
+ 'u' => $path[2]
+ ];
+ } else {
+ return null;
+ }
+ }
+
+ public function getIcon()
+ {
+ return 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-96x96.png';
+ }
+
+ public function getName()
+ {
+ if ($this->queriedContext == 'single') {
+ return 'Reddit r/' . $this->getInput('r');
+ } elseif ($this->queriedContext == 'user') {
+ return 'Reddit u/' . $this->getInput('u');
+ } else {
+ return self::NAME;
+ }
+ }
+
+ public function collectData()
+ {
+ $user = false;
+ $comments = false;
+ $section = $this->getInput('d');
+
+ switch ($this->queriedContext) {
+ case 'single':
+ $subreddits[] = $this->getInput('r');
+ break;
+ case 'multi':
+ $subreddits = explode(',', $this->getInput('rs'));
+ break;
+ case 'user':
+ $subreddits[] = $this->getInput('u');
+ $user = true;
+ $comments = $this->getInput('comments');
+ break;
+ }
+
+ if (!($this->getInput('search') === '')) {
+ $keywords = $this->getInput('search');
+ $keywords = str_replace([',', ' '], '%20', $keywords);
+ $keywords = $keywords . '%20';
+ } else {
+ $keywords = '';
+ }
+
+ foreach ($subreddits as $subreddit) {
+ $name = trim($subreddit);
+ $values = getContents(self::URI
+ . '/search.json?q='
+ . $keywords
+ . ($user ? 'author%3A' : 'subreddit%3A')
+ . $name
+ . '&sort='
+ . $this->getInput('d')
+ . '&include_over_18=on');
+ $decodedValues = json_decode($values);
+
+ foreach ($decodedValues->data->children as $post) {
+ if ($post->kind == 't1' && !$comments) {
+ continue;
+ }
+
+ $data = $post->data;
+
+ if ($data->score < $this->getInput('score')) {
+ continue;
+ }
+
+ $item = [];
+ $item['author'] = $data->author;
+ $item['uid'] = $data->id;
+ $item['timestamp'] = $data->created_utc;
+ $item['uri'] = $this->encodePermalink($data->permalink);
+
+ $item['categories'] = [];
+
+ if ($post->kind == 't1') {
+ $item['title'] = 'Comment: ' . $data->link_title;
+ } else {
+ $item['title'] = $data->title;
+
+ $item['categories'][] = $data->link_flair_text;
+ $item['categories'][] = $data->pinned ? 'Pinned' : null;
+ $item['categories'][] = $data->spoiler ? 'Spoiler' : null;
+ }
+
+ $item['categories'][] = $data->over_18 ? 'NSFW' : null;
+ $item['categories'] = array_filter($item['categories']);
+
+ if ($post->kind == 't1') {
+ // Comment
+
+ $item['content']
+ = htmlspecialchars_decode($data->body_html);
+ } elseif ($data->is_self) {
+ // Text post
+
+ $item['content']
+ = htmlspecialchars_decode($data->selftext_html);
+ } elseif (isset($data->post_hint) ? $data->post_hint == 'link' : false) {
+ // Link with preview
+
+ if (isset($data->media)) {
+ // Reddit embeds content for some sites (e.g. Twitter)
+ $embed = htmlspecialchars_decode(
+ $data->media->oembed->html
+ );
+ } else {
+ $embed = '';
+ }
+
+ $item['content'] = $this->template(
+ $data->url,
+ $data->thumbnail,
+ $data->domain
+ ) . $embed;
+ } elseif (isset($data->post_hint) ? $data->post_hint == 'image' : false) {
+ // Single image
+
+ $item['content'] = $this->link(
+ $this->encodePermalink($data->permalink),
+ '<img src="' . $data->url . '" />'
+ );
+ } elseif (isset($data->is_gallery) ? $data->is_gallery : false) {
+ // Multiple images
+
+ $images = [];
+ foreach ($data->gallery_data->items as $media) {
+ $id = $media->media_id;
+ $type = $data->media_metadata->$id->m == 'image/gif' ? 'gif' : 'u';
+ $src = $data->media_metadata->$id->s->$type;
+ $images[] = '<figure><img src="' . $src . '"/></figure><br>';
+ }
+
+ $item['content'] = implode('', $images);
+ } elseif ($data->is_video) {
+ // Video
+
+ // Higher index -> Higher resolution
+ end($data->preview->images[0]->resolutions);
+ $index = key($data->preview->images[0]->resolutions);
+
+ $item['content'] = $this->template(
+ $data->url,
+ $data->preview->images[0]->resolutions[$index]->url,
+ 'Video'
+ );
+ } elseif (isset($data->media) ? $data->media->type == 'youtube.com' : false) {
+ // Youtube link
+
+ $item['content'] = $this->template(
+ $data->url,
+ $data->media->oembed->thumbnail_url,
+ 'YouTube'
+ );
+ } elseif (explode('.', $data->domain)[0] == 'self') {
+ // Crossposted text post
+ // TODO (optionally?) Fetch content of the original post.
+
+ $item['content'] = $this->link(
+ $this->encodePermalink($data->permalink),
+ 'Crossposted from r/'
+ . explode('.', $data->domain)[1]
+ );
+ } else {
+ // Link WITHOUT preview
+
+ $item['content'] = $this->link($data->url, $data->domain);
+ }
+
+ $this->items[] = $item;
+ }
+ }
+ // Sort the order to put the latest posts first, even for mixed subreddits
+ usort($this->items, function ($a, $b) {
+ return $a['timestamp'] < $b['timestamp'];
+ });
+ }
+
+ private function encodePermalink($link)
+ {
+ return self::URI . implode(
+ '/',
+ array_map('urlencode', explode('/', $link))
+ );
+ }
+
+ private function template($href, $src, $caption)
+ {
+ return '<a href="' . $href . '"><figure><figcaption>'
+ . $caption . '</figcaption><img src="'
+ . $src . '"/></figure></a>';
+ }
+
+ private function link($href, $text)
+ {
+ return '<a href="' . $href . '">' . $text . '</a>';
+ }
}
diff --git a/bridges/Releases3DSBridge.php b/bridges/Releases3DSBridge.php
index 620340ce..56946a47 100644
--- a/bridges/Releases3DSBridge.php
+++ b/bridges/Releases3DSBridge.php
@@ -1,107 +1,117 @@
<?php
-class Releases3DSBridge extends BridgeAbstract {
- const MAINTAINER = 'ORelio';
- const NAME = '3DS Scene Releases';
- const URI = 'http://www.3dsdb.com/';
- const CACHE_TIMEOUT = 10800; // 3h
- const DESCRIPTION = 'Returns the newest scene releases for Nintendo 3DS.';
+class Releases3DSBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = '3DS Scene Releases';
+ const URI = 'http://www.3dsdb.com/';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the newest scene releases for Nintendo 3DS.';
- public function collectData(){
- $this->collectDataUrl(self::URI . 'xml.php');
- }
+ public function collectData()
+ {
+ $this->collectDataUrl(self::URI . 'xml.php');
+ }
- protected function collectDataUrl($dataUrl){
+ protected function collectDataUrl($dataUrl)
+ {
+ $xml = getContents($dataUrl);
+ $limit = 0;
- $xml = getContents($dataUrl);
- $limit = 0;
+ foreach (array_reverse(explode('<release>', $xml)) as $element) {
+ if ($limit >= 5) {
+ break;
+ }
- foreach(array_reverse(explode('<release>', $xml)) as $element) {
- if($limit >= 5) {
- break;
- }
+ if (strpos($element, '</release>') === false) {
+ continue;
+ }
- if(strpos($element, '</release>') === false) {
- continue;
- }
+ $releasename = extractFromDelimiters($element, '<releasename>', '</releasename>');
+ if (empty($releasename)) {
+ continue;
+ }
- $releasename = extractFromDelimiters($element, '<releasename>', '</releasename>');
- if(empty($releasename)) {
- continue;
- }
+ $id = extractFromDelimiters($element, '<id>', '</id>');
+ $name = extractFromDelimiters($element, '<name>', '</name>');
+ $publisher = extractFromDelimiters($element, '<publisher>', '</publisher>');
+ $region = extractFromDelimiters($element, '<region>', '</region>');
+ $group = extractFromDelimiters($element, '<group>', '</group>');
+ $imagesize = extractFromDelimiters($element, '<imagesize>', '</imagesize>');
+ $serial = extractFromDelimiters($element, '<serial>', '</serial>');
+ $titleid = extractFromDelimiters($element, '<titleid>', '</titleid>');
+ $imgcrc = extractFromDelimiters($element, '<imgcrc>', '</imgcrc>');
+ $filename = extractFromDelimiters($element, '<filename>', '</filename>');
+ $trimmedsize = extractFromDelimiters($element, '<trimmedsize>', '</trimmedsize>');
+ $firmware = extractFromDelimiters($element, '<firmware>', '</firmware>');
+ $type = extractFromDelimiters($element, '<type>', '</type>');
+ $card = extractFromDelimiters($element, '<card>', '</card>');
- $id = extractFromDelimiters($element, '<id>', '</id>');
- $name = extractFromDelimiters($element, '<name>', '</name>');
- $publisher = extractFromDelimiters($element, '<publisher>', '</publisher>');
- $region = extractFromDelimiters($element, '<region>', '</region>');
- $group = extractFromDelimiters($element, '<group>', '</group>');
- $imagesize = extractFromDelimiters($element, '<imagesize>', '</imagesize>');
- $serial = extractFromDelimiters($element, '<serial>', '</serial>');
- $titleid = extractFromDelimiters($element, '<titleid>', '</titleid>');
- $imgcrc = extractFromDelimiters($element, '<imgcrc>', '</imgcrc>');
- $filename = extractFromDelimiters($element, '<filename>', '</filename>');
- $trimmedsize = extractFromDelimiters($element, '<trimmedsize>', '</trimmedsize>');
- $firmware = extractFromDelimiters($element, '<firmware>', '</firmware>');
- $type = extractFromDelimiters($element, '<type>', '</type>');
- $card = extractFromDelimiters($element, '<card>', '</card>');
+ //Main section : Release description from 3DS database
+ $releaseDescription = '<h3>Release Details</h3><b>Release ID: </b>' . $id
+ . '<br /><b>Game Name: </b>' . $name
+ . '<br /><b>Publisher: </b>' . $publisher
+ . '<br /><b>Region: </b>' . $region
+ . '<br /><b>Group: </b>' . $group
+ . '<br /><b>Image size: </b>' . (intval($imagesize) / 8)
+ . 'MB<br /><b>Serial: </b>' . $serial
+ . '<br /><b>Title ID: </b>' . $titleid
+ . '<br /><b>Image CRC: </b>' . $imgcrc
+ . '<br /><b>File Name: </b>' . $filename
+ . '<br /><b>Release Name: </b>' . $releasename
+ . '<br /><b>Trimmed size: </b>' . intval(intval($trimmedsize) / 1048576)
+ . 'MB<br /><b>Firmware: </b>' . $firmware
+ . '<br /><b>Type: </b>' . $this->typeToString($type)
+ . '<br /><b>Card: </b>' . $this->cardToString($card)
+ . '<br />';
- //Main section : Release description from 3DS database
- $releaseDescription = '<h3>Release Details</h3><b>Release ID: </b>' . $id
- . '<br /><b>Game Name: </b>' . $name
- . '<br /><b>Publisher: </b>' . $publisher
- . '<br /><b>Region: </b>' . $region
- . '<br /><b>Group: </b>' . $group
- . '<br /><b>Image size: </b>' . (intval($imagesize) / 8)
- . 'MB<br /><b>Serial: </b>' . $serial
- . '<br /><b>Title ID: </b>' . $titleid
- . '<br /><b>Image CRC: </b>' . $imgcrc
- . '<br /><b>File Name: </b>' . $filename
- . '<br /><b>Release Name: </b>' . $releasename
- . '<br /><b>Trimmed size: </b>' . intval(intval($trimmedsize) / 1048576)
- . 'MB<br /><b>Firmware: </b>' . $firmware
- . '<br /><b>Type: </b>' . $this->typeToString($type)
- . '<br /><b>Card: </b>' . $this->cardToString($card)
- . '<br />';
+ //Build search links section to facilitate release search using search engines
+ $releaseNameEncoded = urlencode(str_replace(' ', '+', $releasename));
+ $searchLinkGoogle = 'https://google.com/?q=' . $releaseNameEncoded;
+ $searchLinkDuckDuckGo = 'https://duckduckgo.com/?q=' . $releaseNameEncoded;
+ $searchLinkQwant = 'https://lite.qwant.com/?q=' . $releaseNameEncoded . '&t=web';
+ $releaseSearchLinks = '<h3>Search this release</h3><ul><li><a href="'
+ . $searchLinkGoogle
+ . '">Search using Google</a></li><li><a href="'
+ . $searchLinkDuckDuckGo
+ . '">Search using DuckDuckGo</a></li><li><a href="'
+ . $searchLinkQwant
+ . '">Search using Qwant</a></li></ul>';
- //Build search links section to facilitate release search using search engines
- $releaseNameEncoded = urlencode(str_replace(' ', '+', $releasename));
- $searchLinkGoogle = 'https://google.com/?q=' . $releaseNameEncoded;
- $searchLinkDuckDuckGo = 'https://duckduckgo.com/?q=' . $releaseNameEncoded;
- $searchLinkQwant = 'https://lite.qwant.com/?q=' . $releaseNameEncoded . '&t=web';
- $releaseSearchLinks = '<h3>Search this release</h3><ul><li><a href="'
- . $searchLinkGoogle
- . '">Search using Google</a></li><li><a href="'
- . $searchLinkDuckDuckGo
- . '">Search using DuckDuckGo</a></li><li><a href="'
- . $searchLinkQwant
- . '">Search using Qwant</a></li></ul>';
+ //Build and add final item with the above three sections
+ $item = [];
+ $item['title'] = $name;
+ $item['author'] = $publisher;
+ $item['timestamp'] = $ignDate;
+ $item['enclosures'] = [$ignCoverArt];
+ $item['uri'] = empty($ignLink) ? $searchLinkDuckDuckGo : $ignLink;
+ $item['content'] = $ignDescription . $releaseDescription . $releaseSearchLinks;
+ $this->items[] = $item;
+ $limit++;
+ }
+ }
- //Build and add final item with the above three sections
- $item = array();
- $item['title'] = $name;
- $item['author'] = $publisher;
- $item['timestamp'] = $ignDate;
- $item['enclosures'] = array($ignCoverArt);
- $item['uri'] = empty($ignLink) ? $searchLinkDuckDuckGo : $ignLink;
- $item['content'] = $ignDescription . $releaseDescription . $releaseSearchLinks;
- $this->items[] = $item;
- $limit++;
- }
- }
+ private function typeToString($type)
+ {
+ switch ($type) {
+ case 1:
+ return 'Card Game';
+ case 4:
+ return 'eShop';
+ default:
+ return '??? (' . $type . ')';
+ }
+ }
- private function typeToString($type){
- switch($type) {
- case 1: return 'Card Game';
- case 4: return 'eShop';
- default: return '??? (' . $type . ')';
- }
- }
-
- private function cardToString($card){
- switch($card) {
- case 1: return 'Regular (CARD1)';
- case 2: return 'NAND (CARD2)';
- default: return '??? (' . $card . ')';
- }
- }
+ private function cardToString($card)
+ {
+ switch ($card) {
+ case 1:
+ return 'Regular (CARD1)';
+ case 2:
+ return 'NAND (CARD2)';
+ default:
+ return '??? (' . $card . ')';
+ }
+ }
}
diff --git a/bridges/ReleasesSwitchBridge.php b/bridges/ReleasesSwitchBridge.php
index 89ca76d5..7544278f 100644
--- a/bridges/ReleasesSwitchBridge.php
+++ b/bridges/ReleasesSwitchBridge.php
@@ -2,16 +2,17 @@
// This bridge depends on Releases3DSBridge
if (!class_exists('Releases3DSBridge')) {
- include('Releases3DSBridge.php');
+ include('Releases3DSBridge.php');
}
-class ReleasesSwitchBridge extends Releases3DSBridge {
+class ReleasesSwitchBridge extends Releases3DSBridge
+{
+ const NAME = 'Switch Scene Releases';
+ const URI = 'http://www.nswdb.com/';
+ const DESCRIPTION = 'Returns the newest scene releases for Nintendo Switch.';
- const NAME = 'Switch Scene Releases';
- const URI = 'http://www.nswdb.com/';
- const DESCRIPTION = 'Returns the newest scene releases for Nintendo Switch.';
-
- public function collectData(){
- $this->collectDataUrl(self::URI . 'xml.php');
- }
+ public function collectData()
+ {
+ $this->collectDataUrl(self::URI . 'xml.php');
+ }
}
diff --git a/bridges/ReporterreBridge.php b/bridges/ReporterreBridge.php
index 3b8e2dbe..c441d876 100644
--- a/bridges/ReporterreBridge.php
+++ b/bridges/ReporterreBridge.php
@@ -1,40 +1,43 @@
<?php
-class ReporterreBridge extends BridgeAbstract {
- const MAINTAINER = 'nyutag';
- const NAME = 'Reporterre Bridge';
- const URI = 'https://www.reporterre.net/';
- const DESCRIPTION = 'Returns the newest articles.';
+class ReporterreBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'nyutag';
+ const NAME = 'Reporterre Bridge';
+ const URI = 'https://www.reporterre.net/';
+ const DESCRIPTION = 'Returns the newest articles.';
- private function extractContent($url){
- $html2 = getSimpleHTMLDOM($url);
- $html2 = defaultLinkTo($html2, self::URI);
+ private function extractContent($url)
+ {
+ $html2 = getSimpleHTMLDOM($url);
+ $html2 = defaultLinkTo($html2, self::URI);
- foreach($html2->find('div[style=text-align:justify]') as $e) {
- $text = $e->outertext;
- }
+ foreach ($html2->find('div[style=text-align:justify]') as $e) {
+ $text = $e->outertext;
+ }
- $html2->clear();
- unset($html2);
+ $html2->clear();
+ unset($html2);
- $text = strip_tags($text, '<p><br><a><img>');
- return $text;
- }
+ $text = strip_tags($text, '<p><br><a><img>');
+ return $text;
+ }
- public function collectData(){
- $html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend');
- $limit = 0;
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend');
+ $limit = 0;
- foreach($html->find('item') as $element) {
- if($limit < 5) {
- $item = array();
- $item['title'] = html_entity_decode($element->find('title', 0)->plaintext);
- $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);
- $item['uri'] = $element->find('guid', 0)->innertext;
- $item['content'] = html_entity_decode($this->extractContent($item['uri']));
- $this->items[] = $item;
- $limit++;
- }
- }
- }
+ foreach ($html->find('item') as $element) {
+ if ($limit < 5) {
+ $item = [];
+ $item['title'] = html_entity_decode($element->find('title', 0)->plaintext);
+ $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);
+ $item['uri'] = $element->find('guid', 0)->innertext;
+ $item['content'] = html_entity_decode($this->extractContent($item['uri']));
+ $this->items[] = $item;
+ $limit++;
+ }
+ }
+ }
}
diff --git a/bridges/ReutersBridge.php b/bridges/ReutersBridge.php
index 196139b3..853b134b 100644
--- a/bridges/ReutersBridge.php
+++ b/bridges/ReutersBridge.php
@@ -1,473 +1,483 @@
<?php
+
class ReutersBridge extends BridgeAbstract
{
- const MAINTAINER = 'hollowleviathan, spraynard, csisoap';
- const NAME = 'Reuters Bridge';
- const URI = 'https://www.reuters.com';
- const CACHE_TIMEOUT = 1800; // 30min
- const DESCRIPTION = 'Returns news from Reuters';
-
- private $feedName = self::NAME;
- private $useWireAPI = false;
-
- /**
- * Wireitem types allowed in the final story output
- */
- const ALLOWED_WIREITEM_TYPES = array(
- 'story',
- 'headlines'
- );
-
- /**
- * Wireitem template types allowed in the final story output
- */
- const ALLOWED_TEMPLATE_TYPES = array(
- 'story',
- 'headlines'
- );
-
- const PARAMETERS = array(
- array(
- 'feed' => array(
- 'name' => 'News Feed',
- 'type' => 'list',
- 'title' => 'Feeds from Reuters U.S/International edition',
- 'values' => array(
- 'Top News' => 'home/topnews',
- 'Fact Check' => 'chan:abtpk0vm',
- 'Entertainment' => 'chan:8ym8q8dl',
- 'Politics' => 'politics',
- 'Wire' => 'wire',
- 'Breakingviews' => '/breakingviews',
- 'World' => array(
- 'World' => 'world',
- 'Africa' => '/world/africa',
- 'Americas' => '/world/americas',
- 'Asia-Pacific' => '/world/asia-pacific',
- 'China' => 'china',
- 'europe' => '/world/europe',
- 'India' => '/world/india',
- 'Middle East' => '/world/middle-east',
- 'UK' => 'chan:61leiu7j',
- 'USA News' => 'us',
- 'The Great Reboot' => '/world/the-great-reboot',
- 'Reuters Next' => '/world/reuters-next'
- ),
- 'Business' => array(
- 'Business' => 'business',
- 'Aerospace and Defense' => 'aerospace',
- 'Autos Transportation' => '/business/autos-transportation',
- 'Energy' => 'energy',
- 'Finance' => '/business/finance',
- 'Health' => 'chan:8hw7807a',
- 'Media Telecom' => '/business/media-telecom',
- 'Retail Consumer' => '/business/retail-consumer',
- 'Sustainable Business' => '/business/sustainable-business',
- 'Change Suite' => '/business/change-suite',
- 'Future of Health' => '/business/future-of-health',
- 'Future of Money' => '/business/future-of-money',
- 'Take Five' => '/business/take-five',
- 'Reuters Impact' => '/business/reuters-impact',
- ),
- 'Legal' => array(
- 'Legal' => '/legal',
- 'Government' => '/legal/government',
- 'Legal Industry' => '/legal/legalindustry',
- 'Litigation' => '/legal/litigation',
- 'Transactional' => '/legal/transactional',
- ),
- 'Markets' => array(
- 'Markets' => 'markets',
- 'Asian Markets' => '/markets/asia',
- 'Commodities' => '/markets/commodities',
- 'Currencies' => '/markets/currencies',
- 'Deals' => '/markets/deals',
- 'European Markets' => '/markets/europe',
- 'Funds' => '/markets/fund',
- 'Global Market Data' => '/markets/global-market-data',
- 'Rates & Bonds' => '/markets/rates-bonds',
- 'Stocks' => '/markets/stocks',
- 'U.S Markets' => '/markets/us',
- 'Wealth' => '/markets/wealth',
- 'Macro Matters' => '/markets/macromatters',
- ),
- 'Technology' => array(
- 'Technology' => 'tech',
- 'Disrupted' => '/technology/disrupted',
- 'Reuters Momentum' => '/technology/reuters-momentum',
- ),
- 'Sports' => array(
- 'Sports' => 'sports',
- 'Athletics' => '/lifestyle/sports/athletics',
- 'Cricket' => '/lifestyle/sports/cricket',
- 'Cycling' => '/lifestyle/sports/cycling',
- 'Golf' => '/lifestyle/sports/golf',
- 'Motor Sports' => '/lifestyle/sports/motor-sports',
- 'Soccer' => '/lifestyle/sports/soccer',
- 'Tennis' => '/lifestyle/sports/tennis',
- ),
- 'Lifestyle' => array(
- 'Lifestyle' => 'life',
- 'Oddly Enough' => '/lifestyle/oddly-enough',
- 'Science' => 'science',
- )
- )
- )
- )
- );
-
- const BACKWARD_COMPATIBILITY = array(
- 'world' => '/world',
- 'china' => '/world/china',
- 'chan:61leiu7j' => '/world/uk',
- 'us' => '/world/us',
- 'business' => '/business',
- 'aerospace' => '/business/aerospace-defense',
- 'energy' => '/business/energy',
- 'environment' => '/business/environment',
- 'chan:8hw7807a' => '/business/healthcare-pharmaceuticals',
- 'markets' => '/markets',
- 'tech' => '/technology',
- 'sports' => '/lifestyle/sports',
- 'life' => '/lifestyle',
- 'science' => '/lifestyle/science',
- 'home/topnews' => '/home',
- );
-
- const OLD_WIRE_SECTION = array(
- 'home/topnews',
- 'chan:abtpk0vm',
- 'chan:8ym8q8dl',
- 'politics',
- 'wire'
- );
-
- /**
- * Performs an HTTP request to the Reuters API and returns decoded JSON
- * in the form of an associative array
- * @param string $feed_uri Full API URL to fetch data
- * @return array
- */
- private function getJson($uri)
- {
- $returned_data = getContents($uri);
- return json_decode($returned_data, true);
- }
-
- /**
- * Takes in data from Reuters Wire API and
- * creates structured data in the form of a list
- * of story information.
- * @param array $data JSON collected from the Reuters Wire API
- */
- private function processData($data)
- {
- /**
- * Gets a list of wire items which are groups of templates
- */
- $reuters_allowed_wireitems = array_filter(
- $data, function ($wireitem) {
- return in_array(
- $wireitem['wireitem_type'],
- self::ALLOWED_WIREITEM_TYPES
- );
- }
- );
-
- /*
- * Gets a list of "Templates", which is data containing a story
- */
- $reuters_wireitem_templates = array_reduce(
- $reuters_allowed_wireitems,
- function (array $carry, array $wireitem) {
- $wireitem_templates = $wireitem['templates'];
- return array_merge(
- $carry,
- array_filter(
- $wireitem_templates, function (
- array $template_data
- ) {
- return in_array(
- $template_data['type'],
- self::ALLOWED_TEMPLATE_TYPES
- );
- }
- )
- );
- },
- array()
- );
-
- return $reuters_wireitem_templates;
- }
-
- private function getSectionEndpoint() {
- $endpoint = $this->getInput('feed');
- if(isset(self::BACKWARD_COMPATIBILITY[$endpoint])) {
- $endpoint = self::BACKWARD_COMPATIBILITY[$endpoint];
- } elseif (in_array($endpoint, self::OLD_WIRE_SECTION)) {
- $this->useWireAPI = true;
- }
- return $endpoint;
- }
-
- /**
- * @param string $endpoint - A endpoint is provided could be article URI or ID.
- * @param string $fetch_type - Provide what kind of fetch do you want? Article or Section.
- * @param boolean $is_article_uid {true|false} - A boolean flag to determined if using UID instead of url to fetch.
- * @return string A completed API URL to fetch data
- */
- private function getAPIURL($endpoint, $fetch_type, $is_article_uid = false) {
- $base_url = self::URI . '/pf/api/v3/content/fetch/';
- $wire_url = 'https://wireapi.reuters.com/v8';
- switch($fetch_type) {
- case 'article':
- if($this->useWireAPI) {
- return $wire_url . $endpoint;
- }
-
- $base_query = array(
- 'website' => 'reuters',
- );
- $query = array();
-
- if ($is_article_uid) {
- $query = array(
- 'id' => $endpoint
- );
- } else {
- $query = array(
- 'website_url' => $endpoint,
- );
- }
-
- $query = array_merge($base_query, $query);
- $json_query = json_encode($query);
- return $base_url . 'article-by-id-or-url-v1?query=' . $json_query;
- break;
- case 'section':
- if($this->useWireAPI) {
- if(strpos($endpoint, 'chan:') !== false) {
- // Now checking whether that feed has unique ID or not.
- $feed_uri = "/feed/rapp/us/wirefeed/$endpoint";
- } else {
- $feed_uri = "/feed/rapp/us/tabbar/feeds/$endpoint";
- }
- return $wire_url . $feed_uri;
- }
- $query = array(
- 'section_id' => $endpoint,
- 'size' => 30,
- 'website' => 'reuters'
- );
-
- if ($endpoint != '/home') {
- $query = array_merge($query, array(
- 'fetch_type' => 'section',
- ));
- }
-
- $json_query = json_encode($query);
- return $base_url . 'articles-by-section-alias-or-id-v1?query=' . $json_query;
- break;
- }
- returnServerError('unsupported endpoint');
- }
-
- private function addStories($title, $content, $timestamp, $author, $url, $category) {
- $item = array();
- $item['categories'] = $category;
- $item['author'] = $author;
- $item['content'] = $content;
- $item['title'] = $title;
- $item['timestamp'] = $timestamp;
- $item['uri'] = $url;
- $this->items[] = $item;
- }
-
- private function getArticle($feed_uri, $is_article_uid = false)
- {
- // This will make another request to API to get full detail of article and author's name.
- $url = $this->getAPIURL($feed_uri, 'article', $is_article_uid);
- $rawData = $this->getJson($url);
-
- if(json_last_error() != JSON_ERROR_NONE) { // Checking whether a valid JSON or not
- return $this->handleRedirectedArticle($url);
- }
-
- $article_content = '';
- $authorlist = '';
- $category = array();
- $image_list = array();
- $published_at = '';
- if($this->useWireAPI) {
- $reuters_wireitems = $rawData['wireitems'];
- $processedData = $this->processData($reuters_wireitems);
-
- $first = reset($processedData);
- $article_content = $first['story']['body_items'];
- $authorlist = $first['story']['authors'];
- $category = array($first['story']['channel']['name']);
- $image_list = $first['story']['images'];
- $published_at = $first['story']['published_at'];
- } else {
- $article_content = $rawData['result']['content_elements'];
- $authorlist = $rawData['result']['authors'];
- $category = array($rawData['result']['taxonomy']['ads_primary_section']['name']);
- $image_list = array();
- if(!empty($rawData['result']['related_content']['galleries'])) {
- $galleries = $rawData['result']['related_content']['galleries'];
- foreach($galleries as $gallery) {
- $image_list = array_merge($image_list, $gallery['content_elements']);
- }
- } else if(!empty($rawData['result']['related_content']['images'])) {
- $image_list = $rawData['result']['related_content']['images'];
- }
- $published_at = $rawData['result']['published_time'];
- }
-
- $content_detail = array(
- 'content' => $this->handleArticleContent($article_content),
- 'author' => $this->handleAuthorName($authorlist),
- 'category' => $category,
- 'images' => $this->handleImage($image_list),
- 'published_at' => $published_at
- );
- return $content_detail;
- }
-
- private function handleRedirectedArticle($url) {
- $html = getSimpleHTMLDOMCached($url, 86400); // Duration 24h
-
- $description = '';
- $author = '';
- $images = '';
- $meta_items = $html->find('meta');
- foreach($meta_items as $meta) {
- switch ($meta->name) {
- case 'description':
- $description = $meta->content;
- break;
- case 'author':
- case 'twitter:creator':
- $author = $meta->content;
- break;
- case 'twitter:image:src':
- case 'twitter:image':
- $url = $meta->content;
- $images = "<img src=$url" . '>';
- break;
- }
- }
-
- return array(
- 'content' => $description,
- 'author' => $author,
- 'category' => '',
- 'images' => $images,
- 'published_at' => '',
- 'status' => 'redirected'
- );
- }
-
- private function handleImage($images) {
- $img_placeholder = '';
-
- foreach($images as $image) { // Add more image to article.
- $image_url = $image['url'];
- $image_caption = $image['caption'];
- $image_alt_text = '';
- if(isset($image['alt_text'])) {
- $image_alt_text = $image['alt_text'];
- } else {
- $image_alt_text = $image_caption;
- }
- $img = "<img src=\"$image_url\" alt=\"$image_alt_text\">";
- $img_caption = "<figcaption style=\"text-align: center;\"><i>$image_caption</i></figcaption>";
- $figure = "<figure>$img \t $img_caption</figure>";
- $img_placeholder = $img_placeholder . $figure;
- }
-
- return $img_placeholder;
- }
-
- private function handleAuthorName($authors) {
- $author = '';
- $counter = 0;
- foreach ($authors as $data) {
- //Formatting author's name.
- $name = $data['name'];
- $counter++;
- if($counter == count($authors)) {
- $author .= $name;
- } else {
- $author .= $name . ', ';
- }
- }
- return $author;
- }
-
- private function handleArticleContent($contents) {
- $description = '';
- foreach ($contents as $content) {
- $data;
- if(isset($content['content'])) {
- $data = $content['content'];
- }
- switch($content['type']) {
- case 'paragraph':
- $description = $description . "<p>$data</p>";
- break;
- case 'heading':
- $description = $description . "<h3>$data</h3>";
- break;
- case 'infographics':
- $description = $description . "<img src=\"$data\">";
- break;
- case 'inline_items':
- $item_list = $content['items'];
- $description = $description . '<p>';
- foreach ($item_list as $item) {
- if($item['type'] == 'text') {
- $description = $description . $item['content'];
- } else {
- $description = $description . $item['symbol'];
- }
- }
- $description = $description . '</p>';
- break;
- case 'p_table':
- $description = $description . $content['content'];
- break;
- case 'upstream_embed':
- $media_type = $content['media_type'];
- $cid = $content['cid'];
- $embed = '';
- switch ($media_type) {
- case 'tweet':
- try {
- $tweet_url = "https://twitter.com/dummyname/statuses/$cid";
- $get_embed_url = 'https://publish.twitter.com/oembed?url='
- . urlencode($tweet_url) .
- '&partner=&hide_thread=false';
- $oembed_json = json_decode(getContents($get_embed_url), true);
- $embed .= $oembed_json['html'];
- } catch (Exception $e) { // In case not found any tweet.
- $embed .= '';
- }
- break;
- case 'instagram':
- $url = "https://instagram.com/p/$cid/media/?size=l";
- $embed .= <<<EOD
+ const MAINTAINER = 'hollowleviathan, spraynard, csisoap';
+ const NAME = 'Reuters Bridge';
+ const URI = 'https://www.reuters.com';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns news from Reuters';
+
+ private $feedName = self::NAME;
+ private $useWireAPI = false;
+
+ /**
+ * Wireitem types allowed in the final story output
+ */
+ const ALLOWED_WIREITEM_TYPES = [
+ 'story',
+ 'headlines'
+ ];
+
+ /**
+ * Wireitem template types allowed in the final story output
+ */
+ const ALLOWED_TEMPLATE_TYPES = [
+ 'story',
+ 'headlines'
+ ];
+
+ const PARAMETERS = [
+ [
+ 'feed' => [
+ 'name' => 'News Feed',
+ 'type' => 'list',
+ 'title' => 'Feeds from Reuters U.S/International edition',
+ 'values' => [
+ 'Top News' => 'home/topnews',
+ 'Fact Check' => 'chan:abtpk0vm',
+ 'Entertainment' => 'chan:8ym8q8dl',
+ 'Politics' => 'politics',
+ 'Wire' => 'wire',
+ 'Breakingviews' => '/breakingviews',
+ 'World' => [
+ 'World' => 'world',
+ 'Africa' => '/world/africa',
+ 'Americas' => '/world/americas',
+ 'Asia-Pacific' => '/world/asia-pacific',
+ 'China' => 'china',
+ 'europe' => '/world/europe',
+ 'India' => '/world/india',
+ 'Middle East' => '/world/middle-east',
+ 'UK' => 'chan:61leiu7j',
+ 'USA News' => 'us',
+ 'The Great Reboot' => '/world/the-great-reboot',
+ 'Reuters Next' => '/world/reuters-next'
+ ],
+ 'Business' => [
+ 'Business' => 'business',
+ 'Aerospace and Defense' => 'aerospace',
+ 'Autos Transportation' => '/business/autos-transportation',
+ 'Energy' => 'energy',
+ 'Finance' => '/business/finance',
+ 'Health' => 'chan:8hw7807a',
+ 'Media Telecom' => '/business/media-telecom',
+ 'Retail Consumer' => '/business/retail-consumer',
+ 'Sustainable Business' => '/business/sustainable-business',
+ 'Change Suite' => '/business/change-suite',
+ 'Future of Health' => '/business/future-of-health',
+ 'Future of Money' => '/business/future-of-money',
+ 'Take Five' => '/business/take-five',
+ 'Reuters Impact' => '/business/reuters-impact',
+ ],
+ 'Legal' => [
+ 'Legal' => '/legal',
+ 'Government' => '/legal/government',
+ 'Legal Industry' => '/legal/legalindustry',
+ 'Litigation' => '/legal/litigation',
+ 'Transactional' => '/legal/transactional',
+ ],
+ 'Markets' => [
+ 'Markets' => 'markets',
+ 'Asian Markets' => '/markets/asia',
+ 'Commodities' => '/markets/commodities',
+ 'Currencies' => '/markets/currencies',
+ 'Deals' => '/markets/deals',
+ 'European Markets' => '/markets/europe',
+ 'Funds' => '/markets/fund',
+ 'Global Market Data' => '/markets/global-market-data',
+ 'Rates & Bonds' => '/markets/rates-bonds',
+ 'Stocks' => '/markets/stocks',
+ 'U.S Markets' => '/markets/us',
+ 'Wealth' => '/markets/wealth',
+ 'Macro Matters' => '/markets/macromatters',
+ ],
+ 'Technology' => [
+ 'Technology' => 'tech',
+ 'Disrupted' => '/technology/disrupted',
+ 'Reuters Momentum' => '/technology/reuters-momentum',
+ ],
+ 'Sports' => [
+ 'Sports' => 'sports',
+ 'Athletics' => '/lifestyle/sports/athletics',
+ 'Cricket' => '/lifestyle/sports/cricket',
+ 'Cycling' => '/lifestyle/sports/cycling',
+ 'Golf' => '/lifestyle/sports/golf',
+ 'Motor Sports' => '/lifestyle/sports/motor-sports',
+ 'Soccer' => '/lifestyle/sports/soccer',
+ 'Tennis' => '/lifestyle/sports/tennis',
+ ],
+ 'Lifestyle' => [
+ 'Lifestyle' => 'life',
+ 'Oddly Enough' => '/lifestyle/oddly-enough',
+ 'Science' => 'science',
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ const BACKWARD_COMPATIBILITY = [
+ 'world' => '/world',
+ 'china' => '/world/china',
+ 'chan:61leiu7j' => '/world/uk',
+ 'us' => '/world/us',
+ 'business' => '/business',
+ 'aerospace' => '/business/aerospace-defense',
+ 'energy' => '/business/energy',
+ 'environment' => '/business/environment',
+ 'chan:8hw7807a' => '/business/healthcare-pharmaceuticals',
+ 'markets' => '/markets',
+ 'tech' => '/technology',
+ 'sports' => '/lifestyle/sports',
+ 'life' => '/lifestyle',
+ 'science' => '/lifestyle/science',
+ 'home/topnews' => '/home',
+ ];
+
+ const OLD_WIRE_SECTION = [
+ 'home/topnews',
+ 'chan:abtpk0vm',
+ 'chan:8ym8q8dl',
+ 'politics',
+ 'wire'
+ ];
+
+ /**
+ * Performs an HTTP request to the Reuters API and returns decoded JSON
+ * in the form of an associative array
+ * @param string $feed_uri Full API URL to fetch data
+ * @return array
+ */
+ private function getJson($uri)
+ {
+ $returned_data = getContents($uri);
+ return json_decode($returned_data, true);
+ }
+
+ /**
+ * Takes in data from Reuters Wire API and
+ * creates structured data in the form of a list
+ * of story information.
+ * @param array $data JSON collected from the Reuters Wire API
+ */
+ private function processData($data)
+ {
+ /**
+ * Gets a list of wire items which are groups of templates
+ */
+ $reuters_allowed_wireitems = array_filter(
+ $data,
+ function ($wireitem) {
+ return in_array(
+ $wireitem['wireitem_type'],
+ self::ALLOWED_WIREITEM_TYPES
+ );
+ }
+ );
+
+ /*
+ * Gets a list of "Templates", which is data containing a story
+ */
+ $reuters_wireitem_templates = array_reduce(
+ $reuters_allowed_wireitems,
+ function (array $carry, array $wireitem) {
+ $wireitem_templates = $wireitem['templates'];
+ return array_merge(
+ $carry,
+ array_filter(
+ $wireitem_templates,
+ function (
+ array $template_data
+ ) {
+ return in_array(
+ $template_data['type'],
+ self::ALLOWED_TEMPLATE_TYPES
+ );
+ }
+ )
+ );
+ },
+ []
+ );
+
+ return $reuters_wireitem_templates;
+ }
+
+ private function getSectionEndpoint()
+ {
+ $endpoint = $this->getInput('feed');
+ if (isset(self::BACKWARD_COMPATIBILITY[$endpoint])) {
+ $endpoint = self::BACKWARD_COMPATIBILITY[$endpoint];
+ } elseif (in_array($endpoint, self::OLD_WIRE_SECTION)) {
+ $this->useWireAPI = true;
+ }
+ return $endpoint;
+ }
+
+ /**
+ * @param string $endpoint - A endpoint is provided could be article URI or ID.
+ * @param string $fetch_type - Provide what kind of fetch do you want? Article or Section.
+ * @param boolean $is_article_uid {true|false} - A boolean flag to determined if using UID instead of url to fetch.
+ * @return string A completed API URL to fetch data
+ */
+ private function getAPIURL($endpoint, $fetch_type, $is_article_uid = false)
+ {
+ $base_url = self::URI . '/pf/api/v3/content/fetch/';
+ $wire_url = 'https://wireapi.reuters.com/v8';
+ switch ($fetch_type) {
+ case 'article':
+ if ($this->useWireAPI) {
+ return $wire_url . $endpoint;
+ }
+
+ $base_query = [
+ 'website' => 'reuters',
+ ];
+ $query = [];
+
+ if ($is_article_uid) {
+ $query = [
+ 'id' => $endpoint
+ ];
+ } else {
+ $query = [
+ 'website_url' => $endpoint,
+ ];
+ }
+
+ $query = array_merge($base_query, $query);
+ $json_query = json_encode($query);
+ return $base_url . 'article-by-id-or-url-v1?query=' . $json_query;
+ break;
+ case 'section':
+ if ($this->useWireAPI) {
+ if (strpos($endpoint, 'chan:') !== false) {
+ // Now checking whether that feed has unique ID or not.
+ $feed_uri = "/feed/rapp/us/wirefeed/$endpoint";
+ } else {
+ $feed_uri = "/feed/rapp/us/tabbar/feeds/$endpoint";
+ }
+ return $wire_url . $feed_uri;
+ }
+ $query = [
+ 'section_id' => $endpoint,
+ 'size' => 30,
+ 'website' => 'reuters'
+ ];
+
+ if ($endpoint != '/home') {
+ $query = array_merge($query, [
+ 'fetch_type' => 'section',
+ ]);
+ }
+
+ $json_query = json_encode($query);
+ return $base_url . 'articles-by-section-alias-or-id-v1?query=' . $json_query;
+ break;
+ }
+ returnServerError('unsupported endpoint');
+ }
+
+ private function addStories($title, $content, $timestamp, $author, $url, $category)
+ {
+ $item = [];
+ $item['categories'] = $category;
+ $item['author'] = $author;
+ $item['content'] = $content;
+ $item['title'] = $title;
+ $item['timestamp'] = $timestamp;
+ $item['uri'] = $url;
+ $this->items[] = $item;
+ }
+
+ private function getArticle($feed_uri, $is_article_uid = false)
+ {
+ // This will make another request to API to get full detail of article and author's name.
+ $url = $this->getAPIURL($feed_uri, 'article', $is_article_uid);
+ $rawData = $this->getJson($url);
+
+ if (json_last_error() != JSON_ERROR_NONE) { // Checking whether a valid JSON or not
+ return $this->handleRedirectedArticle($url);
+ }
+
+ $article_content = '';
+ $authorlist = '';
+ $category = [];
+ $image_list = [];
+ $published_at = '';
+ if ($this->useWireAPI) {
+ $reuters_wireitems = $rawData['wireitems'];
+ $processedData = $this->processData($reuters_wireitems);
+
+ $first = reset($processedData);
+ $article_content = $first['story']['body_items'];
+ $authorlist = $first['story']['authors'];
+ $category = [$first['story']['channel']['name']];
+ $image_list = $first['story']['images'];
+ $published_at = $first['story']['published_at'];
+ } else {
+ $article_content = $rawData['result']['content_elements'];
+ $authorlist = $rawData['result']['authors'];
+ $category = [$rawData['result']['taxonomy']['ads_primary_section']['name']];
+ $image_list = [];
+ if (!empty($rawData['result']['related_content']['galleries'])) {
+ $galleries = $rawData['result']['related_content']['galleries'];
+ foreach ($galleries as $gallery) {
+ $image_list = array_merge($image_list, $gallery['content_elements']);
+ }
+ } elseif (!empty($rawData['result']['related_content']['images'])) {
+ $image_list = $rawData['result']['related_content']['images'];
+ }
+ $published_at = $rawData['result']['published_time'];
+ }
+
+ $content_detail = [
+ 'content' => $this->handleArticleContent($article_content),
+ 'author' => $this->handleAuthorName($authorlist),
+ 'category' => $category,
+ 'images' => $this->handleImage($image_list),
+ 'published_at' => $published_at
+ ];
+ return $content_detail;
+ }
+
+ private function handleRedirectedArticle($url)
+ {
+ $html = getSimpleHTMLDOMCached($url, 86400); // Duration 24h
+
+ $description = '';
+ $author = '';
+ $images = '';
+ $meta_items = $html->find('meta');
+ foreach ($meta_items as $meta) {
+ switch ($meta->name) {
+ case 'description':
+ $description = $meta->content;
+ break;
+ case 'author':
+ case 'twitter:creator':
+ $author = $meta->content;
+ break;
+ case 'twitter:image:src':
+ case 'twitter:image':
+ $url = $meta->content;
+ $images = "<img src=$url" . '>';
+ break;
+ }
+ }
+
+ return [
+ 'content' => $description,
+ 'author' => $author,
+ 'category' => '',
+ 'images' => $images,
+ 'published_at' => '',
+ 'status' => 'redirected'
+ ];
+ }
+
+ private function handleImage($images)
+ {
+ $img_placeholder = '';
+
+ foreach ($images as $image) { // Add more image to article.
+ $image_url = $image['url'];
+ $image_caption = $image['caption'];
+ $image_alt_text = '';
+ if (isset($image['alt_text'])) {
+ $image_alt_text = $image['alt_text'];
+ } else {
+ $image_alt_text = $image_caption;
+ }
+ $img = "<img src=\"$image_url\" alt=\"$image_alt_text\">";
+ $img_caption = "<figcaption style=\"text-align: center;\"><i>$image_caption</i></figcaption>";
+ $figure = "<figure>$img \t $img_caption</figure>";
+ $img_placeholder = $img_placeholder . $figure;
+ }
+
+ return $img_placeholder;
+ }
+
+ private function handleAuthorName($authors)
+ {
+ $author = '';
+ $counter = 0;
+ foreach ($authors as $data) {
+ //Formatting author's name.
+ $name = $data['name'];
+ $counter++;
+ if ($counter == count($authors)) {
+ $author .= $name;
+ } else {
+ $author .= $name . ', ';
+ }
+ }
+ return $author;
+ }
+
+ private function handleArticleContent($contents)
+ {
+ $description = '';
+ foreach ($contents as $content) {
+ $data;
+ if (isset($content['content'])) {
+ $data = $content['content'];
+ }
+ switch ($content['type']) {
+ case 'paragraph':
+ $description = $description . "<p>$data</p>";
+ break;
+ case 'heading':
+ $description = $description . "<h3>$data</h3>";
+ break;
+ case 'infographics':
+ $description = $description . "<img src=\"$data\">";
+ break;
+ case 'inline_items':
+ $item_list = $content['items'];
+ $description = $description . '<p>';
+ foreach ($item_list as $item) {
+ if ($item['type'] == 'text') {
+ $description = $description . $item['content'];
+ } else {
+ $description = $description . $item['symbol'];
+ }
+ }
+ $description = $description . '</p>';
+ break;
+ case 'p_table':
+ $description = $description . $content['content'];
+ break;
+ case 'upstream_embed':
+ $media_type = $content['media_type'];
+ $cid = $content['cid'];
+ $embed = '';
+ switch ($media_type) {
+ case 'tweet':
+ try {
+ $tweet_url = "https://twitter.com/dummyname/statuses/$cid";
+ $get_embed_url = 'https://publish.twitter.com/oembed?url='
+ . urlencode($tweet_url) .
+ '&partner=&hide_thread=false';
+ $oembed_json = json_decode(getContents($get_embed_url), true);
+ $embed .= $oembed_json['html'];
+ } catch (Exception $e) { // In case not found any tweet.
+ $embed .= '';
+ }
+ break;
+ case 'instagram':
+ $url = "https://instagram.com/p/$cid/media/?size=l";
+ $embed .= <<<EOD
<img
src="{$url}"
alt="instagram-image-$cid"
>
EOD;
- break;
- case 'youtube':
- $url = "https://www.youtube.com/embed/$cid";
- $embed .= <<<EOD
+ break;
+ case 'youtube':
+ $url = "https://www.youtube.com/embed/$cid";
+ $embed .= <<<EOD
<‌iframe
width="560"
height="315"
@@ -477,151 +487,152 @@ EOD;
>
</iframe>
EOD;
- break;
- }
- $description .= $embed;
- break;
- case 'social_media':
- if ($content['sub_type'] == 'twitter') {
- $description .= $content['html'];
- }
- break;
- case 'table':
- $table = '<table>';
- $theaders = $content['header'];
- $tr = '<tr>';
- foreach($theaders as $header) {
- $tr .= '<th>' . $header . '</th>';
- }
- $tr .= '</tr>';
- $table .= $tr;
- $rows = $content['rows'];
- foreach($rows as $row) {
- $tr = '<tr>';
- foreach($row as $data) {
- $tr .= '<td>' . $data . '</td>';
- }
- $tr .= '</tr>';
- $table .= $tr;
- }
- $table .= '</table>';
- $description .= $table;
- break;
- case 'image':
- $description .= $this->handleImage(array($content));
- }
- }
-
- return $description;
- }
-
- /**
- * @param array $stories
- */
- private function addRelatedStories($stories) {
- foreach($stories as $story) {
- $story_data = $this->getArticle($story['url']);
- $title = $story['caption'];
- $url = self::URI . $story['url'];
- if(isset($story_data['status']) && $story_data['status'] != 'redirected') {
- $article_body = defaultLinkTo($story_data['content'], $this->getURI());
- } else {
- $article_body = $story_data['content'];
- }
- $content = $article_body . $story_data['images'];
- $timestamp = $story_data['published_at'];
- $category = $story_data['category'];
- $author = $story_data['author'];
- $this->addStories($title, $content, $timestamp, $author, $url, $category);
- }
- }
-
- public function getName() {
- return $this->feedName;
- }
-
- public function collectData()
- {
- $endpoint = $this->getSectionEndpoint();
- $url = $this->getAPIURL($endpoint, 'section');
- $data = $this->getJson($url);
-
- $stories = array();
- $section_name = '';
- if($this->useWireAPI) {
- $reuters_wireitems = $data['wireitems'];
- $section_name = $data['wire_name'];
- $processedData = $this->processData($reuters_wireitems);
-
- // Merge all articles from Editor's Highlight section into existing array of templates.
- $top_section = reset($processedData);
- if ($top_section['type'] == 'headlines') {
- $top_section = array_shift($processedData);
- $articles = $top_section['headlines'];
- $processedData = array_merge($articles, $processedData);
- }
- $stories = $processedData;
- } else {
- $section_name = $data['result']['section']['name'];
- if(isset($data['arcResult']['articles'])) {
- $stories = $data['arcResult']['articles'];
- } else {
- $stories = $data['result']['articles'];
- }
- }
- $this->feedName = $section_name . ' | Reuters';
-
- foreach ($stories as $story) {
- $uid = '';
- $author = '';
- $category = array();
- $content = '';
- $title = '';
- $timestamp = '';
- $url = '';
- $article_uri = '';
- $source_type = '';
- if($this->useWireAPI) {
- $uid = $story['story']['usn'];
- $article_uri = $story['template_action']['api_path'];
- $title = $story['story']['hed'];
- $url = $story['template_action']['url'];
- } else {
- $uid = $story['id'];
- $url = self::URI . $story['canonical_url'];
- $title = $story['title'];
- $article_uri = $story['canonical_url'];
- $source_type = $story['source']['name'];
- if (isset($story['related_stories'])) {
- $this->addRelatedStories($story['related_stories']);
- }
- }
-
- // Some article cause unexpected behaviour like redirect to another site not API.
- // Attempt to check article source type to avoid this.
- if(!$this->useWireAPI && $source_type != 'Package') { // Only Reuters PF api have this, Wire don't.
- $author = $this->handleAuthorName($story['authors']);
- $timestamp = $story['published_time'];
- $image_placeholder = '';
- if (isset($story['thumbnail'])) {
- $image_placeholder = $this->handleImage(array($story['thumbnail']));
- }
- $content = $story['description'] . $image_placeholder;
- $category = array($story['primary_section']['name']);
- } else {
- $content_detail = $this->getArticle($article_uri);
- $description = $content_detail['content'];
- $description = defaultLinkTo($description, $this->getURI());
-
- $author = $content_detail['author'];
- $images = $content_detail['images'];
- $category = $content_detail['category'];
- $content = "$description $images";
- $timestamp = $content_detail['published_at'];
- }
-
- $this->addStories($title, $content, $timestamp, $author, $url, $category);
-
- }
- }
+ break;
+ }
+ $description .= $embed;
+ break;
+ case 'social_media':
+ if ($content['sub_type'] == 'twitter') {
+ $description .= $content['html'];
+ }
+ break;
+ case 'table':
+ $table = '<table>';
+ $theaders = $content['header'];
+ $tr = '<tr>';
+ foreach ($theaders as $header) {
+ $tr .= '<th>' . $header . '</th>';
+ }
+ $tr .= '</tr>';
+ $table .= $tr;
+ $rows = $content['rows'];
+ foreach ($rows as $row) {
+ $tr = '<tr>';
+ foreach ($row as $data) {
+ $tr .= '<td>' . $data . '</td>';
+ }
+ $tr .= '</tr>';
+ $table .= $tr;
+ }
+ $table .= '</table>';
+ $description .= $table;
+ break;
+ case 'image':
+ $description .= $this->handleImage([$content]);
+ }
+ }
+
+ return $description;
+ }
+
+ /**
+ * @param array $stories
+ */
+ private function addRelatedStories($stories)
+ {
+ foreach ($stories as $story) {
+ $story_data = $this->getArticle($story['url']);
+ $title = $story['caption'];
+ $url = self::URI . $story['url'];
+ if (isset($story_data['status']) && $story_data['status'] != 'redirected') {
+ $article_body = defaultLinkTo($story_data['content'], $this->getURI());
+ } else {
+ $article_body = $story_data['content'];
+ }
+ $content = $article_body . $story_data['images'];
+ $timestamp = $story_data['published_at'];
+ $category = $story_data['category'];
+ $author = $story_data['author'];
+ $this->addStories($title, $content, $timestamp, $author, $url, $category);
+ }
+ }
+
+ public function getName()
+ {
+ return $this->feedName;
+ }
+
+ public function collectData()
+ {
+ $endpoint = $this->getSectionEndpoint();
+ $url = $this->getAPIURL($endpoint, 'section');
+ $data = $this->getJson($url);
+
+ $stories = [];
+ $section_name = '';
+ if ($this->useWireAPI) {
+ $reuters_wireitems = $data['wireitems'];
+ $section_name = $data['wire_name'];
+ $processedData = $this->processData($reuters_wireitems);
+
+ // Merge all articles from Editor's Highlight section into existing array of templates.
+ $top_section = reset($processedData);
+ if ($top_section['type'] == 'headlines') {
+ $top_section = array_shift($processedData);
+ $articles = $top_section['headlines'];
+ $processedData = array_merge($articles, $processedData);
+ }
+ $stories = $processedData;
+ } else {
+ $section_name = $data['result']['section']['name'];
+ if (isset($data['arcResult']['articles'])) {
+ $stories = $data['arcResult']['articles'];
+ } else {
+ $stories = $data['result']['articles'];
+ }
+ }
+ $this->feedName = $section_name . ' | Reuters';
+
+ foreach ($stories as $story) {
+ $uid = '';
+ $author = '';
+ $category = [];
+ $content = '';
+ $title = '';
+ $timestamp = '';
+ $url = '';
+ $article_uri = '';
+ $source_type = '';
+ if ($this->useWireAPI) {
+ $uid = $story['story']['usn'];
+ $article_uri = $story['template_action']['api_path'];
+ $title = $story['story']['hed'];
+ $url = $story['template_action']['url'];
+ } else {
+ $uid = $story['id'];
+ $url = self::URI . $story['canonical_url'];
+ $title = $story['title'];
+ $article_uri = $story['canonical_url'];
+ $source_type = $story['source']['name'];
+ if (isset($story['related_stories'])) {
+ $this->addRelatedStories($story['related_stories']);
+ }
+ }
+
+ // Some article cause unexpected behaviour like redirect to another site not API.
+ // Attempt to check article source type to avoid this.
+ if (!$this->useWireAPI && $source_type != 'Package') { // Only Reuters PF api have this, Wire don't.
+ $author = $this->handleAuthorName($story['authors']);
+ $timestamp = $story['published_time'];
+ $image_placeholder = '';
+ if (isset($story['thumbnail'])) {
+ $image_placeholder = $this->handleImage([$story['thumbnail']]);
+ }
+ $content = $story['description'] . $image_placeholder;
+ $category = [$story['primary_section']['name']];
+ } else {
+ $content_detail = $this->getArticle($article_uri);
+ $description = $content_detail['content'];
+ $description = defaultLinkTo($description, $this->getURI());
+
+ $author = $content_detail['author'];
+ $images = $content_detail['images'];
+ $category = $content_detail['category'];
+ $content = "$description $images";
+ $timestamp = $content_detail['published_at'];
+ }
+
+ $this->addStories($title, $content, $timestamp, $author, $url, $category);
+ }
+ }
}
diff --git a/bridges/RoadAndTrackBridge.php b/bridges/RoadAndTrackBridge.php
index b81b45c2..d666b6bd 100644
--- a/bridges/RoadAndTrackBridge.php
+++ b/bridges/RoadAndTrackBridge.php
@@ -1,72 +1,71 @@
<?php
-class RoadAndTrackBridge extends BridgeAbstract {
- const MAINTAINER = 'teromene';
- const NAME = 'Road And Track Bridge';
- const URI = 'https://www.roadandtrack.com/';
- const CACHE_TIMEOUT = 86400; // 24h
- const DESCRIPTION = 'Returns the latest news from Road & Track.';
-
- public function collectData() {
-
- $page = getSimpleHTMLDOM(self::URI);
-
- $limit = 5;
-
- foreach($page->find('a.enk2x9t2') as $article) {
- $this->items[] = $this->fetchArticle($article->href);
-
- if (count($this->items) >= $limit) {
- break;
- }
- }
- }
-
- private function fixImages($content) {
-
- $enclosures = array();
- foreach($content->find('img') as $image) {
- $image->src = explode('?', $image->getAttribute('data-src'))[0];
- $enclosures[] = $image->src;
- }
-
- foreach($content->find('.embed-image-wrap, .content-lede-image-wrap') as $imgContainer) {
- $imgContainer->style = '';
- }
-
- return $enclosures;
- }
-
- private function fetchArticle($articleLink) {
-
- $articleLink = self::URI . $articleLink;
- $article = getSimpleHTMLDOM($articleLink);
- $item = array();
-
- $title = $article->find('.content-hed', 0);
- if ($title) {
- $item['title'] = $title->innertext;
- }
-
- $item['author'] = $article->find('.byline-name', 0)->innertext;
- $item['timestamp'] = strtotime($article->find('.content-info-date', 0)->getAttribute('datetime'));
-
- $content = $article->find('.content-container', 0);
- if($content->find('.content-rail', 0) !== null) {
- $content->find('.content-rail', 0)->innertext = '';
- }
-
- $enclosures = $this->fixImages($content);
-
- $item['enclosures'] = $enclosures;
- $item['content'] = $content;
- return $item;
-
- }
-
- private function getArticleContent($article) {
-
- return getContents($article->contentUrl);
-
- }
+class RoadAndTrackBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'teromene';
+ const NAME = 'Road And Track Bridge';
+ const URI = 'https://www.roadandtrack.com/';
+ const CACHE_TIMEOUT = 86400; // 24h
+ const DESCRIPTION = 'Returns the latest news from Road & Track.';
+
+ public function collectData()
+ {
+ $page = getSimpleHTMLDOM(self::URI);
+
+ $limit = 5;
+
+ foreach ($page->find('a.enk2x9t2') as $article) {
+ $this->items[] = $this->fetchArticle($article->href);
+
+ if (count($this->items) >= $limit) {
+ break;
+ }
+ }
+ }
+
+ private function fixImages($content)
+ {
+ $enclosures = [];
+ foreach ($content->find('img') as $image) {
+ $image->src = explode('?', $image->getAttribute('data-src'))[0];
+ $enclosures[] = $image->src;
+ }
+
+ foreach ($content->find('.embed-image-wrap, .content-lede-image-wrap') as $imgContainer) {
+ $imgContainer->style = '';
+ }
+
+ return $enclosures;
+ }
+
+ private function fetchArticle($articleLink)
+ {
+ $articleLink = self::URI . $articleLink;
+ $article = getSimpleHTMLDOM($articleLink);
+ $item = [];
+
+ $title = $article->find('.content-hed', 0);
+ if ($title) {
+ $item['title'] = $title->innertext;
+ }
+
+ $item['author'] = $article->find('.byline-name', 0)->innertext;
+ $item['timestamp'] = strtotime($article->find('.content-info-date', 0)->getAttribute('datetime'));
+
+ $content = $article->find('.content-container', 0);
+ if ($content->find('.content-rail', 0) !== null) {
+ $content->find('.content-rail', 0)->innertext = '';
+ }
+
+ $enclosures = $this->fixImages($content);
+
+ $item['enclosures'] = $enclosures;
+ $item['content'] = $content;
+ return $item;
+ }
+
+ private function getArticleContent($article)
+ {
+ return getContents($article->contentUrl);
+ }
}
diff --git a/bridges/RobinhoodSnacksBridge.php b/bridges/RobinhoodSnacksBridge.php
index 0f2eac83..aecc0265 100644
--- a/bridges/RobinhoodSnacksBridge.php
+++ b/bridges/RobinhoodSnacksBridge.php
@@ -1,113 +1,114 @@
<?php
-class RobinhoodSnacksBridge extends BridgeAbstract {
- const MAINTAINER = 'johnpc';
- const NAME = 'Robinhood Snacks Newsletter';
- const URI = 'https://snacks.robinhood.com/newsletters/';
- const CACHE_TIMEOUT = 86400; // 24h
- const DESCRIPTION = 'Returns newsletters from Robinhood Snacks';
-
- // Work around 403 by pretending to be a legit browser
- const FAKE_HEADERS = array(
- 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0',
- 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
- 'Accept-Language: es-ES,en-US;q=0.7,en;q=0.3',
- 'Accept-Encoding: gzip, deflate, br',
- 'Connection: keep-alive',
- 'Upgrade-Insecure-Requests: 1',
- 'Sec-Fetch-Dest: document',
- 'Sec-Fetch-Mode: navigate',
- 'Sec-Fetch-Site: none',
- 'Sec-Fetch-User: ?1',
- 'Pragma: no-cache',
- 'Cache-Control: no-cache',
- 'TE: trailers'
- );
-
- public function collectData()
- {
- $html = getSimpleHTMLDOM(self::URI, self::FAKE_HEADERS);
- $html = defaultLinkTo($html, $this->getURI());
-
- $elements = $html->find('#__next > div > div > div > div > a');
-
- foreach ($elements as $element) {
- if ($element->href === 'https://snacks.robinhood.com/newsletters/page/2/') {
- continue;
- }
-
- $content = $element->find('div > div', 2);
-
- // Remove element that is not parsed (span with weekly tag)
- $unwanted_selector = 'span';
- foreach($content->find($unwanted_selector) as $found) {
- $found->outertext = '';
- }
-
- $title = $content->find('div', 0)->innertext;
- $timestamp = strtotime($content->find('div', 1)->innertext);
- $uri = $element->href;
-
- $this->items[] = array(
- 'uri' => $uri,
- 'title' => $title,
- 'timestamp' => $timestamp,
- 'content' => self::getArticleContent($uri)
- );
- }
- }
-
- private function getArticleContent($uri)
- {
- $article_html = getSimpleHTMLDOMCached($uri, self::CACHE_TIMEOUT, self::FAKE_HEADERS);
- if(!$article_html) {
- return '';
- }
-
- $content = $article_html->find('#__next > div > div > div > span', 0);
- $content->removeChild($content->find('div', 0));
- $content->removeChild($content->find('h1', 0));
- $content->removeChild($content->find('img', 1));
-
- // Remove elements that are not part of article content
- $unwanted_selector = 'style';
- foreach($content->find($unwanted_selector) as $found) {
- $found->outertext = '';
- }
-
- // Images cleanup
- $already_displayed_pictures = array();
- foreach($content->find('img') as $found) {
- // Skip loader images
- if (str_contains($found->src, 'data:image/gif;base64')) {
- $found->outertext = '';
- continue;
- }
-
- // Skip multiple images with same src
- // and remove duplicated image description
- if (in_array($found->src, $already_displayed_pictures)) {
- $found->parent->parent->parent->outertext = '';
- $found->parent->parent->parent->nextSibling()->nextSibling()->outertext = '';
- continue;
- }
-
- // Remove srcset attribute
- $found->removeAttribute('srcset');
-
- // If relative img, fix path
- if (str_starts_with($found->src, '/_next')) {
- $found->setAttribute('src', 'https://snacks.robinhood.com' . $found->getAttribute('src'));
- }
-
- $already_displayed_pictures[] = $found->src;
- }
-
- $content_text = $content->innertext;
-
- // Remove noscript tag to display images
- $content_text = str_replace('<noscript>', '', $content_text);
-
- return $content_text;
- }
+class RobinhoodSnacksBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'johnpc';
+ const NAME = 'Robinhood Snacks Newsletter';
+ const URI = 'https://snacks.robinhood.com/newsletters/';
+ const CACHE_TIMEOUT = 86400; // 24h
+ const DESCRIPTION = 'Returns newsletters from Robinhood Snacks';
+
+ // Work around 403 by pretending to be a legit browser
+ const FAKE_HEADERS = [
+ 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0',
+ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
+ 'Accept-Language: es-ES,en-US;q=0.7,en;q=0.3',
+ 'Accept-Encoding: gzip, deflate, br',
+ 'Connection: keep-alive',
+ 'Upgrade-Insecure-Requests: 1',
+ 'Sec-Fetch-Dest: document',
+ 'Sec-Fetch-Mode: navigate',
+ 'Sec-Fetch-Site: none',
+ 'Sec-Fetch-User: ?1',
+ 'Pragma: no-cache',
+ 'Cache-Control: no-cache',
+ 'TE: trailers'
+ ];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI, self::FAKE_HEADERS);
+ $html = defaultLinkTo($html, $this->getURI());
+
+ $elements = $html->find('#__next > div > div > div > div > a');
+
+ foreach ($elements as $element) {
+ if ($element->href === 'https://snacks.robinhood.com/newsletters/page/2/') {
+ continue;
+ }
+
+ $content = $element->find('div > div', 2);
+
+ // Remove element that is not parsed (span with weekly tag)
+ $unwanted_selector = 'span';
+ foreach ($content->find($unwanted_selector) as $found) {
+ $found->outertext = '';
+ }
+
+ $title = $content->find('div', 0)->innertext;
+ $timestamp = strtotime($content->find('div', 1)->innertext);
+ $uri = $element->href;
+
+ $this->items[] = [
+ 'uri' => $uri,
+ 'title' => $title,
+ 'timestamp' => $timestamp,
+ 'content' => self::getArticleContent($uri)
+ ];
+ }
+ }
+
+ private function getArticleContent($uri)
+ {
+ $article_html = getSimpleHTMLDOMCached($uri, self::CACHE_TIMEOUT, self::FAKE_HEADERS);
+ if (!$article_html) {
+ return '';
+ }
+
+ $content = $article_html->find('#__next > div > div > div > span', 0);
+ $content->removeChild($content->find('div', 0));
+ $content->removeChild($content->find('h1', 0));
+ $content->removeChild($content->find('img', 1));
+
+ // Remove elements that are not part of article content
+ $unwanted_selector = 'style';
+ foreach ($content->find($unwanted_selector) as $found) {
+ $found->outertext = '';
+ }
+
+ // Images cleanup
+ $already_displayed_pictures = [];
+ foreach ($content->find('img') as $found) {
+ // Skip loader images
+ if (str_contains($found->src, 'data:image/gif;base64')) {
+ $found->outertext = '';
+ continue;
+ }
+
+ // Skip multiple images with same src
+ // and remove duplicated image description
+ if (in_array($found->src, $already_displayed_pictures)) {
+ $found->parent->parent->parent->outertext = '';
+ $found->parent->parent->parent->nextSibling()->nextSibling()->outertext = '';
+ continue;
+ }
+
+ // Remove srcset attribute
+ $found->removeAttribute('srcset');
+
+ // If relative img, fix path
+ if (str_starts_with($found->src, '/_next')) {
+ $found->setAttribute('src', 'https://snacks.robinhood.com' . $found->getAttribute('src'));
+ }
+
+ $already_displayed_pictures[] = $found->src;
+ }
+
+ $content_text = $content->innertext;
+
+ // Remove noscript tag to display images
+ $content_text = str_replace('<noscript>', '', $content_text);
+
+ return $content_text;
+ }
}
diff --git a/bridges/RoosterTeethBridge.php b/bridges/RoosterTeethBridge.php
index 9b85de53..ab4d12ce 100644
--- a/bridges/RoosterTeethBridge.php
+++ b/bridges/RoosterTeethBridge.php
@@ -1,105 +1,106 @@
<?php
-class RoosterTeethBridge extends BridgeAbstract {
+class RoosterTeethBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'tgkenney';
+ const NAME = 'Rooster Teeth';
+ const URI = 'https://roosterteeth.com';
+ const DESCRIPTION = 'Gets the latest channel videos from the Rooster Teeth website';
+ const API = 'https://svod-be.roosterteeth.com/';
- const MAINTAINER = 'tgkenney';
- const NAME = 'Rooster Teeth';
- const URI = 'https://roosterteeth.com';
- const DESCRIPTION = 'Gets the latest channel videos from the Rooster Teeth website';
- const API = 'https://svod-be.roosterteeth.com/';
+ const PARAMETERS = [
+ 'Options' => [
+ 'channel' => [
+ 'type' => 'list',
+ 'name' => 'Channel',
+ 'title' => 'Select a channel to filter by',
+ 'values' => [
+ 'All channels' => 'all',
+ 'Achievement Hunter' => 'achievement-hunter',
+ 'Cow Chop' => 'cow-chop',
+ 'Death Battle' => 'death-battle',
+ 'Funhaus' => 'funhaus',
+ 'Inside Gaming' => 'inside-gaming',
+ 'JT Music' => 'jt-music',
+ 'Kinda Funny' => 'kinda-funny',
+ 'Rooster Teeth' => 'rooster-teeth',
+ 'Sugar Pine 7' => 'sugar-pine-7'
+ ]
+ ],
+ 'sort' => [
+ 'type' => 'list',
+ 'name' => 'Sort',
+ 'title' => 'Select a sort order',
+ 'values' => [
+ 'Newest -> Oldest' => 'desc',
+ 'Oldest -> Newest' => 'asc'
+ ],
+ 'defaultValue' => 'desc'
+ ],
+ 'first' => [
+ 'type' => 'list',
+ 'name' => 'RoosterTeeth First',
+ 'title' => 'Select whether to include "First" videos before they are public',
+ 'values' => [
+ 'True' => true,
+ 'False' => false
+ ]
+ ],
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Maximum number of items to return',
+ 'defaultValue' => 10
+ ]
+ ]
+ ];
- const PARAMETERS = array(
- 'Options' => array(
- 'channel' => array(
- 'type' => 'list',
- 'name' => 'Channel',
- 'title' => 'Select a channel to filter by',
- 'values' => array(
- 'All channels' => 'all',
- 'Achievement Hunter' => 'achievement-hunter',
- 'Cow Chop' => 'cow-chop',
- 'Death Battle' => 'death-battle',
- 'Funhaus' => 'funhaus',
- 'Inside Gaming' => 'inside-gaming',
- 'JT Music' => 'jt-music',
- 'Kinda Funny' => 'kinda-funny',
- 'Rooster Teeth' => 'rooster-teeth',
- 'Sugar Pine 7' => 'sugar-pine-7'
- )
- ),
- 'sort' => array(
- 'type' => 'list',
- 'name' => 'Sort',
- 'title' => 'Select a sort order',
- 'values' => array(
- 'Newest -> Oldest' => 'desc',
- 'Oldest -> Newest' => 'asc'
- ),
- 'defaultValue' => 'desc'
- ),
- 'first' => array(
- 'type' => 'list',
- 'name' => 'RoosterTeeth First',
- 'title' => 'Select whether to include "First" videos before they are public',
- 'values' => array(
- 'True' => true,
- 'False' => false
- )
- ),
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Maximum number of items to return',
- 'defaultValue' => 10
- )
- )
- );
+ public function collectData()
+ {
+ if ($this->getInput('channel') !== 'all') {
+ $uri = self::API
+ . 'api/v1/episodes?per_page='
+ . $this->getInput('limit')
+ . '&channel_id='
+ . $this->getInput('channel')
+ . '&order=' . $this->getInput('sort')
+ . '&page=1';
- public function collectData() {
- if ($this->getInput('channel') !== 'all') {
- $uri = self::API
- . 'api/v1/episodes?per_page='
- . $this->getInput('limit')
- . '&channel_id='
- . $this->getInput('channel')
- . '&order=' . $this->getInput('sort')
- . '&page=1';
+ $htmlJSON = getSimpleHTMLDOM($uri);
+ } else {
+ $uri = self::API
+ . '/api/v1/episodes?per_page='
+ . $this->getInput('limit')
+ . '&filter=all&order='
+ . $this->getInput('sort')
+ . '&page=1';
- $htmlJSON = getSimpleHTMLDOM($uri);
- } else {
- $uri = self::API
- . '/api/v1/episodes?per_page='
- . $this->getInput('limit')
- . '&filter=all&order='
- . $this->getInput('sort')
- . '&page=1';
+ $htmlJSON = getSimpleHTMLDOM($uri);
+ }
- $htmlJSON = getSimpleHTMLDOM($uri);
- }
+ $htmlArray = json_decode($htmlJSON, true);
- $htmlArray = json_decode($htmlJSON, true);
+ foreach ($htmlArray['data'] as $key => $value) {
+ $item = [];
- foreach($htmlArray['data'] as $key => $value) {
- $item = array();
+ if (!$this->getInput('first') && $value['attributes']['is_sponsors_only']) {
+ continue;
+ }
- if (!$this->getInput('first') && $value['attributes']['is_sponsors_only']) {
- continue;
- }
+ $publicDate = date_create($value['attributes']['member_golive_at']);
+ $dateDiff = date_diff($publicDate, date_create(), false);
- $publicDate = date_create($value['attributes']['member_golive_at']);
- $dateDiff = date_diff($publicDate, date_create(), false);
+ if (!$this->getInput('first') && $dateDiff->invert == 1) {
+ continue;
+ }
- if (!$this->getInput('first') && $dateDiff->invert == 1) {
- continue;
- }
+ $item['uri'] = self::URI . $value['canonical_links']['self'];
+ $item['title'] = $value['attributes']['title'];
+ $item['timestamp'] = $value['attributes']['member_golive_at'];
+ $item['author'] = $value['attributes']['show_title'];
- $item['uri'] = self::URI . $value['canonical_links']['self'];
- $item['title'] = $value['attributes']['title'];
- $item['timestamp'] = $value['attributes']['member_golive_at'];
- $item['author'] = $value['attributes']['show_title'];
-
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/RtsBridge.php b/bridges/RtsBridge.php
index 002e4d6a..1253ea3c 100644
--- a/bridges/RtsBridge.php
+++ b/bridges/RtsBridge.php
@@ -1,76 +1,77 @@
<?php
-class RtsBridge extends BridgeAbstract {
- const NAME = 'Radio Télévision Suisse';
- const URI = 'https://www.rts.ch/';
- const MAINTAINER = 'imagoiq';
- const DESCRIPTION = 'Returns newest videos from RTS';
+class RtsBridge extends BridgeAbstract
+{
+ const NAME = 'Radio Télévision Suisse';
+ const URI = 'https://www.rts.ch/';
+ const MAINTAINER = 'imagoiq';
+ const DESCRIPTION = 'Returns newest videos from RTS';
- const PARAMETERS = array(
- 'ID de l\'émission' => array(
- 'idShow' => array(
- 'name' => 'Show id',
- 'required' => true,
- 'exampleValue' => 385418,
- 'title' => 'ex. 385418 pour
+ const PARAMETERS = [
+ 'ID de l\'émission' => [
+ 'idShow' => [
+ 'name' => 'Show id',
+ 'required' => true,
+ 'exampleValue' => 385418,
+ 'title' => 'ex. 385418 pour
https://www.rts.ch/play/tv/emission/a-bon-entendeur?id=385418'
- )
- ),
- 'ID de la section' => array(
- 'idSection' => array(
- 'name' => 'Section id',
- 'required' => true,
- 'exampleValue' => 'ce802a54-8877-49cc-acd6-8d244762829b',
- 'title' => 'ex. ce802a54-8877-49cc-acd6-8d244762829b pour
+ ]
+ ],
+ 'ID de la section' => [
+ 'idSection' => [
+ 'name' => 'Section id',
+ 'required' => true,
+ 'exampleValue' => 'ce802a54-8877-49cc-acd6-8d244762829b',
+ 'title' => 'ex. ce802a54-8877-49cc-acd6-8d244762829b pour
https://www.rts.ch/play/tv/detail/humour?id=ce802a54-8877-49cc-acd6-8d244762829b'
- )
- )
- );
+ ]
+ ]
+ ];
- public function collectData(){
- switch($this->queriedContext) {
- case 'ID de l\'émission':
- $showId = $this->getInput('idShow');
+ public function collectData()
+ {
+ switch ($this->queriedContext) {
+ case 'ID de l\'émission':
+ $showId = $this->getInput('idShow');
- $url = 'https://www.rts.ch/play/v3/api/rts/production/videos-by-show-id?showId='
- . $showId;
- break;
- case 'ID de la section':
- $sectionId = $this->getInput('idSection');
+ $url = 'https://www.rts.ch/play/v3/api/rts/production/videos-by-show-id?showId='
+ . $showId;
+ break;
+ case 'ID de la section':
+ $sectionId = $this->getInput('idSection');
- $url = 'https://www.rts.ch/play/v3/api/rts/production/media-section?sectionId='
- . $sectionId;
- break;
- }
+ $url = 'https://www.rts.ch/play/v3/api/rts/production/media-section?sectionId='
+ . $sectionId;
+ break;
+ }
- $header = array();
- $input = getContents($url, $header);
- $input_json = json_decode($input, true);
+ $header = [];
+ $input = getContents($url, $header);
+ $input_json = json_decode($input, true);
- foreach($input_json['data']['data'] as $element) {
+ foreach ($input_json['data']['data'] as $element) {
+ $item = [];
+ $item['uri'] = 'https://www.rts.ch/play/tv/-/video/-?urn=' . $element['urn'];
+ $item['uid'] = $element['id'];
- $item = array();
- $item['uri'] = 'https://www.rts.ch/play/tv/-/video/-?urn=' . $element['urn'];
- $item['uid'] = $element['id'];
+ $item['timestamp'] = strtotime($element['date']);
+ $item['title'] = $element['show']['title'] . ' - ' . $element['title'];
- $item['timestamp'] = strtotime($element['date']);
- $item['title'] = $element['show']['title'] . ' - ' . $element['title'];
+ $item['duration'] = round((int)$element['duration'] / 60000);
+ $durationInHour = date('g\hi', mktime(0, $item['duration']));
+ $durationInMin = date('i\m\i\n', mktime(0, $item['duration']));
+ $durationText = $item['duration'] > 60 ? $durationInHour : $durationInMin;
- $item['duration'] = round((int)$element['duration'] / 60000);
- $durationInHour = date('g\hi', mktime(0, $item['duration']));
- $durationInMin = date('i\m\i\n', mktime(0, $item['duration']));
- $durationText = $item['duration'] > 60 ? $durationInHour : $durationInMin;
+ $item['content'] = $element['description']
+ . '<br/><br/>'
+ . $durationText
+ . '<br><a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $element['imageUrl']
+ . '/scale/width/700" alt=""/></a>';
- $item['content'] = $element['description']
- . '<br/><br/>'
- . $durationText
- . '<br><a href="'
- . $item['uri']
- . '"><img src="'
- . $element['imageUrl']
- . '/scale/width/700" alt=""/></a>';
-
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/Rue89Bridge.php b/bridges/Rue89Bridge.php
index c6038448..9b446e44 100644
--- a/bridges/Rue89Bridge.php
+++ b/bridges/Rue89Bridge.php
@@ -1,47 +1,46 @@
<?php
-class Rue89Bridge extends BridgeAbstract {
- const MAINTAINER = 'teromene';
- const NAME = 'Rue89';
- const URI = 'https://www.nouvelobs.com/rue89/';
- const DESCRIPTION = 'Returns the newest posts from Rue89';
-
- public function collectData() {
-
- $jsonArticles = getContents('https://appdata.nouvelobs.com/rue89/feed.json');
- $articles = json_decode($jsonArticles)->items;
- foreach($articles as $article) {
- $this->items[] = $this->getArticle($article);
- }
-
- }
-
- private function getArticle($articleInfo) {
-
- $articleJson = getContents($articleInfo->json_url);
- $article = json_decode($articleJson);
- $item = array();
- $item['title'] = $article->title;
- $item['uri'] = $article->url;
- if($article->content_premium !== null) {
- $item['content'] = $article->content_premium;
- } else {
- $item['content'] = $article->content;
- }
- $item['timestamp'] = $article->date_publi;
- $item['author'] = $article->author->show_name;
-
- $item['enclosures'] = array();
- foreach($article->images as $image) {
- $item['enclosures'][] = $image->url;
- }
-
- $item['categories'] = array();
- foreach($article->categories as $category) {
- $item['categories'][] = $category->title;
- }
-
- return $item;
-
- }
+class Rue89Bridge extends BridgeAbstract
+{
+ const MAINTAINER = 'teromene';
+ const NAME = 'Rue89';
+ const URI = 'https://www.nouvelobs.com/rue89/';
+ const DESCRIPTION = 'Returns the newest posts from Rue89';
+
+ public function collectData()
+ {
+ $jsonArticles = getContents('https://appdata.nouvelobs.com/rue89/feed.json');
+ $articles = json_decode($jsonArticles)->items;
+ foreach ($articles as $article) {
+ $this->items[] = $this->getArticle($article);
+ }
+ }
+
+ private function getArticle($articleInfo)
+ {
+ $articleJson = getContents($articleInfo->json_url);
+ $article = json_decode($articleJson);
+ $item = [];
+ $item['title'] = $article->title;
+ $item['uri'] = $article->url;
+ if ($article->content_premium !== null) {
+ $item['content'] = $article->content_premium;
+ } else {
+ $item['content'] = $article->content;
+ }
+ $item['timestamp'] = $article->date_publi;
+ $item['author'] = $article->author->show_name;
+
+ $item['enclosures'] = [];
+ foreach ($article->images as $image) {
+ $item['enclosures'][] = $image->url;
+ }
+
+ $item['categories'] = [];
+ foreach ($article->categories as $category) {
+ $item['categories'][] = $category->title;
+ }
+
+ return $item;
+ }
}
diff --git a/bridges/Rule34Bridge.php b/bridges/Rule34Bridge.php
index 5c8ddc93..05241fb8 100644
--- a/bridges/Rule34Bridge.php
+++ b/bridges/Rule34Bridge.php
@@ -1,10 +1,9 @@
<?php
-class Rule34Bridge extends GelbooruBridge {
-
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Rule34';
- const URI = 'https://rule34.xxx/';
- const DESCRIPTION = 'Returns images from given page';
-
+class Rule34Bridge extends GelbooruBridge
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Rule34';
+ const URI = 'https://rule34.xxx/';
+ const DESCRIPTION = 'Returns images from given page';
}
diff --git a/bridges/Rule34pahealBridge.php b/bridges/Rule34pahealBridge.php
index 37e216df..e738ed69 100644
--- a/bridges/Rule34pahealBridge.php
+++ b/bridges/Rule34pahealBridge.php
@@ -1,28 +1,29 @@
<?php
-class Rule34pahealBridge extends Shimmie2Bridge {
+class Rule34pahealBridge extends Shimmie2Bridge
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Rule34paheal';
+ const URI = 'https://rule34.paheal.net/';
+ const DESCRIPTION = 'Returns images from given page';
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Rule34paheal';
- const URI = 'https://rule34.paheal.net/';
- const DESCRIPTION = 'Returns images from given page';
+ const PATHTODATA = '.shm-thumb';
- const PATHTODATA = '.shm-thumb';
-
- protected function getItemFromElement($element){
- $item = array();
- $item['uri'] = rtrim($this->getURI(), '/') . $element->find('.shm-thumb-link', 0)->href;
- $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
- $item['timestamp'] = time();
- $thumbnailUri = $element->find('a', 1)->href;
- $item['categories'] = explode(' ', $element->getAttribute('data-tags'));
- $item['title'] = $this->getName() . ' | ' . $item['id'];
- $item['content'] = '<a href="'
- . $item['uri']
- . '"><img src="'
- . $thumbnailUri
- . '" /></a><br>Tags: '
- . $element->getAttribute('data-tags');
- return $item;
- }
+ protected function getItemFromElement($element)
+ {
+ $item = [];
+ $item['uri'] = rtrim($this->getURI(), '/') . $element->find('.shm-thumb-link', 0)->href;
+ $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
+ $item['timestamp'] = time();
+ $thumbnailUri = $element->find('a', 1)->href;
+ $item['categories'] = explode(' ', $element->getAttribute('data-tags'));
+ $item['title'] = $this->getName() . ' | ' . $item['id'];
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $thumbnailUri
+ . '" /></a><br>Tags: '
+ . $element->getAttribute('data-tags');
+ return $item;
+ }
}
diff --git a/bridges/RutubeBridge.php b/bridges/RutubeBridge.php
index f4bfcdb4..6e2559d2 100644
--- a/bridges/RutubeBridge.php
+++ b/bridges/RutubeBridge.php
@@ -1,91 +1,98 @@
<?php
-class RutubeBridge extends BridgeAbstract {
- const NAME = 'Rutube';
- const URI = 'https://rutube.ru';
- const MAINTAINER = 'em92';
- const DESCRIPTION = 'Выводит ленту видео';
+class RutubeBridge extends BridgeAbstract
+{
+ const NAME = 'Rutube';
+ const URI = 'https://rutube.ru';
+ const MAINTAINER = 'em92';
+ const DESCRIPTION = 'Выводит ленту видео';
- const PARAMETERS = array(
- 'По каналу' => array(
- 'c' => array(
- 'name' => 'ИД канала',
- 'exampleValue' => 1342940, // Мятежник Джек
- 'type' => 'number',
- 'required' => true
- ),
- ),
- 'По плейлисту' => array(
- 'p' => array(
- 'name' => 'ИД плейлиста',
- 'exampleValue' => 83641, // QRUSH
- 'type' => 'number',
- 'required' => true
- ),
- ),
- );
+ const PARAMETERS = [
+ 'По каналу' => [
+ 'c' => [
+ 'name' => 'ИД канала',
+ 'exampleValue' => 1342940, // Мятежник Джек
+ 'type' => 'number',
+ 'required' => true
+ ],
+ ],
+ 'По плейлисту' => [
+ 'p' => [
+ 'name' => 'ИД плейлиста',
+ 'exampleValue' => 83641, // QRUSH
+ 'type' => 'number',
+ 'required' => true
+ ],
+ ],
+ ];
- protected $title;
+ protected $title;
- public function getURI() {
- if ($this->getInput('c')) {
- return self::URI . '/channel/' . strval($this->getInput('c')) . '/videos/';
- } else if ($this->getInput('p')) {
- return self::URI . '/plst/' . strval($this->getInput('p')) . '/';
- } else {
- return parent::getURI();
- }
- }
+ public function getURI()
+ {
+ if ($this->getInput('c')) {
+ return self::URI . '/channel/' . strval($this->getInput('c')) . '/videos/';
+ } elseif ($this->getInput('p')) {
+ return self::URI . '/plst/' . strval($this->getInput('p')) . '/';
+ } else {
+ return parent::getURI();
+ }
+ }
- public function getIcon() {
- return 'https://static.rutube.ru/static/favicon.ico';
- }
+ public function getIcon()
+ {
+ return 'https://static.rutube.ru/static/favicon.ico';
+ }
- public function getName() {
- if (is_null($this->title)) {
- return parent::getName();
- } else {
- return $this->title . ' - ' . parent::getName();
- }
- }
+ public function getName()
+ {
+ if (is_null($this->title)) {
+ return parent::getName();
+ } else {
+ return $this->title . ' - ' . parent::getName();
+ }
+ }
- private function getJSONData($html) {
- $jsonDataRegex = '/window.reduxState = (.*?);/';
- preg_match($jsonDataRegex, $html, $matches) or returnServerError('Could not find reduxState');
- return json_decode(str_replace('\x', '\\\x', $matches[1]));
- }
+ private function getJSONData($html)
+ {
+ $jsonDataRegex = '/window.reduxState = (.*?);/';
+ preg_match($jsonDataRegex, $html, $matches) or returnServerError('Could not find reduxState');
+ return json_decode(str_replace('\x', '\\\x', $matches[1]));
+ }
- public function collectData(){
- $link = $this->getURI();
+ public function collectData()
+ {
+ $link = $this->getURI();
- $html = getContents($link);
- $reduxState = $this->getJSONData($html);
- $videos = [];
- if ($this->getInput('c')) {
- $videos = $reduxState->userChannel->videos->results;
- $this->title = $reduxState->userChannel->info->name;
- } else if ($this->getInput('p')) {
- $videos = $reduxState->playlist->data->results;
- $this->title = $reduxState->playlist->title;
- }
+ $html = getContents($link);
+ $reduxState = $this->getJSONData($html);
+ $videos = [];
+ if ($this->getInput('c')) {
+ $videos = $reduxState->userChannel->videos->results;
+ $this->title = $reduxState->userChannel->info->name;
+ } elseif ($this->getInput('p')) {
+ $videos = $reduxState->playlist->data->results;
+ $this->title = $reduxState->playlist->title;
+ }
- foreach($videos as $video) {
- $item = new FeedItem();
- $item->setTitle($video->title);
- $item->setURI($video->video_url);
- $content = '<a href="' . $item->getURI() . '">';
- $content .= '<img src="' . $video->thumbnail_url . '" />';
- $content .= '</a><br/>';
- $content .= nl2br(
- // Converting links in plaintext
- // Copied from https://stackoverflow.com/a/12590772
- preg_replace(
- '$(https?://[a-z0-9_./?=&#-]+)(?![^<>]*>)$i', ' <a href="$1" target="_blank">$1</a> ',
- $video->description . ' '
- )
- );
- $item->setContent($content);
- $this->items[] = $item;
- }
- }
+ foreach ($videos as $video) {
+ $item = new FeedItem();
+ $item->setTitle($video->title);
+ $item->setURI($video->video_url);
+ $content = '<a href="' . $item->getURI() . '">';
+ $content .= '<img src="' . $video->thumbnail_url . '" />';
+ $content .= '</a><br/>';
+ $content .= nl2br(
+ // Converting links in plaintext
+ // Copied from https://stackoverflow.com/a/12590772
+ preg_replace(
+ '$(https?://[a-z0-9_./?=&#-]+)(?![^<>]*>)$i',
+ ' <a href="$1" target="_blank">$1</a> ',
+ $video->description . ' '
+ )
+ );
+ $item->setContent($content);
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/SIMARBridge.php b/bridges/SIMARBridge.php
index df79b1d9..8b9cd31a 100644
--- a/bridges/SIMARBridge.php
+++ b/bridges/SIMARBridge.php
@@ -1,62 +1,65 @@
<?php
-class SIMARBridge extends BridgeAbstract {
- const NAME = 'SIMAR';
- const URI = 'http://www.simar-louresodivelas.pt/';
- const DESCRIPTION = 'Verificar estado da rede SIMAR';
- const MAINTAINER = 'somini';
- const PARAMETERS = array(
- 'Público' => array(
- 'interventions' => array(
- 'type' => 'checkbox',
- 'name' => 'Incluir Intervenções?',
- 'defaultValue' => 'checked',
- )
- )
- );
-
- public function collectData() {
- $html = getSimpleHTMLDOM(self::getURI());
- $e_home = $html->find('#home', 0)
- or returnServerError('Invalid site structure');
-
- foreach($e_home->find('span') as $element) {
- $item = array();
-
- $item['title'] = 'Rotura: ' . $element->plaintext;
- $item['content'] = $element->innertext;
- $item['uid'] = 'urn:sha1:' . hash('sha1', $item['content']);
-
- $this->items[] = $item;
- }
-
- if ($this->getInput('interventions')) {
- $e_main1 = $html->find('#menu1', 0)
- or returnServerError('Invalid site structure');
-
- foreach ($e_main1->find('a') as $element) {
- $item = array();
-
- $item['title'] = 'Intervenção: ' . $element->plaintext;
- $item['uri'] = self::getURI() . $element->href;
- $item['content'] = $element->innertext;
-
- /* Try to get the actual contents for this kind of item */
- $item_html = getSimpleHTMLDOMCached($item['uri']);
- if ($item_html) {
- $e_item = $item_html->find('.auto-style59', 0);
- foreach($e_item->find('p') as $paragraph) {
- /* Remove empty paragraphs */
- if (preg_match('/^(\W|&nbsp;)+$/', $paragraph->innertext) == 1) {
- $paragraph->outertext = '';
- }
- }
- if ($e_item) {
- $item['content'] = $e_item->innertext;
- }
- }
-
- $this->items[] = $item;
- }
- }
- }
+
+class SIMARBridge extends BridgeAbstract
+{
+ const NAME = 'SIMAR';
+ const URI = 'http://www.simar-louresodivelas.pt/';
+ const DESCRIPTION = 'Verificar estado da rede SIMAR';
+ const MAINTAINER = 'somini';
+ const PARAMETERS = [
+ 'Público' => [
+ 'interventions' => [
+ 'type' => 'checkbox',
+ 'name' => 'Incluir Intervenções?',
+ 'defaultValue' => 'checked',
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::getURI());
+ $e_home = $html->find('#home', 0)
+ or returnServerError('Invalid site structure');
+
+ foreach ($e_home->find('span') as $element) {
+ $item = [];
+
+ $item['title'] = 'Rotura: ' . $element->plaintext;
+ $item['content'] = $element->innertext;
+ $item['uid'] = 'urn:sha1:' . hash('sha1', $item['content']);
+
+ $this->items[] = $item;
+ }
+
+ if ($this->getInput('interventions')) {
+ $e_main1 = $html->find('#menu1', 0)
+ or returnServerError('Invalid site structure');
+
+ foreach ($e_main1->find('a') as $element) {
+ $item = [];
+
+ $item['title'] = 'Intervenção: ' . $element->plaintext;
+ $item['uri'] = self::getURI() . $element->href;
+ $item['content'] = $element->innertext;
+
+ /* Try to get the actual contents for this kind of item */
+ $item_html = getSimpleHTMLDOMCached($item['uri']);
+ if ($item_html) {
+ $e_item = $item_html->find('.auto-style59', 0);
+ foreach ($e_item->find('p') as $paragraph) {
+ /* Remove empty paragraphs */
+ if (preg_match('/^(\W|&nbsp;)+$/', $paragraph->innertext) == 1) {
+ $paragraph->outertext = '';
+ }
+ }
+ if ($e_item) {
+ $item['content'] = $e_item->innertext;
+ }
+ }
+
+ $this->items[] = $item;
+ }
+ }
+ }
}
diff --git a/bridges/SafebooruBridge.php b/bridges/SafebooruBridge.php
index 0ca7a45d..b0ebea19 100644
--- a/bridges/SafebooruBridge.php
+++ b/bridges/SafebooruBridge.php
@@ -1,15 +1,16 @@
<?php
-class SafebooruBridge extends GelbooruBridge {
+class SafebooruBridge extends GelbooruBridge
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Safebooru';
+ const URI = 'https://safebooru.org/';
+ const DESCRIPTION = 'Returns images from given page';
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Safebooru';
- const URI = 'https://safebooru.org/';
- const DESCRIPTION = 'Returns images from given page';
-
- protected function buildThumbnailURI($element){
- $regex = '/\.\w+$/';
- return $this->getURI() . 'thumbnails/' . $element->directory
- . '/thumbnail_' . preg_replace($regex, '.jpg', $element->image);
- }
+ protected function buildThumbnailURI($element)
+ {
+ $regex = '/\.\w+$/';
+ return $this->getURI() . 'thumbnails/' . $element->directory
+ . '/thumbnail_' . preg_replace($regex, '.jpg', $element->image);
+ }
}
diff --git a/bridges/SchweinfurtBuergerinformationenBridge.php b/bridges/SchweinfurtBuergerinformationenBridge.php
index 1cee949a..c7c935fd 100644
--- a/bridges/SchweinfurtBuergerinformationenBridge.php
+++ b/bridges/SchweinfurtBuergerinformationenBridge.php
@@ -1,121 +1,132 @@
<?php
-class SchweinfurtBuergerinformationenBridge extends BridgeAbstract {
- const MAINTAINER = 'mibe';
- const NAME = 'Schweinfurt Bürgerinformationen';
- const URI = 'https://www.schweinfurt.de/rathaus-politik/pressestelle/buergerinformationen/index.html';
- const ARTICLE_URI = 'https://www.schweinfurt.de/rathaus-politik/pressestelle/buergerinformationen/%d.html';
- const INDEX_CACHE_TIMEOUT = 10800; // 3h
- const ARTICLE_CACHE_TIMEOUT = 21600; // 6h
- const DESCRIPTION = 'Returns the latest news for citizens of Schweinfurt';
- const PARAMETERS = array(
- array(
- 'pages' => array(
- 'name' => 'Number of pages',
- 'type' => 'number',
- 'title' => 'Specifies the number of pages to fetch. Usually one or two are enough.',
- 'exampleValue' => '1',
- 'defaultValue' => '1',
- )
- )
- );
-
- public function getIcon()
- {
- return 'https://www.schweinfurt.de/__/images/favicon.ico';
- }
-
- public function collectData()
- {
- // Get number of pages to retrieve. One page is the minimum.
- $pages = $this->getInput('pages');
- if (!is_int($pages) || $pages < 1)
- $pages = 1;
-
- $articleIDs = array();
-
- for($page = 0; $page < $pages; $page++) {
- $newIDs = $this->getArticleIDsFromPage($page);
- $articleIDs = array_merge($articleIDs, $newIDs);
- }
-
- foreach($articleIDs as $articleID) {
- $this->items[] = $this->generateItemFromArticle($articleID);
-
- if (Debug::isEnabled())
- break;
- }
- }
-
- private function getArticleIDsFromPage($page)
- {
- $url = sprintf(self::URI . '?art_pager=%d', $page);
- $html = getSimpleHTMLDOMCached($url, self::INDEX_CACHE_TIMEOUT)
- or returnServerError('Could not retrieve ' . $url);
-
- $articles = $html->find('div.artikel-uebersicht');
- $articleIDs = array();
-
- foreach($articles as $article) {
- // The article ID is in the 'id' attribute of the div element, prefixed with 'artikel_id_'
- if (preg_match('/artikel_id_(\d+)/', $article->id, $match)) {
- $articleIDs[] = $match[1];
- } else
- returnServerError('Couldn\'t determine article ID from index page.');
- }
-
- return $articleIDs;
- }
-
- private function generateItemFromArticle($id)
- {
- $url = sprintf(self::ARTICLE_URI, $id);
- $html = getSimpleHTMLDOMCached($url, self::ARTICLE_CACHE_TIMEOUT)
- or returnServerError('Could not retrieve ' . $url);
-
- $div = $html->find('div#artikel-detail', 0);
- $divContent = $div->find('.c-content', 0);
- $images = $divContent->find('img');
-
- // Every external link has a little arrow symbol image attached to it.
- // Remove this image. This has to be done before building $content.
- foreach($images as $image)
- if ($image->class == 'imgextlink')
- $image->outertext = '';
-
- $title = $div->find('.c-title', 0)->innertext;
- $teaser = $div->find('.c-teaser', 0)->innertext;
- $content = $divContent->innertext;
-
- // The title can contain HTML entities. These can be converted back
- // to regular UTF-8 characters.
- $title = html_entity_decode($title, ENT_HTML5, 'UTF-8');
-
- // If there's a teaser, make it more eye-catching,
- // so that it is clear, that this is not part of the actual content.
- if (strlen(trim($teaser)) > 0)
- $content = '<i><strong>' . $teaser . '</strong></i>' . $content;
-
- $item = array(
- 'uri' => $url,
- 'title' => $title,
- 'content' => $content,
- 'uid' => $id,
- );
-
- // Let's see if there are images in the content, and if yes, attach
- // them as enclosures, but not images which are used for linking to an external site.
- foreach($images as $image)
- if ($image->class != 'imgextlink')
- $item['enclosures'][] = $image->src;
-
- // Get the date of the article. Example: "zuletzt geändert: 26.05.2020"
- $editDate = $div->find('div#edit', 0)->plaintext;
- $editDate = substr($editDate, strrpos($editDate, ' ') + 1);
- $editDate = DateTime::createFromFormat('d.m.Y', $editDate);
-
- if ($editDate !== false)
- $item['timestamp'] = $editDate->getTimestamp();
-
- return $item;
- }
+
+class SchweinfurtBuergerinformationenBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'mibe';
+ const NAME = 'Schweinfurt Bürgerinformationen';
+ const URI = 'https://www.schweinfurt.de/rathaus-politik/pressestelle/buergerinformationen/index.html';
+ const ARTICLE_URI = 'https://www.schweinfurt.de/rathaus-politik/pressestelle/buergerinformationen/%d.html';
+ const INDEX_CACHE_TIMEOUT = 10800; // 3h
+ const ARTICLE_CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns the latest news for citizens of Schweinfurt';
+ const PARAMETERS = [
+ [
+ 'pages' => [
+ 'name' => 'Number of pages',
+ 'type' => 'number',
+ 'title' => 'Specifies the number of pages to fetch. Usually one or two are enough.',
+ 'exampleValue' => '1',
+ 'defaultValue' => '1',
+ ]
+ ]
+ ];
+
+ public function getIcon()
+ {
+ return 'https://www.schweinfurt.de/__/images/favicon.ico';
+ }
+
+ public function collectData()
+ {
+ // Get number of pages to retrieve. One page is the minimum.
+ $pages = $this->getInput('pages');
+ if (!is_int($pages) || $pages < 1) {
+ $pages = 1;
+ }
+
+ $articleIDs = [];
+
+ for ($page = 0; $page < $pages; $page++) {
+ $newIDs = $this->getArticleIDsFromPage($page);
+ $articleIDs = array_merge($articleIDs, $newIDs);
+ }
+
+ foreach ($articleIDs as $articleID) {
+ $this->items[] = $this->generateItemFromArticle($articleID);
+
+ if (Debug::isEnabled()) {
+ break;
+ }
+ }
+ }
+
+ private function getArticleIDsFromPage($page)
+ {
+ $url = sprintf(self::URI . '?art_pager=%d', $page);
+ $html = getSimpleHTMLDOMCached($url, self::INDEX_CACHE_TIMEOUT)
+ or returnServerError('Could not retrieve ' . $url);
+
+ $articles = $html->find('div.artikel-uebersicht');
+ $articleIDs = [];
+
+ foreach ($articles as $article) {
+ // The article ID is in the 'id' attribute of the div element, prefixed with 'artikel_id_'
+ if (preg_match('/artikel_id_(\d+)/', $article->id, $match)) {
+ $articleIDs[] = $match[1];
+ } else {
+ returnServerError('Couldn\'t determine article ID from index page.');
+ }
+ }
+
+ return $articleIDs;
+ }
+
+ private function generateItemFromArticle($id)
+ {
+ $url = sprintf(self::ARTICLE_URI, $id);
+ $html = getSimpleHTMLDOMCached($url, self::ARTICLE_CACHE_TIMEOUT)
+ or returnServerError('Could not retrieve ' . $url);
+
+ $div = $html->find('div#artikel-detail', 0);
+ $divContent = $div->find('.c-content', 0);
+ $images = $divContent->find('img');
+
+ // Every external link has a little arrow symbol image attached to it.
+ // Remove this image. This has to be done before building $content.
+ foreach ($images as $image) {
+ if ($image->class == 'imgextlink') {
+ $image->outertext = '';
+ }
+ }
+
+ $title = $div->find('.c-title', 0)->innertext;
+ $teaser = $div->find('.c-teaser', 0)->innertext;
+ $content = $divContent->innertext;
+
+ // The title can contain HTML entities. These can be converted back
+ // to regular UTF-8 characters.
+ $title = html_entity_decode($title, ENT_HTML5, 'UTF-8');
+
+ // If there's a teaser, make it more eye-catching,
+ // so that it is clear, that this is not part of the actual content.
+ if (strlen(trim($teaser)) > 0) {
+ $content = '<i><strong>' . $teaser . '</strong></i>' . $content;
+ }
+
+ $item = [
+ 'uri' => $url,
+ 'title' => $title,
+ 'content' => $content,
+ 'uid' => $id,
+ ];
+
+ // Let's see if there are images in the content, and if yes, attach
+ // them as enclosures, but not images which are used for linking to an external site.
+ foreach ($images as $image) {
+ if ($image->class != 'imgextlink') {
+ $item['enclosures'][] = $image->src;
+ }
+ }
+
+ // Get the date of the article. Example: "zuletzt geändert: 26.05.2020"
+ $editDate = $div->find('div#edit', 0)->plaintext;
+ $editDate = substr($editDate, strrpos($editDate, ' ') + 1);
+ $editDate = DateTime::createFromFormat('d.m.Y', $editDate);
+
+ if ($editDate !== false) {
+ $item['timestamp'] = $editDate->getTimestamp();
+ }
+
+ return $item;
+ }
}
diff --git a/bridges/ScmbBridge.php b/bridges/ScmbBridge.php
index 646d7c1c..d2fd0b50 100644
--- a/bridges/ScmbBridge.php
+++ b/bridges/ScmbBridge.php
@@ -1,41 +1,43 @@
<?php
-class ScmbBridge extends BridgeAbstract {
- const MAINTAINER = 'Astalaseven';
- const NAME = 'Se Coucher Moins Bête Bridge';
- const URI = 'https://secouchermoinsbete.fr';
- const CACHE_TIMEOUT = 21600; // 6h
- const DESCRIPTION = 'Returns the newest anecdotes.';
+class ScmbBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Astalaseven';
+ const NAME = 'Se Coucher Moins Bête Bridge';
+ const URI = 'https://secouchermoinsbete.fr';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns the newest anecdotes.';
- public function collectData(){
- $html = '';
- $html = getSimpleHTMLDOM(self::URI);
+ public function collectData()
+ {
+ $html = '';
+ $html = getSimpleHTMLDOM(self::URI);
- foreach($html->find('article') as $article) {
- $item = array();
- $item['uri'] = self::URI . $article->find('p.summary a', 0)->href;
- $item['title'] = $article->find('header h1 a', 0)->innertext;
+ foreach ($html->find('article') as $article) {
+ $item = [];
+ $item['uri'] = self::URI . $article->find('p.summary a', 0)->href;
+ $item['title'] = $article->find('header h1 a', 0)->innertext;
- // remove text "En savoir plus" from anecdote content
- $readMoreButton = $article->find('span.read-more', 0);
- if ($readMoreButton) {
- $readMoreButton->outertext = '';
- }
- $content = $article->find('p.summary a', 0)->innertext;
+ // remove text "En savoir plus" from anecdote content
+ $readMoreButton = $article->find('span.read-more', 0);
+ if ($readMoreButton) {
+ $readMoreButton->outertext = '';
+ }
+ $content = $article->find('p.summary a', 0)->innertext;
- // remove superfluous spaces at the end
- $content = substr($content, 0, strlen($content) - 17);
+ // remove superfluous spaces at the end
+ $content = substr($content, 0, strlen($content) - 17);
- // get publication date
- $str_date = $article->find('time', 0)->datetime;
- list($date, $time) = explode(' ', $str_date);
- list($y, $m, $d) = explode('-', $date);
- list($h, $i) = explode(':', $time);
- $timestamp = mktime($h, $i, 0, $m, $d, $y);
- $item['timestamp'] = $timestamp;
+ // get publication date
+ $str_date = $article->find('time', 0)->datetime;
+ list($date, $time) = explode(' ', $str_date);
+ list($y, $m, $d) = explode('-', $date);
+ list($h, $i) = explode(':', $time);
+ $timestamp = mktime($h, $i, 0, $m, $d, $y);
+ $item['timestamp'] = $timestamp;
- $item['content'] = $content;
- $this->items[] = $item;
- }
- }
+ $item['content'] = $content;
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/ScoopItBridge.php b/bridges/ScoopItBridge.php
index 6cf70ce5..a3de71f1 100644
--- a/bridges/ScoopItBridge.php
+++ b/bridges/ScoopItBridge.php
@@ -1,42 +1,44 @@
<?php
-class ScoopItBridge extends BridgeAbstract {
- const MAINTAINER = 'Pitchoule';
- const NAME = 'ScoopIt';
- const URI = 'https://www.scoop.it/';
- const CACHE_TIMEOUT = 21600; // 6h
- const DESCRIPTION = 'Returns most recent results from ScoopIt.';
+class ScoopItBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Pitchoule';
+ const NAME = 'ScoopIt';
+ const URI = 'https://www.scoop.it/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns most recent results from ScoopIt.';
- const PARAMETERS = array( array(
- 'u' => array(
- 'name' => 'keyword',
- 'exampleValue' => 'docker',
- 'required' => true
- )
- ));
+ const PARAMETERS = [ [
+ 'u' => [
+ 'name' => 'keyword',
+ 'exampleValue' => 'docker',
+ 'required' => true
+ ]
+ ]];
- public function collectData(){
- $this->request = $this->getInput('u');
- $link = self::URI . 'search?q=' . urlencode($this->getInput('u'));
+ public function collectData()
+ {
+ $this->request = $this->getInput('u');
+ $link = self::URI . 'search?q=' . urlencode($this->getInput('u'));
- $html = getSimpleHTMLDOM($link);
+ $html = getSimpleHTMLDOM($link);
- foreach($html->find('div.post-view') as $element) {
- $item = array();
- $item['uri'] = $element->find('a', 0)->href;
- $item['title'] = preg_replace(
- '~[[:cntrl:]]~',
- '',
- $element->find('div.tCustomization_post_title', 0)->plaintext
- );
+ foreach ($html->find('div.post-view') as $element) {
+ $item = [];
+ $item['uri'] = $element->find('a', 0)->href;
+ $item['title'] = preg_replace(
+ '~[[:cntrl:]]~',
+ '',
+ $element->find('div.tCustomization_post_title', 0)->plaintext
+ );
- $item['content'] = preg_replace(
- '~[[:cntrl:]]~',
- '',
- $element->find('div.tCustomization_post_description', 0)->plaintext
- );
+ $item['content'] = preg_replace(
+ '~[[:cntrl:]]~',
+ '',
+ $element->find('div.tCustomization_post_description', 0)->plaintext
+ );
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/ScribdBridge.php b/bridges/ScribdBridge.php
index 17f4740d..9c93b156 100644
--- a/bridges/ScribdBridge.php
+++ b/bridges/ScribdBridge.php
@@ -1,78 +1,81 @@
<?php
-class ScribdBridge extends BridgeAbstract {
- const NAME = 'Scribd Bridge';
- const URI = 'https://www.scribd.com';
- const DESCRIPTION = 'Returns documents uploaded by a user.';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(array(
- 'profile' => array(
- 'name' => 'Profile URL',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Profile URL. Example: https://www.scribd.com/user/164147088/Ars-Technica',
- 'exampleValue' => 'https://www.scribd.com/user/164147088/Ars-Technica'
- ),
- ));
- const CACHE_TIMEOUT = 3600;
+class ScribdBridge extends BridgeAbstract
+{
+ const NAME = 'Scribd Bridge';
+ const URI = 'https://www.scribd.com';
+ const DESCRIPTION = 'Returns documents uploaded by a user.';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [[
+ 'profile' => [
+ 'name' => 'Profile URL',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Profile URL. Example: https://www.scribd.com/user/164147088/Ars-Technica',
+ 'exampleValue' => 'https://www.scribd.com/user/164147088/Ars-Technica'
+ ],
+ ]];
- private $profileUrlRegex = '/scribd\.com\/(user\/[0-9]+\/[\w-]+)\/?/';
- private $feedName = '';
+ const CACHE_TIMEOUT = 3600;
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI());
+ private $profileUrlRegex = '/scribd\.com\/(user\/[0-9]+\/[\w-]+)\/?/';
+ private $feedName = '';
- $this->feedName = $html->find('div.header', 0)->plaintext;
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
- foreach($html->find('ul.document_cells > li') as $index => $li) {
- $item = array();
+ $this->feedName = $html->find('div.header', 0)->plaintext;
- $item['title'] = $li->find('div.under_title', 0)->plaintext;
- $item['uri'] = $li->find('a', 0)->href;
- $item['author'] = $li->find('span.uploader', 0)->plaintext;
- $item['uid'] = $li->find('a', 0)->href;
+ foreach ($html->find('ul.document_cells > li') as $index => $li) {
+ $item = [];
- $pageHtml = getSimpleHTMLDOMCached($item['uri'], 3600);
+ $item['title'] = $li->find('div.under_title', 0)->plaintext;
+ $item['uri'] = $li->find('a', 0)->href;
+ $item['author'] = $li->find('span.uploader', 0)->plaintext;
+ $item['uid'] = $li->find('a', 0)->href;
- $image = $pageHtml->find('meta[property="og:image"]', 0)->content;
- $description = $pageHtml->find('meta[property="og:description"]', 0)->content;
+ $pageHtml = getSimpleHTMLDOMCached($item['uri'], 3600);
- foreach ($pageHtml->find('ul.interest_pills li') as $pills) {
- $item['categories'][] = $pills->plaintext;
- }
+ $image = $pageHtml->find('meta[property="og:image"]', 0)->content;
+ $description = $pageHtml->find('meta[property="og:description"]', 0)->content;
- $item['content'] = <<<EOD
+ foreach ($pageHtml->find('ul.interest_pills li') as $pills) {
+ $item['categories'][] = $pills->plaintext;
+ }
+
+ $item['content'] = <<<EOD
<p>{$description}<p><p><img src="{$image}"></p>
EOD;
- $item['enclosures'][] = $image;
-
- $this->items[] = $item;
-
- if (count($this->items) >= 15) {
- break;
- }
- }
- }
+ $item['enclosures'][] = $image;
- public function getName() {
+ $this->items[] = $item;
- if ($this->feedName) {
- return $this->feedName . ' - Scribd';
- }
+ if (count($this->items) >= 15) {
+ break;
+ }
+ }
+ }
- return parent::getName();
- }
+ public function getName()
+ {
+ if ($this->feedName) {
+ return $this->feedName . ' - Scribd';
+ }
- public function getURI() {
+ return parent::getName();
+ }
- if (!is_null($this->getInput('profile'))) {
- preg_match($this->profileUrlRegex, $this->getInput('profile'), $user)
- or returnServerError('Could not extract user ID and name from given profile URL.');
+ public function getURI()
+ {
+ if (!is_null($this->getInput('profile'))) {
+ preg_match($this->profileUrlRegex, $this->getInput('profile'), $user)
+ or returnServerError('Could not extract user ID and name from given profile URL.');
- return self::URI . '/' . $user[1] . '/uploads';
- }
+ return self::URI . '/' . $user[1] . '/uploads';
+ }
- return parent::getURI();
- }
+ return parent::getURI();
+ }
}
diff --git a/bridges/SensCritiqueBridge.php b/bridges/SensCritiqueBridge.php
index e34beea7..9e42d6a6 100644
--- a/bridges/SensCritiqueBridge.php
+++ b/bridges/SensCritiqueBridge.php
@@ -1,96 +1,104 @@
<?php
-class SensCritiqueBridge extends BridgeAbstract {
- const MAINTAINER = 'kranack';
- const NAME = 'Sens Critique';
- const URI = 'https://www.senscritique.com/';
- const CACHE_TIMEOUT = 21600; // 6h
- const DESCRIPTION = 'Sens Critique news';
+class SensCritiqueBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'kranack';
+ const NAME = 'Sens Critique';
+ const URI = 'https://www.senscritique.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Sens Critique news';
- const PARAMETERS = array( array(
- 's' => array(
- 'name' => 'Series',
- 'type' => 'checkbox',
- 'defaultValue' => 'checked'
- ),
- 'g' => array(
- 'name' => 'Video Games',
- 'type' => 'checkbox'
- ),
- 'b' => array(
- 'name' => 'Books',
- 'type' => 'checkbox'
- ),
- 'bd' => array(
- 'name' => 'BD',
- 'type' => 'checkbox'
- ),
- 'mu' => array(
- 'name' => 'Music',
- 'type' => 'checkbox'
- )
- ));
+ const PARAMETERS = [ [
+ 's' => [
+ 'name' => 'Series',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ],
+ 'g' => [
+ 'name' => 'Video Games',
+ 'type' => 'checkbox'
+ ],
+ 'b' => [
+ 'name' => 'Books',
+ 'type' => 'checkbox'
+ ],
+ 'bd' => [
+ 'name' => 'BD',
+ 'type' => 'checkbox'
+ ],
+ 'mu' => [
+ 'name' => 'Music',
+ 'type' => 'checkbox'
+ ]
+ ]];
- public function collectData(){
- $categories = array();
- foreach(self::PARAMETERS[$this->queriedContext] as $category => $properties) {
- if($this->getInput($category)) {
- $uri = self::URI;
- switch($category) {
- case 's': $uri .= 'series/actualite';
- break;
- case 'g': $uri .= 'jeuxvideo/actualite';
- break;
- case 'b': $uri .= 'livres/actualite';
- break;
- case 'bd': $uri .= 'bd/actualite';
- break;
- case 'mu': $uri .= 'musique/actualite';
- break;
- }
- $html = getSimpleHTMLDOM($uri);
- $list = $html->find('ul.elpr-list', 0);
+ public function collectData()
+ {
+ $categories = [];
+ foreach (self::PARAMETERS[$this->queriedContext] as $category => $properties) {
+ if ($this->getInput($category)) {
+ $uri = self::URI;
+ switch ($category) {
+ case 's':
+ $uri .= 'series/actualite';
+ break;
+ case 'g':
+ $uri .= 'jeuxvideo/actualite';
+ break;
+ case 'b':
+ $uri .= 'livres/actualite';
+ break;
+ case 'bd':
+ $uri .= 'bd/actualite';
+ break;
+ case 'mu':
+ $uri .= 'musique/actualite';
+ break;
+ }
+ $html = getSimpleHTMLDOM($uri);
+ $list = $html->find('ul.elpr-list', 0);
- $this->extractDataFromList($list);
- }
- }
- }
+ $this->extractDataFromList($list);
+ }
+ }
+ }
- private function extractDataFromList($list){
- if($list === null) {
- returnClientError('Cannot extract data from list');
- }
+ private function extractDataFromList($list)
+ {
+ if ($list === null) {
+ returnClientError('Cannot extract data from list');
+ }
- foreach($list->find('li') as $movie) {
- $item = array();
- $item['author'] = htmlspecialchars_decode($movie->find('.elco-title a', 0)->plaintext, ENT_QUOTES)
- . ' '
- . $movie->find('.elco-date', 0)->plaintext;
+ foreach ($list->find('li') as $movie) {
+ $item = [];
+ $item['author'] = htmlspecialchars_decode($movie->find('.elco-title a', 0)->plaintext, ENT_QUOTES)
+ . ' '
+ . $movie->find('.elco-date', 0)->plaintext;
- $item['title'] = $movie->find('.elco-title a', 0)->plaintext
- . ' '
- . $movie->find('.elco-date', 0)->plaintext;
+ $item['title'] = $movie->find('.elco-title a', 0)->plaintext
+ . ' '
+ . $movie->find('.elco-date', 0)->plaintext;
- $item['content'] = '';
- $originalTitle = $movie->find('.elco-original-title', 0);
- $description = $movie->find('.elco-description', 0);
+ $item['content'] = '';
+ $originalTitle = $movie->find('.elco-original-title', 0);
+ $description = $movie->find('.elco-description', 0);
- if ($originalTitle) {
- $item['content'] = '<em>' . $originalTitle->plaintext . '</em><br><br>';
- }
+ if ($originalTitle) {
+ $item['content'] = '<em>' . $originalTitle->plaintext . '</em><br><br>';
+ }
- $item['content'] .= $movie->find('.elco-baseline', 0)->plaintext
- . '<br>'
- . $movie->find('.elco-baseline', 1)->plaintext
- . '<br><br>'
- . ($description ? $description->plaintext : '')
- . '<br><br>'
- . trim($movie->find('.erra-ratings .erra-global', 0)->plaintext)
- . ' / 10';
+ $item['content'] .= $movie->find('.elco-baseline', 0)->plaintext
+ . '<br>'
+ . $movie->find('.elco-baseline', 1)->plaintext
+ . '<br><br>'
+ . ($description ? $description->plaintext : '')
+ . '<br><br>'
+ . trim($movie->find('.erra-ratings .erra-global', 0)->plaintext)
+ . ' / 10';
- $item['id'] = $this->getURI() . ltrim($movie->find('.elco-title a', 0)->href, '/');
- $item['uri'] = $this->getURI() . ltrim($movie->find('.elco-title a', 0)->href, '/');
- $this->items[] = $item;
- }
- }
+ $item['id'] = $this->getURI() . ltrim($movie->find('.elco-title a', 0)->href, '/');
+ $item['uri'] = $this->getURI() . ltrim($movie->find('.elco-title a', 0)->href, '/');
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/SeznamZpravyBridge.php b/bridges/SeznamZpravyBridge.php
index 06072cdd..f052ed1c 100644
--- a/bridges/SeznamZpravyBridge.php
+++ b/bridges/SeznamZpravyBridge.php
@@ -1,125 +1,129 @@
<?php
-class SeznamZpravyBridge extends BridgeAbstract {
- const NAME = 'Seznam Zprávy Bridge';
- const URI = 'https://seznamzpravy.cz';
- const DESCRIPTION = 'Returns newest stories from Seznam Zprávy';
- const MAINTAINER = 'thezeroalpha';
- const PARAMETERS = array(
- 'By Author' => array(
- 'author' => array(
- 'name' => 'Author String',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'The dash-separated author string, as shown in the URL bar.',
- 'pattern' => '[a-z]+-[a-z]+-[0-9]+',
- 'exampleValue' => 'radek-nohl-1'
- ),
- )
- );
-
- private $feedName;
-
- public function getName() {
- if (isset($this->feedName)) {
- return $this->feedName;
- }
- return parent::getName();
- }
-
- public function collectData() {
- $ONE_DAY = 86500;
- switch($this->queriedContext) {
- case 'By Author':
- $url = 'https://www.seznamzpravy.cz/autor/';
- $selectors = array(
- 'breadcrumbs' => 'div[data-dot=ogm-breadcrumb-navigation]',
- 'articleList' => 'ul.ogm-document-timeline-page li article[data-dot=mol-timeline-item]',
- 'articleTitle' => 'a[data-dot=mol-article-card-title]',
- 'articleDM' => 'span.mol-formatted-date__date',
- 'articleTime' => 'span.mol-formatted-date__time',
- 'articleContent' => 'div[data-dot=ogm-article-content]',
- 'articleImage' => 'div[data-dot=ogm-main-media] img',
- 'articleParagraphs' => 'div[data-dot=mol-paragraph]'
- );
-
- $html = getSimpleHTMLDOMCached($url . $this->getInput('author'), $ONE_DAY);
- $mainBreadcrumbs = $html->find($selectors['breadcrumbs'], 0)
- or returnServerError('Could not get breadcrumbs for: ' . $this->getURI());
-
- $author = $mainBreadcrumbs->last_child()->plaintext
- or returnServerError('Could not get author for: ' . $this->getURI());
-
- $this->feedName = $author . ' - Seznam Zprávy';
-
- $articles = $html->find($selectors['articleList'])
- or returnServerError('Could not find articles for: ' . $this->getURI());
-
- foreach ($articles as $article) {
- // Get article URL
- $titleLink = $article->find($selectors['articleTitle'], 0)
- or returnServerError('Could not find title for: ' . $this->getURI());
- $articleURL = $titleLink->href;
-
- $articleContentHTML = getSimpleHTMLDOMCached($articleURL, $ONE_DAY);
-
- // Article header image
- $articleImageElem = $articleContentHTML->find($selectors['articleImage'], 0);
-
- // Article text content
- $contentElem = $articleContentHTML->find($selectors['articleContent'], 0)
- or returnServerError('Could not get article content for: ' . $articleURL);
- $contentParagraphs = $contentElem->find($selectors['articleParagraphs'])
- or returnServerError('Could not find paragraphs for: ' . $articleURL);
-
- // If the article has an image, put that image at the start
- $contentInitialValue = isset($articleImageElem) ? $articleImageElem->outertext : '';
- $contentText = array_reduce($contentParagraphs, function($s, $elem) {
- return $s . $elem->innertext;
- }, $contentInitialValue);
-
- // Article categories
- $breadcrumbsElem = $articleContentHTML->find($selectors['breadcrumbs'], 0)
- or returnServerError('Could not find breadcrumbs for: ' . $articleURL);
- $breadcrumbs = $breadcrumbsElem->children();
- $numBreadcrumbs = count($breadcrumbs);
- $categories = array();
- foreach ($breadcrumbs as $cat) {
- if (--$numBreadcrumbs <= 0) {
- break;
- }
- $categories[] = trim($cat->plaintext);
- }
-
- // Article date & time
- $articleTimeElem = $article->find($selectors['articleTime'], 0)
- or returnServerError('Could not find article time for: ' . $articleURL);
- $articleTime = $articleTimeElem->plaintext;
-
- $articleDMElem = $article->find($selectors['articleDM'], 0);
- if (isset($articleDMElem)) {
- $articleDMText = $articleDMElem->plaintext;
- } else {
- // If there is no date but only a time, the article was published today
- $articleDMText = date('d.m.');
- }
- $articleDMY = preg_replace('/[^0-9\.]/', '', $articleDMText) . date('Y');
-
- // Add article to items, potentially with header image as enclosure
- $item = array(
- 'title' => $titleLink->plaintext,
- 'uri' => $titleLink->href,
- 'timestamp' => strtotime($articleDMY . ' ' . $articleTime),
- 'author' => $author,
- 'content' => $contentText,
- 'categories' => $categories
- );
- if (isset($articleImageElem)) {
- $item['enclosures'] = array('https:' . $articleImageElem->src);
- }
- $this->items[] = $item;
- }
- break;
- }
- $this->items[] = $item;
- }
+
+class SeznamZpravyBridge extends BridgeAbstract
+{
+ const NAME = 'Seznam Zprávy Bridge';
+ const URI = 'https://seznamzpravy.cz';
+ const DESCRIPTION = 'Returns newest stories from Seznam Zprávy';
+ const MAINTAINER = 'thezeroalpha';
+ const PARAMETERS = [
+ 'By Author' => [
+ 'author' => [
+ 'name' => 'Author String',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'The dash-separated author string, as shown in the URL bar.',
+ 'pattern' => '[a-z]+-[a-z]+-[0-9]+',
+ 'exampleValue' => 'radek-nohl-1'
+ ],
+ ]
+ ];
+
+ private $feedName;
+
+ public function getName()
+ {
+ if (isset($this->feedName)) {
+ return $this->feedName;
+ }
+ return parent::getName();
+ }
+
+ public function collectData()
+ {
+ $ONE_DAY = 86500;
+ switch ($this->queriedContext) {
+ case 'By Author':
+ $url = 'https://www.seznamzpravy.cz/autor/';
+ $selectors = [
+ 'breadcrumbs' => 'div[data-dot=ogm-breadcrumb-navigation]',
+ 'articleList' => 'ul.ogm-document-timeline-page li article[data-dot=mol-timeline-item]',
+ 'articleTitle' => 'a[data-dot=mol-article-card-title]',
+ 'articleDM' => 'span.mol-formatted-date__date',
+ 'articleTime' => 'span.mol-formatted-date__time',
+ 'articleContent' => 'div[data-dot=ogm-article-content]',
+ 'articleImage' => 'div[data-dot=ogm-main-media] img',
+ 'articleParagraphs' => 'div[data-dot=mol-paragraph]'
+ ];
+
+ $html = getSimpleHTMLDOMCached($url . $this->getInput('author'), $ONE_DAY);
+ $mainBreadcrumbs = $html->find($selectors['breadcrumbs'], 0)
+ or returnServerError('Could not get breadcrumbs for: ' . $this->getURI());
+
+ $author = $mainBreadcrumbs->last_child()->plaintext
+ or returnServerError('Could not get author for: ' . $this->getURI());
+
+ $this->feedName = $author . ' - Seznam Zprávy';
+
+ $articles = $html->find($selectors['articleList'])
+ or returnServerError('Could not find articles for: ' . $this->getURI());
+
+ foreach ($articles as $article) {
+ // Get article URL
+ $titleLink = $article->find($selectors['articleTitle'], 0)
+ or returnServerError('Could not find title for: ' . $this->getURI());
+ $articleURL = $titleLink->href;
+
+ $articleContentHTML = getSimpleHTMLDOMCached($articleURL, $ONE_DAY);
+
+ // Article header image
+ $articleImageElem = $articleContentHTML->find($selectors['articleImage'], 0);
+
+ // Article text content
+ $contentElem = $articleContentHTML->find($selectors['articleContent'], 0)
+ or returnServerError('Could not get article content for: ' . $articleURL);
+ $contentParagraphs = $contentElem->find($selectors['articleParagraphs'])
+ or returnServerError('Could not find paragraphs for: ' . $articleURL);
+
+ // If the article has an image, put that image at the start
+ $contentInitialValue = isset($articleImageElem) ? $articleImageElem->outertext : '';
+ $contentText = array_reduce($contentParagraphs, function ($s, $elem) {
+ return $s . $elem->innertext;
+ }, $contentInitialValue);
+
+ // Article categories
+ $breadcrumbsElem = $articleContentHTML->find($selectors['breadcrumbs'], 0)
+ or returnServerError('Could not find breadcrumbs for: ' . $articleURL);
+ $breadcrumbs = $breadcrumbsElem->children();
+ $numBreadcrumbs = count($breadcrumbs);
+ $categories = [];
+ foreach ($breadcrumbs as $cat) {
+ if (--$numBreadcrumbs <= 0) {
+ break;
+ }
+ $categories[] = trim($cat->plaintext);
+ }
+
+ // Article date & time
+ $articleTimeElem = $article->find($selectors['articleTime'], 0)
+ or returnServerError('Could not find article time for: ' . $articleURL);
+ $articleTime = $articleTimeElem->plaintext;
+
+ $articleDMElem = $article->find($selectors['articleDM'], 0);
+ if (isset($articleDMElem)) {
+ $articleDMText = $articleDMElem->plaintext;
+ } else {
+ // If there is no date but only a time, the article was published today
+ $articleDMText = date('d.m.');
+ }
+ $articleDMY = preg_replace('/[^0-9\.]/', '', $articleDMText) . date('Y');
+
+ // Add article to items, potentially with header image as enclosure
+ $item = [
+ 'title' => $titleLink->plaintext,
+ 'uri' => $titleLink->href,
+ 'timestamp' => strtotime($articleDMY . ' ' . $articleTime),
+ 'author' => $author,
+ 'content' => $contentText,
+ 'categories' => $categories
+ ];
+ if (isset($articleImageElem)) {
+ $item['enclosures'] = ['https:' . $articleImageElem->src];
+ }
+ $this->items[] = $item;
+ }
+ break;
+ }
+ $this->items[] = $item;
+ }
}
diff --git a/bridges/ShanaprojectBridge.php b/bridges/ShanaprojectBridge.php
index 2ae793b0..ee9fac7c 100644
--- a/bridges/ShanaprojectBridge.php
+++ b/bridges/ShanaprojectBridge.php
@@ -1,182 +1,193 @@
<?php
-class ShanaprojectBridge extends BridgeAbstract {
- const MAINTAINER = 'logmanoriginal';
- const NAME = 'Shanaproject Bridge';
- const URI = 'https://www.shanaproject.com';
- const DESCRIPTION = 'Returns a list of anime from the current Season Anime List';
- const PARAMETERS = array(
- array(
- 'min_episodes' => array(
- 'name' => 'Minimum Episodes',
- 'type' => 'number',
- 'title' => 'Minimum number of episodes before including in feed',
- 'defaultValue' => 0,
- ),
- 'min_total_episodes' => array(
- 'name' => 'Minimum Total Episodes',
- 'type' => 'number',
- 'title' => 'Minimum total number of episodes before including in feed',
- 'defaultValue' => 0,
- ),
- 'require_banner' => array(
- 'name' => 'Require Banner',
- 'type' => 'checkbox',
- 'title' => 'Only include anime with custom banner image',
- 'defaultValue' => false,
- ),
- ),
- );
-
- private $uri;
-
- public function getURI() {
- return isset($this->uri) ? $this->uri : parent::getURI();
- }
-
- public function collectData(){
- $html = $this->loadSeasonAnimeList();
-
- $animes = $html->find('div.header_display_box_info')
- or returnServerError('Could not find anime headers!');
-
- $min_episodes = $this->getInput('min_episodes') ?: 0;
- $min_total_episodes = $this->getInput('min_total_episodes') ?: 0;
-
- foreach($animes as $anime) {
-
- list(
- $episodes_released,
- /* of */,
- $episodes_total
- ) = explode(' ', $this->extractAnimeEpisodeInformation($anime));
-
- // Skip if not enough episodes yet
- if ($episodes_released < $min_episodes) {
- continue;
- }
-
- // Skip if too many episodes in total
- if ($episodes_total !== '?' && $episodes_total < $min_total_episodes) {
- continue;
- }
-
- // Skip if https://static.shanaproject.com/no-art.jpg
- if ($this->getInput('require_banner')
- && strpos($this->extractAnimeBackgroundImage($anime), 'no-art') !== false) {
- continue;
- }
-
- $this->items[] = array(
- 'title' => $this->extractAnimeTitle($anime),
- 'author' => $this->extractAnimeAuthor($anime),
- 'uri' => $this->extractAnimeUri($anime),
- 'timestamp' => $this->extractAnimeTimestamp($anime),
- 'content' => $this->buildAnimeContent($anime),
- );
-
- }
- }
-
- // Returns an html object for the Season Anime List (latest season)
- private function loadSeasonAnimeList(){
-
- $html = getSimpleHTMLDOM(self::URI . '/seasons');
-
- $html = defaultLinkTo($html, self::URI . '/seasons');
-
- $season = $html->find('div.follows_menu > a', 1)
- or returnServerError('Could not find \'Season Anime List\'!');
-
- $html = getSimpleHTMLDOM($season->href);
-
- $this->uri = $season->href;
-
- $html = defaultLinkTo($html, $season->href);
-
- return $html;
-
- }
-
- // Extracts the anime title
- private function extractAnimeTitle($anime){
- $title = $anime->find('a', 0)
- or returnServerError('Could not find anime title!');
- return trim($title->innertext);
- }
-
- // Extracts the anime URI
- private function extractAnimeUri($anime){
- $uri = $anime->find('a', 0)
- or returnServerError('Could not find anime URI!');
- return $uri->href;
- }
-
- // Extracts the anime release date (timestamp)
- private function extractAnimeTimestamp($anime){
- $timestamp = $anime->find('span.header_info_block', 1);
-
- if(!$timestamp) {
- return null;
- }
-
- return strtotime($timestamp->innertext);
- }
-
- // Extracts the anime studio name (author)
- private function extractAnimeAuthor($anime){
- $author = $anime->find('span.header_info_block', 2);
-
- if(!$author) {
- return null; // Sometimes the studio is unknown, so leave empty
- }
-
- return trim($author->innertext);
- }
-
- // Extracts the episode information (x of y released)
- private function extractAnimeEpisodeInformation($anime){
- $episode = $anime->find('div.header_info_episode', 0)
- or returnServerError('Could not find anime episode information!');
-
- $retVal = preg_replace('/\r|\n/', ' ', $episode->plaintext);
- $retVal = preg_replace('/\s+/', ' ', $retVal);
-
- return $retVal;
- }
-
- // Extracts the background image
- private function extractAnimeBackgroundImage($anime){
- // Getting the picture is a little bit tricky as it is part of the style.
- // Luckily the style is part of the parent div :)
-
- if(preg_match('/url\(\/\/([^\)]+)\)/i', $anime->parent->style, $matches)) {
- return $matches[1];
- }
-
- returnServerError('Could not extract background image!');
- }
-
- // Builds an URI to search for a specific anime (subber is left empty)
- private function buildAnimeSearchUri($anime){
- return self::URI
- . '/search/?title='
- . urlencode($this->extractAnimeTitle($anime))
- . '&subber=';
- }
-
- // Builds the content string for a given anime
- private function buildAnimeContent($anime){
- // We'll use a template string to place our contents
- return '<a href="'
- . $this->extractAnimeUri($anime)
- . '"><img src="http://'
- . $this->extractAnimeBackgroundImage($anime)
- . '" alt="'
- . htmlspecialchars($this->extractAnimeTitle($anime))
- . '" style="border: 1px solid black"></a><br><p>'
- . $this->extractAnimeEpisodeInformation($anime)
- . '</p><br><p><a href="'
- . $this->buildAnimeSearchUri($anime)
- . '">Search episodes</a></p>';
- }
+
+class ShanaprojectBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Shanaproject Bridge';
+ const URI = 'https://www.shanaproject.com';
+ const DESCRIPTION = 'Returns a list of anime from the current Season Anime List';
+ const PARAMETERS = [
+ [
+ 'min_episodes' => [
+ 'name' => 'Minimum Episodes',
+ 'type' => 'number',
+ 'title' => 'Minimum number of episodes before including in feed',
+ 'defaultValue' => 0,
+ ],
+ 'min_total_episodes' => [
+ 'name' => 'Minimum Total Episodes',
+ 'type' => 'number',
+ 'title' => 'Minimum total number of episodes before including in feed',
+ 'defaultValue' => 0,
+ ],
+ 'require_banner' => [
+ 'name' => 'Require Banner',
+ 'type' => 'checkbox',
+ 'title' => 'Only include anime with custom banner image',
+ 'defaultValue' => false,
+ ],
+ ],
+ ];
+
+ private $uri;
+
+ public function getURI()
+ {
+ return isset($this->uri) ? $this->uri : parent::getURI();
+ }
+
+ public function collectData()
+ {
+ $html = $this->loadSeasonAnimeList();
+
+ $animes = $html->find('div.header_display_box_info')
+ or returnServerError('Could not find anime headers!');
+
+ $min_episodes = $this->getInput('min_episodes') ?: 0;
+ $min_total_episodes = $this->getInput('min_total_episodes') ?: 0;
+
+ foreach ($animes as $anime) {
+ list(
+ $episodes_released,
+ /* of */,
+ $episodes_total
+ ) = explode(' ', $this->extractAnimeEpisodeInformation($anime));
+
+ // Skip if not enough episodes yet
+ if ($episodes_released < $min_episodes) {
+ continue;
+ }
+
+ // Skip if too many episodes in total
+ if ($episodes_total !== '?' && $episodes_total < $min_total_episodes) {
+ continue;
+ }
+
+ // Skip if https://static.shanaproject.com/no-art.jpg
+ if (
+ $this->getInput('require_banner')
+ && strpos($this->extractAnimeBackgroundImage($anime), 'no-art') !== false
+ ) {
+ continue;
+ }
+
+ $this->items[] = [
+ 'title' => $this->extractAnimeTitle($anime),
+ 'author' => $this->extractAnimeAuthor($anime),
+ 'uri' => $this->extractAnimeUri($anime),
+ 'timestamp' => $this->extractAnimeTimestamp($anime),
+ 'content' => $this->buildAnimeContent($anime),
+ ];
+ }
+ }
+
+ // Returns an html object for the Season Anime List (latest season)
+ private function loadSeasonAnimeList()
+ {
+ $html = getSimpleHTMLDOM(self::URI . '/seasons');
+
+ $html = defaultLinkTo($html, self::URI . '/seasons');
+
+ $season = $html->find('div.follows_menu > a', 1)
+ or returnServerError('Could not find \'Season Anime List\'!');
+
+ $html = getSimpleHTMLDOM($season->href);
+
+ $this->uri = $season->href;
+
+ $html = defaultLinkTo($html, $season->href);
+
+ return $html;
+ }
+
+ // Extracts the anime title
+ private function extractAnimeTitle($anime)
+ {
+ $title = $anime->find('a', 0)
+ or returnServerError('Could not find anime title!');
+ return trim($title->innertext);
+ }
+
+ // Extracts the anime URI
+ private function extractAnimeUri($anime)
+ {
+ $uri = $anime->find('a', 0)
+ or returnServerError('Could not find anime URI!');
+ return $uri->href;
+ }
+
+ // Extracts the anime release date (timestamp)
+ private function extractAnimeTimestamp($anime)
+ {
+ $timestamp = $anime->find('span.header_info_block', 1);
+
+ if (!$timestamp) {
+ return null;
+ }
+
+ return strtotime($timestamp->innertext);
+ }
+
+ // Extracts the anime studio name (author)
+ private function extractAnimeAuthor($anime)
+ {
+ $author = $anime->find('span.header_info_block', 2);
+
+ if (!$author) {
+ return null; // Sometimes the studio is unknown, so leave empty
+ }
+
+ return trim($author->innertext);
+ }
+
+ // Extracts the episode information (x of y released)
+ private function extractAnimeEpisodeInformation($anime)
+ {
+ $episode = $anime->find('div.header_info_episode', 0)
+ or returnServerError('Could not find anime episode information!');
+
+ $retVal = preg_replace('/\r|\n/', ' ', $episode->plaintext);
+ $retVal = preg_replace('/\s+/', ' ', $retVal);
+
+ return $retVal;
+ }
+
+ // Extracts the background image
+ private function extractAnimeBackgroundImage($anime)
+ {
+ // Getting the picture is a little bit tricky as it is part of the style.
+ // Luckily the style is part of the parent div :)
+
+ if (preg_match('/url\(\/\/([^\)]+)\)/i', $anime->parent->style, $matches)) {
+ return $matches[1];
+ }
+
+ returnServerError('Could not extract background image!');
+ }
+
+ // Builds an URI to search for a specific anime (subber is left empty)
+ private function buildAnimeSearchUri($anime)
+ {
+ return self::URI
+ . '/search/?title='
+ . urlencode($this->extractAnimeTitle($anime))
+ . '&subber=';
+ }
+
+ // Builds the content string for a given anime
+ private function buildAnimeContent($anime)
+ {
+ // We'll use a template string to place our contents
+ return '<a href="'
+ . $this->extractAnimeUri($anime)
+ . '"><img src="http://'
+ . $this->extractAnimeBackgroundImage($anime)
+ . '" alt="'
+ . htmlspecialchars($this->extractAnimeTitle($anime))
+ . '" style="border: 1px solid black"></a><br><p>'
+ . $this->extractAnimeEpisodeInformation($anime)
+ . '</p><br><p><a href="'
+ . $this->buildAnimeSearchUri($anime)
+ . '">Search episodes</a></p>';
+ }
}
diff --git a/bridges/Shimmie2Bridge.php b/bridges/Shimmie2Bridge.php
index a279c77d..0a87d65e 100644
--- a/bridges/Shimmie2Bridge.php
+++ b/bridges/Shimmie2Bridge.php
@@ -1,37 +1,39 @@
<?php
-class Shimmie2Bridge extends DanbooruBridge {
+class Shimmie2Bridge extends DanbooruBridge
+{
+ const NAME = 'Shimmie v2';
+ const URI = 'https://shimmie.shishnet.org/';
+ const DESCRIPTION = 'Returns images from given page';
- const NAME = 'Shimmie v2';
- const URI = 'https://shimmie.shishnet.org/';
- const DESCRIPTION = 'Returns images from given page';
+ const PATHTODATA = '.shm-thumb-link';
+ const IDATTRIBUTE = 'data-post-id';
- const PATHTODATA = '.shm-thumb-link';
- const IDATTRIBUTE = 'data-post-id';
+ protected function getFullURI()
+ {
+ return $this->getURI()
+ . 'post/list/'
+ . $this->getInput('t')
+ . '/'
+ . $this->getInput('p');
+ }
- protected function getFullURI(){
- return $this->getURI()
- . 'post/list/'
- . $this->getInput('t')
- . '/'
- . $this->getInput('p');
- }
+ protected function getItemFromElement($element)
+ {
+ $item = [];
+ $item['uri'] = $this->getURI() . $element->href;
+ $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
+ $item['timestamp'] = time();
+ $thumbnailUri = $this->getURI() . $element->find('img', 0)->src;
+ $item['categories'] = explode(' ', $element->getAttribute('data-tags'));
+ $item['title'] = $this->getName() . ' | ' . $item['id'];
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $thumbnailUri
+ . '" /></a><br>Tags: '
+ . $element->getAttribute('data-tags');
- protected function getItemFromElement($element){
- $item = array();
- $item['uri'] = $this->getURI() . $element->href;
- $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
- $item['timestamp'] = time();
- $thumbnailUri = $this->getURI() . $element->find('img', 0)->src;
- $item['categories'] = explode(' ', $element->getAttribute('data-tags'));
- $item['title'] = $this->getName() . ' | ' . $item['id'];
- $item['content'] = '<a href="'
- . $item['uri']
- . '"><img src="'
- . $thumbnailUri
- . '" /></a><br>Tags: '
- . $element->getAttribute('data-tags');
-
- return $item;
- }
+ return $item;
+ }
}
diff --git a/bridges/SkimfeedBridge.php b/bridges/SkimfeedBridge.php
index d4475c77..0555af0f 100644
--- a/bridges/SkimfeedBridge.php
+++ b/bridges/SkimfeedBridge.php
@@ -1,713 +1,681 @@
<?php
-class SkimfeedBridge extends BridgeAbstract {
-
- const CONTEXT_NEWS_BOX = 'News box';
- const CONTEXT_HOT_TOPICS = 'Hot topics';
- const CONTEXT_TECH_NEWS = 'Tech news';
- const CONTEXT_CUSTOM = 'Custom feed';
-
- const NAME = 'Skimfeed Bridge';
- const URI = 'https://skimfeed.com';
- const DESCRIPTION = 'Returns feeds from Skimfeed, also supports custom feeds!';
- const MAINTAINER = 'logmanoriginal';
- const CACHE_TIMEOUT = 3600;
-
- const PARAMETERS = array(
- self::CONTEXT_NEWS_BOX => array( // auto-generated (see below)
- 'box_channel' => array(
- 'name' => 'Channel',
- 'type' => 'list',
- 'title' => 'Select your channel',
- 'values' => array(
- 'Hacker News' => '/news/hacker-news.html',
- 'QZ' => '/news/qz.html',
- 'The Verge' => '/news/the-verge.html',
- 'Slashdot' => '/news/slashdot.html',
- 'Lifehacker' => '/news/lifehacker.html',
- 'Gizmag' => '/news/gizmag.html',
- 'Fast Company' => '/news/fast-company.html',
- 'Engadget' => '/news/engadget.html',
- 'Wired' => '/news/wired.html',
- 'MakeUseOf' => '/news/makeuseof.html',
- 'Techcrunch' => '/news/techcrunch.html',
- 'Apple Insider' => '/news/apple-insider.html',
- 'ArsTechnica' => '/news/arstechnica.html',
- 'Tech in Asia' => '/news/tech-in-asia.html',
- 'FastCoExist' => '/news/fastcoexist.html',
- 'Digital Trends' => '/news/digital-trends.html',
- 'AnandTech' => '/news/anandtech.html',
- 'How to Geek' => '/news/how-to-geek.html',
- 'Geek' => '/news/geek.html',
- 'BBC Technology' => '/news/bbc-technology.html',
- 'Extreme Tech' => '/news/extreme-tech.html',
- 'Packet Storm Sec' => '/news/packet-storm-sec.html',
- 'MedGadget' => '/news/medgadget.html',
- 'Design' => '/news/design.html',
- 'The Next Web' => '/news/the-next-web.html',
- 'Bit-Tech' => '/news/bit-tech.html',
- 'Next Big Future' => '/news/next-big-future.html',
- 'A VC' => '/news/a-vc.html',
- 'Copyblogger' => '/news/copyblogger.html',
- 'Smashing Mag' => '/news/smashing-mag.html',
- 'Continuations' => '/news/continuations.html',
- 'Cult of Mac' => '/news/cult-of-mac.html',
- 'SecuriTeam' => '/news/securiteam.html',
- 'The Tech Block' => '/news/the-tech-block.html',
- 'BetaBeat' => '/news/betabeat.html',
- 'PC Mag' => '/news/pc-mag.html',
- 'Venture Beat' => '/news/venture-beat.html',
- 'ReadWriteWeb' => '/news/readwriteweb.html',
- 'High Scalability' => '/news/high-scalability.html',
- )
- )
- ),
- self::CONTEXT_HOT_TOPICS => array(),
- self::CONTEXT_TECH_NEWS => array( // auto-generated (see below)
- 'tech_channel' => array(
- 'name' => 'Tech channel',
- 'type' => 'list',
- 'title' => 'Select your tech channel',
- 'values' => array(
- 'Agg' => array(
- 'Reddit' => '/news/reddit.html',
- 'Tech Insider' => '/news/tech-insider.html',
- 'Digg' => '/news/digg.html',
- 'Meta Filter' => '/news/meta-filter.html',
- 'Fark' => '/news/fark.html',
- 'Mashable' => '/news/mashable.html',
- 'Ad Week' => '/news/ad-week.html',
- 'The Chive' => '/news/the-chive.html',
- 'BoingBoing' => '/news/boingboing.html',
- 'Vice' => '/news/vice.html',
- 'ClientsFromHell' => '/news/clientsfromhell.html',
- 'How Stuff Works' => '/news/how-stuff-works.html',
- 'Buzzfeed' => '/news/buzzfeed.html',
- 'BoingBoing' => '/news/boingboing.html',
- 'Cracked' => '/news/cracked.html',
- 'Weird News' => '/news/weird-news.html',
- 'ITOTD' => '/news/itotd.html',
- 'Metafilter' => '/news/metafilter.html',
- 'TheOnion' => '/news/theonion.html',
- ),
- 'Cars' => array(
- 'Reddit Cars' => '/news/reddit-cars.html',
- 'NYT Auto' => '/news/nyt-auto.html',
- 'Truth About Cars' => '/news/truth-about-cars.html',
- 'AutoBlog' => '/news/autoblog.html',
- 'AutoSpies' => '/news/autospies.html',
- 'Autoweek' => '/news/autoweek.html',
- 'The Garage' => '/news/the-garage.html',
- 'Car and Driver' => '/news/car-and-driver.html',
- 'EGM Car Tech' => '/news/egm-car-tech.html',
- 'Top Gear' => '/news/top-gear.html',
- 'eGarage' => '/news/egarage.html',
- ),
- 'Comics' => array(
- 'Penny Arcade' => '/news/penny-arcade.html',
- 'XKCD' => '/news/xkcd.html',
- 'Channelate' => '/news/channelate.html',
- 'Savage Chicken' => '/news/savage-chicken.html',
- 'Dinosaur Comics' => '/news/dinosaur-comics.html',
- 'Explosm' => '/news/explosm.html',
- 'PoorlyDLines' => '/news/poorlydlines.html',
- 'Moonbeard' => '/news/moonbeard.html',
- 'Nedroid' => '/news/nedroid.html',
- ),
- 'Design' => array(
- 'FastCoCreate' => '/news/fastcocreate.html',
- 'Dezeen' => '/news/dezeen.html',
- 'Design Boom' => '/news/design-boom.html',
- 'Mmminimal' => '/news/mmminimal.html',
- 'We Heart' => '/news/we-heart.html',
- 'CreativeBloq' => '/news/creativebloq.html',
- 'TheDSGNblog' => '/news/thedsgnblog.html',
- 'Grainedit' => '/news/grainedit.html',
- ),
- 'Football' => array(
- 'Mail Football' => '/news/mail-football.html',
- 'Yahoo Football' => '/news/yahoo-football.html',
- 'FourFourTwo' => '/news/fourfourtwo.html',
- 'Goal' => '/news/goal.html',
- 'BBC Football' => '/news/bbc-football.html',
- 'TalkSport' => '/news/talksport.html',
- '101 Great Goals' => '/news/101-great-goals.html',
- 'Who Scored' => '/news/who-scored.html',
- 'Football365 Champ' => '/news/football365-champ.html',
- 'Football365 Premier' => '/news/football365-premier.html',
- 'BleacherReport' => '/news/bleacherreport.html',
- ),
- 'Gaming' => array(
- 'Polygon' => '/news/polygon.html',
- 'Gamespot' => '/news/gamespot.html',
- 'RockPaperShotgun' => '/news/rockpapershotgun.html',
- 'VG247' => '/news/vg247.html',
- 'IGN' => '/news/ign.html',
- 'Reddit Games' => '/news/reddit-games.html',
- 'TouchArcade' => '/news/toucharcade.html',
- 'GamesRadar' => '/news/gamesradar.html',
- 'Siliconera' => '/news/siliconera.html',
- 'Reddit GameDeals' => '/news/reddit-gamedeals.html',
- 'Joystiq' => '/news/joystiq.html',
- 'GameInformer' => '/news/gameinformer.html',
- 'PSN Blog' => '/news/psn-blog.html',
- 'Reddit GamerNews' => '/news/reddit-gamernews.html',
- 'Steam' => '/news/steam.html',
- 'DualShockers' => '/news/dualshockers.html',
- 'ShackNews' => '/news/shacknews.html',
- 'CheapAssGamer' => '/news/cheapassgamer.html',
- 'Eurogamer' => '/news/eurogamer.html',
- 'Major Nelson' => '/news/major-nelson.html',
- 'Reddit Truegaming' => '/news/reddit-truegaming.html',
- 'GameTrailers' => '/news/gametrailers.html',
- 'GamaSutra' => '/news/gamasutra.html',
- 'USGamer' => '/news/usgamer.html',
- 'Shoryuken' => '/news/shoryuken.html',
- 'Destructoid' => '/news/destructoid.html',
- 'ArsGaming' => '/news/arsgaming.html',
- 'XBOX Blog' => '/news/xbox-blog.html',
- 'GiantBomb' => '/news/giantbomb.html',
- 'VideoGamer' => '/news/videogamer.html',
- 'Pocket Tactics' => '/news/pocket-tactics.html',
- 'WiredGaming' => '/news/wiredgaming.html',
- 'AllGamesBeta' => '/news/allgamesbeta.html',
- 'OnGamers' => '/news/ongamers.html',
- 'Reddit GameBundles' => '/news/reddit-gamebundles.html',
- 'Kotaku' => '/news/kotaku.html',
- 'PCGamer' => '/news/pcgamer.html',
- ),
- 'Investing' => array(
- 'Seeking Alpha' => '/news/seeking-alpha.html',
- 'BBC Business' => '/news/bbc-business.html',
- 'Harvard Biz' => '/news/harvard-biz.html',
- 'Market Watch' => '/news/market-watch.html',
- 'Investor Place' => '/news/investor-place.html',
- 'Money Week' => '/news/money-week.html',
- 'Moneybeat' => '/news/moneybeat.html',
- 'Dealbook' => '/news/dealbook.html',
- 'Economist Business' => '/news/economist-business.html',
- 'Economist' => '/news/economist.html',
- 'Economist CN' => '/news/economist-cn.html',
- ),
- 'Long' => array(
- 'The Atlantic' => '/news/the-atlantic.html',
- 'Reddit Long' => '/news/reddit-long.html',
- 'Paris Review' => '/news/paris-review.html',
- 'New Yorker' => '/news/new-yorker.html',
- 'LongForm' => '/news/longform.html',
- 'LongReads' => '/news/longreads.html',
- 'The Browser' => '/news/the-browser.html',
- 'The Feature' => '/news/the-feature.html',
- ),
- 'MMA' => array(
- 'MMA Weekly' => '/news/mma-weekly.html',
- 'MMAFighting' => '/news/mmafighting.html',
- 'Reddit MMA' => '/news/reddit-mma.html',
- 'Sherdog Articles' => '/news/sherdog-articles.html',
- 'FightLand Vice' => '/news/fightland-vice.html',
- 'Sherdog Forum' => '/news/sherdog-forum.html',
- 'MMA Junkie' => '/news/mma-junkie.html',
- 'Sherdog MMA Video' => '/news/sherdog-mma-video.html',
- 'BloodyElbow' => '/news/bloodyelbow.html',
- 'CageWriter' => '/news/cagewriter.html',
- 'Sherdog News' => '/news/sherdog-news.html',
- 'MMAForum' => '/news/mmaforum.html',
- 'MMA Junkie Radio' => '/news/mma-junkie-radio.html',
- 'UFC News' => '/news/ufc-news.html',
- 'FightLinker' => '/news/fightlinker.html',
- 'Bodybuilding MMA' => '/news/bodybuilding-mma.html',
- 'BleacherReport MMA' => '/news/bleacherreport-mma.html',
- 'FiveOuncesofPain' => '/news/fiveouncesofpain.html',
- 'Sherdog Pictures' => '/news/sherdog-pictures.html',
- 'CagePotato' => '/news/cagepotato.html',
- 'Sherdog Radio' => '/news/sherdog-radio.html',
- 'ProMMARadio' => '/news/prommaradio.html',
- ),
- 'Mobile' => array(
- 'Macrumors' => '/news/macrumors.html',
- 'Android Police' => '/news/android-police.html',
- 'GSM Arena' => '/news/gsm-arena.html',
- 'DigiTrend Mobile' => '/news/digitrend-mobile.html',
- 'Mobile Nation' => '/news/mobile-nation.html',
- 'TechRadar' => '/news/techradar.html',
- 'ZDNET Mobile' => '/news/zdnet-mobile.html',
- 'MacWorld' => '/news/macworld.html',
- 'Android Dev Blog' => '/news/android-dev-blog.html',
- ),
- 'News' => array(
- 'Daily Mail' => '/news/daily-mail.html',
- 'Business Insider' => '/news/business-insider.html',
- 'The Guardian' => '/news/the-guardian.html',
- 'Fox' => '/news/fox.html',
- 'BBC World' => '/news/bbc-world.html',
- 'MSNBC' => '/news/msnbc.html',
- 'ABC News' => '/news/abc-news.html',
- 'Al Jazeera' => '/news/al-jazeera.html',
- 'Business Insider India' => '/news/business-insider-india.html',
- 'Observer' => '/news/observer.html',
- 'NYT Tech' => '/news/nyt-tech.html',
- 'NYT World' => '/news/nyt-world.html',
- 'CNN' => '/news/cnn.html',
- 'Japan Times' => '/news/japan-times.html',
- 'WorldCrunch' => '/news/worldcrunch.html',
- 'Pro publica' => '/news/pro-publica.html',
- 'OZY' => '/news/ozy.html',
- 'Times of India' => '/news/times-of-india.html',
- 'The Australian' => '/news/the-australian.html',
- 'Harpers' => '/news/harpers.html',
- 'Moscow Times' => '/news/moscow-times.html',
- 'The Times' => '/news/the-times.html',
- 'Reuters Tech' => '/news/reuters-tech.html',
- ),
- 'Politics' => array(
- 'FreeRepublic' => '/news/freerepublic.html',
- 'Salon' => '/news/salon.html',
- 'DrudgeReport' => '/news/drudgereport.html',
- 'TheHill' => '/news/thehill.html',
- 'TheBlaze' => '/news/theblaze.html',
- 'InfoWars' => '/news/infowars.html',
- 'New Republic' => '/news/new-republic.html',
- 'WashTimes' => '/news/washtimes.html',
- 'RealCleanPol' => '/news/realcleanpol.html',
- 'Fact Check' => '/news/fact-check.html',
- 'DailyKos' => '/news/dailykos.html',
- 'NewsMax' => '/news/newsmax.html',
- 'Politico' => '/news/politico.html',
- 'Michelle Malkin' => '/news/michelle-malkin.html',
- ),
- 'Reddit' => array(
- 'R Movies' => '/news/r-movies.html',
- 'R News' => '/news/r-news.html',
- 'Futurology' => '/news/futurology.html',
- 'R All' => '/news/r-all.html',
- 'R Music' => '/news/r-music.html',
- 'R Askscience' => '/news/r-askscience.html',
- 'R Technology' => '/news/r-technology.html',
- 'R Bestof' => '/news/r-bestof.html',
- 'R Askreddit' => '/news/r-askreddit.html',
- 'R Worldnews' => '/news/r-worldnews.html',
- 'R Explainlikeimfive' => '/news/r-explainlikeimfive.html',
- 'R Iama' => '/news/r-iama.html',
- ),
- 'Science' => array(
- 'PhysOrg' => '/news/physorg.html',
- 'Hack-a-day' => '/news/hack-a-day.html',
- 'Reddit Science' => '/news/reddit-science.html',
- 'Stats Blog' => '/news/stats-blog.html',
- 'Flowing Data' => '/news/flowing-data.html',
- 'Eureka Alert' => '/news/eureka-alert.html',
- 'Robotics BizRev' => '/news/robotics-bizrev.html',
- 'Planet big Data' => '/news/planet-big-data.html',
- 'Makezine' => '/news/makezine.html',
- 'MIT Tech' => '/news/mit-tech.html',
- 'R Bloggers' => '/news/r-bloggers.html',
- 'DataIsBeautiful' => '/news/dataisbeautiful.html',
- 'Ted Videos' => '/news/ted-videos.html',
- 'Advanced Science' => '/news/advanced-science.html',
- 'Robotiq' => '/news/robotiq.html',
- 'Science Daily' => '/news/science-daily.html',
- 'IEEE Robotics' => '/news/ieee-robotics.html',
- 'PSFK' => '/news/psfk.html',
- 'Discover Magazine' => '/news/discover-magazine.html',
- 'DataTau' => '/news/datatau.html',
- 'RoboHub' => '/news/robohub.html',
- 'Discovery' => '/news/discovery.html',
- 'Smart Data' => '/news/smart-data.html',
- 'Whats Big Data' => '/news/whats-big-data.html',
- ),
- 'Tech' => array(
- 'Hacker News' => '/news/hacker-news.html',
- 'The Verge' => '/news/the-verge.html',
- 'Lifehacker' => '/news/lifehacker.html',
- 'Fast Company' => '/news/fast-company.html',
- 'ArsTechnica' => '/news/arstechnica.html',
- 'MakeUseOf' => '/news/makeuseof.html',
- 'FastCoExist' => '/news/fastcoexist.html',
- 'How to Geek' => '/news/how-to-geek.html',
- 'The Next Web' => '/news/the-next-web.html',
- 'Engadget' => '/news/engadget.html',
- 'Gizmag' => '/news/gizmag.html',
- 'QZ' => '/news/qz.html',
- 'Wired' => '/news/wired.html',
- 'Techcrunch' => '/news/techcrunch.html',
- 'Slashdot' => '/news/slashdot.html',
- 'Extreme Tech' => '/news/extreme-tech.html',
- 'AnandTech' => '/news/anandtech.html',
- 'Digital Trends' => '/news/digital-trends.html',
- 'Next Big Future' => '/news/next-big-future.html',
- 'Apple Insider' => '/news/apple-insider.html',
- 'Geek' => '/news/geek.html',
- 'BBC Technology' => '/news/bbc-technology.html',
- 'Bit-Tech' => '/news/bit-tech.html',
- 'Packet Storm Sec' => '/news/packet-storm-sec.html',
- 'Design' => '/news/design.html',
- 'High Scalability' => '/news/high-scalability.html',
- 'Smashing Mag' => '/news/smashing-mag.html',
- 'The Tech Block' => '/news/the-tech-block.html',
- 'A VC' => '/news/a-vc.html',
- 'Tech in Asia' => '/news/tech-in-asia.html',
- 'ReadWriteWeb' => '/news/readwriteweb.html',
- 'PC Mag' => '/news/pc-mag.html',
- 'Continuations' => '/news/continuations.html',
- 'Copyblogger' => '/news/copyblogger.html',
- 'Cult of Mac' => '/news/cult-of-mac.html',
- 'BetaBeat' => '/news/betabeat.html',
- 'MedGadget' => '/news/medgadget.html',
- 'SecuriTeam' => '/news/securiteam.html',
- 'Venture Beat' => '/news/venture-beat.html',
- ),
- 'Trend' => array(
- 'Trend Hunter' => '/news/trend-hunter.html',
- 'ApartmentT' => '/news/apartmentt.html',
- 'GQ' => '/news/gq.html',
- 'Digital Trends' => '/news/digital-trends.html',
- 'Cool Hunting' => '/news/cool-hunting.html',
- 'FastCoDesign' => '/news/fastcodesign.html',
- 'TC Startups' => '/news/tc-startups.html',
- 'Killer Startups' => '/news/killer-startups.html',
- 'DigiInfo' => '/news/digiinfo.html',
- 'New Startups' => '/news/new-startups.html',
- 'DigiTrends' => '/news/digitrends.html',
- ),
- 'Watches' => array(
- 'Hodinkee' => '/news/hodinkee.html',
- 'Quill and Pad' => '/news/quill-and-pad.html',
- 'Monochrome' => '/news/monochrome.html',
- 'Deployant' => '/news/deployant.html',
- 'Watches by SJX' => '/news/watches-by-sjx.html',
- 'Fratello Watches' => '/news/fratello-watches.html',
- 'A Blog to Watch' => '/news/a-blog-to-watch.html',
- 'Wound for Life' => '/news/wound-for-life.html',
- 'Watch Paper' => '/news/watch-paper.html',
- 'Watch Report' => '/news/watch-report.html',
- 'Perpetuelle' => '/news/perpetuelle.html',
- ),
- 'Youtube' => array(
- 'LinusTechTips' => '/news/linustechtips.html',
- 'MetalJesusRocks' => '/news/metaljesusrocks.html',
- 'TotalBiscuit' => '/news/totalbiscuit.html',
- 'DexBonus' => '/news/dexbonus.html',
- 'Lon Siedman' => '/news/lon-siedman.html',
- 'MKBHD' => '/news/mkbhd.html',
- 'Terry A Davis' => '/news/terry-a-davis.html',
- 'HappyConsole' => '/news/happyconsole.html',
- 'Austin Evans' => '/news/austin-evans.html',
- 'NCIX' => '/news/ncix.html',
- ),
- )
- ),
- ),
- self::CONTEXT_CUSTOM => array(
- 'config' => array(
- 'name' => 'Configuration',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Enter feed numbers from Skimfeed! e.g: 5,8,2,l,p,9,23',
- 'exampleValue' => '5'
- )
- ),
- 'global' => array(
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'title' => 'Limits the number of returned items in the feed',
- 'exampleValue' => 10
- )
- )
- );
-
- public function getURI() {
-
- switch($this->queriedContext) {
-
- case self::CONTEXT_NEWS_BOX:
-
- $channel = $this->getInput('box_channel');
-
- if($channel) {
- return static::URI . $channel;
- }
-
- break;
-
- case self::CONTEXT_HOT_TOPICS:
- return static::URI;
+class SkimfeedBridge extends BridgeAbstract
+{
+ const CONTEXT_NEWS_BOX = 'News box';
+ const CONTEXT_HOT_TOPICS = 'Hot topics';
+ const CONTEXT_TECH_NEWS = 'Tech news';
+ const CONTEXT_CUSTOM = 'Custom feed';
+
+ const NAME = 'Skimfeed Bridge';
+ const URI = 'https://skimfeed.com';
+ const DESCRIPTION = 'Returns feeds from Skimfeed, also supports custom feeds!';
+ const MAINTAINER = 'logmanoriginal';
+ const CACHE_TIMEOUT = 3600;
+
+ const PARAMETERS = [
+ self::CONTEXT_NEWS_BOX => [ // auto-generated (see below)
+ 'box_channel' => [
+ 'name' => 'Channel',
+ 'type' => 'list',
+ 'title' => 'Select your channel',
+ 'values' => [
+ 'Hacker News' => '/news/hacker-news.html',
+ 'QZ' => '/news/qz.html',
+ 'The Verge' => '/news/the-verge.html',
+ 'Slashdot' => '/news/slashdot.html',
+ 'Lifehacker' => '/news/lifehacker.html',
+ 'Gizmag' => '/news/gizmag.html',
+ 'Fast Company' => '/news/fast-company.html',
+ 'Engadget' => '/news/engadget.html',
+ 'Wired' => '/news/wired.html',
+ 'MakeUseOf' => '/news/makeuseof.html',
+ 'Techcrunch' => '/news/techcrunch.html',
+ 'Apple Insider' => '/news/apple-insider.html',
+ 'ArsTechnica' => '/news/arstechnica.html',
+ 'Tech in Asia' => '/news/tech-in-asia.html',
+ 'FastCoExist' => '/news/fastcoexist.html',
+ 'Digital Trends' => '/news/digital-trends.html',
+ 'AnandTech' => '/news/anandtech.html',
+ 'How to Geek' => '/news/how-to-geek.html',
+ 'Geek' => '/news/geek.html',
+ 'BBC Technology' => '/news/bbc-technology.html',
+ 'Extreme Tech' => '/news/extreme-tech.html',
+ 'Packet Storm Sec' => '/news/packet-storm-sec.html',
+ 'MedGadget' => '/news/medgadget.html',
+ 'Design' => '/news/design.html',
+ 'The Next Web' => '/news/the-next-web.html',
+ 'Bit-Tech' => '/news/bit-tech.html',
+ 'Next Big Future' => '/news/next-big-future.html',
+ 'A VC' => '/news/a-vc.html',
+ 'Copyblogger' => '/news/copyblogger.html',
+ 'Smashing Mag' => '/news/smashing-mag.html',
+ 'Continuations' => '/news/continuations.html',
+ 'Cult of Mac' => '/news/cult-of-mac.html',
+ 'SecuriTeam' => '/news/securiteam.html',
+ 'The Tech Block' => '/news/the-tech-block.html',
+ 'BetaBeat' => '/news/betabeat.html',
+ 'PC Mag' => '/news/pc-mag.html',
+ 'Venture Beat' => '/news/venture-beat.html',
+ 'ReadWriteWeb' => '/news/readwriteweb.html',
+ 'High Scalability' => '/news/high-scalability.html',
+ ]
+ ]
+ ],
+ self::CONTEXT_HOT_TOPICS => [],
+ self::CONTEXT_TECH_NEWS => [ // auto-generated (see below)
+ 'tech_channel' => [
+ 'name' => 'Tech channel',
+ 'type' => 'list',
+ 'title' => 'Select your tech channel',
+ 'values' => [
+ 'Agg' => [
+ 'Reddit' => '/news/reddit.html',
+ 'Tech Insider' => '/news/tech-insider.html',
+ 'Digg' => '/news/digg.html',
+ 'Meta Filter' => '/news/meta-filter.html',
+ 'Fark' => '/news/fark.html',
+ 'Mashable' => '/news/mashable.html',
+ 'Ad Week' => '/news/ad-week.html',
+ 'The Chive' => '/news/the-chive.html',
+ 'BoingBoing' => '/news/boingboing.html',
+ 'Vice' => '/news/vice.html',
+ 'ClientsFromHell' => '/news/clientsfromhell.html',
+ 'How Stuff Works' => '/news/how-stuff-works.html',
+ 'Buzzfeed' => '/news/buzzfeed.html',
+ 'BoingBoing' => '/news/boingboing.html',
+ 'Cracked' => '/news/cracked.html',
+ 'Weird News' => '/news/weird-news.html',
+ 'ITOTD' => '/news/itotd.html',
+ 'Metafilter' => '/news/metafilter.html',
+ 'TheOnion' => '/news/theonion.html',
+ ],
+ 'Cars' => [
+ 'Reddit Cars' => '/news/reddit-cars.html',
+ 'NYT Auto' => '/news/nyt-auto.html',
+ 'Truth About Cars' => '/news/truth-about-cars.html',
+ 'AutoBlog' => '/news/autoblog.html',
+ 'AutoSpies' => '/news/autospies.html',
+ 'Autoweek' => '/news/autoweek.html',
+ 'The Garage' => '/news/the-garage.html',
+ 'Car and Driver' => '/news/car-and-driver.html',
+ 'EGM Car Tech' => '/news/egm-car-tech.html',
+ 'Top Gear' => '/news/top-gear.html',
+ 'eGarage' => '/news/egarage.html',
+ ],
+ 'Comics' => [
+ 'Penny Arcade' => '/news/penny-arcade.html',
+ 'XKCD' => '/news/xkcd.html',
+ 'Channelate' => '/news/channelate.html',
+ 'Savage Chicken' => '/news/savage-chicken.html',
+ 'Dinosaur Comics' => '/news/dinosaur-comics.html',
+ 'Explosm' => '/news/explosm.html',
+ 'PoorlyDLines' => '/news/poorlydlines.html',
+ 'Moonbeard' => '/news/moonbeard.html',
+ 'Nedroid' => '/news/nedroid.html',
+ ],
+ 'Design' => [
+ 'FastCoCreate' => '/news/fastcocreate.html',
+ 'Dezeen' => '/news/dezeen.html',
+ 'Design Boom' => '/news/design-boom.html',
+ 'Mmminimal' => '/news/mmminimal.html',
+ 'We Heart' => '/news/we-heart.html',
+ 'CreativeBloq' => '/news/creativebloq.html',
+ 'TheDSGNblog' => '/news/thedsgnblog.html',
+ 'Grainedit' => '/news/grainedit.html',
+ ],
+ 'Football' => [
+ 'Mail Football' => '/news/mail-football.html',
+ 'Yahoo Football' => '/news/yahoo-football.html',
+ 'FourFourTwo' => '/news/fourfourtwo.html',
+ 'Goal' => '/news/goal.html',
+ 'BBC Football' => '/news/bbc-football.html',
+ 'TalkSport' => '/news/talksport.html',
+ '101 Great Goals' => '/news/101-great-goals.html',
+ 'Who Scored' => '/news/who-scored.html',
+ 'Football365 Champ' => '/news/football365-champ.html',
+ 'Football365 Premier' => '/news/football365-premier.html',
+ 'BleacherReport' => '/news/bleacherreport.html',
+ ],
+ 'Gaming' => [
+ 'Polygon' => '/news/polygon.html',
+ 'Gamespot' => '/news/gamespot.html',
+ 'RockPaperShotgun' => '/news/rockpapershotgun.html',
+ 'VG247' => '/news/vg247.html',
+ 'IGN' => '/news/ign.html',
+ 'Reddit Games' => '/news/reddit-games.html',
+ 'TouchArcade' => '/news/toucharcade.html',
+ 'GamesRadar' => '/news/gamesradar.html',
+ 'Siliconera' => '/news/siliconera.html',
+ 'Reddit GameDeals' => '/news/reddit-gamedeals.html',
+ 'Joystiq' => '/news/joystiq.html',
+ 'GameInformer' => '/news/gameinformer.html',
+ 'PSN Blog' => '/news/psn-blog.html',
+ 'Reddit GamerNews' => '/news/reddit-gamernews.html',
+ 'Steam' => '/news/steam.html',
+ 'DualShockers' => '/news/dualshockers.html',
+ 'ShackNews' => '/news/shacknews.html',
+ 'CheapAssGamer' => '/news/cheapassgamer.html',
+ 'Eurogamer' => '/news/eurogamer.html',
+ 'Major Nelson' => '/news/major-nelson.html',
+ 'Reddit Truegaming' => '/news/reddit-truegaming.html',
+ 'GameTrailers' => '/news/gametrailers.html',
+ 'GamaSutra' => '/news/gamasutra.html',
+ 'USGamer' => '/news/usgamer.html',
+ 'Shoryuken' => '/news/shoryuken.html',
+ 'Destructoid' => '/news/destructoid.html',
+ 'ArsGaming' => '/news/arsgaming.html',
+ 'XBOX Blog' => '/news/xbox-blog.html',
+ 'GiantBomb' => '/news/giantbomb.html',
+ 'VideoGamer' => '/news/videogamer.html',
+ 'Pocket Tactics' => '/news/pocket-tactics.html',
+ 'WiredGaming' => '/news/wiredgaming.html',
+ 'AllGamesBeta' => '/news/allgamesbeta.html',
+ 'OnGamers' => '/news/ongamers.html',
+ 'Reddit GameBundles' => '/news/reddit-gamebundles.html',
+ 'Kotaku' => '/news/kotaku.html',
+ 'PCGamer' => '/news/pcgamer.html',
+ ],
+ 'Investing' => [
+ 'Seeking Alpha' => '/news/seeking-alpha.html',
+ 'BBC Business' => '/news/bbc-business.html',
+ 'Harvard Biz' => '/news/harvard-biz.html',
+ 'Market Watch' => '/news/market-watch.html',
+ 'Investor Place' => '/news/investor-place.html',
+ 'Money Week' => '/news/money-week.html',
+ 'Moneybeat' => '/news/moneybeat.html',
+ 'Dealbook' => '/news/dealbook.html',
+ 'Economist Business' => '/news/economist-business.html',
+ 'Economist' => '/news/economist.html',
+ 'Economist CN' => '/news/economist-cn.html',
+ ],
+ 'Long' => [
+ 'The Atlantic' => '/news/the-atlantic.html',
+ 'Reddit Long' => '/news/reddit-long.html',
+ 'Paris Review' => '/news/paris-review.html',
+ 'New Yorker' => '/news/new-yorker.html',
+ 'LongForm' => '/news/longform.html',
+ 'LongReads' => '/news/longreads.html',
+ 'The Browser' => '/news/the-browser.html',
+ 'The Feature' => '/news/the-feature.html',
+ ],
+ 'MMA' => [
+ 'MMA Weekly' => '/news/mma-weekly.html',
+ 'MMAFighting' => '/news/mmafighting.html',
+ 'Reddit MMA' => '/news/reddit-mma.html',
+ 'Sherdog Articles' => '/news/sherdog-articles.html',
+ 'FightLand Vice' => '/news/fightland-vice.html',
+ 'Sherdog Forum' => '/news/sherdog-forum.html',
+ 'MMA Junkie' => '/news/mma-junkie.html',
+ 'Sherdog MMA Video' => '/news/sherdog-mma-video.html',
+ 'BloodyElbow' => '/news/bloodyelbow.html',
+ 'CageWriter' => '/news/cagewriter.html',
+ 'Sherdog News' => '/news/sherdog-news.html',
+ 'MMAForum' => '/news/mmaforum.html',
+ 'MMA Junkie Radio' => '/news/mma-junkie-radio.html',
+ 'UFC News' => '/news/ufc-news.html',
+ 'FightLinker' => '/news/fightlinker.html',
+ 'Bodybuilding MMA' => '/news/bodybuilding-mma.html',
+ 'BleacherReport MMA' => '/news/bleacherreport-mma.html',
+ 'FiveOuncesofPain' => '/news/fiveouncesofpain.html',
+ 'Sherdog Pictures' => '/news/sherdog-pictures.html',
+ 'CagePotato' => '/news/cagepotato.html',
+ 'Sherdog Radio' => '/news/sherdog-radio.html',
+ 'ProMMARadio' => '/news/prommaradio.html',
+ ],
+ 'Mobile' => [
+ 'Macrumors' => '/news/macrumors.html',
+ 'Android Police' => '/news/android-police.html',
+ 'GSM Arena' => '/news/gsm-arena.html',
+ 'DigiTrend Mobile' => '/news/digitrend-mobile.html',
+ 'Mobile Nation' => '/news/mobile-nation.html',
+ 'TechRadar' => '/news/techradar.html',
+ 'ZDNET Mobile' => '/news/zdnet-mobile.html',
+ 'MacWorld' => '/news/macworld.html',
+ 'Android Dev Blog' => '/news/android-dev-blog.html',
+ ],
+ 'News' => [
+ 'Daily Mail' => '/news/daily-mail.html',
+ 'Business Insider' => '/news/business-insider.html',
+ 'The Guardian' => '/news/the-guardian.html',
+ 'Fox' => '/news/fox.html',
+ 'BBC World' => '/news/bbc-world.html',
+ 'MSNBC' => '/news/msnbc.html',
+ 'ABC News' => '/news/abc-news.html',
+ 'Al Jazeera' => '/news/al-jazeera.html',
+ 'Business Insider India' => '/news/business-insider-india.html',
+ 'Observer' => '/news/observer.html',
+ 'NYT Tech' => '/news/nyt-tech.html',
+ 'NYT World' => '/news/nyt-world.html',
+ 'CNN' => '/news/cnn.html',
+ 'Japan Times' => '/news/japan-times.html',
+ 'WorldCrunch' => '/news/worldcrunch.html',
+ 'Pro publica' => '/news/pro-publica.html',
+ 'OZY' => '/news/ozy.html',
+ 'Times of India' => '/news/times-of-india.html',
+ 'The Australian' => '/news/the-australian.html',
+ 'Harpers' => '/news/harpers.html',
+ 'Moscow Times' => '/news/moscow-times.html',
+ 'The Times' => '/news/the-times.html',
+ 'Reuters Tech' => '/news/reuters-tech.html',
+ ],
+ 'Politics' => [
+ 'FreeRepublic' => '/news/freerepublic.html',
+ 'Salon' => '/news/salon.html',
+ 'DrudgeReport' => '/news/drudgereport.html',
+ 'TheHill' => '/news/thehill.html',
+ 'TheBlaze' => '/news/theblaze.html',
+ 'InfoWars' => '/news/infowars.html',
+ 'New Republic' => '/news/new-republic.html',
+ 'WashTimes' => '/news/washtimes.html',
+ 'RealCleanPol' => '/news/realcleanpol.html',
+ 'Fact Check' => '/news/fact-check.html',
+ 'DailyKos' => '/news/dailykos.html',
+ 'NewsMax' => '/news/newsmax.html',
+ 'Politico' => '/news/politico.html',
+ 'Michelle Malkin' => '/news/michelle-malkin.html',
+ ],
+ 'Reddit' => [
+ 'R Movies' => '/news/r-movies.html',
+ 'R News' => '/news/r-news.html',
+ 'Futurology' => '/news/futurology.html',
+ 'R All' => '/news/r-all.html',
+ 'R Music' => '/news/r-music.html',
+ 'R Askscience' => '/news/r-askscience.html',
+ 'R Technology' => '/news/r-technology.html',
+ 'R Bestof' => '/news/r-bestof.html',
+ 'R Askreddit' => '/news/r-askreddit.html',
+ 'R Worldnews' => '/news/r-worldnews.html',
+ 'R Explainlikeimfive' => '/news/r-explainlikeimfive.html',
+ 'R Iama' => '/news/r-iama.html',
+ ],
+ 'Science' => [
+ 'PhysOrg' => '/news/physorg.html',
+ 'Hack-a-day' => '/news/hack-a-day.html',
+ 'Reddit Science' => '/news/reddit-science.html',
+ 'Stats Blog' => '/news/stats-blog.html',
+ 'Flowing Data' => '/news/flowing-data.html',
+ 'Eureka Alert' => '/news/eureka-alert.html',
+ 'Robotics BizRev' => '/news/robotics-bizrev.html',
+ 'Planet big Data' => '/news/planet-big-data.html',
+ 'Makezine' => '/news/makezine.html',
+ 'MIT Tech' => '/news/mit-tech.html',
+ 'R Bloggers' => '/news/r-bloggers.html',
+ 'DataIsBeautiful' => '/news/dataisbeautiful.html',
+ 'Ted Videos' => '/news/ted-videos.html',
+ 'Advanced Science' => '/news/advanced-science.html',
+ 'Robotiq' => '/news/robotiq.html',
+ 'Science Daily' => '/news/science-daily.html',
+ 'IEEE Robotics' => '/news/ieee-robotics.html',
+ 'PSFK' => '/news/psfk.html',
+ 'Discover Magazine' => '/news/discover-magazine.html',
+ 'DataTau' => '/news/datatau.html',
+ 'RoboHub' => '/news/robohub.html',
+ 'Discovery' => '/news/discovery.html',
+ 'Smart Data' => '/news/smart-data.html',
+ 'Whats Big Data' => '/news/whats-big-data.html',
+ ],
+ 'Tech' => [
+ 'Hacker News' => '/news/hacker-news.html',
+ 'The Verge' => '/news/the-verge.html',
+ 'Lifehacker' => '/news/lifehacker.html',
+ 'Fast Company' => '/news/fast-company.html',
+ 'ArsTechnica' => '/news/arstechnica.html',
+ 'MakeUseOf' => '/news/makeuseof.html',
+ 'FastCoExist' => '/news/fastcoexist.html',
+ 'How to Geek' => '/news/how-to-geek.html',
+ 'The Next Web' => '/news/the-next-web.html',
+ 'Engadget' => '/news/engadget.html',
+ 'Gizmag' => '/news/gizmag.html',
+ 'QZ' => '/news/qz.html',
+ 'Wired' => '/news/wired.html',
+ 'Techcrunch' => '/news/techcrunch.html',
+ 'Slashdot' => '/news/slashdot.html',
+ 'Extreme Tech' => '/news/extreme-tech.html',
+ 'AnandTech' => '/news/anandtech.html',
+ 'Digital Trends' => '/news/digital-trends.html',
+ 'Next Big Future' => '/news/next-big-future.html',
+ 'Apple Insider' => '/news/apple-insider.html',
+ 'Geek' => '/news/geek.html',
+ 'BBC Technology' => '/news/bbc-technology.html',
+ 'Bit-Tech' => '/news/bit-tech.html',
+ 'Packet Storm Sec' => '/news/packet-storm-sec.html',
+ 'Design' => '/news/design.html',
+ 'High Scalability' => '/news/high-scalability.html',
+ 'Smashing Mag' => '/news/smashing-mag.html',
+ 'The Tech Block' => '/news/the-tech-block.html',
+ 'A VC' => '/news/a-vc.html',
+ 'Tech in Asia' => '/news/tech-in-asia.html',
+ 'ReadWriteWeb' => '/news/readwriteweb.html',
+ 'PC Mag' => '/news/pc-mag.html',
+ 'Continuations' => '/news/continuations.html',
+ 'Copyblogger' => '/news/copyblogger.html',
+ 'Cult of Mac' => '/news/cult-of-mac.html',
+ 'BetaBeat' => '/news/betabeat.html',
+ 'MedGadget' => '/news/medgadget.html',
+ 'SecuriTeam' => '/news/securiteam.html',
+ 'Venture Beat' => '/news/venture-beat.html',
+ ],
+ 'Trend' => [
+ 'Trend Hunter' => '/news/trend-hunter.html',
+ 'ApartmentT' => '/news/apartmentt.html',
+ 'GQ' => '/news/gq.html',
+ 'Digital Trends' => '/news/digital-trends.html',
+ 'Cool Hunting' => '/news/cool-hunting.html',
+ 'FastCoDesign' => '/news/fastcodesign.html',
+ 'TC Startups' => '/news/tc-startups.html',
+ 'Killer Startups' => '/news/killer-startups.html',
+ 'DigiInfo' => '/news/digiinfo.html',
+ 'New Startups' => '/news/new-startups.html',
+ 'DigiTrends' => '/news/digitrends.html',
+ ],
+ 'Watches' => [
+ 'Hodinkee' => '/news/hodinkee.html',
+ 'Quill and Pad' => '/news/quill-and-pad.html',
+ 'Monochrome' => '/news/monochrome.html',
+ 'Deployant' => '/news/deployant.html',
+ 'Watches by SJX' => '/news/watches-by-sjx.html',
+ 'Fratello Watches' => '/news/fratello-watches.html',
+ 'A Blog to Watch' => '/news/a-blog-to-watch.html',
+ 'Wound for Life' => '/news/wound-for-life.html',
+ 'Watch Paper' => '/news/watch-paper.html',
+ 'Watch Report' => '/news/watch-report.html',
+ 'Perpetuelle' => '/news/perpetuelle.html',
+ ],
+ 'Youtube' => [
+ 'LinusTechTips' => '/news/linustechtips.html',
+ 'MetalJesusRocks' => '/news/metaljesusrocks.html',
+ 'TotalBiscuit' => '/news/totalbiscuit.html',
+ 'DexBonus' => '/news/dexbonus.html',
+ 'Lon Siedman' => '/news/lon-siedman.html',
+ 'MKBHD' => '/news/mkbhd.html',
+ 'Terry A Davis' => '/news/terry-a-davis.html',
+ 'HappyConsole' => '/news/happyconsole.html',
+ 'Austin Evans' => '/news/austin-evans.html',
+ 'NCIX' => '/news/ncix.html',
+ ],
+ ]
+ ],
+ ],
+ self::CONTEXT_CUSTOM => [
+ 'config' => [
+ 'name' => 'Configuration',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Enter feed numbers from Skimfeed! e.g: 5,8,2,l,p,9,23',
+ 'exampleValue' => '5'
+ ]
+ ],
+ 'global' => [
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'title' => 'Limits the number of returned items in the feed',
+ 'exampleValue' => 10
+ ]
+ ]
+ ];
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case self::CONTEXT_NEWS_BOX:
+ $channel = $this->getInput('box_channel');
+
+ if ($channel) {
+ return static::URI . $channel;
+ }
+
+ break;
+
+ case self::CONTEXT_HOT_TOPICS:
+ return static::URI;
+
+ case self::CONTEXT_TECH_NEWS:
+ $channel = $this->getInput('tech_channel');
+
+ if ($channel) {
+ return static::URI . $channel;
+ }
+
+ break;
+
+ case self::CONTEXT_CUSTOM:
+ $config = $this->getInput('config');
+
+ return static::URI . '/custom.php?f=' . urlencode($config);
+ }
+
+ return parent::getURI();
+ }
+
+ public function detectParameters($url)
+ {
+ if (0 !== strpos($url, static::URI)) {
+ return null;
+ }
+
+ foreach (self::PARAMETERS as $channels) {
+ foreach ($channels as $box_name => $box) {
+ foreach ($box['values'] as $name => $channel_url) {
+ if (static::URI . $channel_url === $url) {
+ return [
+ $box_name => $name,
+ ];
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case self::CONTEXT_NEWS_BOX:
+ $channel = $this->getInput('box_channel');
+
+ $title = array_search(
+ $channel,
+ static::PARAMETERS[self::CONTEXT_NEWS_BOX]['box_channel']['values']
+ );
+
+ return $title . ' - ' . static::NAME;
+
+ case self::CONTEXT_HOT_TOPICS:
+ return 'Hot topics - ' . static::NAME;
+
+ case self::CONTEXT_TECH_NEWS:
+ $channel = $this->getInput('tech_channel');
+
+ $titles = [];
+
+ foreach (static::PARAMETERS[self::CONTEXT_TECH_NEWS]['tech_channel']['values'] as $ch) {
+ $titles = array_merge($titles, $ch);
+ }
+
+ $title = array_search($channel, $titles);
+
+ return $title . ' - ' . static::NAME;
+
+ case self::CONTEXT_CUSTOM:
+ return 'Custom - ' . static::NAME;
+ }
+
+ return parent::getName();
+ }
+
+ public function collectData()
+ {
+ // enable to export parameter lists
+ // $this->exportBoxChannels(); die;
+ // $this->exportTechChannels(); die;
- case self::CONTEXT_TECH_NEWS:
+ $html = getSimpleHTMLDOM($this->getURI());
- $channel = $this->getInput('tech_channel');
+ defaultLinkTo($html, static::URI);
- if($channel) {
- return static::URI . $channel;
- }
+ switch ($this->queriedContext) {
+ case self::CONTEXT_NEWS_BOX:
+ $author = array_search(
+ $this->getInput('box_channel'),
+ static::PARAMETERS[self::CONTEXT_NEWS_BOX]['box_channel']['values']
+ );
- break;
+ $author = '<a href="'
+ . $this->getURI()
+ . '">'
+ . $author
+ . '</a>';
- case self::CONTEXT_CUSTOM:
+ $this->extractFeed($html, $author);
+ break;
- $config = $this->getInput('config');
+ case self::CONTEXT_HOT_TOPICS:
+ $this->extractHotTopics($html);
+ break;
- return static::URI . '/custom.php?f=' . urlencode($config);
+ case self::CONTEXT_TECH_NEWS:
+ $authors = [];
- }
+ foreach (static::PARAMETERS[self::CONTEXT_TECH_NEWS]['tech_channel']['values'] as $ch) {
+ $authors = array_merge($authors, $ch);
+ }
- return parent::getURI();
+ $author = '<a href="'
+ . $this->getURI()
+ . '">'
+ . array_search($this->getInput('tech_channel'), $authors)
+ . '</a>';
- }
+ $this->extractFeed($html, $author);
+ break;
- public function detectParameters($url) {
+ case self::CONTEXT_CUSTOM:
+ $this->extractCustomFeed($html);
+ break;
+ }
+ }
- if (0 !== strpos($url, static::URI)) {
- return null;
- }
+ private function extractFeed($html, $author)
+ {
+ $articles = $html->find('li')
+ or returnServerError('Could not find articles!');
- foreach(self::PARAMETERS as $channels) {
+ if (
+ count($articles) === 1
+ && stristr($articles[0]->plaintext, 'Nothing new in the last 48 hours')
+ ) {
+ return; // Nothing to show
+ }
- foreach($channels as $box_name => $box) {
+ $limit = $this->getInput('limit') ?: -1;
- foreach($box['values'] as $name => $channel_url) {
+ foreach ($articles as $article) {
+ $anchor = $article->find('a', 0)
+ or returnServerError('Could not find anchor!');
- if (static::URI . $channel_url === $url) {
- return array(
- $box_name => $name,
- );
+ $item = [];
- }
+ $item['uri'] = $this->getTarget($anchor);
+ $item['title'] = trim($anchor->plaintext);
- }
+ // The timestamp is encoded as relative time (max. the last 48 hours)
+ // like this: "- 7 hours". It should always be at the end of the article:
+ $age = substr($article->plaintext, strrpos($article->plaintext, '-'));
- }
+ $item['timestamp'] = strtotime($age);
+ $item['author'] = $author;
- }
+ $this->items[] = $item;
- return null;
+ if ($limit > 0 && count($this->items) >= $limit) {
+ return;
+ }
+ }
+ }
- }
+ private function extractHotTopics($html)
+ {
+ $topics = $html->find('#popbox ul li')
+ or returnServerError('Could not find topics!');
+
+ $limit = $this->getInput('limit') ?: -1;
- public function getName() {
+ foreach ($topics as $topic) {
+ $anchor = $topic->find('a', 0)
+ or returnServerError('Could not find anchor!');
+
+ $item = [];
+
+ $item['uri'] = $this->getTarget($anchor);
+ $item['title'] = $anchor->title;
- switch($this->queriedContext) {
+ $this->items[] = $item;
- case self::CONTEXT_NEWS_BOX:
+ if ($limit > 0 && count($this->items) >= $limit) {
+ return;
+ }
+ }
+ }
+
+ private function extractCustomFeed($html)
+ {
+ $boxes = $html->find('#boxx .boxes')
+ or returnServerError('Could not find boxes!');
+
+ foreach ($boxes as $box) {
+ $anchor = $box->find('span.boxtitles a', 0)
+ or returnServerError('Could not find box anchor!');
- $channel = $this->getInput('box_channel');
+ $author = '<a href="' . $anchor->href . '">' . trim($anchor->plaintext) . '</a>';
+ $uri = $anchor->href;
- $title = array_search(
- $channel,
- static::PARAMETERS[self::CONTEXT_NEWS_BOX]['box_channel']['values']
- );
+ $box_html = getSimpleHTMLDOM($uri)
+ or returnServerError('Could not load custom feed!');
- return $title . ' - ' . static::NAME;
+ $this->extractFeed($box_html, $author);
+ }
+ }
- case self::CONTEXT_HOT_TOPICS:
- return 'Hot topics - ' . static::NAME;
+ private function getTarget($anchor)
+ {
+ // Anchors are linked to Skimfeed, luckily the target URI is encoded
+ // in that URI via '&u=<URI>':
+ $query = parse_url($anchor->href, PHP_URL_QUERY);
+
+ foreach (explode('&', $query) as $parameter) {
+ list($key, $value) = explode('=', $parameter);
- case self::CONTEXT_TECH_NEWS:
+ if ($key !== 'u') {
+ continue;
+ }
- $channel = $this->getInput('tech_channel');
+ return urldecode($value);
+ }
+ }
- $titles = array();
+ /**
+ * dev-mode!
+ * Requires '&format=Html'
+ *
+ * Returns the 'box' array from the source site
+ */
+ private function exportBoxChannels()
+ {
+ $html = getSimpleHTMLDOMCached(static::URI)
+ or returnServerError('No contents received from Skimfeed!');
- foreach(static::PARAMETERS[self::CONTEXT_TECH_NEWS]['tech_channel']['values'] as $ch) {
- $titles = array_merge($titles, $ch);
- }
+ if (!$this->isCompatible($html)) {
+ returnServerError('Skimfeed version is not compatible!');
+ }
- $title = array_search($channel, $titles);
+ $boxes = $html->find('#boxx .boxes')
+ or returnServerError('Could not find boxes!');
- return $title . ' - ' . static::NAME;
-
- case self::CONTEXT_CUSTOM:
- return 'Custom - ' . static::NAME;
-
- }
-
- return parent::getName();
-
- }
-
- public function collectData() {
-
- // enable to export parameter lists
- // $this->exportBoxChannels(); die;
- // $this->exportTechChannels(); die;
-
- $html = getSimpleHTMLDOM($this->getURI());
-
- defaultLinkTo($html, static::URI);
-
- switch($this->queriedContext) {
-
- case self::CONTEXT_NEWS_BOX:
-
- $author = array_search(
- $this->getInput('box_channel'),
- static::PARAMETERS[self::CONTEXT_NEWS_BOX]['box_channel']['values']
- );
-
- $author = '<a href="'
- . $this->getURI()
- . '">'
- . $author
- . '</a>';
-
- $this->extractFeed($html, $author);
- break;
-
- case self::CONTEXT_HOT_TOPICS:
- $this->extractHotTopics($html);
- break;
-
- case self::CONTEXT_TECH_NEWS:
- $authors = array();
-
- foreach(static::PARAMETERS[self::CONTEXT_TECH_NEWS]['tech_channel']['values'] as $ch) {
- $authors = array_merge($authors, $ch);
- }
-
- $author = '<a href="'
- . $this->getURI()
- . '">'
- . array_search($this->getInput('tech_channel'), $authors)
- . '</a>';
-
- $this->extractFeed($html, $author);
- break;
-
- case self::CONTEXT_CUSTOM:
- $this->extractCustomFeed($html);
- break;
-
- }
-
- }
-
- private function extractFeed($html, $author) {
-
- $articles = $html->find('li')
- or returnServerError('Could not find articles!');
-
- if(count($articles) === 1
- && stristr($articles[0]->plaintext, 'Nothing new in the last 48 hours')) {
- return; // Nothing to show
- }
-
- $limit = $this->getInput('limit') ?: -1;
-
- foreach($articles as $article) {
-
- $anchor = $article->find('a', 0)
- or returnServerError('Could not find anchor!');
-
- $item = array();
-
- $item['uri'] = $this->getTarget($anchor);
- $item['title'] = trim($anchor->plaintext);
-
- // The timestamp is encoded as relative time (max. the last 48 hours)
- // like this: "- 7 hours". It should always be at the end of the article:
- $age = substr($article->plaintext, strrpos($article->plaintext, '-'));
-
- $item['timestamp'] = strtotime($age);
- $item['author'] = $author;
-
- $this->items[] = $item;
-
- if($limit > 0 && count($this->items) >= $limit) {
- return;
- }
-
- }
-
- }
-
- private function extractHotTopics($html) {
-
- $topics = $html->find('#popbox ul li')
- or returnServerError('Could not find topics!');
-
- $limit = $this->getInput('limit') ?: -1;
-
- foreach($topics as $topic) {
-
- $anchor = $topic->find('a', 0)
- or returnServerError('Could not find anchor!');
-
- $item = array();
-
- $item['uri'] = $this->getTarget($anchor);
- $item['title'] = $anchor->title;
-
- $this->items[] = $item;
-
- if($limit > 0 && count($this->items) >= $limit) {
- return;
- }
-
- }
-
- }
-
- private function extractCustomFeed($html) {
-
- $boxes = $html->find('#boxx .boxes')
- or returnServerError('Could not find boxes!');
-
- foreach($boxes as $box) {
-
- $anchor = $box->find('span.boxtitles a', 0)
- or returnServerError('Could not find box anchor!');
-
- $author = '<a href="' . $anchor->href . '">' . trim($anchor->plaintext) . '</a>';
- $uri = $anchor->href;
-
- $box_html = getSimpleHTMLDOM($uri)
- or returnServerError('Could not load custom feed!');
-
- $this->extractFeed($box_html, $author);
-
- }
-
- }
-
- private function getTarget($anchor) {
-
- // Anchors are linked to Skimfeed, luckily the target URI is encoded
- // in that URI via '&u=<URI>':
- $query = parse_url($anchor->href, PHP_URL_QUERY);
-
- foreach(explode('&', $query) as $parameter) {
-
- list($key, $value) = explode('=', $parameter);
-
- if($key !== 'u') {
- continue;
- }
-
- return urldecode($value);
-
- }
-
- }
-
- /**
- * dev-mode!
- * Requires '&format=Html'
- *
- * Returns the 'box' array from the source site
- */
- private function exportBoxChannels() {
- $html = getSimpleHTMLDOMCached(static::URI)
- or returnServerError('No contents received from Skimfeed!');
-
- if(!$this->isCompatible($html)) {
- returnServerError('Skimfeed version is not compatible!');
- }
-
- $boxes = $html->find('#boxx .boxes')
- or returnServerError('Could not find boxes!');
-
- // begin of 'channel' list
- $message = <<<EOD
+ // begin of 'channel' list
+ $message = <<<EOD
'box_channel' => array(
'name' => 'Channel',
'type' => 'list',
@@ -717,26 +685,24 @@ class SkimfeedBridge extends BridgeAbstract {
EOD;
- foreach($boxes as $box) {
-
- $anchor = $box->find('span.boxtitles a', 0)
- or returnServerError('Could not find box anchor!');
-
- $title = trim($anchor->plaintext);
- $uri = $anchor->href;
+ foreach ($boxes as $box) {
+ $anchor = $box->find('span.boxtitles a', 0)
+ or returnServerError('Could not find box anchor!');
- // add value
- $message .= "\t\t'{$title}' => '{$uri}', \n";
+ $title = trim($anchor->plaintext);
+ $uri = $anchor->href;
- }
+ // add value
+ $message .= "\t\t'{$title}' => '{$uri}', \n";
+ }
- // end of 'box' list
- $message .= <<<EOD
+ // end of 'box' list
+ $message .= <<<EOD
)
),
EOD;
- echo <<<EOD
+ echo <<<EOD
<!DOCTYPE html>
<html>
@@ -745,28 +711,28 @@ EOD;
</body>
</html>
EOD;
-
- }
-
- /**
- * dev-mode!
- * Requires '&format=Html'
- *
- * Returns the 'techs' array from the source site
- */
- private function exportTechChannels() {
- $html = getSimpleHTMLDOMCached(static::URI)
- or returnServerError('No contents received from Skimfeed!');
-
- if(!$this->isCompatible($html)) {
- returnServerError('Skimfeed version is not compatible!');
- }
-
- $channels = $html->find('#menubar a')
- or returnServerError('Could not find channels!');
-
- // begin of 'tech_channel' list
- $message = <<<EOD
+ }
+
+ /**
+ * dev-mode!
+ * Requires '&format=Html'
+ *
+ * Returns the 'techs' array from the source site
+ */
+ private function exportTechChannels()
+ {
+ $html = getSimpleHTMLDOMCached(static::URI)
+ or returnServerError('No contents received from Skimfeed!');
+
+ if (!$this->isCompatible($html)) {
+ returnServerError('Skimfeed version is not compatible!');
+ }
+
+ $channels = $html->find('#menubar a')
+ or returnServerError('Could not find channels!');
+
+ // begin of 'tech_channel' list
+ $message = <<<EOD
'tech_channel' => array(
'name' => 'Tech channel',
'type' => 'list',
@@ -776,50 +742,48 @@ EOD;
EOD;
- foreach($channels as $channel) {
-
- if($channel->href === '#'
- || $channel->class === 'homelink'
- || $channel->plaintext === 'Twitter'
- || $channel->plaintext === 'Weather'
- || $channel->plaintext === '+Custom') {
- continue;
- }
-
- $title = trim($channel->plaintext);
- $uri = '/' . $channel->href;
-
- $message .= "\t\t'{$title}' => array(\n";
-
- $channel_html = getSimpleHTMLDOMCached(static::URI . $uri)
- or returnServerError('Could not load tech channel ' . $channel->plaintext . '!');
+ foreach ($channels as $channel) {
+ if (
+ $channel->href === '#'
+ || $channel->class === 'homelink'
+ || $channel->plaintext === 'Twitter'
+ || $channel->plaintext === 'Weather'
+ || $channel->plaintext === '+Custom'
+ ) {
+ continue;
+ }
- $boxes = $channel_html->find('#boxx .boxes')
- or returnServerError('Could not find boxes!');
+ $title = trim($channel->plaintext);
+ $uri = '/' . $channel->href;
- foreach($boxes as $box) {
+ $message .= "\t\t'{$title}' => array(\n";
- $anchor = $box->find('span.boxtitles a', 0)
- or returnServerError('Could not find box anchor!');
+ $channel_html = getSimpleHTMLDOMCached(static::URI . $uri)
+ or returnServerError('Could not load tech channel ' . $channel->plaintext . '!');
- $boxtitle = trim($anchor->plaintext);
- $boxuri = $anchor->href;
+ $boxes = $channel_html->find('#boxx .boxes')
+ or returnServerError('Could not find boxes!');
- $message .= "\t\t\t'{$boxtitle}' => '{$boxuri}', \n";
+ foreach ($boxes as $box) {
+ $anchor = $box->find('span.boxtitles a', 0)
+ or returnServerError('Could not find box anchor!');
- }
+ $boxtitle = trim($anchor->plaintext);
+ $boxuri = $anchor->href;
- $message .= "\t\t),\n";
+ $message .= "\t\t\t'{$boxtitle}' => '{$boxuri}', \n";
+ }
- }
+ $message .= "\t\t),\n";
+ }
- // end of 'box' list
- $message .= <<<EOD
+ // end of 'box' list
+ $message .= <<<EOD
)
),
EOD;
- echo <<<EOD
+ echo <<<EOD
<!DOCTYPE html>
<html>
@@ -828,22 +792,23 @@ EOD;
</body>
</html>
EOD;
- }
+ }
- /**
- * Checks if the reported skimfeed version is compatible
- */
- private function isCompatible($html) {
- $title = $html->find('title', 0);
+ /**
+ * Checks if the reported skimfeed version is compatible
+ */
+ private function isCompatible($html)
+ {
+ $title = $html->find('title', 0);
- if(!$title) {
- return false;
- }
+ if (!$title) {
+ return false;
+ }
- if($title->plaintext === 'Skimfeed V5.5 - Tech News') {
- return true;
- }
+ if ($title->plaintext === 'Skimfeed V5.5 - Tech News') {
+ return true;
+ }
- return false;
- }
+ return false;
+ }
}
diff --git a/bridges/SlusheBridge.php b/bridges/SlusheBridge.php
index b05acec5..12bed13a 100644
--- a/bridges/SlusheBridge.php
+++ b/bridges/SlusheBridge.php
@@ -1,176 +1,179 @@
<?php
-class SlusheBridge extends BridgeAbstract {
- const MAINTAINER = 'quickwick';
- const NAME = 'Slushe';
- const URI = 'https://slushe.com';
- const DESCRIPTION = 'Returns latest posts from Slushe';
+class SlusheBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'quickwick';
+ const NAME = 'Slushe';
+ const URI = 'https://slushe.com';
+ const DESCRIPTION = 'Returns latest posts from Slushe';
- const PARAMETERS = array(
- 'Artist' => array(
- 'artist_name' => array(
- 'name' => 'Artist name',
- 'required' => true,
- 'exampleValue' => 'lexx228',
- 'title' => 'Enter an artist name'
- )
- ),
- 'Category' => array(
- 'category' => array(
- 'name' => 'Category',
- 'type' => 'list',
- 'defaultValue' => 'Safe for Work',
- 'title' => 'Choose a category',
- 'values' => array(
- '2D' => '29',
- '3DX' => '58',
- 'Animation' => '60',
- 'Anime Fan Art' => '46',
- 'BDSM' => '47',
- 'Big Butt' => '73',
- 'Big Dick' => '52',
- 'Bit Tits' => '49',
- 'Bisexual' => '69',
- 'Comic' => '51',
- 'Couple' => '3',
- 'Dickgirl/Futanari' => '56',
- 'Feet' => '75',
- 'Game Fan Art' => '63',
- 'Gay' => '36',
- 'GIF' => '42',
- 'Group Sex/ Orgy' => '62',
- 'Lesbian' => '67',
- 'Mature' => '72',
- 'Misc. Fan Art' => '68',
- 'Monster' => '64',
- 'Pin-Up' => '28',
- 'Safe for Work' => '71',
- 'SFM' => '70',
- 'Solo' => '66',
- 'Threesome' => '38',
- 'TV & Film Fan Art' => '34',
- 'Western Fan Art' => '33'
- )
- )
- ),
- 'Search' => array(
- 'search_term' => array(
- 'name' => 'Search term(s)',
- 'required' => true,
- 'exampleValue' => 'pole dance',
- 'title' => 'Enter one or more search terms, separated by spaces'
- )
- )
- );
+ const PARAMETERS = [
+ 'Artist' => [
+ 'artist_name' => [
+ 'name' => 'Artist name',
+ 'required' => true,
+ 'exampleValue' => 'lexx228',
+ 'title' => 'Enter an artist name'
+ ]
+ ],
+ 'Category' => [
+ 'category' => [
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'defaultValue' => 'Safe for Work',
+ 'title' => 'Choose a category',
+ 'values' => [
+ '2D' => '29',
+ '3DX' => '58',
+ 'Animation' => '60',
+ 'Anime Fan Art' => '46',
+ 'BDSM' => '47',
+ 'Big Butt' => '73',
+ 'Big Dick' => '52',
+ 'Bit Tits' => '49',
+ 'Bisexual' => '69',
+ 'Comic' => '51',
+ 'Couple' => '3',
+ 'Dickgirl/Futanari' => '56',
+ 'Feet' => '75',
+ 'Game Fan Art' => '63',
+ 'Gay' => '36',
+ 'GIF' => '42',
+ 'Group Sex/ Orgy' => '62',
+ 'Lesbian' => '67',
+ 'Mature' => '72',
+ 'Misc. Fan Art' => '68',
+ 'Monster' => '64',
+ 'Pin-Up' => '28',
+ 'Safe for Work' => '71',
+ 'SFM' => '70',
+ 'Solo' => '66',
+ 'Threesome' => '38',
+ 'TV & Film Fan Art' => '34',
+ 'Western Fan Art' => '33'
+ ]
+ ]
+ ],
+ 'Search' => [
+ 'search_term' => [
+ 'name' => 'Search term(s)',
+ 'required' => true,
+ 'exampleValue' => 'pole dance',
+ 'title' => 'Enter one or more search terms, separated by spaces'
+ ]
+ ]
+ ];
- public function getName(){
- switch($this->queriedContext) {
- case 'Artist':
- return 'Slushe Artist: ' . $this->getInput('artist_name');
- break;
- case 'Category':
- return 'Slushe Category: ' . $this->getInput('category');
- break;
- case 'Search':
- return 'Slushe Search: ' . $this->getInput('search_term');
- break;
- default:
- return self::NAME;
- }
- }
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Artist':
+ return 'Slushe Artist: ' . $this->getInput('artist_name');
+ break;
+ case 'Category':
+ return 'Slushe Category: ' . $this->getInput('category');
+ break;
+ case 'Search':
+ return 'Slushe Search: ' . $this->getInput('search_term');
+ break;
+ default:
+ return self::NAME;
+ }
+ }
- public function collectData(){
- switch($this->queriedContext) {
- case 'Artist':
- $uri = self::URI . '/' . $this->getInput('artist_name');
- break;
- case 'Category':
- $uri = self::URI . '/search/posts/channels?niche=' .
- $this->getInput('category');
- break;
- case 'Search':
- $uri = self::URI . '/search/posts/' . $this->getInput('search_term') .
- '?s=1';
- break;
- }
+ public function collectData()
+ {
+ switch ($this->queriedContext) {
+ case 'Artist':
+ $uri = self::URI . '/' . $this->getInput('artist_name');
+ break;
+ case 'Category':
+ $uri = self::URI . '/search/posts/channels?niche=' .
+ $this->getInput('category');
+ break;
+ case 'Search':
+ $uri = self::URI . '/search/posts/' . $this->getInput('search_term') .
+ '?s=1';
+ break;
+ }
- $headers = array(
- 'Authority : slushe.com',
- 'Cookie: age-verify=1;',
- 'sec-ch-ua: "Chromium";v="100", " Not A;Brand";v="99"',
- 'sec-ch-ua-mobile: ?0',
- 'sec-ch-ua-platform: "Windows"',
- 'sec-fetch-dest: document',
- 'sec-fetch-mode: navigate',
- 'sec-fetch-site: same-origin',
- 'sec-fetch-user: ?1',
- 'upgrade-insecure-requests: 1'
- );
- // Add user-agent string to headers with implode, due to line length limit
- $user_agent_string = [
- 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/',
- '537.36(KHTML, like Gecko) Chrome/100.0.4896.147 Safari/537.36'
- ];
- $headers[] = implode('', $user_agent_string);
+ $headers = [
+ 'Authority : slushe.com',
+ 'Cookie: age-verify=1;',
+ 'sec-ch-ua: "Chromium";v="100", " Not A;Brand";v="99"',
+ 'sec-ch-ua-mobile: ?0',
+ 'sec-ch-ua-platform: "Windows"',
+ 'sec-fetch-dest: document',
+ 'sec-fetch-mode: navigate',
+ 'sec-fetch-site: same-origin',
+ 'sec-fetch-user: ?1',
+ 'upgrade-insecure-requests: 1'
+ ];
+ // Add user-agent string to headers with implode, due to line length limit
+ $user_agent_string = [
+ 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/',
+ '537.36(KHTML, like Gecko) Chrome/100.0.4896.147 Safari/537.36'
+ ];
+ $headers[] = implode('', $user_agent_string);
- $html = getSimpleHTMLDOM($uri, $headers);
+ $html = getSimpleHTMLDOM($uri, $headers);
- //Debug::log($html);
- //Debug::log($html->find('div.blog-item')[0]);
+ //Debug::log($html);
+ //Debug::log($html->find('div.blog-item')[0]);
- //Loop on each entry
- foreach($html->find('div.blog-item') as $element) {
- //Debug::log($element);
+ //Loop on each entry
+ foreach ($html->find('div.blog-item') as $element) {
+ //Debug::log($element);
- $title = $element->find('h3.title', 0)->first_child()->innertext;
- $article_uri = $element->find('h3.title', 0)->first_child()->href;
- $timestamp = $element->find('div.publication-date', 0)->innertext;
- $author = $element->find('div.artist', 0)->
- first_child()->first_child()->innertext;
+ $title = $element->find('h3.title', 0)->first_child()->innertext;
+ $article_uri = $element->find('h3.title', 0)->first_child()->href;
+ $timestamp = $element->find('div.publication-date', 0)->innertext;
+ $author = $element->find('div.artist', 0)->
+ first_child()->first_child()->innertext;
- // Create & populate item
- $item = array();
- $item['uri'] = $article_uri;
- $item['id'] = $item['uri'];
- $item['timestamp'] = $timestamp;
- $item['title'] = $title;
- $item['author'] = $author;
+ // Create & populate item
+ $item = [];
+ $item['uri'] = $article_uri;
+ $item['id'] = $item['uri'];
+ $item['timestamp'] = $timestamp;
+ $item['title'] = $title;
+ $item['author'] = $author;
- $media_html = '';
+ $media_html = '';
- // Look for image thumbnails
- $media_uris = $element->find('div.thumb', 0);
- if (isset($media_uris)) {
- // Add gallery image count, if it exists
- $gallery_count = $media_uris->find('span.count', 0);
- if (isset($gallery_count)) {
- $media_html .= '<p>Gallery count: ' .
- $gallery_count->first_child()->innertext . '</p>';
- }
- // Add image thumbnail(s)
- foreach($media_uris->find('img') as $media_uri) {
- $media_html .= '<a href="' . $article_uri . '">' . $media_uri . '</a>';
- //Debug::log('Adding to enclosures: ' . str_replace(' ', '%20', $media_uri->src));
- $item['enclosures'][] = str_replace(' ', '%20', $media_uri->src);
- }
- }
+ // Look for image thumbnails
+ $media_uris = $element->find('div.thumb', 0);
+ if (isset($media_uris)) {
+ // Add gallery image count, if it exists
+ $gallery_count = $media_uris->find('span.count', 0);
+ if (isset($gallery_count)) {
+ $media_html .= '<p>Gallery count: ' .
+ $gallery_count->first_child()->innertext . '</p>';
+ }
+ // Add image thumbnail(s)
+ foreach ($media_uris->find('img') as $media_uri) {
+ $media_html .= '<a href="' . $article_uri . '">' . $media_uri . '</a>';
+ //Debug::log('Adding to enclosures: ' . str_replace(' ', '%20', $media_uri->src));
+ $item['enclosures'][] = str_replace(' ', '%20', $media_uri->src);
+ }
+ }
- // Look for video thumbnails
- $media_uris = $element->find('div.thumb-holder', 0);
- // Add video thumbnail(s)
- if (isset($media_uris)) {
- foreach($media_uris->find('img') as $media_uri) {
- $media_html .= '<p>Video:</p><a href="' .
- $article_uri . '">' . $media_uri . '</a>';
- //Debug::log('Adding to enclosures: ' . $media_uri->src);
- $item['enclosures'][] = $media_uri->src;
- }
- }
- $item['content'] = $media_html;
+ // Look for video thumbnails
+ $media_uris = $element->find('div.thumb-holder', 0);
+ // Add video thumbnail(s)
+ if (isset($media_uris)) {
+ foreach ($media_uris->find('img') as $media_uri) {
+ $media_html .= '<p>Video:</p><a href="' .
+ $article_uri . '">' . $media_uri . '</a>';
+ //Debug::log('Adding to enclosures: ' . $media_uri->src);
+ $item['enclosures'][] = $media_uri->src;
+ }
+ }
+ $item['content'] = $media_html;
- if(isset($item['title'])) {
- $this->items[] = $item;
- }
- }
- }
+ if (isset($item['title'])) {
+ $this->items[] = $item;
+ }
+ }
+ }
}
diff --git a/bridges/SoundcloudBridge.php b/bridges/SoundcloudBridge.php
index 8df7c5e4..b2fce61d 100644
--- a/bridges/SoundcloudBridge.php
+++ b/bridges/SoundcloudBridge.php
@@ -1,237 +1,254 @@
<?php
-class SoundCloudBridge extends BridgeAbstract {
- const MAINTAINER = 'kranack, Roliga';
- const NAME = 'Soundcloud Bridge';
- const URI = 'https://soundcloud.com/';
- const CACHE_TIMEOUT = 600; // 10min
- const DESCRIPTION = 'Returns 10 newest music from user profile';
-
- const PARAMETERS = array(array(
- 'u' => array(
- 'name' => 'username',
- 'exampleValue' => 'thekidlaroi',
- 'required' => true
- ),
- 't' => array(
- 'name' => 'Content',
- 'type' => 'list',
- 'defaultValue' => 'tracks',
- 'values' => array(
- 'All (except likes)' => 'all',
- 'Tracks' => 'tracks',
- 'Albums' => 'albums',
- 'Playlists' => 'playlists',
- 'Reposts' => 'reposts',
- 'Likes' => 'likes'
- )
- )
- ));
-
- private $apiUrl = 'https://api-v2.soundcloud.com/';
- // Without url=http, player URL returns a 404
- private $playerUrl = 'https://w.soundcloud.com/player/?url=http';
- private $widgetUrl = 'https://widget.sndcdn.com/';
-
- private $feedTitle = null;
- private $feedIcon = null;
- private $clientIDCache = null;
-
- private $clientIdRegex = '/client_id.*?"(.+?)"/';
- private $widgetRegex = '/widget-.+?\.js/';
-
- public function collectData() {
- $res = $this->getUser($this->getInput('u'));
-
- $this->feedTitle = $res->username;
- $this->feedIcon = $res->avatar_url;
-
- $apiItems = $this->getUserItems($res->id, $this->getInput('t'))
- or returnServerError('No results for ' . $this->getInput('t'));
-
- $hasTrackObject = array('all', 'reposts', 'likes');
-
- foreach ($apiItems->collection as $index => $apiItem) {
- if (in_array($this->getInput('t'), $hasTrackObject) === true) {
- $apiItem = $apiItem->track;
- }
-
- $item = array();
- $item['author'] = $apiItem->user->username;
- $item['title'] = $apiItem->user->username . ' - ' . $apiItem->title;
- $item['timestamp'] = strtotime($apiItem->created_at);
-
- $description = nl2br($apiItem->description);
-
- $item['content'] = <<<HTML
+
+class SoundCloudBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'kranack, Roliga';
+ const NAME = 'Soundcloud Bridge';
+ const URI = 'https://soundcloud.com/';
+ const CACHE_TIMEOUT = 600; // 10min
+ const DESCRIPTION = 'Returns 10 newest music from user profile';
+
+ const PARAMETERS = [[
+ 'u' => [
+ 'name' => 'username',
+ 'exampleValue' => 'thekidlaroi',
+ 'required' => true
+ ],
+ 't' => [
+ 'name' => 'Content',
+ 'type' => 'list',
+ 'defaultValue' => 'tracks',
+ 'values' => [
+ 'All (except likes)' => 'all',
+ 'Tracks' => 'tracks',
+ 'Albums' => 'albums',
+ 'Playlists' => 'playlists',
+ 'Reposts' => 'reposts',
+ 'Likes' => 'likes'
+ ]
+ ]
+ ]];
+
+ private $apiUrl = 'https://api-v2.soundcloud.com/';
+ // Without url=http, player URL returns a 404
+ private $playerUrl = 'https://w.soundcloud.com/player/?url=http';
+ private $widgetUrl = 'https://widget.sndcdn.com/';
+
+ private $feedTitle = null;
+ private $feedIcon = null;
+ private $clientIDCache = null;
+
+ private $clientIdRegex = '/client_id.*?"(.+?)"/';
+ private $widgetRegex = '/widget-.+?\.js/';
+
+ public function collectData()
+ {
+ $res = $this->getUser($this->getInput('u'));
+
+ $this->feedTitle = $res->username;
+ $this->feedIcon = $res->avatar_url;
+
+ $apiItems = $this->getUserItems($res->id, $this->getInput('t'))
+ or returnServerError('No results for ' . $this->getInput('t'));
+
+ $hasTrackObject = ['all', 'reposts', 'likes'];
+
+ foreach ($apiItems->collection as $index => $apiItem) {
+ if (in_array($this->getInput('t'), $hasTrackObject) === true) {
+ $apiItem = $apiItem->track;
+ }
+
+ $item = [];
+ $item['author'] = $apiItem->user->username;
+ $item['title'] = $apiItem->user->username . ' - ' . $apiItem->title;
+ $item['timestamp'] = strtotime($apiItem->created_at);
+
+ $description = nl2br($apiItem->description);
+
+ $item['content'] = <<<HTML
<p>{$description}</p>
HTML;
- if (isset($apiItem->tracks) && $apiItem->track_count > 0) {
- $list = $this->getTrackList($apiItem->tracks);
+ if (isset($apiItem->tracks) && $apiItem->track_count > 0) {
+ $list = $this->getTrackList($apiItem->tracks);
- $item['content'] .= <<<HTML
+ $item['content'] .= <<<HTML
<p><strong>Tracks ({$apiItem->track_count})</strong></p>
{$list}
HTML;
- }
-
- $item['enclosures'][] = $apiItem->artwork_url;
- $item['id'] = $apiItem->permalink_url;
- $item['uri'] = $apiItem->permalink_url;
- $this->items[] = $item;
-
- if (count($this->items) >= 10) {
- break;
- }
- }
- }
-
- public function getIcon(){
- if ($this->feedIcon) {
- return $this->feedIcon;
- }
-
- return parent::getIcon();
- }
-
- public function getURI() {
- if ($this->getInput('u')) {
- return self::URI . $this->getInput('u') . '/' . $this->getInput('t');
- }
-
- return parent::getURI();
- }
-
- public function getName() {
- if($this->feedTitle) {
- return $this->feedTitle . ' - ' . ucfirst($this->getInput('t')) . ' - ' . self::NAME;
- }
-
- return parent::getName();
- }
-
- private function initClientIDCache(){
- if($this->clientIDCache !== null)
- return;
-
- $cacheFac = new CacheFactory();
-
- $this->clientIDCache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $this->clientIDCache->setScope(get_called_class());
- $this->clientIDCache->setKey(array('client_id'));
- }
-
- private function getClientID(){
- $this->initClientIDCache();
-
- $clientID = $this->clientIDCache->loadData();
-
- if($clientID == null) {
- return $this->refreshClientID();
- } else {
- return $clientID;
- }
- }
-
- private function refreshClientID(){
- $this->initClientIDCache();
-
- $playerHTML = getContents($this->playerUrl);
-
- // Extract widget JS filenames from player page
- if(preg_match_all($this->widgetRegex, $playerHTML, $matches) == false)
- returnServerError('Unable to find widget JS URL.');
-
- $clientID = '';
-
- // Loop widget js files and extract client ID
- foreach ($matches[0] as $widgetFile) {
- $widgetURL = $this->widgetUrl . $widgetFile;
-
- $widgetJS = getContents($widgetURL);
-
- if(preg_match($this->clientIdRegex, $widgetJS, $matches)) {
- $clientID = $matches[1];
- $this->clientIDCache->saveData($clientID);
-
- return $clientID;
- }
- }
-
- if (empty($clientID)) {
- returnServerError('Unable to find client ID.');
- }
- }
-
- private function buildApiUrl($endpoint, $parameters) {
- return $this->apiUrl
- . $endpoint
- . '?'
- . http_build_query($parameters);
- }
-
- private function getUser($username) {
- $parameters = array('url' => self::URI . $username);
-
- return $this->getApi('resolve', $parameters);
- }
-
- private function getUserItems($userId, $type) {
- $parameters = array('limit' => 10);
- $endpoint = 'users/' . $userId . '/' . $type;
-
- if ($type === 'playlists') {
- $endpoint = 'users/' . $userId . '/playlists_without_albums';
- }
-
- if ($type === 'all') {
- $endpoint = 'stream/users/' . $userId;
- }
-
- if ($type === 'reposts') {
- $endpoint = 'stream/users/' . $userId . '/' . $type;
- }
-
- return $this->getApi($endpoint, $parameters);
- }
-
- private function getApi($endpoint, $parameters) {
- $parameters['client_id'] = $this->getClientID();
- $url = $this->buildApiUrl($endpoint, $parameters);
-
- try {
- return json_decode(getContents($url));
- } catch (Exception $e) {
- // Retry once with refreshed client ID
- $parameters['client_id'] = $this->refreshClientID();
- $url = $this->buildApiUrl($endpoint, $parameters);
-
- return json_decode(getContents($url));
- }
- }
-
- private function getTrackList($tracks) {
- $trackids = '';
-
- foreach ($tracks as $track) {
- $trackids .= $track->id . ',';
- }
-
- $apiItems = $this->getApi(
- 'tracks', array('ids' => $trackids)
- );
-
- $list = '';
- foreach($apiItems as $track) {
- $list .= <<<HTML
+ }
+
+ $item['enclosures'][] = $apiItem->artwork_url;
+ $item['id'] = $apiItem->permalink_url;
+ $item['uri'] = $apiItem->permalink_url;
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10) {
+ break;
+ }
+ }
+ }
+
+ public function getIcon()
+ {
+ if ($this->feedIcon) {
+ return $this->feedIcon;
+ }
+
+ return parent::getIcon();
+ }
+
+ public function getURI()
+ {
+ if ($this->getInput('u')) {
+ return self::URI . $this->getInput('u') . '/' . $this->getInput('t');
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if ($this->feedTitle) {
+ return $this->feedTitle . ' - ' . ucfirst($this->getInput('t')) . ' - ' . self::NAME;
+ }
+
+ return parent::getName();
+ }
+
+ private function initClientIDCache()
+ {
+ if ($this->clientIDCache !== null) {
+ return;
+ }
+
+ $cacheFac = new CacheFactory();
+
+ $this->clientIDCache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $this->clientIDCache->setScope(get_called_class());
+ $this->clientIDCache->setKey(['client_id']);
+ }
+
+ private function getClientID()
+ {
+ $this->initClientIDCache();
+
+ $clientID = $this->clientIDCache->loadData();
+
+ if ($clientID == null) {
+ return $this->refreshClientID();
+ } else {
+ return $clientID;
+ }
+ }
+
+ private function refreshClientID()
+ {
+ $this->initClientIDCache();
+
+ $playerHTML = getContents($this->playerUrl);
+
+ // Extract widget JS filenames from player page
+ if (preg_match_all($this->widgetRegex, $playerHTML, $matches) == false) {
+ returnServerError('Unable to find widget JS URL.');
+ }
+
+ $clientID = '';
+
+ // Loop widget js files and extract client ID
+ foreach ($matches[0] as $widgetFile) {
+ $widgetURL = $this->widgetUrl . $widgetFile;
+
+ $widgetJS = getContents($widgetURL);
+
+ if (preg_match($this->clientIdRegex, $widgetJS, $matches)) {
+ $clientID = $matches[1];
+ $this->clientIDCache->saveData($clientID);
+
+ return $clientID;
+ }
+ }
+
+ if (empty($clientID)) {
+ returnServerError('Unable to find client ID.');
+ }
+ }
+
+ private function buildApiUrl($endpoint, $parameters)
+ {
+ return $this->apiUrl
+ . $endpoint
+ . '?'
+ . http_build_query($parameters);
+ }
+
+ private function getUser($username)
+ {
+ $parameters = ['url' => self::URI . $username];
+
+ return $this->getApi('resolve', $parameters);
+ }
+
+ private function getUserItems($userId, $type)
+ {
+ $parameters = ['limit' => 10];
+ $endpoint = 'users/' . $userId . '/' . $type;
+
+ if ($type === 'playlists') {
+ $endpoint = 'users/' . $userId . '/playlists_without_albums';
+ }
+
+ if ($type === 'all') {
+ $endpoint = 'stream/users/' . $userId;
+ }
+
+ if ($type === 'reposts') {
+ $endpoint = 'stream/users/' . $userId . '/' . $type;
+ }
+
+ return $this->getApi($endpoint, $parameters);
+ }
+
+ private function getApi($endpoint, $parameters)
+ {
+ $parameters['client_id'] = $this->getClientID();
+ $url = $this->buildApiUrl($endpoint, $parameters);
+
+ try {
+ return json_decode(getContents($url));
+ } catch (Exception $e) {
+ // Retry once with refreshed client ID
+ $parameters['client_id'] = $this->refreshClientID();
+ $url = $this->buildApiUrl($endpoint, $parameters);
+
+ return json_decode(getContents($url));
+ }
+ }
+
+ private function getTrackList($tracks)
+ {
+ $trackids = '';
+
+ foreach ($tracks as $track) {
+ $trackids .= $track->id . ',';
+ }
+
+ $apiItems = $this->getApi(
+ 'tracks',
+ ['ids' => $trackids]
+ );
+
+ $list = '';
+ foreach ($apiItems as $track) {
+ $list .= <<<HTML
<li>{$track->user->username} — <a href="{$track->permalink_url}">{$track->title}</a></li>
HTML;
- }
+ }
- $html = <<<HTML
+ $html = <<<HTML
<ul>{$list}</ul>
HTML;
- return $html;
- }
+ return $html;
+ }
}
diff --git a/bridges/SplCenterBridge.php b/bridges/SplCenterBridge.php
index 492f6a40..396de3b5 100644
--- a/bridges/SplCenterBridge.php
+++ b/bridges/SplCenterBridge.php
@@ -1,63 +1,66 @@
<?php
-class SplCenterBridge extends FeedExpander {
- const NAME = 'Southern Poverty Law Center Bridge';
- const URI = 'https://www.splcenter.org';
- const DESCRIPTION = 'Returns the newest posts from the Southern Poverty Law Center';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(array(
- 'content' => array(
- 'name' => 'Content',
- 'type' => 'list',
- 'values' => array(
- 'News' => 'news',
- 'Hatewatch' => 'hatewatch',
- ),
- 'defaultValue' => 'news',
- )
- )
- );
-
- const CACHE_TIMEOUT = 3600; // 1 hour
-
- protected function parseItem($item) {
- $item = parent::parseItem($item);
-
- $articleHtml = getSimpleHTMLDOMCached($item['uri']);
-
- foreach ($articleHtml->find('.file') as $index => $media) {
- $articleHtml->find('div.file', $index)->outertext = '<em>' . $media->outertext . '</em>';
- }
-
- $item['content'] = $articleHtml->find('div#group-content-container', 0)->innertext;
- $item['enclosures'][] = $articleHtml->find('meta[name="twitter:image"]', 0)->content;
-
- return $item;
- }
-
- public function collectData() {
- $this->collectExpandableDatas($this->getURI() . '/rss.xml');
- }
-
- public function getURI() {
-
- if (!is_null($this->getInput('content'))) {
- return self::URI . '/' . $this->getInput('content');
- }
-
- return parent::getURI();
- }
-
- public function getName() {
-
- if (!is_null($this->getInput('content'))) {
- $parameters = $this->getParameters();
-
- $contentValues = array_flip($parameters[0]['content']['values']);
-
- return $contentValues[$this->getInput('content')] . ' - Southern Poverty Law Center';
- }
-
- return parent::getName();
- }
+class SplCenterBridge extends FeedExpander
+{
+ const NAME = 'Southern Poverty Law Center Bridge';
+ const URI = 'https://www.splcenter.org';
+ const DESCRIPTION = 'Returns the newest posts from the Southern Poverty Law Center';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [[
+ 'content' => [
+ 'name' => 'Content',
+ 'type' => 'list',
+ 'values' => [
+ 'News' => 'news',
+ 'Hatewatch' => 'hatewatch',
+ ],
+ 'defaultValue' => 'news',
+ ]
+ ]
+ ];
+
+ const CACHE_TIMEOUT = 3600; // 1 hour
+
+ protected function parseItem($item)
+ {
+ $item = parent::parseItem($item);
+
+ $articleHtml = getSimpleHTMLDOMCached($item['uri']);
+
+ foreach ($articleHtml->find('.file') as $index => $media) {
+ $articleHtml->find('div.file', $index)->outertext = '<em>' . $media->outertext . '</em>';
+ }
+
+ $item['content'] = $articleHtml->find('div#group-content-container', 0)->innertext;
+ $item['enclosures'][] = $articleHtml->find('meta[name="twitter:image"]', 0)->content;
+
+ return $item;
+ }
+
+ public function collectData()
+ {
+ $this->collectExpandableDatas($this->getURI() . '/rss.xml');
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('content'))) {
+ return self::URI . '/' . $this->getInput('content');
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('content'))) {
+ $parameters = $this->getParameters();
+
+ $contentValues = array_flip($parameters[0]['content']['values']);
+
+ return $contentValues[$this->getInput('content')] . ' - Southern Poverty Law Center';
+ }
+
+ return parent::getName();
+ }
}
diff --git a/bridges/SpotifyBridge.php b/bridges/SpotifyBridge.php
index e6598f23..312eddc6 100644
--- a/bridges/SpotifyBridge.php
+++ b/bridges/SpotifyBridge.php
@@ -1,238 +1,264 @@
<?php
-class SpotifyBridge extends BridgeAbstract {
- const NAME = 'Spotify';
- const URI = 'https://spotify.com/';
- const DESCRIPTION = 'Fetches the latest ten albums from one or more artists';
- const MAINTAINER = 'Paroleen';
- const CACHE_TIMEOUT = 3600;
- const PARAMETERS = array( array(
- 'clientid' => array(
- 'name' => 'Client ID',
- 'type' => 'text',
- 'required' => true
- ),
- 'clientsecret' => array(
- 'name' => 'Client secret',
- 'type' => 'text',
- 'required' => true
- ),
- 'spotifyuri' => array(
- 'name' => 'Spotify URIs',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'spotify:artist:4lianjyuR1tqf6oUX8kjrZ [,spotify:artist:3JsMj0DEzyWc0VDlHuy9Bx]',
- ),
- 'albumtype' => array(
- 'name' => 'Album type',
- 'type' => 'text',
- 'required' => false,
- 'exampleValue' => 'album,single,appears_on,compilation',
- 'defaultValue' => 'album,single'
- ),
- 'country' => array(
- 'name' => 'Country',
- 'type' => 'text',
- 'required' => false,
- 'exampleValue' => 'US',
- 'defaultValue' => 'US'
- )
- ));
-
- const TOKENURI = 'https://accounts.spotify.com/api/token';
- const APIURI = 'https://api.spotify.com/v1/';
-
- private $uri = '';
- private $name = '';
- private $token = '';
- private $artists = array();
- private $albums = array();
-
- public function getURI() {
- if(empty($this->uri))
- $this->getArtist();
-
- return $this->uri;
- }
-
- public function getName() {
- if(empty($this->name))
- $this->getArtist();
-
- return $this->name;
- }
-
- public function getIcon() {
- return 'https://www.scdn.co/i/_global/favicon.png';
- }
-
- private function getId($artist) {
- return explode(':', $artist)[2];
- }
-
- private function getDate($album_date) {
- if(strlen($album_date) == 4)
- $album_date .= '-01-01';
- elseif(strlen($album_date) == 7)
- $album_date .= '-01';
-
- return DateTime::createFromFormat('Y-m-d', $album_date)->getTimestamp();
- }
-
- private function getAlbumType() {
- return $this->getInput('albumtype');
- }
-
- private function getCountry() {
- return $this->getInput('country');
- }
-
- private function getToken() {
- $cacheFac = new CacheFactory();
-
- $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $cache->setScope(get_called_class());
- $cache->setKey(array('token'));
-
- if($cache->getTime()) {
- $time = (new DateTime)->getTimestamp() - $cache->getTime();
- Debug::log('Token time: ' . $time);
- }
-
- if($cache->getTime() == false || $time >= 3600) {
- Debug::log('Fetching token from Spotify');
- $this->fetchToken();
- $cache->saveData($this->token);
- } else {
- Debug::log('Loading token from cache');
- $this->token = $cache->loadData();
- }
-
- Debug::log('Token: ' . $this->token);
- }
-
- private function getArtist() {
- if(!is_null($this->getInput('spotifyuri')) && strpos($this->getInput('spotifyuri'), ',') === false) {
- $artist = $this->fetchContent(self::APIURI . 'artists/'
- . $this->getId($this->artists[0]));
- $this->uri = $artist['external_urls']['spotify'];
- $this->name = $artist['name'] . ' - Spotify';
- } else {
- $this->uri = parent::getURI();
- $this->name = parent::getName();
- }
- }
-
- private function getAllArtists() {
- Debug::log('Parsing all artists');
- $this->artists = explode(',', $this->getInput('spotifyuri'));
- }
-
- private function getAllAlbums() {
- $this->albums = array();
-
- $this->getAllArtists();
-
- Debug::log('Fetching all albums');
- foreach($this->artists as $artist) {
- $fetch = true;
- $offset = 0;
-
- while($fetch) {
- $partial_albums = $this->fetchContent(self::APIURI . 'artists/'
- . $this->getId($artist)
- . '/albums?limit=50&include_groups='
- . $this->getAlbumType()
- . '&country='
- . $this->getCountry()
- . '&offset='
- . $offset);
-
- if(!empty($partial_albums['items']))
- $this->albums = array_merge($this->albums,
- $partial_albums['items']);
- else
- $fetch = false;
-
- $offset += 50;
- }
- }
- }
-
- private function fetchToken() {
- $curl = curl_init();
-
- curl_setopt($curl, CURLOPT_URL, self::TOKENURI);
- curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
- curl_setopt($curl, CURLOPT_POST, 1);
- curl_setopt($curl, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
- curl_setopt($curl, CURLOPT_HTTPHEADER, array('Authorization: Basic '
- . base64_encode($this->getInput('clientid')
- . ':'
- . $this->getInput('clientsecret'))));
-
- $json = curl_exec($curl);
- $json = json_decode($json)->access_token;
- curl_close($curl);
-
- $this->token = $json;
- }
-
- private function fetchContent($url) {
- $this->getToken();
- $curl = curl_init();
-
- curl_setopt($curl, CURLOPT_URL, $url);
- curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
- curl_setopt($curl, CURLOPT_HTTPHEADER, array('Authorization: Bearer '
- . $this->token));
-
- Debug::log('Fetching content from ' . $url);
- $json = curl_exec($curl);
- $json = json_decode($json, true);
- curl_close($curl);
-
- return $json;
- }
-
- private function sortAlbums() {
- Debug::log('Sorting albums');
- usort($this->albums, function($album1, $album2) {
- if($this->getDate($album1['release_date']) < $this->getDate($album2['release_date']))
- return 1;
- else
- return -1;
- });
- }
-
- public function collectData() {
- $offset = 0;
-
- $this->getAllAlbums();
- $this->sortAlbums();
-
- Debug::log('Building RSS feed');
- foreach($this->albums as $album) {
- $item = array();
- $item['title'] = $album['name'];
- $item['uri'] = $album['external_urls']['spotify'];
-
- $item['timestamp'] = $this->getDate($album['release_date']);
- $item['author'] = $album['artists'][0]['name'];
- $item['categories'] = array($album['album_type']);
-
- $item['content'] = '<img style="width: 256px" src="'
- . $album['images'][0]['url']
- . '">';
-
- if($album['total_tracks'] > 1)
- $item['content'] .= '<p>Total tracks: '
- . $album['total_tracks']
- . '</p>';
-
- $this->items[] = $item;
-
- if(count($this->items) >= 10)
- break;
- }
- }
+
+class SpotifyBridge extends BridgeAbstract
+{
+ const NAME = 'Spotify';
+ const URI = 'https://spotify.com/';
+ const DESCRIPTION = 'Fetches the latest ten albums from one or more artists';
+ const MAINTAINER = 'Paroleen';
+ const CACHE_TIMEOUT = 3600;
+ const PARAMETERS = [ [
+ 'clientid' => [
+ 'name' => 'Client ID',
+ 'type' => 'text',
+ 'required' => true
+ ],
+ 'clientsecret' => [
+ 'name' => 'Client secret',
+ 'type' => 'text',
+ 'required' => true
+ ],
+ 'spotifyuri' => [
+ 'name' => 'Spotify URIs',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'spotify:artist:4lianjyuR1tqf6oUX8kjrZ [,spotify:artist:3JsMj0DEzyWc0VDlHuy9Bx]',
+ ],
+ 'albumtype' => [
+ 'name' => 'Album type',
+ 'type' => 'text',
+ 'required' => false,
+ 'exampleValue' => 'album,single,appears_on,compilation',
+ 'defaultValue' => 'album,single'
+ ],
+ 'country' => [
+ 'name' => 'Country',
+ 'type' => 'text',
+ 'required' => false,
+ 'exampleValue' => 'US',
+ 'defaultValue' => 'US'
+ ]
+ ]];
+
+ const TOKENURI = 'https://accounts.spotify.com/api/token';
+ const APIURI = 'https://api.spotify.com/v1/';
+
+ private $uri = '';
+ private $name = '';
+ private $token = '';
+ private $artists = [];
+ private $albums = [];
+
+ public function getURI()
+ {
+ if (empty($this->uri)) {
+ $this->getArtist();
+ }
+
+ return $this->uri;
+ }
+
+ public function getName()
+ {
+ if (empty($this->name)) {
+ $this->getArtist();
+ }
+
+ return $this->name;
+ }
+
+ public function getIcon()
+ {
+ return 'https://www.scdn.co/i/_global/favicon.png';
+ }
+
+ private function getId($artist)
+ {
+ return explode(':', $artist)[2];
+ }
+
+ private function getDate($album_date)
+ {
+ if (strlen($album_date) == 4) {
+ $album_date .= '-01-01';
+ } elseif (strlen($album_date) == 7) {
+ $album_date .= '-01';
+ }
+
+ return DateTime::createFromFormat('Y-m-d', $album_date)->getTimestamp();
+ }
+
+ private function getAlbumType()
+ {
+ return $this->getInput('albumtype');
+ }
+
+ private function getCountry()
+ {
+ return $this->getInput('country');
+ }
+
+ private function getToken()
+ {
+ $cacheFac = new CacheFactory();
+
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope(get_called_class());
+ $cache->setKey(['token']);
+
+ if ($cache->getTime()) {
+ $time = (new DateTime())->getTimestamp() - $cache->getTime();
+ Debug::log('Token time: ' . $time);
+ }
+
+ if ($cache->getTime() == false || $time >= 3600) {
+ Debug::log('Fetching token from Spotify');
+ $this->fetchToken();
+ $cache->saveData($this->token);
+ } else {
+ Debug::log('Loading token from cache');
+ $this->token = $cache->loadData();
+ }
+
+ Debug::log('Token: ' . $this->token);
+ }
+
+ private function getArtist()
+ {
+ if (!is_null($this->getInput('spotifyuri')) && strpos($this->getInput('spotifyuri'), ',') === false) {
+ $artist = $this->fetchContent(self::APIURI . 'artists/'
+ . $this->getId($this->artists[0]));
+ $this->uri = $artist['external_urls']['spotify'];
+ $this->name = $artist['name'] . ' - Spotify';
+ } else {
+ $this->uri = parent::getURI();
+ $this->name = parent::getName();
+ }
+ }
+
+ private function getAllArtists()
+ {
+ Debug::log('Parsing all artists');
+ $this->artists = explode(',', $this->getInput('spotifyuri'));
+ }
+
+ private function getAllAlbums()
+ {
+ $this->albums = [];
+
+ $this->getAllArtists();
+
+ Debug::log('Fetching all albums');
+ foreach ($this->artists as $artist) {
+ $fetch = true;
+ $offset = 0;
+
+ while ($fetch) {
+ $partial_albums = $this->fetchContent(self::APIURI . 'artists/'
+ . $this->getId($artist)
+ . '/albums?limit=50&include_groups='
+ . $this->getAlbumType()
+ . '&country='
+ . $this->getCountry()
+ . '&offset='
+ . $offset);
+
+ if (!empty($partial_albums['items'])) {
+ $this->albums = array_merge(
+ $this->albums,
+ $partial_albums['items']
+ );
+ } else {
+ $fetch = false;
+ }
+
+ $offset += 50;
+ }
+ }
+ }
+
+ private function fetchToken()
+ {
+ $curl = curl_init();
+
+ curl_setopt($curl, CURLOPT_URL, self::TOKENURI);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
+ curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Basic '
+ . base64_encode($this->getInput('clientid')
+ . ':'
+ . $this->getInput('clientsecret'))]);
+
+ $json = curl_exec($curl);
+ $json = json_decode($json)->access_token;
+ curl_close($curl);
+
+ $this->token = $json;
+ }
+
+ private function fetchContent($url)
+ {
+ $this->getToken();
+ $curl = curl_init();
+
+ curl_setopt($curl, CURLOPT_URL, $url);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer '
+ . $this->token]);
+
+ Debug::log('Fetching content from ' . $url);
+ $json = curl_exec($curl);
+ $json = json_decode($json, true);
+ curl_close($curl);
+
+ return $json;
+ }
+
+ private function sortAlbums()
+ {
+ Debug::log('Sorting albums');
+ usort($this->albums, function ($album1, $album2) {
+ if ($this->getDate($album1['release_date']) < $this->getDate($album2['release_date'])) {
+ return 1;
+ } else {
+ return -1;
+ }
+ });
+ }
+
+ public function collectData()
+ {
+ $offset = 0;
+
+ $this->getAllAlbums();
+ $this->sortAlbums();
+
+ Debug::log('Building RSS feed');
+ foreach ($this->albums as $album) {
+ $item = [];
+ $item['title'] = $album['name'];
+ $item['uri'] = $album['external_urls']['spotify'];
+
+ $item['timestamp'] = $this->getDate($album['release_date']);
+ $item['author'] = $album['artists'][0]['name'];
+ $item['categories'] = [$album['album_type']];
+
+ $item['content'] = '<img style="width: 256px" src="'
+ . $album['images'][0]['url']
+ . '">';
+
+ if ($album['total_tracks'] > 1) {
+ $item['content'] .= '<p>Total tracks: '
+ . $album['total_tracks']
+ . '</p>';
+ }
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10) {
+ break;
+ }
+ }
+ }
}
diff --git a/bridges/SpottschauBridge.php b/bridges/SpottschauBridge.php
index c9d1952f..a2720274 100644
--- a/bridges/SpottschauBridge.php
+++ b/bridges/SpottschauBridge.php
@@ -1,39 +1,42 @@
<?php
-class SpottschauBridge extends BridgeAbstract {
- const NAME = 'Härringers Spottschau Bridge';
- const URI = 'https://spottschau.com/';
- const DESCRIPTION = 'Der Fußball-Comic';
- const MAINTAINER = 'sal0max';
- const PARAMETERS = array();
- const CACHE_TIMEOUT = 3600; // 1 hour
+class SpottschauBridge extends BridgeAbstract
+{
+ const NAME = 'Härringers Spottschau Bridge';
+ const URI = 'https://spottschau.com/';
+ const DESCRIPTION = 'Der Fußball-Comic';
+ const MAINTAINER = 'sal0max';
+ const PARAMETERS = [];
- public function collectData() {
- $html = getSimpleHTMLDOM(self::URI);
+ const CACHE_TIMEOUT = 3600; // 1 hour
- $item = array();
- $item['uri'] = urljoin(self::URI, $html->find('div.strip>a', 0)->attr['href']);
- $item['title'] = $html->find('div.text>h2', 0)->innertext;
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
- $date = preg_replace('/.*, /', '', $item['title']);
- $date = preg_replace('/\\d\\d\\.\\//', '', $date);
- try {
- $item['timestamp'] = DateTime::createFromFormat('d.m.y', $date)
- ->setTimezone(new DateTimeZone('Europe/Berlin'))
- ->setTime(0, 0)
- ->getTimestamp();
- } catch (Throwable $ignored) {
- $item['timestamp'] = null;
- }
+ $item = [];
+ $item['uri'] = urljoin(self::URI, $html->find('div.strip>a', 0)->attr['href']);
+ $item['title'] = $html->find('div.text>h2', 0)->innertext;
- $image = $html->find('div.strip>a>img', 0);
- $imageUrl = urljoin(self::URI, $image->attr['src']);
- $imageAlt = $image->attr['alt'];
+ $date = preg_replace('/.*, /', '', $item['title']);
+ $date = preg_replace('/\\d\\d\\.\\//', '', $date);
+ try {
+ $item['timestamp'] = DateTime::createFromFormat('d.m.y', $date)
+ ->setTimezone(new DateTimeZone('Europe/Berlin'))
+ ->setTime(0, 0)
+ ->getTimestamp();
+ } catch (Throwable $ignored) {
+ $item['timestamp'] = null;
+ }
- $item['content'] = <<<EOD
+ $image = $html->find('div.strip>a>img', 0);
+ $imageUrl = urljoin(self::URI, $image->attr['src']);
+ $imageAlt = $image->attr['alt'];
+
+ $item['content'] = <<<EOD
<img src="{$imageUrl}" alt="{$imageAlt}"/>
<br/>
EOD;
- $this->items[] = $item;
- }
+ $this->items[] = $item;
+ }
}
diff --git a/bridges/StanfordSIRbookreviewBridge.php b/bridges/StanfordSIRbookreviewBridge.php
index f57a0b1b..b60f8fc1 100644
--- a/bridges/StanfordSIRbookreviewBridge.php
+++ b/bridges/StanfordSIRbookreviewBridge.php
@@ -1,42 +1,44 @@
<?php
-class StanfordSIRbookreviewBridge extends BridgeAbstract {
- const MAINTAINER = 'Kidman1670';
- const NAME = 'StanfordSIRbookreviewBridge';
- const URI = 'https://ssir.org/books/';
- const CACHE_TIMEOUT = 21600;
- const DESCRIPTION = 'Return results from SSIR book review.';
- const PARAMETERS = array( array(
- 'style' => array(
- 'name' => 'style',
- 'type' => 'list',
- 'values' => array(
- 'reviews' => 'reviews',
- 'excerpts' => 'excerpts',
- )
- )
- )
- );
- public function collectData() {
- switch($this->getInput('style')) {
- case 'reviews':
- $url = self::URI . 'reviews';
- break;
- case 'excerpts':
- $url = self::URI . 'excerpts';
- break;
- }
+class StanfordSIRbookreviewBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Kidman1670';
+ const NAME = 'StanfordSIRbookreviewBridge';
+ const URI = 'https://ssir.org/books/';
+ const CACHE_TIMEOUT = 21600;
+ const DESCRIPTION = 'Return results from SSIR book review.';
+ const PARAMETERS = [ [
+ 'style' => [
+ 'name' => 'style',
+ 'type' => 'list',
+ 'values' => [
+ 'reviews' => 'reviews',
+ 'excerpts' => 'excerpts',
+ ]
+ ]
+ ]
+ ];
- $html = getSimpleHTMLDOM($url)
- or returnServerError('Failed loading content!');
- foreach($html->find('article') as $element) {
- $item = array();
- $item['title'] = $element->find('div > h4 > a', 0)->plaintext;
- $item['uri'] = $element->find('div > h4 > a', 0)->href;
- $item['content'] = $element->find('div > div.article-entry > p', 2)->plaintext;
- $item['author'] = $element->find('div > div > p', 0)->plaintext;
- $this->items[] = $item;
+ public function collectData()
+ {
+ switch ($this->getInput('style')) {
+ case 'reviews':
+ $url = self::URI . 'reviews';
+ break;
+ case 'excerpts':
+ $url = self::URI . 'excerpts';
+ break;
+ }
- }
- }
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Failed loading content!');
+ foreach ($html->find('article') as $element) {
+ $item = [];
+ $item['title'] = $element->find('div > h4 > a', 0)->plaintext;
+ $item['uri'] = $element->find('div > h4 > a', 0)->href;
+ $item['content'] = $element->find('div > div.article-entry > p', 2)->plaintext;
+ $item['author'] = $element->find('div > div > p', 0)->plaintext;
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/SteamBridge.php b/bridges/SteamBridge.php
index 47d1bdac..c4a720fc 100644
--- a/bridges/SteamBridge.php
+++ b/bridges/SteamBridge.php
@@ -1,121 +1,111 @@
<?php
-class SteamBridge extends BridgeAbstract {
- const NAME = 'Steam Bridge';
- const URI = 'https://store.steampowered.com/';
- const CACHE_TIMEOUT = 3600; // 1h
- const DESCRIPTION = 'Returns apps list';
- const MAINTAINER = 'jacknumber';
- const PARAMETERS = array(
- 'Wishlist' => array(
- 'userid' => array(
- 'name' => 'Steamid64 (find it on steamid.io)',
- 'title' => 'User ID (17 digits). Find your user ID with steamid.io or steamidfinder.com',
- 'required' => true,
- 'exampleValue' => '76561198821231205',
- 'pattern' => '[0-9]{17}',
- ),
- 'only_discount' => array(
- 'name' => 'Only discount',
- 'type' => 'checkbox',
- )
- )
- );
-
- public function collectData(){
-
- $userid = $this->getInput('userid');
-
- $sourceUrl = self::URI . 'wishlist/profiles/' . $userid . '/wishlistdata?p=0';
- $sort = array();
-
- $json = getContents($sourceUrl);
-
- $appsData = json_decode($json);
-
- foreach($appsData as $id => $element) {
-
- $appType = $element->type;
- $appIsBuyable = 0;
- $appHasDiscount = 0;
- $appIsFree = 0;
-
- if($element->subs) {
- $appIsBuyable = 1;
- $priceBlock = str_get_html($element->subs[0]->discount_block);
- $appPrice = str_replace('--', '00', $priceBlock->find('.discount_final_price', 0)->plaintext);
-
- if($element->subs[0]->discount_pct) {
-
- $appHasDiscount = 1;
- $discountBlock = str_get_html($element->subs[0]->discount_block);
- $appDiscountValue = $discountBlock->find('.discount_pct', 0)->plaintext;
- $appOldPrice = $discountBlock->find('.discount_original_price', 0)->plaintext;
-
- } else {
-
- if($this->getInput('only_discount')) {
- continue;
- }
-
- }
-
- } else {
-
- if($this->getInput('only_discount')) {
- continue;
- }
-
- if(isset($element->free) && $element->free = 1) {
- $appIsFree = 1;
- }
- }
-
- $coverUrl = str_replace('_292x136', '', strtok($element->capsule, '?'));
- $picturesPath = pathinfo($coverUrl)['dirname'] . '/';
-
- $item = array();
- $item['uri'] = "http://store.steampowered.com/app/$id/";
- $item['title'] = $element->name;
- $item['type'] = $appType;
- $item['cover'] = $coverUrl;
- $item['timestamp'] = $element->added;
- $item['isBuyable'] = $appIsBuyable;
- $item['hasDiscount'] = $appHasDiscount;
- $item['isFree'] = $appIsFree;
- $item['priority'] = $element->priority;
-
- if($appIsBuyable) {
-
- $item['price'] = floatval(str_replace(',', '.', $appPrice));
- $item['content'] = $appPrice;
-
- }
-
- if($appIsFree) {
- $item['content'] = 'Free';
- }
-
- if($appHasDiscount) {
-
- $item['discount']['value'] = $appDiscountValue;
- $item['discount']['oldPrice'] = $appOldPrice;
- $item['content'] = '<s>' . $appOldPrice . '</s> <b>' . $appPrice . '</b> (' . $appDiscountValue . ')';
-
- }
-
- $item['enclosures'] = array();
- $item['enclosures'][] = $coverUrl;
-
- foreach($element->screenshots as $screenshotFileName) {
- $item['enclosures'][] = $picturesPath . $screenshotFileName;
- }
-
- $sort[$id] = $element->priority;
-
- $this->items[] = $item;
- }
-
- array_multisort($sort, SORT_ASC, $this->items);
- }
+class SteamBridge extends BridgeAbstract
+{
+ const NAME = 'Steam Bridge';
+ const URI = 'https://store.steampowered.com/';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Returns apps list';
+ const MAINTAINER = 'jacknumber';
+ const PARAMETERS = [
+ 'Wishlist' => [
+ 'userid' => [
+ 'name' => 'Steamid64 (find it on steamid.io)',
+ 'title' => 'User ID (17 digits). Find your user ID with steamid.io or steamidfinder.com',
+ 'required' => true,
+ 'exampleValue' => '76561198821231205',
+ 'pattern' => '[0-9]{17}',
+ ],
+ 'only_discount' => [
+ 'name' => 'Only discount',
+ 'type' => 'checkbox',
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $userid = $this->getInput('userid');
+
+ $sourceUrl = self::URI . 'wishlist/profiles/' . $userid . '/wishlistdata?p=0';
+ $sort = [];
+
+ $json = getContents($sourceUrl);
+
+ $appsData = json_decode($json);
+
+ foreach ($appsData as $id => $element) {
+ $appType = $element->type;
+ $appIsBuyable = 0;
+ $appHasDiscount = 0;
+ $appIsFree = 0;
+
+ if ($element->subs) {
+ $appIsBuyable = 1;
+ $priceBlock = str_get_html($element->subs[0]->discount_block);
+ $appPrice = str_replace('--', '00', $priceBlock->find('.discount_final_price', 0)->plaintext);
+
+ if ($element->subs[0]->discount_pct) {
+ $appHasDiscount = 1;
+ $discountBlock = str_get_html($element->subs[0]->discount_block);
+ $appDiscountValue = $discountBlock->find('.discount_pct', 0)->plaintext;
+ $appOldPrice = $discountBlock->find('.discount_original_price', 0)->plaintext;
+ } else {
+ if ($this->getInput('only_discount')) {
+ continue;
+ }
+ }
+ } else {
+ if ($this->getInput('only_discount')) {
+ continue;
+ }
+
+ if (isset($element->free) && $element->free = 1) {
+ $appIsFree = 1;
+ }
+ }
+
+ $coverUrl = str_replace('_292x136', '', strtok($element->capsule, '?'));
+ $picturesPath = pathinfo($coverUrl)['dirname'] . '/';
+
+ $item = [];
+ $item['uri'] = "http://store.steampowered.com/app/$id/";
+ $item['title'] = $element->name;
+ $item['type'] = $appType;
+ $item['cover'] = $coverUrl;
+ $item['timestamp'] = $element->added;
+ $item['isBuyable'] = $appIsBuyable;
+ $item['hasDiscount'] = $appHasDiscount;
+ $item['isFree'] = $appIsFree;
+ $item['priority'] = $element->priority;
+
+ if ($appIsBuyable) {
+ $item['price'] = floatval(str_replace(',', '.', $appPrice));
+ $item['content'] = $appPrice;
+ }
+
+ if ($appIsFree) {
+ $item['content'] = 'Free';
+ }
+
+ if ($appHasDiscount) {
+ $item['discount']['value'] = $appDiscountValue;
+ $item['discount']['oldPrice'] = $appOldPrice;
+ $item['content'] = '<s>' . $appOldPrice . '</s> <b>' . $appPrice . '</b> (' . $appDiscountValue . ')';
+ }
+
+ $item['enclosures'] = [];
+ $item['enclosures'][] = $coverUrl;
+
+ foreach ($element->screenshots as $screenshotFileName) {
+ $item['enclosures'][] = $picturesPath . $screenshotFileName;
+ }
+
+ $sort[$id] = $element->priority;
+
+ $this->items[] = $item;
+ }
+
+ array_multisort($sort, SORT_ASC, $this->items);
+ }
}
diff --git a/bridges/SteamCommunityBridge.php b/bridges/SteamCommunityBridge.php
index b0f08cf0..37ed555d 100644
--- a/bridges/SteamCommunityBridge.php
+++ b/bridges/SteamCommunityBridge.php
@@ -1,191 +1,209 @@
<?php
-class SteamCommunityBridge extends BridgeAbstract {
- const NAME = 'Steam Community';
- const URI = 'https://www.steamcommunity.com';
- const DESCRIPTION = 'Get the latest community updates for a game on Steam.';
- const MAINTAINER = 'thefranke';
- const CACHE_TIMEOUT = 3600; // 1h
-
- const PARAMETERS = array(
- array(
- 'i' => array(
- 'name' => 'App ID',
- 'exampleValue' => '730',
- 'required' => true
- ),
- 'category' => array(
- 'name' => 'category',
- 'type' => 'list',
- 'exampleValue' => 'Artwork',
- 'title' => 'Select a category',
- 'values' => array(
- 'Artwork' => 'images',
- 'Screenshots' => 'screenshots',
- 'Videos' => 'videos',
- 'Workshop' => 'workshop'
- )
- )
- )
- );
-
- public function getIcon() {
- return self::URI . '/favicon.ico';
- }
-
- protected function getMainPage() {
- $category = $this->getInput('category');
- $html = getSimpleHTMLDOM($this->getURI());
-
- return $html;
- }
-
- public function getName() {
- $category = $this->getInput('category');
-
- if (is_null('i') || is_null($category)) {
- return self::NAME;
- }
-
- $html = $this->getMainPage();
-
- $titleItem = $html->find('div.apphub_AppName', 0);
-
- if (!$titleItem)
- return self::NAME;
-
- return $titleItem->innertext . ' (' . ucwords($category) . ')';
- }
-
- public function getURI() {
- if ($this->getInput('category') === 'workshop')
- return self::URI . '/workshop/browse/?appid='
- . $this->getInput('i') . '&browsesort=mostrecent';
-
- return self::URI . '/app/'
- . $this->getInput('i') . '/'
- . $this->getInput('category')
- . '/?p=1&browsefilter=mostrecent';
- }
-
- private function collectMedia() {
- $category = $this->getInput('category');
- $html = $this->getMainPage();
- $cards = $html->find('div.apphub_Card');
-
- foreach($cards as $card) {
- $uri = $card->getAttribute('data-modal-content-url');
-
- $htmlCard = getSimpleHTMLDOMCached($uri);
-
- $author = $card->find('div.apphub_CardContentAuthorName', 0)->innertext;
- $author = strip_tags($author);
-
- $title = $author . '\'s screenshot';
-
- if ($category != 'screenshots')
- $title = $htmlCard->find('div.workshopItemTitle', 0)->innertext;
-
- $date = $htmlCard->find('div.detailsStatRight', 0)->innertext;
-
- // create item
- $item = array();
- $item['title'] = $title;
- $item['uri'] = $uri;
- $item['timestamp'] = strtotime($date);
- $item['author'] = $author;
- $item['categories'] = $category;
-
- $media = $htmlCard->getElementById('ActualMedia');
- $mediaURI = $media->getAttribute('src');
- $downloadURI = $mediaURI;
-
- if ($category == 'videos') {
- preg_match('/.*\/embed\/(.*)\?/', $mediaURI, $result);
- $youtubeID = $result[1];
- $mediaURI = 'https://img.youtube.com/vi/' . $youtubeID . '/hqdefault.jpg';
- $downloadURI = 'https://www.youtube.com/watch?v=' . $youtubeID;
- }
-
- $desc = '';
-
- if ($category == 'screenshots') {
- $descItem = $htmlCard->find('div.screenshotDescription', 0);
- if ($descItem)
- $desc = $descItem->innertext;
- }
- if ($category == 'images') {
- $descItem = $htmlCard->find('div.nonScreenshotDescription', 0);
- if ($descItem)
- $desc = $descItem->innertext;
- $downloadURI = $htmlCard->find('a.downloadImage', 0)->href;
- }
-
- $item['content'] = '<p><a href="' . $downloadURI . '"><img src="' . $mediaURI . '"/></a></p>';
- $item['content'] .= '<p>' . $desc . '</p>';
-
- $this->items[] = $item;
-
- if (count($this->items) >= 10)
- break;
- }
- }
-
- private function collectWorkshop() {
- $category = $this->getInput('category');
- $html = $this->getMainPage();
- $workShopItems = $html->find('div.workshopItem');
-
- foreach($workShopItems as $workShopItem) {
- $author = $workShopItem->find('div.workshopItemAuthorName', 0)->find('a', 0);
- $author = $author->innertext;
-
- $fileRating = $workShopItem->find('img.fileRating', 0);
-
- $uri = $workShopItem->find('a.ugc', 0)->getAttribute('href');
-
- $htmlItem = getSimpleHTMLDOMCached($uri);
-
- $title = $htmlItem->find('div.workshopItemTitle', 0)->innertext;
- $date = $htmlItem->find('div.detailsStatRight', 0)->innertext;
- $description = $htmlItem->find('div.workshopItemDescription', 0)->innertext;
-
- $previewImage = $htmlItem->find('#previewImage', 0);
-
- $htmlTags = $htmlItem->find('div.workshopTags');
-
- $tags = '';
-
- foreach($htmlTags as $htmlTag) {
- if ($tags !== '')
- $tags .= ',';
-
- $tags .= $htmlTag->find('a', 0)->innertext;
- }
-
- // create item
- $item = array();
- $item['title'] = $title;
- $item['uri'] = $uri;
- $item['timestamp'] = strtotime($date);
- $item['author'] = $author;
- $item['categories'] = $category;
-
- $item['content'] = '<p><a href="' . $uri . '">'
- . $previewImage . '</a></p><p>' . $fileRating
- . '</p><p>' . $description . '</p>';
-
- $this->items[] = $item;
-
- if (count($this->items) >= 10)
- break;
- }
- }
-
- public function collectData() {
- if ($this->getInput('category') === 'workshop')
- $this->collectWorkshop();
- else
- $this->collectMedia();
- }
+class SteamCommunityBridge extends BridgeAbstract
+{
+ const NAME = 'Steam Community';
+ const URI = 'https://www.steamcommunity.com';
+ const DESCRIPTION = 'Get the latest community updates for a game on Steam.';
+ const MAINTAINER = 'thefranke';
+ const CACHE_TIMEOUT = 3600; // 1h
+
+ const PARAMETERS = [
+ [
+ 'i' => [
+ 'name' => 'App ID',
+ 'exampleValue' => '730',
+ 'required' => true
+ ],
+ 'category' => [
+ 'name' => 'category',
+ 'type' => 'list',
+ 'exampleValue' => 'Artwork',
+ 'title' => 'Select a category',
+ 'values' => [
+ 'Artwork' => 'images',
+ 'Screenshots' => 'screenshots',
+ 'Videos' => 'videos',
+ 'Workshop' => 'workshop'
+ ]
+ ]
+ ]
+ ];
+
+ public function getIcon()
+ {
+ return self::URI . '/favicon.ico';
+ }
+
+ protected function getMainPage()
+ {
+ $category = $this->getInput('category');
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ return $html;
+ }
+
+ public function getName()
+ {
+ $category = $this->getInput('category');
+
+ if (is_null('i') || is_null($category)) {
+ return self::NAME;
+ }
+
+ $html = $this->getMainPage();
+
+ $titleItem = $html->find('div.apphub_AppName', 0);
+
+ if (!$titleItem) {
+ return self::NAME;
+ }
+
+ return $titleItem->innertext . ' (' . ucwords($category) . ')';
+ }
+
+ public function getURI()
+ {
+ if ($this->getInput('category') === 'workshop') {
+ return self::URI . '/workshop/browse/?appid='
+ . $this->getInput('i') . '&browsesort=mostrecent';
+ }
+
+ return self::URI . '/app/'
+ . $this->getInput('i') . '/'
+ . $this->getInput('category')
+ . '/?p=1&browsefilter=mostrecent';
+ }
+
+ private function collectMedia()
+ {
+ $category = $this->getInput('category');
+ $html = $this->getMainPage();
+ $cards = $html->find('div.apphub_Card');
+
+ foreach ($cards as $card) {
+ $uri = $card->getAttribute('data-modal-content-url');
+
+ $htmlCard = getSimpleHTMLDOMCached($uri);
+
+ $author = $card->find('div.apphub_CardContentAuthorName', 0)->innertext;
+ $author = strip_tags($author);
+
+ $title = $author . '\'s screenshot';
+
+ if ($category != 'screenshots') {
+ $title = $htmlCard->find('div.workshopItemTitle', 0)->innertext;
+ }
+
+ $date = $htmlCard->find('div.detailsStatRight', 0)->innertext;
+
+ // create item
+ $item = [];
+ $item['title'] = $title;
+ $item['uri'] = $uri;
+ $item['timestamp'] = strtotime($date);
+ $item['author'] = $author;
+ $item['categories'] = $category;
+
+ $media = $htmlCard->getElementById('ActualMedia');
+ $mediaURI = $media->getAttribute('src');
+ $downloadURI = $mediaURI;
+
+ if ($category == 'videos') {
+ preg_match('/.*\/embed\/(.*)\?/', $mediaURI, $result);
+ $youtubeID = $result[1];
+ $mediaURI = 'https://img.youtube.com/vi/' . $youtubeID . '/hqdefault.jpg';
+ $downloadURI = 'https://www.youtube.com/watch?v=' . $youtubeID;
+ }
+
+ $desc = '';
+
+ if ($category == 'screenshots') {
+ $descItem = $htmlCard->find('div.screenshotDescription', 0);
+ if ($descItem) {
+ $desc = $descItem->innertext;
+ }
+ }
+
+ if ($category == 'images') {
+ $descItem = $htmlCard->find('div.nonScreenshotDescription', 0);
+ if ($descItem) {
+ $desc = $descItem->innertext;
+ }
+ $downloadURI = $htmlCard->find('a.downloadImage', 0)->href;
+ }
+
+ $item['content'] = '<p><a href="' . $downloadURI . '"><img src="' . $mediaURI . '"/></a></p>';
+ $item['content'] .= '<p>' . $desc . '</p>';
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10) {
+ break;
+ }
+ }
+ }
+
+ private function collectWorkshop()
+ {
+ $category = $this->getInput('category');
+ $html = $this->getMainPage();
+ $workShopItems = $html->find('div.workshopItem');
+
+ foreach ($workShopItems as $workShopItem) {
+ $author = $workShopItem->find('div.workshopItemAuthorName', 0)->find('a', 0);
+ $author = $author->innertext;
+
+ $fileRating = $workShopItem->find('img.fileRating', 0);
+
+ $uri = $workShopItem->find('a.ugc', 0)->getAttribute('href');
+
+ $htmlItem = getSimpleHTMLDOMCached($uri);
+
+ $title = $htmlItem->find('div.workshopItemTitle', 0)->innertext;
+ $date = $htmlItem->find('div.detailsStatRight', 0)->innertext;
+ $description = $htmlItem->find('div.workshopItemDescription', 0)->innertext;
+
+ $previewImage = $htmlItem->find('#previewImage', 0);
+
+ $htmlTags = $htmlItem->find('div.workshopTags');
+
+ $tags = '';
+
+ foreach ($htmlTags as $htmlTag) {
+ if ($tags !== '') {
+ $tags .= ',';
+ }
+
+ $tags .= $htmlTag->find('a', 0)->innertext;
+ }
+
+ // create item
+ $item = [];
+ $item['title'] = $title;
+ $item['uri'] = $uri;
+ $item['timestamp'] = strtotime($date);
+ $item['author'] = $author;
+ $item['categories'] = $category;
+
+ $item['content'] = '<p><a href="' . $uri . '">'
+ . $previewImage . '</a></p><p>' . $fileRating
+ . '</p><p>' . $description . '</p>';
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10) {
+ break;
+ }
+ }
+ }
+
+ public function collectData()
+ {
+ if ($this->getInput('category') === 'workshop') {
+ $this->collectWorkshop();
+ } else {
+ $this->collectMedia();
+ }
+ }
}
diff --git a/bridges/StockFilingsBridge.php b/bridges/StockFilingsBridge.php
index f774244a..2817dd97 100644
--- a/bridges/StockFilingsBridge.php
+++ b/bridges/StockFilingsBridge.php
@@ -1,80 +1,86 @@
<?php
-class StockFilingsBridge extends FeedExpander {
- const MAINTAINER = 'captn3m0';
- const NAME = 'SEC Stock filings';
- const URI = 'https://www.sec.gov/edgar/searchedgar/companysearch.html';
- const CACHE_TIMEOUT = 3600; // 1h
- const DESCRIPTION = 'Tracks SEC Filings for a single company';
- const SEARCH_URL = 'https://www.sec.gov/cgi-bin/browse-edgar?owner=exclude&action=getcompany&CIK=';
- const WEBSITE_ROOT = 'https://www.sec.gov';
+class StockFilingsBridge extends FeedExpander
+{
+ const MAINTAINER = 'captn3m0';
+ const NAME = 'SEC Stock filings';
+ const URI = 'https://www.sec.gov/edgar/searchedgar/companysearch.html';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Tracks SEC Filings for a single company';
+ const SEARCH_URL = 'https://www.sec.gov/cgi-bin/browse-edgar?owner=exclude&action=getcompany&CIK=';
+ const WEBSITE_ROOT = 'https://www.sec.gov';
- const PARAMETERS = array(
- array(
- 'ticker' => array(
- 'name' => 'cik',
- 'required' => true,
- 'exampleValue' => 'AMD',
- // https://stackoverflow.com/a/12827734
- 'pattern' => '[A-Za-z0-9]+',
- ),
- ));
+ const PARAMETERS = [
+ [
+ 'ticker' => [
+ 'name' => 'cik',
+ 'required' => true,
+ 'exampleValue' => 'AMD',
+ // https://stackoverflow.com/a/12827734
+ 'pattern' => '[A-Za-z0-9]+',
+ ],
+ ]];
- public function getIcon() {
- return 'https://www.sec.gov/favicon.ico';
- }
+ public function getIcon()
+ {
+ return 'https://www.sec.gov/favicon.ico';
+ }
- /**
- * Generates search URL
- */
- private function getSearchUrl() {
- return self::SEARCH_URL . $this->getInput('ticker');
- }
+ /**
+ * Generates search URL
+ */
+ private function getSearchUrl()
+ {
+ return self::SEARCH_URL . $this->getInput('ticker');
+ }
- /**
- * Returns the Company Name
- */
- private function getRssFeed($html) {
- $links = $html->find('#contentDiv a');
+ /**
+ * Returns the Company Name
+ */
+ private function getRssFeed($html)
+ {
+ $links = $html->find('#contentDiv a');
- foreach ($links as $link) {
- $href = $link->href;
+ foreach ($links as $link) {
+ $href = $link->href;
- if (substr($href, 0, 4) !== 'http') {
- $href = self::WEBSITE_ROOT . $href;
- }
- parse_str(html_entity_decode(parse_url($href, PHP_URL_QUERY)), $query);
+ if (substr($href, 0, 4) !== 'http') {
+ $href = self::WEBSITE_ROOT . $href;
+ }
+ parse_str(html_entity_decode(parse_url($href, PHP_URL_QUERY)), $query);
- if (isset($query['output']) and ($query['output'] == 'atom')) {
- return $href;
- }
- }
+ if (isset($query['output']) and ($query['output'] == 'atom')) {
+ return $href;
+ }
+ }
- return false;
- }
+ return false;
+ }
- /**
- * Return \simple_html_dom object
- * for the entire html of the product page
- */
- private function getHtml() {
- $uri = $this->getSearchUrl();
+ /**
+ * Return \simple_html_dom object
+ * for the entire html of the product page
+ */
+ private function getHtml()
+ {
+ $uri = $this->getSearchUrl();
- return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request SEC.');
- }
+ return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request SEC.');
+ }
- /**
- * Scrape the SEC Stock Filings RSS Feed URL
- * and redirect there
- */
- public function collectData() {
- $html = $this->getHtml();
- $rssFeedUrl = $this->getRssFeed($html);
+ /**
+ * Scrape the SEC Stock Filings RSS Feed URL
+ * and redirect there
+ */
+ public function collectData()
+ {
+ $html = $this->getHtml();
+ $rssFeedUrl = $this->getRssFeed($html);
- if ($rssFeedUrl) {
- parent::collectExpandableDatas($rssFeedUrl);
- } else {
- returnClientError('Could not find RSS Feed URL. Are you sure you used a valid CIK?');
- }
- }
+ if ($rssFeedUrl) {
+ parent::collectExpandableDatas($rssFeedUrl);
+ } else {
+ returnClientError('Could not find RSS Feed URL. Are you sure you used a valid CIK?');
+ }
+ }
}
diff --git a/bridges/StripeAPIChangeLogBridge.php b/bridges/StripeAPIChangeLogBridge.php
index 1db34c14..43b80e01 100644
--- a/bridges/StripeAPIChangeLogBridge.php
+++ b/bridges/StripeAPIChangeLogBridge.php
@@ -1,22 +1,25 @@
<?php
-class StripeAPIChangeLogBridge extends BridgeAbstract {
- const MAINTAINER = 'Pierre Mazière';
- const NAME = 'Stripe API Changelog';
- const URI = 'https://stripe.com/docs/upgrades';
- const CACHE_TIMEOUT = 86400; // 24h
- const DESCRIPTION = 'Returns the changes made to the stripe.com API';
- public function collectData(){
- $html = getSimpleHTMLDOM(self::URI);
+class StripeAPIChangeLogBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Pierre Mazière';
+ const NAME = 'Stripe API Changelog';
+ const URI = 'https://stripe.com/docs/upgrades';
+ const CACHE_TIMEOUT = 86400; // 24h
+ const DESCRIPTION = 'Returns the changes made to the stripe.com API';
- foreach($html->find('h3') as $change) {
- $item = array();
- $item['title'] = trim($change->plaintext);
- $item['uri'] = self::URI . '#' . $item['title'];
- $item['author'] = 'stripe';
- $item['content'] = $change->nextSibling()->outertext;
- $item['timestamp'] = strtotime($item['title']);
- $this->items[] = $item;
- }
- }
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
+
+ foreach ($html->find('h3') as $change) {
+ $item = [];
+ $item['title'] = trim($change->plaintext);
+ $item['uri'] = self::URI . '#' . $item['title'];
+ $item['author'] = 'stripe';
+ $item['content'] = $change->nextSibling()->outertext;
+ $item['timestamp'] = strtotime($item['title']);
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/SummitsOnTheAirBridge.php b/bridges/SummitsOnTheAirBridge.php
index 83383330..53bba7ab 100644
--- a/bridges/SummitsOnTheAirBridge.php
+++ b/bridges/SummitsOnTheAirBridge.php
@@ -1,37 +1,38 @@
<?php
-class SummitsOnTheAirBridge extends BridgeAbstract {
- const MAINTAINER = 's0lesurviv0r';
- const NAME = 'Summits On The Air Spots';
- const URI = 'https://api2.sota.org.uk/api/spots/';
- const CACHE_TIMEOUT = 60; // 1m
- const DESCRIPTION = 'Summits On The Air Activator Spots';
-
- const PARAMETERS = array(
- 'Count' => array(
- 'c' => array(
- 'name' => 'count',
- 'required' => true,
- 'defaultValue' => 10
- )
- )
- );
-
- public function collectData()
- {
- $header = array('Content-type:application/json');
- $opts = array(CURLOPT_HTTPGET => 1);
- $json = getContents($this->getURI() . $this->getInput('c'), $header, $opts);
-
- $spots = json_decode($json, true);
-
- foreach ($spots as $spot) {
- $summit = $spot['associationCode'] . '/' . $spot['summitCode'];
-
- $title = $spot['activatorCallsign'] . ' @ ' . $summit . ' ' .
- $spot['frequency'] . ' MHz';
-
- $content = <<<EOL
+class SummitsOnTheAirBridge extends BridgeAbstract
+{
+ const MAINTAINER = 's0lesurviv0r';
+ const NAME = 'Summits On The Air Spots';
+ const URI = 'https://api2.sota.org.uk/api/spots/';
+ const CACHE_TIMEOUT = 60; // 1m
+ const DESCRIPTION = 'Summits On The Air Activator Spots';
+
+ const PARAMETERS = [
+ 'Count' => [
+ 'c' => [
+ 'name' => 'count',
+ 'required' => true,
+ 'defaultValue' => 10
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $header = ['Content-type:application/json'];
+ $opts = [CURLOPT_HTTPGET => 1];
+ $json = getContents($this->getURI() . $this->getInput('c'), $header, $opts);
+
+ $spots = json_decode($json, true);
+
+ foreach ($spots as $spot) {
+ $summit = $spot['associationCode'] . '/' . $spot['summitCode'];
+
+ $title = $spot['activatorCallsign'] . ' @ ' . $summit . ' ' .
+ $spot['frequency'] . ' MHz';
+
+ $content = <<<EOL
<a href="http://summits.sota.org.uk/summit/{$summit}">
{$summit}, {$spot['summitDetails']}</a><br />
Frequency: {$spot['frequency']} MHz<br />
@@ -39,12 +40,12 @@ class SummitsOnTheAirBridge extends BridgeAbstract {
Comments: {$spot['comments']}
EOL;
- $this->items[] = array(
- 'uri' => 'https://sotawatch.sota.org.uk/en/',
- 'title' => $title,
- 'content' => $content,
- 'timestamp' => $spot['timeStamp']
- );
- }
- }
+ $this->items[] = [
+ 'uri' => 'https://sotawatch.sota.org.uk/en/',
+ 'title' => $title,
+ 'content' => $content,
+ 'timestamp' => $spot['timeStamp']
+ ];
+ }
+ }
}
diff --git a/bridges/SuperSmashBlogBridge.php b/bridges/SuperSmashBlogBridge.php
index fd3ace63..4a6478ea 100644
--- a/bridges/SuperSmashBlogBridge.php
+++ b/bridges/SuperSmashBlogBridge.php
@@ -1,45 +1,46 @@
<?php
-class SuperSmashBlogBridge extends BridgeAbstract {
- const MAINTAINER = 'corenting';
- const NAME = 'Super Smash Blog';
- const URI = 'https://www.smashbros.com/en_US/blog/index.html';
- const CACHE_TIMEOUT = 7200; // 2h
- const DESCRIPTION = 'Latest articles from the Super Smash Blog blog';
-
- public function collectData(){
- $dlUrl = 'https://www.smashbros.com/data/bs/en_US/json/en_US.json';
-
- $jsonString = getContents($dlUrl);
- $json = json_decode($jsonString, true);
-
- foreach($json as $article) {
-
- // Build content
- $picture = $article['acf']['image1']['url'];
- if (strlen($picture) != 0) {
- $picture = str_get_html('<img src="https://www.smashbros.com/' . substr($picture, 8) . '"/>');
- } else {
- $picture = '';
- }
-
- $video = $article['acf']['link_url'];
- if (strlen($video) != 0) {
- $video = str_get_html('<a href="' . $video . '">Youtube video</a>');
- } else {
- $video = '';
- }
- $text = str_get_html($article['acf']['editor']);
- $content = $picture . $video . $text;
-
- // Build final item
- $item = array();
- $item['title'] = $article['title']['rendered'];
- $item['timestamp'] = strtotime($article['date']);
- $item['content'] = $content;
- $item['uri'] = self::URI . '?post=' . $article['id'];
-
- $this->items[] = $item;
- }
- }
+class SuperSmashBlogBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'corenting';
+ const NAME = 'Super Smash Blog';
+ const URI = 'https://www.smashbros.com/en_US/blog/index.html';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Latest articles from the Super Smash Blog blog';
+
+ public function collectData()
+ {
+ $dlUrl = 'https://www.smashbros.com/data/bs/en_US/json/en_US.json';
+
+ $jsonString = getContents($dlUrl);
+ $json = json_decode($jsonString, true);
+
+ foreach ($json as $article) {
+ // Build content
+ $picture = $article['acf']['image1']['url'];
+ if (strlen($picture) != 0) {
+ $picture = str_get_html('<img src="https://www.smashbros.com/' . substr($picture, 8) . '"/>');
+ } else {
+ $picture = '';
+ }
+
+ $video = $article['acf']['link_url'];
+ if (strlen($video) != 0) {
+ $video = str_get_html('<a href="' . $video . '">Youtube video</a>');
+ } else {
+ $video = '';
+ }
+ $text = str_get_html($article['acf']['editor']);
+ $content = $picture . $video . $text;
+
+ // Build final item
+ $item = [];
+ $item['title'] = $article['title']['rendered'];
+ $item['timestamp'] = strtotime($article['date']);
+ $item['content'] = $content;
+ $item['uri'] = self::URI . '?post=' . $article['id'];
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/SymfonyCastsBridge.php b/bridges/SymfonyCastsBridge.php
index 63b908ce..29ba87cd 100644
--- a/bridges/SymfonyCastsBridge.php
+++ b/bridges/SymfonyCastsBridge.php
@@ -1,33 +1,34 @@
<?php
-class SymfonyCastsBridge extends BridgeAbstract {
- const NAME = 'SymfonyCasts Bridge';
- const URI = 'https://symfonycasts.com/';
- const DESCRIPTION = 'Follow new updates on symfonycasts.com';
- const MAINTAINER = 'Park0';
- const CACHE_TIMEOUT = 3600;
+class SymfonyCastsBridge extends BridgeAbstract
+{
+ const NAME = 'SymfonyCasts Bridge';
+ const URI = 'https://symfonycasts.com/';
+ const DESCRIPTION = 'Follow new updates on symfonycasts.com';
+ const MAINTAINER = 'Park0';
+ const CACHE_TIMEOUT = 3600;
- public function collectData() {
- $html = getSimpleHTMLDOM('https://symfonycasts.com/updates/find');
- $dives = $html->find('div');
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM('https://symfonycasts.com/updates/find');
+ $dives = $html->find('div');
- /* @var simple_html_dom $div */
- foreach ($dives as $div) {
- $id = $div->getAttribute('data-mark-update-id-value');
- $type = $div->find('h5', 0);
- $title = $div->find('span', 0);
- $dateString = $div->find('h5.font-gray', 0);
- $href = $div->find('a', 0);
- $url = 'https://symfonycasts.com' . $href->getAttribute('href');
+ /* @var simple_html_dom $div */
+ foreach ($dives as $div) {
+ $id = $div->getAttribute('data-mark-update-id-value');
+ $type = $div->find('h5', 0);
+ $title = $div->find('span', 0);
+ $dateString = $div->find('h5.font-gray', 0);
+ $href = $div->find('a', 0);
+ $url = 'https://symfonycasts.com' . $href->getAttribute('href');
- $item = array(); // Create an empty item
- $item['uid'] = $id;
- $item['title'] = $title->innertext;
- $item['timestamp'] = $dateString->innertext;
- $item['content'] = $type->plaintext . '<a href="' . $url . '">' . $title . '</a>';
- $item['uri'] = $url;
- $this->items[] = $item; // Add item to the list
- }
-
- }
+ $item = []; // Create an empty item
+ $item['uid'] = $id;
+ $item['title'] = $title->innertext;
+ $item['timestamp'] = $dateString->innertext;
+ $item['content'] = $type->plaintext . '<a href="' . $url . '">' . $title . '</a>';
+ $item['uri'] = $url;
+ $this->items[] = $item; // Add item to the list
+ }
+ }
}
diff --git a/bridges/TbibBridge.php b/bridges/TbibBridge.php
index 509b1e11..88f046fc 100644
--- a/bridges/TbibBridge.php
+++ b/bridges/TbibBridge.php
@@ -1,15 +1,16 @@
<?php
-class TbibBridge extends GelbooruBridge {
+class TbibBridge extends GelbooruBridge
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Tbib';
+ const URI = 'https://tbib.org/';
+ const DESCRIPTION = 'Returns images from given page';
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Tbib';
- const URI = 'https://tbib.org/';
- const DESCRIPTION = 'Returns images from given page';
-
- protected function buildThumbnailURI($element){
- $regex = '/\.\w+$/';
- return $this->getURI() . 'thumbnails/' . $element->directory
- . '/thumbnail_' . preg_replace($regex, '.jpg', $element->image);
- }
+ protected function buildThumbnailURI($element)
+ {
+ $regex = '/\.\w+$/';
+ return $this->getURI() . 'thumbnails/' . $element->directory
+ . '/thumbnail_' . preg_replace($regex, '.jpg', $element->image);
+ }
}
diff --git a/bridges/TebeoBridge.php b/bridges/TebeoBridge.php
index ba44d0e2..aef303c4 100644
--- a/bridges/TebeoBridge.php
+++ b/bridges/TebeoBridge.php
@@ -1,41 +1,45 @@
<?php
-class TebeoBridge extends FeedExpander {
- const NAME = 'Tébéo Bridge';
- const URI = 'http://www.tebeo.bzh/';
- const CACHE_TIMEOUT = 21600; //6h
- const DESCRIPTION = 'Returns the newest Tébéo videos by category';
- const MAINTAINER = 'Mitsukarenai';
- const PARAMETERS = array( array(
- 'cat' => array(
- 'name' => 'Catégorie',
- 'type' => 'list',
- 'values' => array(
- 'Toutes les vidéos' => '/',
- 'Actualité' => '/14-actualite',
- 'Sport' => '/3-sport',
- 'Culture-Loisirs' => '/5-culture-loisirs',
- 'Société' => '/15-societe',
- 'Langue Bretonne' => '/9-langue-bretonne'
- )
- )
- ));
+class TebeoBridge extends FeedExpander
+{
+ const NAME = 'Tébéo Bridge';
+ const URI = 'http://www.tebeo.bzh/';
+ const CACHE_TIMEOUT = 21600; //6h
+ const DESCRIPTION = 'Returns the newest Tébéo videos by category';
+ const MAINTAINER = 'Mitsukarenai';
- public function getIcon() {
- return self::URI . 'images/header_logo.png';
- }
+ const PARAMETERS = [ [
+ 'cat' => [
+ 'name' => 'Catégorie',
+ 'type' => 'list',
+ 'values' => [
+ 'Toutes les vidéos' => '/',
+ 'Actualité' => '/14-actualite',
+ 'Sport' => '/3-sport',
+ 'Culture-Loisirs' => '/5-culture-loisirs',
+ 'Société' => '/15-societe',
+ 'Langue Bretonne' => '/9-langue-bretonne'
+ ]
+ ]
+ ]];
- public function collectData(){
- $url = self::URI . '/le-replay/' . $this->getInput('cat');
- $html = getSimpleHTMLDOM($url);
+ public function getIcon()
+ {
+ return self::URI . 'images/header_logo.png';
+ }
- foreach($html->find('div[id=items_replay] div.replay') as $element) {
- $item = array();
- $item['uri'] = $element->find('a', 0)->href;
- $item['title'] = $element->find('h3', 0)->plaintext;
- $item['timestamp'] = strtotime($element->find('p.moment-format-day', 0)->plaintext);
- $item['content'] = '<a href="' . $item['uri'] . '"><img alt="" src="' . $element->find('img', 0)->src . '"></a>';
- $this->items[] = $item;
- }
- }
+ public function collectData()
+ {
+ $url = self::URI . '/le-replay/' . $this->getInput('cat');
+ $html = getSimpleHTMLDOM($url);
+
+ foreach ($html->find('div[id=items_replay] div.replay') as $element) {
+ $item = [];
+ $item['uri'] = $element->find('a', 0)->href;
+ $item['title'] = $element->find('h3', 0)->plaintext;
+ $item['timestamp'] = strtotime($element->find('p.moment-format-day', 0)->plaintext);
+ $item['content'] = '<a href="' . $item['uri'] . '"><img alt="" src="' . $element->find('img', 0)->src . '"></a>';
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/TelegramBridge.php b/bridges/TelegramBridge.php
index 415bc636..a980f57a 100644
--- a/bridges/TelegramBridge.php
+++ b/bridges/TelegramBridge.php
@@ -1,355 +1,372 @@
<?php
-class TelegramBridge extends BridgeAbstract {
- const NAME = 'Telegram Bridge';
- const URI = 'https://t.me';
- const DESCRIPTION = 'Returns newest posts from a public Telegram channel';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(array(
- 'username' => array(
- 'name' => 'Username',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => '@rssbridge',
- )
- )
- );
- const TEST_DETECT_PARAMETERS = array(
- 'https://t.me/s/durov' => array('username' => 'durov'),
- 'https://t.me/durov' => array('username' => 'durov'),
- 'http://t.me/durov' => array('username' => 'durov'),
- );
-
- const CACHE_TIMEOUT = 900; // 15 mins
-
- private $feedName = '';
- private $enclosures = array();
- private $itemTitle = '';
-
- private $backgroundImageRegex = "/background-image:url\('(.*)'\)/";
- private $detectParamsRegex = '/^https?:\/\/t.me\/(?:s\/)?([\w]+)$/';
-
- public function detectParameters($url) {
- $params = array();
-
- if(preg_match($this->detectParamsRegex, $url, $matches) > 0) {
- $params['username'] = $matches[1];
- return $params;
- }
-
- return null;
- }
-
- public function collectData() {
-
- $html = getSimpleHTMLDOM($this->getURI());
-
- $channelTitle = htmlspecialchars_decode(
- $html->find('div.tgme_channel_info_header_title span', 0)->plaintext,
- ENT_QUOTES
- );
-
- $this->feedName = $channelTitle . ' (@' . $this->processUsername() . ')';
-
- foreach($html->find('div.tgme_widget_message_wrap.js-widget_message_wrap') as $index => $messageDiv) {
- $this->itemTitle = '';
- $this->enclosures = array();
- $item = array();
-
- $item['uri'] = $this->processUri($messageDiv);
- $item['content'] = $this->processContent($messageDiv);
- $item['title'] = $this->itemTitle;
- $item['timestamp'] = $this->processDate($messageDiv);
- $item['enclosures'] = $this->enclosures;
- $author = trim($messageDiv->find('a.tgme_widget_message_owner_name', 0)->plaintext);
- $item['author'] = html_entity_decode($author, ENT_QUOTES);
-
- $this->items[] = $item;
- }
- $this->items = array_reverse($this->items);
- }
-
- public function getURI() {
- if (!is_null($this->getInput('username'))) {
- return self::URI . '/s/' . $this->processUsername();
- }
-
- return parent::getURI();
- }
-
- public function getName() {
- if (!empty($this->feedName)) {
- return $this->feedName . ' - Telegram';
- }
-
- return parent::getName();
- }
-
- private function processUsername() {
- if (substr($this->getInput('username'), 0, 1) === '@') {
- return substr($this->getInput('username'), 1);
- }
-
- return $this->getInput('username');
- }
-
- private function processUri($messageDiv) {
- return $messageDiv->find('a.tgme_widget_message_date', 0)->href;
- }
-
- private function processDate($messageDiv) {
- $messageMeta = $messageDiv->find('span.tgme_widget_message_meta', 0);
- return $messageMeta->find('time', 0)->datetime;
- }
-
- private function processContent($messageDiv) {
- $message = '';
-
- if ($messageDiv->find('div.tgme_widget_message_forwarded_from', 0)) {
- $message = $messageDiv->find('div.tgme_widget_message_forwarded_from', 0)->innertext . '<br><br>';
- }
-
- if ($messageDiv->find('a.tgme_widget_message_reply', 0)) {
- $message .= $this->processReply($messageDiv);
- }
- if ($messageDiv->find('div.tgme_widget_message_sticker_wrap', 0)) {
- $message .= $this->processSticker($messageDiv);
- }
-
- if ($messageDiv->find('div.tgme_widget_message_poll', 0)) {
- $message .= $this->processPoll($messageDiv);
- }
-
- if ($messageDiv->find('video', 0)) {
- $message .= $this->processVideo($messageDiv);
- }
-
- if ($messageDiv->find('a.tgme_widget_message_photo_wrap', 0)) {
- $message .= $this->processPhoto($messageDiv);
- }
-
- if ($messageDiv->find('a.not_supported', 0)) {
- $message .= $this->processNotSupported($messageDiv);
- }
-
- if ($messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)) {
- $message .= $messageDiv->find('div.tgme_widget_message_text.js-message_text', 0);
-
- $this->itemTitle = $this->ellipsisTitle(
- $messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)->plaintext
- );
- }
-
- if ($messageDiv->find('div.tgme_widget_message_document', 0)) {
- $message .= $this->processAttachment($messageDiv);
- }
-
- if ($messageDiv->find('a.tgme_widget_message_link_preview', 0)) {
- $message .= $this->processLinkPreview($messageDiv);
- }
-
- if ($messageDiv->find('a.tgme_widget_message_location_wrap', 0)) {
- $message .= $this->processLocation($messageDiv);
- }
-
- return $message;
- }
-
- private function processReply($messageDiv) {
- $reply = $messageDiv->find('a.tgme_widget_message_reply', 0);
- $author = $reply->find('span.tgme_widget_message_author_name', 0)->plaintext;
- $text = '';
-
- if ($reply->find('div.tgme_widget_message_metatext', 0)) {
- $text = $reply->find('div.tgme_widget_message_metatext', 0)->innertext;
- }
-
- if ($reply->find('div.tgme_widget_message_text', 0)) {
- $text = $reply->find('div.tgme_widget_message_text', 0)->innertext;
- }
-
- return <<<EOD
+class TelegramBridge extends BridgeAbstract
+{
+ const NAME = 'Telegram Bridge';
+ const URI = 'https://t.me';
+ const DESCRIPTION = 'Returns newest posts from a public Telegram channel';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [[
+ 'username' => [
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => '@rssbridge',
+ ]
+ ]
+ ];
+ const TEST_DETECT_PARAMETERS = [
+ 'https://t.me/s/durov' => ['username' => 'durov'],
+ 'https://t.me/durov' => ['username' => 'durov'],
+ 'http://t.me/durov' => ['username' => 'durov'],
+ ];
+
+ const CACHE_TIMEOUT = 900; // 15 mins
+
+ private $feedName = '';
+ private $enclosures = [];
+ private $itemTitle = '';
+
+ private $backgroundImageRegex = "/background-image:url\('(.*)'\)/";
+ private $detectParamsRegex = '/^https?:\/\/t.me\/(?:s\/)?([\w]+)$/';
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ if (preg_match($this->detectParamsRegex, $url, $matches) > 0) {
+ $params['username'] = $matches[1];
+ return $params;
+ }
+
+ return null;
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $channelTitle = htmlspecialchars_decode(
+ $html->find('div.tgme_channel_info_header_title span', 0)->plaintext,
+ ENT_QUOTES
+ );
+
+ $this->feedName = $channelTitle . ' (@' . $this->processUsername() . ')';
+
+ foreach ($html->find('div.tgme_widget_message_wrap.js-widget_message_wrap') as $index => $messageDiv) {
+ $this->itemTitle = '';
+ $this->enclosures = [];
+ $item = [];
+
+ $item['uri'] = $this->processUri($messageDiv);
+ $item['content'] = $this->processContent($messageDiv);
+ $item['title'] = $this->itemTitle;
+ $item['timestamp'] = $this->processDate($messageDiv);
+ $item['enclosures'] = $this->enclosures;
+ $author = trim($messageDiv->find('a.tgme_widget_message_owner_name', 0)->plaintext);
+ $item['author'] = html_entity_decode($author, ENT_QUOTES);
+
+ $this->items[] = $item;
+ }
+ $this->items = array_reverse($this->items);
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('username'))) {
+ return self::URI . '/s/' . $this->processUsername();
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if (!empty($this->feedName)) {
+ return $this->feedName . ' - Telegram';
+ }
+
+ return parent::getName();
+ }
+
+ private function processUsername()
+ {
+ if (substr($this->getInput('username'), 0, 1) === '@') {
+ return substr($this->getInput('username'), 1);
+ }
+
+ return $this->getInput('username');
+ }
+
+ private function processUri($messageDiv)
+ {
+ return $messageDiv->find('a.tgme_widget_message_date', 0)->href;
+ }
+
+ private function processDate($messageDiv)
+ {
+ $messageMeta = $messageDiv->find('span.tgme_widget_message_meta', 0);
+ return $messageMeta->find('time', 0)->datetime;
+ }
+
+ private function processContent($messageDiv)
+ {
+ $message = '';
+
+ if ($messageDiv->find('div.tgme_widget_message_forwarded_from', 0)) {
+ $message = $messageDiv->find('div.tgme_widget_message_forwarded_from', 0)->innertext . '<br><br>';
+ }
+
+ if ($messageDiv->find('a.tgme_widget_message_reply', 0)) {
+ $message .= $this->processReply($messageDiv);
+ }
+
+ if ($messageDiv->find('div.tgme_widget_message_sticker_wrap', 0)) {
+ $message .= $this->processSticker($messageDiv);
+ }
+
+ if ($messageDiv->find('div.tgme_widget_message_poll', 0)) {
+ $message .= $this->processPoll($messageDiv);
+ }
+
+ if ($messageDiv->find('video', 0)) {
+ $message .= $this->processVideo($messageDiv);
+ }
+
+ if ($messageDiv->find('a.tgme_widget_message_photo_wrap', 0)) {
+ $message .= $this->processPhoto($messageDiv);
+ }
+
+ if ($messageDiv->find('a.not_supported', 0)) {
+ $message .= $this->processNotSupported($messageDiv);
+ }
+
+ if ($messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)) {
+ $message .= $messageDiv->find('div.tgme_widget_message_text.js-message_text', 0);
+
+ $this->itemTitle = $this->ellipsisTitle(
+ $messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)->plaintext
+ );
+ }
+
+ if ($messageDiv->find('div.tgme_widget_message_document', 0)) {
+ $message .= $this->processAttachment($messageDiv);
+ }
+
+ if ($messageDiv->find('a.tgme_widget_message_link_preview', 0)) {
+ $message .= $this->processLinkPreview($messageDiv);
+ }
+
+ if ($messageDiv->find('a.tgme_widget_message_location_wrap', 0)) {
+ $message .= $this->processLocation($messageDiv);
+ }
+
+ return $message;
+ }
+
+ private function processReply($messageDiv)
+ {
+ $reply = $messageDiv->find('a.tgme_widget_message_reply', 0);
+ $author = $reply->find('span.tgme_widget_message_author_name', 0)->plaintext;
+ $text = '';
+
+ if ($reply->find('div.tgme_widget_message_metatext', 0)) {
+ $text = $reply->find('div.tgme_widget_message_metatext', 0)->innertext;
+ }
+
+ if ($reply->find('div.tgme_widget_message_text', 0)) {
+ $text = $reply->find('div.tgme_widget_message_text', 0)->innertext;
+ }
+
+ return <<<EOD
<blockquote>{$author}<br>
{$text}
<a href="{$reply->href}">{$reply->href}</a></blockquote><hr>
EOD;
- }
-
- private function processSticker($messageDiv) {
- if (empty($this->itemTitle)) {
- $this->itemTitle = '@' . $this->processUsername() . ' posted a sticker';
- }
-
- $stickerDiv = $messageDiv->find('div.tgme_widget_message_sticker_wrap', 0);
+ }
- if ($stickerDiv->find('picture', 0)) {
- $stickerDiv->find('picture', 0)->find('div', 0)->style = '';
- $stickerDiv->find('picture', 0)->style = '';
+ private function processSticker($messageDiv)
+ {
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = '@' . $this->processUsername() . ' posted a sticker';
+ }
- return $stickerDiv;
+ $stickerDiv = $messageDiv->find('div.tgme_widget_message_sticker_wrap', 0);
- } elseif (preg_match($this->backgroundImageRegex, $stickerDiv->find('i', 0)->style, $sticker)) {
+ if ($stickerDiv->find('picture', 0)) {
+ $stickerDiv->find('picture', 0)->find('div', 0)->style = '';
+ $stickerDiv->find('picture', 0)->style = '';
- return <<<EOD
+ return $stickerDiv;
+ } elseif (preg_match($this->backgroundImageRegex, $stickerDiv->find('i', 0)->style, $sticker)) {
+ return <<<EOD
<a href="{$stickerDiv->children(0)->herf}"><img src="{$sticker[1]}"></a>
EOD;
- }
- }
+ }
+ }
- private function processPoll($messageDiv) {
+ private function processPoll($messageDiv)
+ {
+ $poll = $messageDiv->find('div.tgme_widget_message_poll', 0);
- $poll = $messageDiv->find('div.tgme_widget_message_poll', 0);
+ $title = $poll->find('div.tgme_widget_message_poll_question', 0)->plaintext;
+ $type = $poll->find('div.tgme_widget_message_poll_type', 0)->plaintext;
- $title = $poll->find('div.tgme_widget_message_poll_question', 0)->plaintext;
- $type = $poll->find('div.tgme_widget_message_poll_type', 0)->plaintext;
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = $title;
+ }
- if (empty($this->itemTitle)) {
- $this->itemTitle = $title;
- }
+ $pollOptions = '<ul>';
- $pollOptions = '<ul>';
+ foreach ($poll->find('div.tgme_widget_message_poll_option') as $option) {
+ $pollOptions .= '<li>' . $option->children(0)->plaintext . ' - ' .
+ $option->find('div.tgme_widget_message_poll_option_text', 0)->plaintext . '</li>';
+ }
+ $pollOptions .= '</ul>';
- foreach ($poll->find('div.tgme_widget_message_poll_option') as $option) {
- $pollOptions .= '<li>' . $option->children(0)->plaintext . ' - ' .
- $option->find('div.tgme_widget_message_poll_option_text', 0)->plaintext . '</li>';
- }
- $pollOptions .= '</ul>';
-
- return <<<EOD
+ return <<<EOD
{$title}<br><small>$type</small><br>{$pollOptions}
EOD;
- }
-
- private function processLinkPreview($messageDiv) {
- $image = '';
- $title = '';
- $site = '';
- $description = '';
+ }
- $preview = $messageDiv->find('a.tgme_widget_message_link_preview', 0);
+ private function processLinkPreview($messageDiv)
+ {
+ $image = '';
+ $title = '';
+ $site = '';
+ $description = '';
- if (trim($preview->innertext) === '') {
- return '';
- }
+ $preview = $messageDiv->find('a.tgme_widget_message_link_preview', 0);
- if($preview->find('i', 0) &&
- preg_match($this->backgroundImageRegex, $preview->find('i', 0)->style, $photo)) {
+ if (trim($preview->innertext) === '') {
+ return '';
+ }
- $image = '<img src="' . $photo[1] . '"/>';
- }
+ if (
+ $preview->find('i', 0) &&
+ preg_match($this->backgroundImageRegex, $preview->find('i', 0)->style, $photo)
+ ) {
+ $image = '<img src="' . $photo[1] . '"/>';
+ }
- if ($preview->find('div.link_preview_title', 0)) {
- $title = $preview->find('div.link_preview_title', 0)->plaintext;
- }
+ if ($preview->find('div.link_preview_title', 0)) {
+ $title = $preview->find('div.link_preview_title', 0)->plaintext;
+ }
- if ($preview->find('div.link_preview_site_name', 0)) {
- $site = $preview->find('div.link_preview_site_name', 0)->plaintext;
- }
+ if ($preview->find('div.link_preview_site_name', 0)) {
+ $site = $preview->find('div.link_preview_site_name', 0)->plaintext;
+ }
- if ($preview->find('div.link_preview_description', 0)) {
- $description = $preview->find('div.link_preview_description', 0)->plaintext;
- }
+ if ($preview->find('div.link_preview_description', 0)) {
+ $description = $preview->find('div.link_preview_description', 0)->plaintext;
+ }
- return <<<EOD
+ return <<<EOD
<blockquote><a href="{$preview->href}">{$image}</a><br><a href="{$preview->href}">
{$title} - {$site}</a><br>{$description}</blockquote>
EOD;
- }
+ }
- private function processVideo($messageDiv) {
- if (empty($this->itemTitle)) {
- $this->itemTitle = '@' . $this->processUsername() . ' posted a video';
- }
+ private function processVideo($messageDiv)
+ {
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = '@' . $this->processUsername() . ' posted a video';
+ }
- if ($messageDiv->find('i.tgme_widget_message_video_thumb')) {
- preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo);
- } elseif ($messageDiv->find('i.link_preview_video_thumb')) {
- preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo);
- }
+ if ($messageDiv->find('i.tgme_widget_message_video_thumb')) {
+ preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo);
+ } elseif ($messageDiv->find('i.link_preview_video_thumb')) {
+ preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo);
+ }
- $this->enclosures[] = $photo[1];
+ $this->enclosures[] = $photo[1];
- return <<<EOD
+ return <<<EOD
<video controls="" poster="{$photo[1]}" style="max-width:100%;" preload="none">
<source src="{$messageDiv->find('video', 0)->src}" type="video/mp4">
</video>
EOD;
- }
+ }
- private function processPhoto($messageDiv) {
- if (empty($this->itemTitle)) {
- $this->itemTitle = '@' . $this->processUsername() . ' posted a photo';
- }
+ private function processPhoto($messageDiv)
+ {
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = '@' . $this->processUsername() . ' posted a photo';
+ }
- $photos = '';
+ $photos = '';
- foreach ($messageDiv->find('a.tgme_widget_message_photo_wrap') as $photoWrap) {
- preg_match($this->backgroundImageRegex, $photoWrap->style, $photo);
+ foreach ($messageDiv->find('a.tgme_widget_message_photo_wrap') as $photoWrap) {
+ preg_match($this->backgroundImageRegex, $photoWrap->style, $photo);
- $photos .= <<<EOD
+ $photos .= <<<EOD
<a href="{$photoWrap->href}"><img src="{$photo[1]}"/></a><br>
EOD;
- }
- return $photos;
- }
-
- private function processNotSupported($messageDiv) {
- if (empty($this->itemTitle)) {
- $this->itemTitle = '@' . $this->processUsername() . ' posted a video';
- }
-
- if ($messageDiv->find('i.tgme_widget_message_video_thumb')) {
- preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo);
- } elseif ($messageDiv->find('i.link_preview_video_thumb')) {
- preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo);
- }
-
- return <<<EOD
+ }
+ return $photos;
+ }
+
+ private function processNotSupported($messageDiv)
+ {
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = '@' . $this->processUsername() . ' posted a video';
+ }
+
+ if ($messageDiv->find('i.tgme_widget_message_video_thumb')) {
+ preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo);
+ } elseif ($messageDiv->find('i.link_preview_video_thumb')) {
+ preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo);
+ }
+
+ return <<<EOD
<a href="{$messageDiv->find('a.not_supported', 0)->href}">
{$messageDiv->find('div.message_media_not_supported_label', 0)->innertext}<br><br>
{$messageDiv->find('span.message_media_view_in_telegram', 0)->innertext}<br><br>
<img src="{$photo[1]}"/></a>
EOD;
- }
+ }
- private function processAttachment($messageDiv) {
- $attachments = 'File attachments:<br>';
+ private function processAttachment($messageDiv)
+ {
+ $attachments = 'File attachments:<br>';
- if (empty($this->itemTitle)) {
- $this->itemTitle = '@' . $this->processUsername() . ' posted an attachment';
- }
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = '@' . $this->processUsername() . ' posted an attachment';
+ }
- foreach ($messageDiv->find('div.tgme_widget_message_document') as $document) {
- $attachments .= <<<EOD
+ foreach ($messageDiv->find('div.tgme_widget_message_document') as $document) {
+ $attachments .= <<<EOD
{$document->find('div.tgme_widget_message_document_title', 0)->plaintext} -
{$document->find('div.tgme_widget_message_document_extra', 0)->plaintext}<br>
EOD;
- }
+ }
- return $attachments;
- }
+ return $attachments;
+ }
- private function processLocation($messageDiv) {
- if (empty($this->itemTitle)) {
- $this->itemTitle = '@' . $this->processUsername() . ' posted a location';
- }
+ private function processLocation($messageDiv)
+ {
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = '@' . $this->processUsername() . ' posted a location';
+ }
- preg_match($this->backgroundImageRegex, $messageDiv->find('div.tgme_widget_message_location', 0)->style, $image);
+ preg_match($this->backgroundImageRegex, $messageDiv->find('div.tgme_widget_message_location', 0)->style, $image);
- $link = $messageDiv->find('a.tgme_widget_message_location_wrap', 0)->href;
+ $link = $messageDiv->find('a.tgme_widget_message_location_wrap', 0)->href;
- return <<<EOD
+ return <<<EOD
<a href="{$link}"><img src="{$image[1]}"></a>
EOD;
- }
-
- private function ellipsisTitle($text) {
- $length = 100;
-
- if (strlen($text) > $length) {
- $text = explode('<br>', wordwrap($text, $length, '<br>'));
- return $text[0] . '...';
- }
- return $text;
- }
+ }
+
+ private function ellipsisTitle($text)
+ {
+ $length = 100;
+
+ if (strlen($text) > $length) {
+ $text = explode('<br>', wordwrap($text, $length, '<br>'));
+ return $text[0] . '...';
+ }
+ return $text;
+ }
}
diff --git a/bridges/TheFarSideBridge.php b/bridges/TheFarSideBridge.php
index f8e5a37f..cd3ad9ae 100644
--- a/bridges/TheFarSideBridge.php
+++ b/bridges/TheFarSideBridge.php
@@ -1,49 +1,52 @@
<?php
-class TheFarSideBridge extends BridgeAbstract {
- const NAME = 'The Far Side Bridge';
- const URI = 'https://www.thefarside.com';
- const DESCRIPTION = 'Returns the daily dose';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array();
- const CACHE_TIMEOUT = 3600; // 1 hour
+class TheFarSideBridge extends BridgeAbstract
+{
+ const NAME = 'The Far Side Bridge';
+ const URI = 'https://www.thefarside.com';
+ const DESCRIPTION = 'Returns the daily dose';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [];
- public function collectData() {
- $html = getSimpleHTMLDOM(self::URI);
+ const CACHE_TIMEOUT = 3600; // 1 hour
- $div = $html->find('div.tfs-page-container__cows', 0);
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
- $item = array();
- $item['uri'] = $html->find('meta[property="og:url"]', 0)->content;
- $item['title'] = $div->find('h3', 0)->innertext;
- $item['timestamp'] = $div->find('h3', 0)->innertext;
- $item['content'] = '';
+ $div = $html->find('div.tfs-page-container__cows', 0);
- foreach($div->find('div.card-body') as $index => $card) {
- $image = $card->find('img', 0);
- $imageUrl = $image->attr['data-src'];
+ $item = [];
+ $item['uri'] = $html->find('meta[property="og:url"]', 0)->content;
+ $item['title'] = $div->find('h3', 0)->innertext;
+ $item['timestamp'] = $div->find('h3', 0)->innertext;
+ $item['content'] = '';
- // Images are downloaded to bypass the hotlink protection.
- $image = getContents($imageUrl, array('Referer: ' . self::URI));
+ foreach ($div->find('div.card-body') as $index => $card) {
+ $image = $card->find('img', 0);
+ $imageUrl = $image->attr['data-src'];
- // Encode image as base64
- $imageBase64 = base64_encode($image);
+ // Images are downloaded to bypass the hotlink protection.
+ $image = getContents($imageUrl, ['Referer: ' . self::URI]);
- $caption = '';
+ // Encode image as base64
+ $imageBase64 = base64_encode($image);
- if ($card->find('figcaption', 0)) {
- $caption = $card->find('figcaption', 0)->innertext;
- }
+ $caption = '';
- $item['content'] .= <<<EOD
+ if ($card->find('figcaption', 0)) {
+ $caption = $card->find('figcaption', 0)->innertext;
+ }
+
+ $item['content'] .= <<<EOD
<figure>
<img title="{$caption}" src="data:image/jpeg;base64,{$imageBase64}"/>
<figcaption>{$caption}</figcaption>
</figure>
<br/>
EOD;
- }
+ }
- $this->items[] = $item;
- }
+ $this->items[] = $item;
+ }
}
diff --git a/bridges/TheGuardianBridge.php b/bridges/TheGuardianBridge.php
index e655f0ef..d3b1147c 100644
--- a/bridges/TheGuardianBridge.php
+++ b/bridges/TheGuardianBridge.php
@@ -1,96 +1,101 @@
<?php
-class TheGuardianBridge extends FeedExpander {
- const MAINTAINER = 'IceWreck';
- const NAME = 'The Guardian Bridge';
- const URI = 'https://www.theguardian.com/';
- const CACHE_TIMEOUT = 600; // This is a news site, so don't cache for more than 10 mins
- const DESCRIPTION = 'RSS feed for The Guardian';
- const PARAMETERS = array( array(
- 'feed' => array(
- 'name' => 'Feed',
- 'type' => 'list',
- 'values' => array(
- 'World News' => 'world/rss',
- 'US News' => '/us-news/rss',
- 'UK News' => '/uk-news/rss',
- 'Europe News' => '/world/europe-news/rss',
- 'Asia News' => '/world/asia/rss',
- 'Tech' => '/uk/technology/rss',
- 'Business News' => '/uk/business/rss',
- 'Opinion' => '/uk/commentisfree/rss',
- 'Lifestyle' => '/uk/lifeandstyle/rss',
- 'Culture' => '/uk/culture/rss',
- 'Sports' => '/uk/sport/rss'
- )
- )
-
- /*
-
- Topicwise Links
-
- You can find the base feed for any topic by appending /rss to the url.
-
- Example:
-
- https://feeds.theguardian.com/theguardian/uk-news/rss
- https://feeds.theguardian.com/theguardian/us-news/rss
-
- Or simply
-
- https://www.theguardian.com/world/rss
-
- Just add that topic as a value in the PARAMETERS const.
-
- */
-
-
- ));
-
- public function collectData(){
- $feed = $this->getInput('feed');
- $feedURL = 'https://feeds.theguardian.com/theguardian/' . $feed;
- $this->collectExpandableDatas($feedURL, 10);
- }
-
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
-
- // --- Recovering the article ---
-
- // $articlePage gets the entire page's contents
- $articlePage = getSimpleHTMLDOM($newsItem->link);
- // figure contain's the main article image
- $article = $articlePage->find('figure', 0);
- // content__article-body has the actual article
- foreach($articlePage->find('.content__article-body') as $element)
- $article = $article . $element;
-
- // --- Fixing ugly elements ---
-
- // Replace the image viewer and BS with the image itself
- foreach($articlePage->find('a.article__img-container') as $uslElementLoc) {
- $main_img = $uslElementLoc->find('img', 0);
- $article = str_replace($uslElementLoc, $main_img, $article);
- }
-
- // List of all the crap in the article
- $uselessElements = array(
- '#show-caption',
- '.element-atom',
- '.submeta',
- 'youtube-media-atom',
- 'svg'
- );
-
- // Remove the listed crap
- foreach($uselessElements as $uslElement) {
- foreach($articlePage->find($uslElement) as $uslElementLoc) {
- $article = str_replace($uslElementLoc, '', $article);
- }
- }
-
- $item['content'] = $article;
-
- return $item;
- }
+
+class TheGuardianBridge extends FeedExpander
+{
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'The Guardian Bridge';
+ const URI = 'https://www.theguardian.com/';
+ const CACHE_TIMEOUT = 600; // This is a news site, so don't cache for more than 10 mins
+ const DESCRIPTION = 'RSS feed for The Guardian';
+ const PARAMETERS = [ [
+ 'feed' => [
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'values' => [
+ 'World News' => 'world/rss',
+ 'US News' => '/us-news/rss',
+ 'UK News' => '/uk-news/rss',
+ 'Europe News' => '/world/europe-news/rss',
+ 'Asia News' => '/world/asia/rss',
+ 'Tech' => '/uk/technology/rss',
+ 'Business News' => '/uk/business/rss',
+ 'Opinion' => '/uk/commentisfree/rss',
+ 'Lifestyle' => '/uk/lifeandstyle/rss',
+ 'Culture' => '/uk/culture/rss',
+ 'Sports' => '/uk/sport/rss'
+ ]
+ ]
+
+ /*
+
+ Topicwise Links
+
+ You can find the base feed for any topic by appending /rss to the url.
+
+ Example:
+
+ https://feeds.theguardian.com/theguardian/uk-news/rss
+ https://feeds.theguardian.com/theguardian/us-news/rss
+
+ Or simply
+
+ https://www.theguardian.com/world/rss
+
+ Just add that topic as a value in the PARAMETERS const.
+
+ */
+
+
+ ]];
+
+ public function collectData()
+ {
+ $feed = $this->getInput('feed');
+ $feedURL = 'https://feeds.theguardian.com/theguardian/' . $feed;
+ $this->collectExpandableDatas($feedURL, 10);
+ }
+
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+
+ // --- Recovering the article ---
+
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+ // figure contain's the main article image
+ $article = $articlePage->find('figure', 0);
+ // content__article-body has the actual article
+ foreach ($articlePage->find('.content__article-body') as $element) {
+ $article = $article . $element;
+ }
+
+ // --- Fixing ugly elements ---
+
+ // Replace the image viewer and BS with the image itself
+ foreach ($articlePage->find('a.article__img-container') as $uslElementLoc) {
+ $main_img = $uslElementLoc->find('img', 0);
+ $article = str_replace($uslElementLoc, $main_img, $article);
+ }
+
+ // List of all the crap in the article
+ $uselessElements = [
+ '#show-caption',
+ '.element-atom',
+ '.submeta',
+ 'youtube-media-atom',
+ 'svg'
+ ];
+
+ // Remove the listed crap
+ foreach ($uselessElements as $uslElement) {
+ foreach ($articlePage->find($uslElement) as $uslElementLoc) {
+ $article = str_replace($uslElementLoc, '', $article);
+ }
+ }
+
+ $item['content'] = $article;
+
+ return $item;
+ }
}
diff --git a/bridges/TheHackerNewsBridge.php b/bridges/TheHackerNewsBridge.php
index b91b9504..dfe07543 100644
--- a/bridges/TheHackerNewsBridge.php
+++ b/bridges/TheHackerNewsBridge.php
@@ -1,79 +1,77 @@
<?php
-class TheHackerNewsBridge extends BridgeAbstract {
- const MAINTAINER = 'ORelio';
- const NAME = 'The Hacker News Bridge';
- const URI = 'https://thehackernews.com/';
- const DESCRIPTION = 'Cyber Security, Hacking, Technology News.';
+class TheHackerNewsBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = 'The Hacker News Bridge';
+ const URI = 'https://thehackernews.com/';
+ const DESCRIPTION = 'Cyber Security, Hacking, Technology News.';
- public function collectData(){
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+ $limit = 0;
- $html = getSimpleHTMLDOM($this->getURI());
- $limit = 0;
+ foreach ($html->find('div.body-post') as $element) {
+ if ($limit < 5) {
+ $article_url = $element->find('a.story-link', 0)->href;
+ $article_author = trim($element->find('i.icon-user', 0)->parent()->plaintext);
+ $article_author = str_replace('&#59396;', '', $article_author);
+ $article_title = $element->find('h2.home-title', 0)->plaintext;
- foreach($html->find('div.body-post') as $element) {
- if($limit < 5) {
+ //Date without time
+ $article_timestamp = strtotime(
+ extractFromDelimiters(
+ $element->find('i.icon-calendar', 0)->parent()->outertext,
+ '</i>',
+ '<span>'
+ )
+ );
- $article_url = $element->find('a.story-link', 0)->href;
- $article_author = trim($element->find('i.icon-user', 0)->parent()->plaintext);
- $article_author = str_replace('&#59396;', '', $article_author);
- $article_title = $element->find('h2.home-title', 0)->plaintext;
+ //Article thumbnail in lazy-loading image
+ if (is_object($element->find('img[data-echo]', 0))) {
+ $article_thumbnail = [
+ extractFromDelimiters(
+ $element->find('img[data-echo]', 0)->outertext,
+ "data-echo='",
+ "'"
+ )
+ ];
+ } else {
+ $article_thumbnail = [];
+ }
- //Date without time
- $article_timestamp = strtotime(
- extractFromDelimiters(
- $element->find('i.icon-calendar', 0)->parent()->outertext,
- '</i>',
- '<span>'
- )
- );
+ if ($article = getSimpleHTMLDOMCached($article_url)) {
+ //Article body
+ $contents = $article->find('div.articlebody', 0)->innertext;
+ $contents = stripRecursiveHtmlSection($contents, 'div', '<div class="ad_');
+ $contents = stripWithDelimiters($contents, 'id="google_ads', '</iframe>');
+ $contents = stripWithDelimiters($contents, '<script', '</script>');
- //Article thumbnail in lazy-loading image
- if (is_object($element->find('img[data-echo]', 0))) {
- $article_thumbnail = array(
- extractFromDelimiters(
- $element->find('img[data-echo]', 0)->outertext,
- "data-echo='",
- "'"
- )
- );
- } else {
- $article_thumbnail = array();
- }
+ //Date with time
+ if (is_object($article->find('meta[itemprop=dateModified]', 0))) {
+ $article_timestamp = strtotime(
+ extractFromDelimiters(
+ $article->find('meta[itemprop=dateModified]', 0)->outertext,
+ "content='",
+ "'"
+ )
+ );
+ }
+ } else {
+ $contents = 'Could not request TheHackerNews: ' . $article_url;
+ }
- if ($article = getSimpleHTMLDOMCached($article_url)) {
-
- //Article body
- $contents = $article->find('div.articlebody', 0)->innertext;
- $contents = stripRecursiveHtmlSection($contents, 'div', '<div class="ad_');
- $contents = stripWithDelimiters($contents, 'id="google_ads', '</iframe>');
- $contents = stripWithDelimiters($contents, '<script', '</script>');
-
- //Date with time
- if (is_object($article->find('meta[itemprop=dateModified]', 0))) {
- $article_timestamp = strtotime(
- extractFromDelimiters(
- $article->find('meta[itemprop=dateModified]', 0)->outertext,
- "content='",
- "'"
- )
- );
- }
- } else {
- $contents = 'Could not request TheHackerNews: ' . $article_url;
- }
-
- $item = array();
- $item['uri'] = $article_url;
- $item['title'] = $article_title;
- $item['author'] = $article_author;
- $item['enclosures'] = $article_thumbnail;
- $item['timestamp'] = $article_timestamp;
- $item['content'] = trim($contents);
- $this->items[] = $item;
- $limit++;
- }
- }
-
- }
+ $item = [];
+ $item['uri'] = $article_url;
+ $item['title'] = $article_title;
+ $item['author'] = $article_author;
+ $item['enclosures'] = $article_thumbnail;
+ $item['timestamp'] = $article_timestamp;
+ $item['content'] = trim($contents);
+ $this->items[] = $item;
+ $limit++;
+ }
+ }
+ }
}
diff --git a/bridges/ThePirateBayBridge.php b/bridges/ThePirateBayBridge.php
index 68c39d5e..13095062 100644
--- a/bridges/ThePirateBayBridge.php
+++ b/bridges/ThePirateBayBridge.php
@@ -3,309 +3,325 @@
/**
* Much of the logic here is copied from https://thepiratebay.org/static/main.js
*/
-class ThePirateBayBridge extends BridgeAbstract {
-
- const MAINTAINER = 'dvikan';
- const NAME = 'The Pirate Bay';
- const URI = 'https://thepiratebay.org';
- const DESCRIPTION = 'Returns results for the keywords. You can put several
+class ThePirateBayBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'dvikan';
+ const NAME = 'The Pirate Bay';
+ const URI = 'https://thepiratebay.org';
+ const DESCRIPTION = 'Returns results for the keywords. You can put several
list of keywords by separating them with a semicolon (e.g. "one show;another
show"). Category based search needs the category number as input. User based
search takes the Uploader name. Search can be done in a specified category';
- const PARAMETERS = array( array(
- 'q' => array(
- 'name' => 'keywords/username/category, separated by semicolons',
- 'exampleValue' => 'simpsons',
- 'required' => true
- ),
- 'crit' => array(
- 'type' => 'list',
- 'name' => 'Search type',
- 'values' => array(
- 'search' => 'search',
- 'category' => 'cat',
- 'user' => 'usr',
- )
- ),
- 'catCheck' => array(
- 'type' => 'checkbox',
- 'name' => 'Specify category for keyword search ?',
- ),
- 'cat' => array(
- 'name' => 'Category number',
- 'exampleValue' => '100, 200… See TPB for category number'
- ),
- 'trusted' => array(
- 'type' => 'checkbox',
- 'name' => 'Only get results from Trusted or VIP users ?',
- ),
- ));
+ const PARAMETERS = [ [
+ 'q' => [
+ 'name' => 'keywords/username/category, separated by semicolons',
+ 'exampleValue' => 'simpsons',
+ 'required' => true
+ ],
+ 'crit' => [
+ 'type' => 'list',
+ 'name' => 'Search type',
+ 'values' => [
+ 'search' => 'search',
+ 'category' => 'cat',
+ 'user' => 'usr',
+ ]
+ ],
+ 'catCheck' => [
+ 'type' => 'checkbox',
+ 'name' => 'Specify category for keyword search ?',
+ ],
+ 'cat' => [
+ 'name' => 'Category number',
+ 'exampleValue' => '100, 200… See TPB for category number'
+ ],
+ 'trusted' => [
+ 'type' => 'checkbox',
+ 'name' => 'Only get results from Trusted or VIP users ?',
+ ],
+ ]];
- const STATIC_SERVER = 'https://torrindex.net';
+ const STATIC_SERVER = 'https://torrindex.net';
- const CATEGORIES = array(
- '1' => 'Audio',
- '2' => 'Video',
- '3' => 'Applications',
- '4' => 'Games',
- '5' => 'Porn',
- '6' => 'Other',
- '101' => 'Music',
- '102' => 'Audio Books',
- '103' => 'Sound clips',
- '104' => 'FLAC',
- '199' => 'Other',
- '201' => 'Movies',
- '202' => 'Movies DVDR',
- '203' => 'Music videos',
- '204' => 'Movie Clips',
- '205' => 'TV-Shows',
- '206' => 'Handheld',
- '207' => 'HD Movies',
- '208' => 'HD TV-Shows',
- '209' => '3D',
- '299' => 'Other',
- '301' => 'Windows',
- '302' => 'Mac/Apple',
- '303' => 'UNIX',
- '304' => 'Handheld',
- '305' => 'IOS(iPad/iPhone)',
- '306' => 'Android',
- '399' => 'Other OS',
- '401' => 'PC',
- '402' => 'Mac/Apple',
- '403' => 'PSx',
- '404' => 'XBOX360',
- '405' => 'Wii',
- '406' => 'Handheld',
- '407' => 'IOS(iPad/iPhone)',
- '408' => 'Android',
- '499' => 'Other OS',
- '501' => 'Movies',
- '502' => 'Movies DVDR',
- '503' => 'Pictures',
- '504' => 'Games',
- '505' => 'HD-Movies',
- '506' => 'Movie Clips',
- '599' => 'Other',
- '601' => 'E-books',
- '602' => 'Comics',
- '603' => 'Pictures',
- '604' => 'Covers',
- '605' => 'Physibles',
- '699' => 'Other',
- );
+ const CATEGORIES = [
+ '1' => 'Audio',
+ '2' => 'Video',
+ '3' => 'Applications',
+ '4' => 'Games',
+ '5' => 'Porn',
+ '6' => 'Other',
+ '101' => 'Music',
+ '102' => 'Audio Books',
+ '103' => 'Sound clips',
+ '104' => 'FLAC',
+ '199' => 'Other',
+ '201' => 'Movies',
+ '202' => 'Movies DVDR',
+ '203' => 'Music videos',
+ '204' => 'Movie Clips',
+ '205' => 'TV-Shows',
+ '206' => 'Handheld',
+ '207' => 'HD Movies',
+ '208' => 'HD TV-Shows',
+ '209' => '3D',
+ '299' => 'Other',
+ '301' => 'Windows',
+ '302' => 'Mac/Apple',
+ '303' => 'UNIX',
+ '304' => 'Handheld',
+ '305' => 'IOS(iPad/iPhone)',
+ '306' => 'Android',
+ '399' => 'Other OS',
+ '401' => 'PC',
+ '402' => 'Mac/Apple',
+ '403' => 'PSx',
+ '404' => 'XBOX360',
+ '405' => 'Wii',
+ '406' => 'Handheld',
+ '407' => 'IOS(iPad/iPhone)',
+ '408' => 'Android',
+ '499' => 'Other OS',
+ '501' => 'Movies',
+ '502' => 'Movies DVDR',
+ '503' => 'Pictures',
+ '504' => 'Games',
+ '505' => 'HD-Movies',
+ '506' => 'Movie Clips',
+ '599' => 'Other',
+ '601' => 'E-books',
+ '602' => 'Comics',
+ '603' => 'Pictures',
+ '604' => 'Covers',
+ '605' => 'Physibles',
+ '699' => 'Other',
+ ];
- public function collectData() {
- $keywords = explode(';', $this->getInput('q'));
+ public function collectData()
+ {
+ $keywords = explode(';', $this->getInput('q'));
- foreach($keywords as $keyword) {
- $this->processKeyword($keyword);
- }
- }
+ foreach ($keywords as $keyword) {
+ $this->processKeyword($keyword);
+ }
+ }
- private function processKeyword($keyword)
- {
- $keyword = trim($keyword);
- switch ($this->getInput('crit')) {
- case 'search':
- $catCheck = $this->getInput('catCheck');
- if ($catCheck) {
- $categories = $this->getInput('cat');
- $query = sprintf(
- '/q.php?q=%s&cat=%s',
- rawurlencode($keyword),
- rawurlencode($categories)
- );
- } else {
- $query = sprintf('/q.php?q=%s', rawurlencode($keyword));
- }
- break;
- case 'cat':
- $query = sprintf('/q.php?q=category:%s', rawurlencode($keyword));
- break;
- case 'usr':
- $query = sprintf('/q.php?q=user:%s', rawurlencode($keyword));
- break;
- default:
- returnClientError('Impossible');
- }
- $api = 'https://apibay.org';
- $json = getContents($api . $query);
- $result = json_decode($json);
+ private function processKeyword($keyword)
+ {
+ $keyword = trim($keyword);
+ switch ($this->getInput('crit')) {
+ case 'search':
+ $catCheck = $this->getInput('catCheck');
+ if ($catCheck) {
+ $categories = $this->getInput('cat');
+ $query = sprintf(
+ '/q.php?q=%s&cat=%s',
+ rawurlencode($keyword),
+ rawurlencode($categories)
+ );
+ } else {
+ $query = sprintf('/q.php?q=%s', rawurlencode($keyword));
+ }
+ break;
+ case 'cat':
+ $query = sprintf('/q.php?q=category:%s', rawurlencode($keyword));
+ break;
+ case 'usr':
+ $query = sprintf('/q.php?q=user:%s', rawurlencode($keyword));
+ break;
+ default:
+ returnClientError('Impossible');
+ }
+ $api = 'https://apibay.org';
+ $json = getContents($api . $query);
+ $result = json_decode($json);
- if ($result[0]->name === 'No results returned') {
- return;
- }
- foreach ($result as $torrent) {
- // This is the check for whether to include results from Trusted or VIP users
- if ($this->getInput('trusted')
- && !in_array($torrent->status, array('vip', 'trusted'))
- ) {
- continue;
- }
- $this->processTorrent($torrent);
- }
- }
+ if ($result[0]->name === 'No results returned') {
+ return;
+ }
+ foreach ($result as $torrent) {
+ // This is the check for whether to include results from Trusted or VIP users
+ if (
+ $this->getInput('trusted')
+ && !in_array($torrent->status, ['vip', 'trusted'])
+ ) {
+ continue;
+ }
+ $this->processTorrent($torrent);
+ }
+ }
- private function processTorrent($torrent)
- {
- // Extracted these trackers from the magnet links on thepiratebay.org
- $trackers = array(
- 'udp://tracker.coppersurfer.tk:6969/announce',
- 'udp://tracker.openbittorrent.com:6969/announce',
- 'udp://9.rarbg.to:2710/announce',
- 'udp://9.rarbg.me:2780/announce',
- 'udp://9.rarbg.to:2730/announce',
- 'udp://tracker.opentrackr.org:1337',
- 'http://p4p.arenabg.com:1337/announce',
- 'udp://tracker.torrent.eu.org:451/announce',
- 'udp://tracker.tiny-vps.com:6969/announce',
- 'udp://open.stealth.si:80/announce',
- );
+ private function processTorrent($torrent)
+ {
+ // Extracted these trackers from the magnet links on thepiratebay.org
+ $trackers = [
+ 'udp://tracker.coppersurfer.tk:6969/announce',
+ 'udp://tracker.openbittorrent.com:6969/announce',
+ 'udp://9.rarbg.to:2710/announce',
+ 'udp://9.rarbg.me:2780/announce',
+ 'udp://9.rarbg.to:2730/announce',
+ 'udp://tracker.opentrackr.org:1337',
+ 'http://p4p.arenabg.com:1337/announce',
+ 'udp://tracker.torrent.eu.org:451/announce',
+ 'udp://tracker.tiny-vps.com:6969/announce',
+ 'udp://open.stealth.si:80/announce',
+ ];
- $magnetLink = sprintf(
- 'magnet:?xt=urn:btih:%s&dn=%s',
- $torrent->info_hash,
- rawurlencode($torrent->name)
- );
- foreach ($trackers as $tracker) {
- // Build magnet link manually instead of using http_build_query because it
- // creates undesirable query such as ?tr[0]=foo&tr[1]=bar&tr[2]=baz
- $magnetLink .= '&tr=' . rawurlencode($tracker);
- }
+ $magnetLink = sprintf(
+ 'magnet:?xt=urn:btih:%s&dn=%s',
+ $torrent->info_hash,
+ rawurlencode($torrent->name)
+ );
+ foreach ($trackers as $tracker) {
+ // Build magnet link manually instead of using http_build_query because it
+ // creates undesirable query such as ?tr[0]=foo&tr[1]=bar&tr[2]=baz
+ $magnetLink .= '&tr=' . rawurlencode($tracker);
+ }
- $item = array();
+ $item = [];
- $item['title'] = $torrent->name;
- // This uri should be a magnet link so that feed readers can easily pick it up.
- // However, rss-bridge only allows http or https schemes
- $item['uri'] = sprintf('%s/description.php?id=%s', self::URI, $torrent->id);
- $item['timestamp'] = $torrent->added;
- $item['author'] = $torrent->username;
+ $item['title'] = $torrent->name;
+ // This uri should be a magnet link so that feed readers can easily pick it up.
+ // However, rss-bridge only allows http or https schemes
+ $item['uri'] = sprintf('%s/description.php?id=%s', self::URI, $torrent->id);
+ $item['timestamp'] = $torrent->added;
+ $item['author'] = $torrent->username;
- $content = '<b>Type:</b> '
- . $this->renderCategory($torrent->category) . '<br>';
- $content .= "<b>Files:</b> $torrent->num_files<br>";
- $content .= '<b>Size:</b> ' . $this->renderSize($torrent->size) . '<br><br>';
+ $content = '<b>Type:</b> '
+ . $this->renderCategory($torrent->category) . '<br>';
+ $content .= "<b>Files:</b> $torrent->num_files<br>";
+ $content .= '<b>Size:</b> ' . $this->renderSize($torrent->size) . '<br><br>';
- $content .= '<b>Uploaded:</b> '
- . $this->renderUploadDate($torrent->added) . '<br>';
- $content .= '<b>By:</b> ' . $this->renderUser($torrent) . '<br>';
+ $content .= '<b>Uploaded:</b> '
+ . $this->renderUploadDate($torrent->added) . '<br>';
+ $content .= '<b>By:</b> ' . $this->renderUser($torrent) . '<br>';
- $content .= "<b>Seeders:</b> {$torrent->seeders}<br>";
- $content .= "<b>Leechers:</b> {$torrent->leechers}<br>";
- $content .= "<b>Info hash:</b> {$torrent->info_hash}<br><br>";
+ $content .= "<b>Seeders:</b> {$torrent->seeders}<br>";
+ $content .= "<b>Leechers:</b> {$torrent->leechers}<br>";
+ $content .= "<b>Info hash:</b> {$torrent->info_hash}<br><br>";
- if ($torrent->imdb) {
- $content .= '<b>Imdb:</b> '
- . $this->renderImdbLink($torrent->imdb) . '<br><br>';
- }
+ if ($torrent->imdb) {
+ $content .= '<b>Imdb:</b> '
+ . $this->renderImdbLink($torrent->imdb) . '<br><br>';
+ }
- $html = <<<HTML
+ $html = <<<HTML
<a href="%s">
<img src="%s/images/icon-magnet.gif"> GET THIS TORRENT
</a>
<br>
HTML;
- $content .= sprintf($html, $magnetLink, self::STATIC_SERVER);
+ $content .= sprintf($html, $magnetLink, self::STATIC_SERVER);
- $item['content'] = $content;
+ $item['content'] = $content;
- $this->items[] = $item;
- }
+ $this->items[] = $item;
+ }
- private function renderSize($size)
- {
- if ($size < 1024) return $size . ' B';
- if ($size < pow(1024, 2)) return round($size / 1024, 2) . ' KB';
- if ($size < pow(1024, 3)) return round($size / pow(1024, 2), 2) . ' MB';
- if ($size < pow(1024, 4)) return round($size / pow(1024, 3), 2) . ' GB';
+ private function renderSize($size)
+ {
+ if ($size < 1024) {
+ return $size . ' B';
+ }
+ if ($size < pow(1024, 2)) {
+ return round($size / 1024, 2) . ' KB';
+ }
+ if ($size < pow(1024, 3)) {
+ return round($size / pow(1024, 2), 2) . ' MB';
+ }
+ if ($size < pow(1024, 4)) {
+ return round($size / pow(1024, 3), 2) . ' GB';
+ }
- return round($size / pow(1024, 4), 2) . ' TB';
- }
+ return round($size / pow(1024, 4), 2) . ' TB';
+ }
- private function renderUploadDate($added)
- {
- return date('Y-m-d', $added ?: time());
- }
+ private function renderUploadDate($added)
+ {
+ return date('Y-m-d', $added ?: time());
+ }
- private function renderCategory($category)
- {
- $mainCategory = sprintf(
- '<a href="%s/search.php?q=category:%s">%s</a>',
- self::URI,
- $category[0] . '00',
- self::CATEGORIES[$category[0]]
- );
+ private function renderCategory($category)
+ {
+ $mainCategory = sprintf(
+ '<a href="%s/search.php?q=category:%s">%s</a>',
+ self::URI,
+ $category[0] . '00',
+ self::CATEGORIES[$category[0]]
+ );
- $subCategory = sprintf(
- '<a href="%s/search.php?q=category:%s">%s</a>',
- self::URI,
- $category,
- self::CATEGORIES[$category]
- );
+ $subCategory = sprintf(
+ '<a href="%s/search.php?q=category:%s">%s</a>',
+ self::URI,
+ $category,
+ self::CATEGORIES[$category]
+ );
- return sprintf('%s > %s', $mainCategory, $subCategory);
- }
+ return sprintf('%s > %s', $mainCategory, $subCategory);
+ }
- private function renderUser($torrent)
- {
- if ($torrent->username === 'Anonymous') {
- return $torrent->username . ' ' . $this->renderStatusImage($torrent->status);
- }
- return sprintf(
- '<a href="%s/search.php?q=user:%s">%s %s</a>',
- self::URI,
- $torrent->username,
- $torrent->username,
- $this->renderStatusImage($torrent->status)
- );
- }
+ private function renderUser($torrent)
+ {
+ if ($torrent->username === 'Anonymous') {
+ return $torrent->username . ' ' . $this->renderStatusImage($torrent->status);
+ }
+ return sprintf(
+ '<a href="%s/search.php?q=user:%s">%s %s</a>',
+ self::URI,
+ $torrent->username,
+ $torrent->username,
+ $this->renderStatusImage($torrent->status)
+ );
+ }
- private function renderStatusImage($status)
- {
- if ($status == 'trusted')
- return sprintf(
- '<img src="%s/images/trusted.png" title="Trusted"/>',
- self::STATIC_SERVER
- );
- if ($status == 'vip')
- return sprintf(
- '<img src="%s/images/vip.gif" title="VIP"/>',
- self::STATIC_SERVER
- );
- if ($status == 'helper')
- return sprintf(
- '<img src="%s/images/helper.png" title="Helper"/>',
- self::STATIC_SERVER
- );
- if ($status == 'moderator')
- return sprintf(
- '<img src="%s/images/moderator.gif" title="Moderator"/>',
- self::STATIC_SERVER
- );
- if ($status == 'supermod')
- return sprintf(
- '<img src="%s/images/supermod.png" title="Super Mod"/>',
- self::STATIC_SERVER
- );
- if ($status == 'admin')
- return sprintf(
- '<img src="%s/images/admin.gif" title="Admin"/>',
- self::STATIC_SERVER
- );
+ private function renderStatusImage($status)
+ {
+ if ($status == 'trusted') {
+ return sprintf(
+ '<img src="%s/images/trusted.png" title="Trusted"/>',
+ self::STATIC_SERVER
+ );
+ }
+ if ($status == 'vip') {
+ return sprintf(
+ '<img src="%s/images/vip.gif" title="VIP"/>',
+ self::STATIC_SERVER
+ );
+ }
+ if ($status == 'helper') {
+ return sprintf(
+ '<img src="%s/images/helper.png" title="Helper"/>',
+ self::STATIC_SERVER
+ );
+ }
+ if ($status == 'moderator') {
+ return sprintf(
+ '<img src="%s/images/moderator.gif" title="Moderator"/>',
+ self::STATIC_SERVER
+ );
+ }
+ if ($status == 'supermod') {
+ return sprintf(
+ '<img src="%s/images/supermod.png" title="Super Mod"/>',
+ self::STATIC_SERVER
+ );
+ }
+ if ($status == 'admin') {
+ return sprintf(
+ '<img src="%s/images/admin.gif" title="Admin"/>',
+ self::STATIC_SERVER
+ );
+ }
- return '';
- }
+ return '';
+ }
- private function renderImdbLink($imdb)
- {
- return sprintf(
- '<a href="%s">%s</a>',
- "https://www.imdb.com/title/$imdb",
- "https://www.imdb.com/title/$imdb"
- );
- }
+ private function renderImdbLink($imdb)
+ {
+ return sprintf(
+ '<a href="%s">%s</a>',
+ "https://www.imdb.com/title/$imdb",
+ "https://www.imdb.com/title/$imdb"
+ );
+ }
}
diff --git a/bridges/TheWhiteboardBridge.php b/bridges/TheWhiteboardBridge.php
index e455d100..c36cc5f6 100644
--- a/bridges/TheWhiteboardBridge.php
+++ b/bridges/TheWhiteboardBridge.php
@@ -1,22 +1,25 @@
<?php
-class TheWhiteboardBridge extends BridgeAbstract {
- const NAME = 'The Whiteboard';
- const URI = 'https://www.the-whiteboard.com/';
- const DESCRIPTION = 'Get the latest comic from The Whiteboard';
- const MAINTAINER = 'CyberJacob';
- public function collectData() {
- $item = array();
+class TheWhiteboardBridge extends BridgeAbstract
+{
+ const NAME = 'The Whiteboard';
+ const URI = 'https://www.the-whiteboard.com/';
+ const DESCRIPTION = 'Get the latest comic from The Whiteboard';
+ const MAINTAINER = 'CyberJacob';
- $html = getSimpleHTMLDOM(self::URI);
+ public function collectData()
+ {
+ $item = [];
- $image = $html->find('center', 1)->find('img', 0);
- $image->src = self::URI . '/' . $image->src;
+ $html = getSimpleHTMLDOM(self::URI);
- $item['title'] = explode("\r\n", $html->find('center', 1)->plaintext)[0];
- $item['content'] = $image;
- $item['timestamp'] = explode("\r\n", $html->find('center', 1)->plaintext)[0];
+ $image = $html->find('center', 1)->find('img', 0);
+ $image->src = self::URI . '/' . $image->src;
- $this->items[] = $item;
- }
+ $item['title'] = explode("\r\n", $html->find('center', 1)->plaintext)[0];
+ $item['content'] = $image;
+ $item['timestamp'] = explode("\r\n", $html->find('center', 1)->plaintext)[0];
+
+ $this->items[] = $item;
+ }
}
diff --git a/bridges/TheYeteeBridge.php b/bridges/TheYeteeBridge.php
index b7867ae9..5c7d8856 100644
--- a/bridges/TheYeteeBridge.php
+++ b/bridges/TheYeteeBridge.php
@@ -1,39 +1,39 @@
<?php
-class TheYeteeBridge extends BridgeAbstract {
- const MAINTAINER = 'Monsieur Poutounours';
- const NAME = 'TheYetee';
- const URI = 'https://theyetee.com';
- const CACHE_TIMEOUT = 14400; // 4 h
- const DESCRIPTION = 'Fetch daily shirts from The Yetee';
-
- public function collectData(){
-
- $html = getSimpleHTMLDOM(self::URI);
-
- $div = $html->find('.module_timed-item.is--full');
- foreach($div as $element) {
-
- $item = array();
- $item['enclosures'] = array();
-
- $title = $element->find('h2', 0)->plaintext;
- $item['title'] = $title;
-
- $author = trim($element->find('.module_timed-item--artist a', 0)->plaintext);
- $item['author'] = $author;
-
- $item['uri'] = static::URI;
-
- $content = '<p>' . $title . ' by ' . $author . '</p>';
- $photos = $element->find('a.img');
- foreach($photos as $photo) {
- $content = $content . "<br /><img src='$photo->href' />";
- $item['enclosures'][] = $photo->src;
- }
- $item['content'] = $content;
-
- $this->items[] = $item;
- }
- }
+class TheYeteeBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Monsieur Poutounours';
+ const NAME = 'TheYetee';
+ const URI = 'https://theyetee.com';
+ const CACHE_TIMEOUT = 14400; // 4 h
+ const DESCRIPTION = 'Fetch daily shirts from The Yetee';
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(self::URI);
+
+ $div = $html->find('.module_timed-item.is--full');
+ foreach ($div as $element) {
+ $item = [];
+ $item['enclosures'] = [];
+
+ $title = $element->find('h2', 0)->plaintext;
+ $item['title'] = $title;
+
+ $author = trim($element->find('.module_timed-item--artist a', 0)->plaintext);
+ $item['author'] = $author;
+
+ $item['uri'] = static::URI;
+
+ $content = '<p>' . $title . ' by ' . $author . '</p>';
+ $photos = $element->find('a.img');
+ foreach ($photos as $photo) {
+ $content = $content . "<br /><img src='$photo->href' />";
+ $item['enclosures'][] = $photo->src;
+ }
+ $item['content'] = $content;
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/TikTokBridge.php b/bridges/TikTokBridge.php
index c58363e3..41da917f 100644
--- a/bridges/TikTokBridge.php
+++ b/bridges/TikTokBridge.php
@@ -1,87 +1,95 @@
<?php
-class TikTokBridge extends BridgeAbstract {
- const NAME = 'TikTok Bridge';
- const URI = 'https://www.tiktok.com';
- const DESCRIPTION = 'Returns posts';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(
- 'By user' => array(
- 'username' => array(
- 'name' => 'Username',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => '@tiktok',
- )
- ));
-
- const TEST_DETECT_PARAMETERS = array(
- 'https://www.tiktok.com/@tiktok' => array(
- 'context' => 'By user', 'username' => '@tiktok'
- )
- );
-
- const CACHE_TIMEOUT = 900; // 15 minutes
-
- private $feedName = '';
-
- public function detectParameters($url) {
-
- if(preg_match('/tiktok\.com\/(@[\w]+)/', $url, $matches) > 0) {
- return array(
- 'context' => 'By user',
- 'username' => $matches[1]
- );
- }
-
- return null;
- }
-
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI());
-
- $this->feedName = htmlspecialchars_decode($html->find('h1', 0)->plaintext);
-
- foreach ($html->find('div.tiktok-x6y88p-DivItemContainerV2') as $div) {
- $item = [];
-
- $link = $div->find('a', 0)->href;
- $image = $div->find('img', 0)->src;
- $views = $div->find('strong.video-count', 0)->plaintext;
-
- $item['uri'] = $link;
- $item['title'] = $div->find('a', 1)->plaintext;
- $item['enclosures'][] = $image;
-
- $item['content'] = <<<EOD
+
+class TikTokBridge extends BridgeAbstract
+{
+ const NAME = 'TikTok Bridge';
+ const URI = 'https://www.tiktok.com';
+ const DESCRIPTION = 'Returns posts';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [
+ 'By user' => [
+ 'username' => [
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => '@tiktok',
+ ]
+ ]];
+
+ const TEST_DETECT_PARAMETERS = [
+ 'https://www.tiktok.com/@tiktok' => [
+ 'context' => 'By user', 'username' => '@tiktok'
+ ]
+ ];
+
+ const CACHE_TIMEOUT = 900; // 15 minutes
+
+ private $feedName = '';
+
+ public function detectParameters($url)
+ {
+ if (preg_match('/tiktok\.com\/(@[\w]+)/', $url, $matches) > 0) {
+ return [
+ 'context' => 'By user',
+ 'username' => $matches[1]
+ ];
+ }
+
+ return null;
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $this->feedName = htmlspecialchars_decode($html->find('h1', 0)->plaintext);
+
+ foreach ($html->find('div.tiktok-x6y88p-DivItemContainerV2') as $div) {
+ $item = [];
+
+ $link = $div->find('a', 0)->href;
+ $image = $div->find('img', 0)->src;
+ $views = $div->find('strong.video-count', 0)->plaintext;
+
+ $item['uri'] = $link;
+ $item['title'] = $div->find('a', 1)->plaintext;
+ $item['enclosures'][] = $image;
+
+ $item['content'] = <<<EOD
<a href="{$link}"><img src="{$image}"/></a>
<p>{$views} views<p>
EOD;
- $this->items[] = $item;
- }
- }
-
- public function getURI() {
- switch($this->queriedContext) {
- case 'By user':
- return self::URI . '/' . $this->processUsername();
- default: return parent::getURI();
- }
- }
-
- public function getName() {
- switch($this->queriedContext) {
- case 'By user':
- return $this->feedName . ' (' . $this->processUsername() . ') - TikTok';
- default: return parent::getName();
- }
- }
-
- private function processUsername() {
- if (substr($this->getInput('username'), 0, 1) !== '@') {
- return '@' . $this->getInput('username');
- }
-
- return $this->getInput('username');
- }
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'By user':
+ return self::URI . '/' . $this->processUsername();
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'By user':
+ return $this->feedName . ' (' . $this->processUsername() . ') - TikTok';
+ default:
+ return parent::getName();
+ }
+ }
+
+ private function processUsername()
+ {
+ if (substr($this->getInput('username'), 0, 1) !== '@') {
+ return '@' . $this->getInput('username');
+ }
+
+ return $this->getInput('username');
+ }
}
diff --git a/bridges/TinyLetterBridge.php b/bridges/TinyLetterBridge.php
index 96c53331..0ba9bf1d 100644
--- a/bridges/TinyLetterBridge.php
+++ b/bridges/TinyLetterBridge.php
@@ -1,54 +1,58 @@
<?php
-class TinyLetterBridge extends BridgeAbstract {
- const NAME = 'Tiny Letter';
- const URI = 'https://tinyletter.com/';
- const DESCRIPTION = 'Tiny Letter is a mailing list service';
- const MAINTAINER = 'somini';
- const PARAMETERS = array(
- array(
- 'username' => array(
- 'name' => 'User Name',
- 'required' => true,
- 'exampleValue' => 'forwards',
- )
- )
- );
-
- public function getName() {
- $username = $this->getInput('username');
- if (!is_null($username)) {
- return static::NAME . ' | ' . $username;
- }
-
- return parent::getName();
- }
-
- public function getURI() {
- $username = $this->getInput('username');
- if (!is_null($username)) {
- return static::URI . urlencode($username);
- }
-
- return parent::getURI();
- }
-
- public function collectData() {
- $archives = self::getURI() . '/archive';
- $html = getSimpleHTMLDOMCached($archives);
-
- foreach($html->find('.message-list li') as $element) {
- $item = array();
-
- $snippet = $element->find('p.message-snippet', 0);
- $link = $element->find('.message-link', 0);
-
- $item['title'] = $link->plaintext;
- $item['content'] = $snippet->innertext;
- $item['uri'] = $link->href;
- $item['timestamp'] = strtotime($element->find('.message-date', 0)->plaintext);
-
- $this->items[] = $item;
- }
-
- }
+
+class TinyLetterBridge extends BridgeAbstract
+{
+ const NAME = 'Tiny Letter';
+ const URI = 'https://tinyletter.com/';
+ const DESCRIPTION = 'Tiny Letter is a mailing list service';
+ const MAINTAINER = 'somini';
+ const PARAMETERS = [
+ [
+ 'username' => [
+ 'name' => 'User Name',
+ 'required' => true,
+ 'exampleValue' => 'forwards',
+ ]
+ ]
+ ];
+
+ public function getName()
+ {
+ $username = $this->getInput('username');
+ if (!is_null($username)) {
+ return static::NAME . ' | ' . $username;
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI()
+ {
+ $username = $this->getInput('username');
+ if (!is_null($username)) {
+ return static::URI . urlencode($username);
+ }
+
+ return parent::getURI();
+ }
+
+ public function collectData()
+ {
+ $archives = self::getURI() . '/archive';
+ $html = getSimpleHTMLDOMCached($archives);
+
+ foreach ($html->find('.message-list li') as $element) {
+ $item = [];
+
+ $snippet = $element->find('p.message-snippet', 0);
+ $link = $element->find('.message-link', 0);
+
+ $item['title'] = $link->plaintext;
+ $item['content'] = $snippet->innertext;
+ $item['uri'] = $link->href;
+ $item['timestamp'] = strtotime($element->find('.message-date', 0)->plaintext);
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/TorrentGalaxyBridge.php b/bridges/TorrentGalaxyBridge.php
index 22f839ef..052af262 100644
--- a/bridges/TorrentGalaxyBridge.php
+++ b/bridges/TorrentGalaxyBridge.php
@@ -1,81 +1,82 @@
<?php
-class TorrentGalaxyBridge extends BridgeAbstract {
+class TorrentGalaxyBridge extends BridgeAbstract
+{
+ const NAME = 'Torrent Galaxy Bridge';
+ const URI = 'https://torrentgalaxy.to';
+ const DESCRIPTION = 'Returns latest torrents';
+ const MAINTAINER = 'GregThib';
+ const CACHE_TIMEOUT = 14400; // 24h = 86400s
- const NAME = 'Torrent Galaxy Bridge';
- const URI = 'https://torrentgalaxy.to';
- const DESCRIPTION = 'Returns latest torrents';
- const MAINTAINER = 'GregThib';
- const CACHE_TIMEOUT = 14400; // 24h = 86400s
+ const PARAMETERS = [
+ [
+ 'search' => [
+ 'name' => 'search',
+ 'required' => true,
+ 'exampleValue' => 'simpsons',
+ 'title' => 'Type your query'
+ ],
+ 'lang' => [
+ 'name' => 'language',
+ 'type' => 'list',
+ 'exampleValue' => 'All languages',
+ 'title' => 'Select your language',
+ 'values' => [
+ 'All languages' => '0',
+ 'English' => '1',
+ 'French' => '2',
+ 'German' => '3',
+ 'Italian' => '4',
+ 'Japanese' => '5',
+ 'Spanish' => '6',
+ 'Russian' => '7',
+ 'Hindi' => '8',
+ 'Other / Multiple' => '9',
+ 'Korean' => '10',
+ 'Danish' => '11',
+ 'Norwegian' => '12',
+ 'Dutch' => '13',
+ 'Manderin' => '14',
+ 'Portuguese' => '15',
+ 'Bengali' => '16',
+ 'Polish' => '17',
+ 'Turkish' => '18',
+ 'Telugu' => '19',
+ 'Urdu' => '20',
+ 'Arabic' => '21',
+ 'Swedish' => '22',
+ 'Romanian' => '23'
+ ]
+ ]
+ ]
+ ];
- const PARAMETERS = array(
- array(
- 'search' => array(
- 'name' => 'search',
- 'required' => true,
- 'exampleValue' => 'simpsons',
- 'title' => 'Type your query'
- ),
- 'lang' => array(
- 'name' => 'language',
- 'type' => 'list',
- 'exampleValue' => 'All languages',
- 'title' => 'Select your language',
- 'values' => array(
- 'All languages' => '0',
- 'English' => '1',
- 'French' => '2',
- 'German' => '3',
- 'Italian' => '4',
- 'Japanese' => '5',
- 'Spanish' => '6',
- 'Russian' => '7',
- 'Hindi' => '8',
- 'Other / Multiple' => '9',
- 'Korean' => '10',
- 'Danish' => '11',
- 'Norwegian' => '12',
- 'Dutch' => '13',
- 'Manderin' => '14',
- 'Portuguese' => '15',
- 'Bengali' => '16',
- 'Polish' => '17',
- 'Turkish' => '18',
- 'Telugu' => '19',
- 'Urdu' => '20',
- 'Arabic' => '21',
- 'Swedish' => '22',
- 'Romanian' => '23'
- )
- )
- )
- );
+ public function collectData()
+ {
+ $url = self::URI
+ . '/torrents.php?search=' . urlencode($this->getInput('search'))
+ . '&lang=' . $this->getInput('lang')
+ . '&sort=id&order=desc';
+ $html = getSimpleHTMLDOM($url);
- public function collectData(){
- $url = self::URI
- . '/torrents.php?search=' . urlencode($this->getInput('search'))
- . '&lang=' . $this->getInput('lang')
- . '&sort=id&order=desc';
- $html = getSimpleHTMLDOM($url);
+ foreach ($html->find('div.tgxtablerow') as $result) {
+ $identity = $result->find('div.tgxtablecell', 3)->find('div a', 0);
+ $authorid = $result->find('div.tgxtablecell', 6)->find('a', 0);
+ $creadate = $result->find('div.tgxtablecell', 11)->plaintext;
+ $glxlinks = $result->find('div.tgxtablecell', 4);
- foreach($html->find('div.tgxtablerow') as $result) {
- $identity = $result->find('div.tgxtablecell', 3)->find('div a', 0);
- $authorid = $result->find('div.tgxtablecell', 6)->find('a', 0);
- $creadate = $result->find('div.tgxtablecell', 11)->plaintext;
- $glxlinks = $result->find('div.tgxtablecell', 4);
+ $item = [];
+ $item['uri'] = self::URI . $identity->href;
+ $item['title'] = $identity->plaintext;
- $item = array();
- $item['uri'] = self::URI . $identity->href;
- $item['title'] = $identity->plaintext;
+ // todo: parse date strings such as '1Hr ago' etc.
+ $createdAt = DateTime::createFromFormat('d/m/y H:i', $creadate);
+ if ($createdAt) {
+ $item['timestamp'] = $createdAt->format('U');
+ }
- // todo: parse date strings such as '1Hr ago' etc.
- $createdAt = DateTime::createFromFormat('d/m/y H:i', $creadate);
- if ($createdAt) {
- $item['timestamp'] = $createdAt->format('U');
- }
-
- $item['author'] = $authorid->plaintext;
- $item['content'] = <<<HTML
+ $item['author'] = $authorid->plaintext;
+ $item['content'] = <<<HTML
<h1>{$identity->plaintext}</h1>
<h2>Links</h2>
<p><a href="{$glxlinks->find('a', 1)->href}" title="magnet link">magnet</a></p>
@@ -85,42 +86,46 @@ class TorrentGalaxyBridge extends BridgeAbstract {
<p>Added by: <a href="{$authorid->href}" title="author profile">{$authorid->plaintext}</a></p>
<p>Upload time: {$creadate}</p>
HTML;
- $item['enclosures'] = array($glxlinks->find('a', 0)->href);
- $item['categories'] = array($result->find('div.tgxtablecell', 0)->plaintext);
- if (preg_match('#/torrent/([^/]+)/#', self::URI . $identity->href, $torrentid)) {
- $item['uid'] = $torrentid[1];
- }
- $this->items[] = $item;
- }
- }
+ $item['enclosures'] = [$glxlinks->find('a', 0)->href];
+ $item['categories'] = [$result->find('div.tgxtablecell', 0)->plaintext];
+ if (preg_match('#/torrent/([^/]+)/#', self::URI . $identity->href, $torrentid)) {
+ $item['uid'] = $torrentid[1];
+ }
+ $this->items[] = $item;
+ }
+ }
- public function getName(){
- if(!is_null($this->getInput('search'))) {
- return $this->getInput('search') . ' : ' . self::NAME;
- }
- return parent::getName();
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('search'))) {
+ return $this->getInput('search') . ' : ' . self::NAME;
+ }
+ return parent::getName();
+ }
- public function getURI(){
- if(!is_null($this->getInput('search'))) {
- return self::URI
- . '/torrents.php?search=' . urlencode($this->getInput('search'))
- . '&lang=' . $this->getInput('lang');
- }
- return parent::getURI();
- }
+ public function getURI()
+ {
+ if (!is_null($this->getInput('search'))) {
+ return self::URI
+ . '/torrents.php?search=' . urlencode($this->getInput('search'))
+ . '&lang=' . $this->getInput('lang');
+ }
+ return parent::getURI();
+ }
- public function getDescription(){
- if(!is_null($this->getInput('search'))) {
- return 'Latest torrents for "' . $this->getInput('search') . '"';
- }
- return parent::getDescription();
- }
+ public function getDescription()
+ {
+ if (!is_null($this->getInput('search'))) {
+ return 'Latest torrents for "' . $this->getInput('search') . '"';
+ }
+ return parent::getDescription();
+ }
- public function getIcon(){
- if(!is_null($this->getInput('search'))) {
- return self::URI . '/common/favicon/favicon.ico';
- }
- return parent::getIcon();
- }
+ public function getIcon()
+ {
+ if (!is_null($this->getInput('search'))) {
+ return self::URI . '/common/favicon/favicon.ico';
+ }
+ return parent::getIcon();
+ }
}
diff --git a/bridges/TrelloBridge.php b/bridges/TrelloBridge.php
index 5cf69050..ea7eb71b 100644
--- a/bridges/TrelloBridge.php
+++ b/bridges/TrelloBridge.php
@@ -1,686 +1,700 @@
<?php
-class TrelloBridge extends BridgeAbstract {
- const NAME = 'Trello Bridge';
- const URI = 'https://trello.com/';
- const CACHE_TIMEOUT = 300; // 5min
- const DESCRIPTION = 'Returns activity on Trello boards or cards';
- const MAINTAINER = 'Roliga';
- const PARAMETERS = array(
- 'Board' => array(
- 'b' => array(
- 'name' => 'Board ID',
- 'required' => true,
- 'exampleValue' => 'g9mdhdzg',
- 'title' => 'Taken from Trello URL, e.g. trello.com/b/[Board ID]'
- )
- ),
- 'Card' => array(
- 'c' => array(
- 'name' => 'Card ID',
- 'required' => true,
- 'exampleValue' => '8vddc9pE',
- 'title' => 'Taken from Trello URL, e.g. trello.com/c/[Card ID]'
- )
- )
- );
- /*
- * This was extracted from webpack on a Trello page, e.g. trello.com/b/g9mdhdzg
- * In the browser's inspector/debugger go to the Debugger (Firefox) or
- * Sources (Chromium) tab, these values can be found at:
- * webpack:///resources/strings/actions/en.json
- */
- const ACTION_TEXTS = array(
- 'action_accept_enterprise_join_request'
- => '{memberCreator} added team {organization} to the enterprise {enterprise}',
- 'action_add_attachment_to_card'
- => '{memberCreator} attached {attachment} to {card} {attachmentPreview}',
- 'action_add_attachment_to_card@card'
- => '{memberCreator} attached {attachment} to this card {attachmentPreview}',
- 'action_add_checklist_to_card'
- => '{memberCreator} added {checklist} to {card}',
- 'action_add_checklist_to_card@card'
- => '{memberCreator} added {checklist} to this card',
- 'action_add_label_to_card'
- => '{memberCreator} added the {label} label to {card}',
- 'action_add_label_to_card@card'
- => '{memberCreator} added the {label} label to this card',
- 'action_add_organization_to_enterprise'
- => '{memberCreator} added team {organization} to the enterprise {enterprise}',
- 'action_add_to_organization_board'
- => '{memberCreator} added {board} to {organization}',
- 'action_add_to_organization_board@board'
- => '{memberCreator} added this board to {organization}',
- 'action_added_a_due_date'
- => '{memberCreator} set {card} to be due {date}',
- 'action_added_a_due_date@card'
- => '{memberCreator} set this card to be due {date}',
- 'action_added_list_to_board'
- => '{memberCreator} added list {list} to {board}',
- 'action_added_list_to_board@board'
- => '{memberCreator} added {list} to this board',
- 'action_added_member_to_board'
- => '{memberCreator} added {member} to {board}',
- 'action_added_member_to_board@board'
- => '{memberCreator} added {member} to this board',
- 'action_added_member_to_board_as_admin'
- => '{memberCreator} added {member} to {board} as an admin',
- 'action_added_member_to_board_as_admin@board'
- => '{memberCreator} added {member} to this board as an admin',
- 'action_added_member_to_board_as_observer'
- => '{memberCreator} added {member} to {board} as an observer',
- 'action_added_member_to_board_as_observer@board'
- => '{memberCreator} added {member} to this board as an observer',
- 'action_added_member_to_card'
- => '{memberCreator} added {member} to {card}',
- 'action_added_member_to_card@card'
- => '{memberCreator} added {member} to this card',
- 'action_added_member_to_organization'
- => '{memberCreator} added {member} to {organization}',
- 'action_added_member_to_organization_as_admin'
- => '{memberCreator} added {member} to {organization} as an admin',
- 'action_admins_visibility'
- => 'its admins',
- 'action_another_board'
- => 'another board',
- 'action_archived_card'
- => '{memberCreator} archived {card}',
- 'action_archived_card@card'
- => '{memberCreator} archived this card',
- 'action_archived_list'
- => '{memberCreator} archived list {list}',
- 'action_became_a_normal_user_in_organization'
- => '{memberCreator} became a normal user in {organization}',
- 'action_became_a_normal_user_on'
- => '{memberCreator} became a normal user on {board}',
- 'action_became_a_normal_user_on@board'
- => '{memberCreator} became a normal user on this board',
- 'action_became_an_admin_of_organization'
- => '{memberCreator} became an admin of {organization}',
- 'action_board_perm_level'
- => '{memberCreator} made {board} visible to {level}',
- 'action_board_perm_level@board'
- => '{memberCreator} made this board visible to {level}',
- 'action_calendar'
- => 'calendar',
- 'action_cardAging'
- => 'card aging',
- 'action_changed_a_due_date'
- => '{memberCreator} changed the due date of {card} to {date}',
- 'action_changed_a_due_date@card'
- => '{memberCreator} changed the due date of this card to {date}',
- 'action_changed_board_background'
- => '{memberCreator} changed the background of {board}',
- 'action_changed_board_background@board'
- => '{memberCreator} changed the background of this board',
- 'action_changed_description_of_card'
- => '{memberCreator} changed description of {card}',
- 'action_changed_description_of_card@card'
- => '{memberCreator} changed description of this card',
- 'action_changed_description_of_organization'
- => '{memberCreator} changed description of {organization}',
- 'action_changed_display_name_of_organization'
- => '{memberCreator} changed display name of {organization}',
- 'action_changed_name_of_organization'
- => '{memberCreator} changed name of {organization}',
- 'action_changed_website_of_organization'
- => '{memberCreator} changed website of {organization}',
- 'action_closed_board'
- => '{memberCreator} closed {board}',
- 'action_closed_board@board'
- => '{memberCreator} closed this board',
- 'action_comment_on_card'
- => '{memberCreator} {contextOn} {card} {comment}',
- 'action_comment_on_card@card'
- => '{memberCreator} {comment}',
- 'action_completed_checkitem'
- => '{memberCreator} completed {checkitem} on {card}',
- 'action_completed_checkitem@card'
- => '{memberCreator} completed {checkitem} on this card',
- 'action_convert_to_card_from_checkitem'
- => '{memberCreator} converted {card} from a checklist item on {cardSource}',
- 'action_convert_to_card_from_checkitem@card'
- => '{memberCreator} converted this card from a checklist item on {cardSource}',
- 'action_convert_to_card_from_checkitem@cardSource'
- => '{memberCreator} converted {card} from a checklist item on this card',
- 'action_copy_board'
- => '{memberCreator} copied this board from {board}',
- 'action_copy_card'
- => '{memberCreator} copied {card} from {cardSource} in list {list}',
- 'action_copy_card@card'
- => '{memberCreator} copied this card from {cardSource} in list {list}',
- 'action_copy_comment_from_card'
- => '{memberCreator} copied comment by {member} from card {card} {comment}',
- 'action_create_board'
- => '{memberCreator} created {board}',
- 'action_create_board@board'
- => '{memberCreator} created this board',
- 'action_create_card'
- => '{memberCreator} added {card} to {list}',
- 'action_create_card@card'
- => '{memberCreator} added this card to {list}',
- 'action_create_custom_field'
- => '{memberCreator} created the {customField} custom field on {board}',
- 'action_create_custom_field@board'
- => '{memberCreator} created the {customField} custom field on this board',
- 'action_create_enterprise_join_request'
- => '{memberCreator} requested to add team {organization} to the enterprise {enterprise}',
- 'action_created_an_invitation_to_board'
- => '{memberCreator} created an invitation to {board}',
- 'action_created_an_invitation_to_board@board'
- => '{memberCreator} created an invitation to this board',
- 'action_created_an_invitation_to_organization'
- => '{memberCreator} created an invitation to {organization}',
- 'action_created_checklist_on_board'
- => '{memberCreator} created {checklist} on {board}',
- 'action_created_checklist_on_board@board'
- => '{memberCreator} created {checklist} on this board',
- 'action_created_organization'
- => '{memberCreator} created {organization}',
- 'action_decline_enterprise_join_request'
- => '{memberCreator} declined the request to add team {organization} to the enterprise {enterprise}',
- 'action_delete_attachment_from_card'
- => '{memberCreator} deleted the {attachment} attachment from {card}',
- 'action_delete_attachment_from_card@card'
- => '{memberCreator} deleted the {attachment} attachment from this card',
- 'action_delete_card'
- => '{memberCreator} deleted card #{idCard} from {list}',
- 'action_delete_custom_field'
- => '{memberCreator} deleted the {customField} custom field from {board}',
- 'action_delete_custom_field@board'
- => '{memberCreator} deleted the {customField} custom field from this board',
- 'action_deleted_account'
- => '[deleted account]',
- 'action_deleted_an_invitation_to_board'
- => '{memberCreator} deleted an invitation to {board}',
- 'action_deleted_an_invitation_to_board@board'
- => '{memberCreator} deleted an invitation to this board',
- 'action_deleted_an_invitation_to_organization'
- => '{memberCreator} deleted an invitation to {organization}',
- 'action_deleted_checkitem'
- => '{memberCreator} deleted task {checkitem} on {checklist}',
- 'action_disabled_calendar_feed'
- => '{memberCreator} disabled the iCalendar feed on {board}',
- 'action_disabled_calendar_feed@board'
- => '{memberCreator} disabled the iCalendar feed on this board',
- 'action_disabled_card_covers'
- => '{memberCreator} disabled card cover images on {board}',
- 'action_disabled_card_covers@board'
- => '{memberCreator} disabled card cover images on this board',
- 'action_disabled_commenting'
- => '{memberCreator} disabled commenting on {board}',
- 'action_disabled_commenting@board'
- => '{memberCreator} disabled commenting on this board',
- 'action_disabled_inviting'
- => '{memberCreator} disabled inviting on {board}',
- 'action_disabled_inviting@board'
- => '{memberCreator} disabled inviting on this board',
- 'action_disabled_plugin'
- => '{memberCreator} disabled the {plugin} Power-Up',
- 'action_disabled_powerup'
- => '{memberCreator} disabled the {powerup} Power-Up',
- 'action_disabled_self_join'
- => '{memberCreator} disabled self join on {board}',
- 'action_disabled_self_join@board'
- => '{memberCreator} disabled self join on this board',
- 'action_disabled_voting'
- => '{memberCreator} disabled voting on {board}',
- 'action_disabled_voting@board'
- => '{memberCreator} disabled voting on this board',
- 'action_due_date_change'
- => '{memberCreator}',
- 'action_email_card'
- => '{memberCreator} emailed {card} to {list}',
- 'action_email_card@card'
- => '{memberCreator} emailed this card to {list}',
- 'action_email_card_from'
- => '{memberCreator} emailed {card} to {list} from {from}',
- 'action_email_card_from@card'
- => '{memberCreator} emailed this card to {list} from {from}',
- 'action_enabled_calendar_feed'
- => '{memberCreator} enabled the iCalendar feed on {board}',
- 'action_enabled_calendar_feed@board'
- => '{memberCreator} enabled the iCalendar feed on this board',
- 'action_enabled_card_covers'
- => '{memberCreator} enabled card cover images on {board}',
- 'action_enabled_card_covers@board'
- => '{memberCreator} enabled card cover images on this board',
- 'action_enabled_plugin'
- => '{memberCreator} enabled the {plugin} Power-Up',
- 'action_enabled_powerup'
- => '{memberCreator} enabled the {powerup} Power-Up',
- 'action_enabled_self_join'
- => '{memberCreator} enabled self join on {board}',
- 'action_enabled_self_join@board'
- => '{memberCreator} enabled self join on this board',
- 'action_hid_board'
- => '{memberCreator} hid {board}',
- 'action_hid_board@board'
- => '{memberCreator} hid this board',
- 'action_invited_an_unconfirmed_member_to_board'
- => '{memberCreator} invited an unconfirmed member to {board}',
- 'action_invited_an_unconfirmed_member_to_board@board'
- => '{memberCreator} invited an unconfirmed member to this board',
- 'action_invited_an_unconfirmed_member_to_organization'
- => '{memberCreator} invited an unconfirmed member to {organization}',
- 'action_joined_board'
- => '{memberCreator} joined {board}',
- 'action_joined_board@board'
- => '{memberCreator} joined this board',
- 'action_joined_board_by_invitation_link'
- => '{memberCreator} joined {board} with an invitation link from {memberInviter}',
- 'action_joined_board_by_invitation_link@board'
- => '{memberCreator} joined this board with an invitation link from {memberInviter}',
- 'action_joined_organization'
- => '{memberCreator} joined {organization}',
- 'action_joined_organization_by_invitation_link'
- => '{memberCreator} joined {organization} with an invitation link from {memberInviter}',
- 'action_left_board'
- => '{memberCreator} left {board}',
- 'action_left_board@board'
- => '{memberCreator} left this board',
- 'action_left_organization'
- => '{memberCreator} left {organization}',
- 'action_made_a_normal_user_in_organization'
- => '{memberCreator} made {member} a normal user in {organization}',
- 'action_made_a_normal_user_on'
- => '{memberCreator} made {member} a normal user on {board}',
- 'action_made_a_normal_user_on@board'
- => '{memberCreator} made {member} a normal user on this board',
- 'action_made_admin_of_board'
- => '{memberCreator} made {member} an admin of {board}',
- 'action_made_admin_of_board@board'
- => '{memberCreator} made {member} an admin of this board',
- 'action_made_an_admin_of_organization'
- => '{memberCreator} made {member} an admin of {organization}',
- 'action_made_commenting_on'
- => '{memberCreator} made commenting on {board} available to {level}',
- 'action_made_commenting_on@board'
- => '{memberCreator} made commenting on this board available to {level}',
- 'action_made_inviting_on'
- => '{memberCreator} made inviting on {board} available to {level}',
- 'action_made_inviting_on@board'
- => '{memberCreator} made inviting on this board available to {level}',
- 'action_made_observer_of_board'
- => '{memberCreator} made {member} an observer of {board}',
- 'action_made_observer_of_board@board'
- => '{memberCreator} made {member} an observer of this board',
- 'action_made_self_admin_of_board'
- => '{memberCreator} made themselves an admin of {board}',
- 'action_made_self_admin_of_board@board'
- => '{memberCreator} made themselves an admin of this board',
- 'action_made_self_observer_of_board'
- => '{memberCreator} became an observer of {board}',
- 'action_made_self_observer_of_board@board'
- => '{memberCreator} became an observer of this board',
- 'action_made_voting_on'
- => '{memberCreator} made voting on {board} available to {level}',
- 'action_made_voting_on@board'
- => '{memberCreator} made voting on this board available to {level}',
- 'action_marked_checkitem_incomplete'
- => '{memberCreator} marked {checkitem} incomplete on {card}',
- 'action_marked_checkitem_incomplete@card'
- => '{memberCreator} marked {checkitem} incomplete on this card',
- 'action_marked_the_due_date_complete'
- => '{memberCreator} marked the due date on {card} complete',
- 'action_marked_the_due_date_complete@card'
- => '{memberCreator} marked the due date complete',
- 'action_marked_the_due_date_incomplete'
- => '{memberCreator} marked the due date on {card} incomplete',
- 'action_marked_the_due_date_incomplete@card'
- => '{memberCreator} marked the due date incomplete',
- 'action_member_joined_card'
- => '{memberCreator} joined {card}',
- 'action_member_joined_card@card'
- => '{memberCreator} joined this card',
- 'action_member_left_card'
- => '{memberCreator} left {card}',
- 'action_member_left_card@card'
- => '{memberCreator} left this card',
- 'action_members_visibility'
- => 'its members',
- 'action_move_card_from_board'
- => '{memberCreator} transferred {card} to {board}',
- 'action_move_card_from_board@card'
- => '{memberCreator} transferred this card to {board}',
- 'action_move_card_from_list_to_list'
- => '{memberCreator} moved {card} from {listBefore} to {listAfter}',
- 'action_move_card_from_list_to_list@card'
- => '{memberCreator} moved this card from {listBefore} to {listAfter}',
- 'action_move_card_to_board'
- => '{memberCreator} transferred {card} from {board}',
- 'action_move_card_to_board@card'
- => '{memberCreator} transferred this card from {board}',
- 'action_move_list_from_board'
- => '{memberCreator} transferred {list} to {board}',
- 'action_move_list_to_board'
- => '{memberCreator} transferred {list} from {board}',
- 'action_moved_card_higher'
- => '{memberCreator} moved {card} higher',
- 'action_moved_card_higher@card'
- => '{memberCreator} moved this card higher',
- 'action_moved_card_lower'
- => '{memberCreator} moved {card} lower',
- 'action_moved_card_lower@card'
- => '{memberCreator} moved this card lower',
- 'action_moved_checkitem_higher'
- => '{memberCreator} moved {checkitem} higher in the checklist {checklist}',
- 'action_moved_checkitem_lower'
- => '{memberCreator} moved {checkitem} higher in the checklist {checklist}',
- 'action_moved_list_left'
- => '{memberCreator} moved list {list} left on {board}',
- 'action_moved_list_left@board'
- => '{memberCreator} moved {list} left on this board',
- 'action_moved_list_right'
- => '{memberCreator} moved list {list} right on {board}',
- 'action_moved_list_right@board'
- => '{memberCreator} moved {list} right on this board',
- 'action_observers_visibility'
- => 'members and observers',
- 'action_on'
- => 'on',
- 'action_org_visibility'
- => 'members of its team',
- 'action_public_visibility'
- => 'the public',
- 'action_remove_checklist_from_card'
- => '{memberCreator} removed {checklist} from {card}',
- 'action_remove_checklist_from_card@card'
- => '{memberCreator} removed {checklist} from this card',
- 'action_remove_from_organization_board'
- => '{memberCreator} removed {board} from {organization}',
- 'action_remove_from_organization_board@board'
- => '{memberCreator} removed this board from {organization}',
- 'action_remove_label_from_card'
- => '{memberCreator} removed the {label} label from {card}',
- 'action_remove_label_from_card@card'
- => '{memberCreator} removed the {label} label from this card',
- 'action_remove_organization_from_enterprise'
- => '{memberCreator} removed team {organization} from the enterprise {enterprise}',
- 'action_removed_a_due_date'
- => '{memberCreator} removed the due date from {card}',
- 'action_removed_a_due_date@card'
- => '{memberCreator} removed the due date from this card',
- 'action_removed_from_board'
- => '{memberCreator} removed {member} from {board}',
- 'action_removed_from_board@board'
- => '{memberCreator} removed {member} from this board',
- 'action_removed_member_from_card'
- => '{memberCreator} removed {member} from {card}',
- 'action_removed_member_from_card@card'
- => '{memberCreator} removed {member} from this card',
- 'action_removed_member_from_organization'
- => '{memberCreator} removed {member} from {organization}',
- 'action_removed_vote_for_card'
- => '{memberCreator} removed vote for {card}',
- 'action_removed_vote_for_card@card'
- => '{memberCreator} removed vote for this card',
- 'action_rename_custom_field'
- => '{memberCreator} renamed the {customField} custom field on {board} (from {name})',
- 'action_rename_custom_field@board'
- => '{memberCreator} renamed the {customField} custom field on this board (from {name})',
- 'action_renamed_card'
- => '{memberCreator} renamed {card} (from {name})',
- 'action_renamed_card@card'
- => '{memberCreator} renamed this card (from {name})',
- 'action_renamed_checkitem'
- => '{memberCreator} renamed {checkitem} (from {name})',
- 'action_renamed_checklist'
- => '{memberCreator} renamed {checklist} (from {name})',
- 'action_renamed_list'
- => '{memberCreator} renamed list {list} (from {name})',
- 'action_reopened_board'
- => '{memberCreator} re-opened {board}',
- 'action_reopened_board@board'
- => '{memberCreator} re-opened this board',
- 'action_sent_card_to_board'
- => '{memberCreator} sent {card} to the board',
- 'action_sent_card_to_board@card'
- => '{memberCreator} sent this card to the board',
- 'action_sent_list_to_board'
- => '{memberCreator} sent list {list} to the board',
- 'action_set_card_aging_mode_pirate'
- => '{memberCreator} changed card aging to pirate mode',
- 'action_set_card_aging_mode_regular'
- => '{memberCreator} changed card aging to regular mode',
- 'action_update_board_desc'
- => '{memberCreator} changed description of {board}',
- 'action_update_board_desc@board'
- => '{memberCreator} changed description of this board',
- 'action_update_board_name'
- => '{memberCreator} renamed {board} (from {name})',
- 'action_update_board_name@board'
- => '{memberCreator} renamed this board (from {name})',
- 'action_update_custom_field'
- => '{memberCreator} updated the {customField} custom field on {board}',
- 'action_update_custom_field@board'
- => '{memberCreator} updated the {customField} custom field on this board',
- 'action_update_custom_field_item'
- => '{memberCreator} updated the value for the {customFieldItem} custom field on {card}',
- 'action_update_custom_field_item@card'
- => '{memberCreator} updated the value for the {customFieldItem} custom field on this card',
- 'action_updated_their_bio'
- => '{memberCreator} updated their bio',
- 'action_updated_their_display_name'
- => '{memberCreator} updated their display name',
- 'action_updated_their_initials'
- => '{memberCreator} updated their initials',
- 'action_updated_their_username'
- => '{memberCreator} updated their username',
- 'action_vote_on_card'
- => '{memberCreator} voted for {card}',
- 'action_vote_on_card@card'
- => '{memberCreator} voted for this card',
- 'action_voting'
- => 'voting',
- 'action_withdraw_enterprise_join_request'
- => '{memberCreator} withdrew a request to add team {organization} to the enterprise {enterprise}'
- );
+class TrelloBridge extends BridgeAbstract
+{
+ const NAME = 'Trello Bridge';
+ const URI = 'https://trello.com/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns activity on Trello boards or cards';
+ const MAINTAINER = 'Roliga';
+ const PARAMETERS = [
+ 'Board' => [
+ 'b' => [
+ 'name' => 'Board ID',
+ 'required' => true,
+ 'exampleValue' => 'g9mdhdzg',
+ 'title' => 'Taken from Trello URL, e.g. trello.com/b/[Board ID]'
+ ]
+ ],
+ 'Card' => [
+ 'c' => [
+ 'name' => 'Card ID',
+ 'required' => true,
+ 'exampleValue' => '8vddc9pE',
+ 'title' => 'Taken from Trello URL, e.g. trello.com/c/[Card ID]'
+ ]
+ ]
+ ];
- const REQUEST_ACTIONS_BOARDS = array(
- 'addAttachmentToCard',
- 'addChecklistToCard',
- 'addMemberToCard',
- 'commentCard',
- 'copyCommentCard',
- 'convertToCardFromCheckItem',
- 'createCard',
- 'copyCard',
- 'deleteAttachmentFromCard',
- 'emailCard',
- 'moveCardFromBoard',
- 'moveCardToBoard',
- 'removeChecklistFromCard',
- 'removeMemberFromCard',
- 'updateCard:idList',
- 'updateCard:closed',
- 'updateCard:due',
- 'updateCard:dueComplete',
- 'updateCheckItemStateOnCard',
- 'updateCustomFieldItem',
- 'addMemberToBoard',
- 'addToOrganizationBoard',
- 'copyBoard',
- 'createBoard',
- 'createCustomField',
- 'createList',
- 'deleteCard',
- 'deleteCustomField',
- 'disablePlugin',
- 'disablePowerUp',
- 'enablePlugin',
- 'enablePowerUp',
- 'makeAdminOfBoard',
- 'makeNormalMemberOfBoard',
- 'makeObserverOfBoard',
- 'moveListFromBoard',
- 'moveListToBoard',
- 'removeFromOrganizationBoard',
- 'unconfirmedBoardInvitation',
- 'unconfirmedOrganizationInvitation',
- 'updateBoard',
- 'updateCustomField',
- 'updateList:closed'
- );
+ /*
+ * This was extracted from webpack on a Trello page, e.g. trello.com/b/g9mdhdzg
+ * In the browser's inspector/debugger go to the Debugger (Firefox) or
+ * Sources (Chromium) tab, these values can be found at:
+ * webpack:///resources/strings/actions/en.json
+ */
+ const ACTION_TEXTS = [
+ 'action_accept_enterprise_join_request'
+ => '{memberCreator} added team {organization} to the enterprise {enterprise}',
+ 'action_add_attachment_to_card'
+ => '{memberCreator} attached {attachment} to {card} {attachmentPreview}',
+ 'action_add_attachment_to_card@card'
+ => '{memberCreator} attached {attachment} to this card {attachmentPreview}',
+ 'action_add_checklist_to_card'
+ => '{memberCreator} added {checklist} to {card}',
+ 'action_add_checklist_to_card@card'
+ => '{memberCreator} added {checklist} to this card',
+ 'action_add_label_to_card'
+ => '{memberCreator} added the {label} label to {card}',
+ 'action_add_label_to_card@card'
+ => '{memberCreator} added the {label} label to this card',
+ 'action_add_organization_to_enterprise'
+ => '{memberCreator} added team {organization} to the enterprise {enterprise}',
+ 'action_add_to_organization_board'
+ => '{memberCreator} added {board} to {organization}',
+ 'action_add_to_organization_board@board'
+ => '{memberCreator} added this board to {organization}',
+ 'action_added_a_due_date'
+ => '{memberCreator} set {card} to be due {date}',
+ 'action_added_a_due_date@card'
+ => '{memberCreator} set this card to be due {date}',
+ 'action_added_list_to_board'
+ => '{memberCreator} added list {list} to {board}',
+ 'action_added_list_to_board@board'
+ => '{memberCreator} added {list} to this board',
+ 'action_added_member_to_board'
+ => '{memberCreator} added {member} to {board}',
+ 'action_added_member_to_board@board'
+ => '{memberCreator} added {member} to this board',
+ 'action_added_member_to_board_as_admin'
+ => '{memberCreator} added {member} to {board} as an admin',
+ 'action_added_member_to_board_as_admin@board'
+ => '{memberCreator} added {member} to this board as an admin',
+ 'action_added_member_to_board_as_observer'
+ => '{memberCreator} added {member} to {board} as an observer',
+ 'action_added_member_to_board_as_observer@board'
+ => '{memberCreator} added {member} to this board as an observer',
+ 'action_added_member_to_card'
+ => '{memberCreator} added {member} to {card}',
+ 'action_added_member_to_card@card'
+ => '{memberCreator} added {member} to this card',
+ 'action_added_member_to_organization'
+ => '{memberCreator} added {member} to {organization}',
+ 'action_added_member_to_organization_as_admin'
+ => '{memberCreator} added {member} to {organization} as an admin',
+ 'action_admins_visibility'
+ => 'its admins',
+ 'action_another_board'
+ => 'another board',
+ 'action_archived_card'
+ => '{memberCreator} archived {card}',
+ 'action_archived_card@card'
+ => '{memberCreator} archived this card',
+ 'action_archived_list'
+ => '{memberCreator} archived list {list}',
+ 'action_became_a_normal_user_in_organization'
+ => '{memberCreator} became a normal user in {organization}',
+ 'action_became_a_normal_user_on'
+ => '{memberCreator} became a normal user on {board}',
+ 'action_became_a_normal_user_on@board'
+ => '{memberCreator} became a normal user on this board',
+ 'action_became_an_admin_of_organization'
+ => '{memberCreator} became an admin of {organization}',
+ 'action_board_perm_level'
+ => '{memberCreator} made {board} visible to {level}',
+ 'action_board_perm_level@board'
+ => '{memberCreator} made this board visible to {level}',
+ 'action_calendar'
+ => 'calendar',
+ 'action_cardAging'
+ => 'card aging',
+ 'action_changed_a_due_date'
+ => '{memberCreator} changed the due date of {card} to {date}',
+ 'action_changed_a_due_date@card'
+ => '{memberCreator} changed the due date of this card to {date}',
+ 'action_changed_board_background'
+ => '{memberCreator} changed the background of {board}',
+ 'action_changed_board_background@board'
+ => '{memberCreator} changed the background of this board',
+ 'action_changed_description_of_card'
+ => '{memberCreator} changed description of {card}',
+ 'action_changed_description_of_card@card'
+ => '{memberCreator} changed description of this card',
+ 'action_changed_description_of_organization'
+ => '{memberCreator} changed description of {organization}',
+ 'action_changed_display_name_of_organization'
+ => '{memberCreator} changed display name of {organization}',
+ 'action_changed_name_of_organization'
+ => '{memberCreator} changed name of {organization}',
+ 'action_changed_website_of_organization'
+ => '{memberCreator} changed website of {organization}',
+ 'action_closed_board'
+ => '{memberCreator} closed {board}',
+ 'action_closed_board@board'
+ => '{memberCreator} closed this board',
+ 'action_comment_on_card'
+ => '{memberCreator} {contextOn} {card} {comment}',
+ 'action_comment_on_card@card'
+ => '{memberCreator} {comment}',
+ 'action_completed_checkitem'
+ => '{memberCreator} completed {checkitem} on {card}',
+ 'action_completed_checkitem@card'
+ => '{memberCreator} completed {checkitem} on this card',
+ 'action_convert_to_card_from_checkitem'
+ => '{memberCreator} converted {card} from a checklist item on {cardSource}',
+ 'action_convert_to_card_from_checkitem@card'
+ => '{memberCreator} converted this card from a checklist item on {cardSource}',
+ 'action_convert_to_card_from_checkitem@cardSource'
+ => '{memberCreator} converted {card} from a checklist item on this card',
+ 'action_copy_board'
+ => '{memberCreator} copied this board from {board}',
+ 'action_copy_card'
+ => '{memberCreator} copied {card} from {cardSource} in list {list}',
+ 'action_copy_card@card'
+ => '{memberCreator} copied this card from {cardSource} in list {list}',
+ 'action_copy_comment_from_card'
+ => '{memberCreator} copied comment by {member} from card {card} {comment}',
+ 'action_create_board'
+ => '{memberCreator} created {board}',
+ 'action_create_board@board'
+ => '{memberCreator} created this board',
+ 'action_create_card'
+ => '{memberCreator} added {card} to {list}',
+ 'action_create_card@card'
+ => '{memberCreator} added this card to {list}',
+ 'action_create_custom_field'
+ => '{memberCreator} created the {customField} custom field on {board}',
+ 'action_create_custom_field@board'
+ => '{memberCreator} created the {customField} custom field on this board',
+ 'action_create_enterprise_join_request'
+ => '{memberCreator} requested to add team {organization} to the enterprise {enterprise}',
+ 'action_created_an_invitation_to_board'
+ => '{memberCreator} created an invitation to {board}',
+ 'action_created_an_invitation_to_board@board'
+ => '{memberCreator} created an invitation to this board',
+ 'action_created_an_invitation_to_organization'
+ => '{memberCreator} created an invitation to {organization}',
+ 'action_created_checklist_on_board'
+ => '{memberCreator} created {checklist} on {board}',
+ 'action_created_checklist_on_board@board'
+ => '{memberCreator} created {checklist} on this board',
+ 'action_created_organization'
+ => '{memberCreator} created {organization}',
+ 'action_decline_enterprise_join_request'
+ => '{memberCreator} declined the request to add team {organization} to the enterprise {enterprise}',
+ 'action_delete_attachment_from_card'
+ => '{memberCreator} deleted the {attachment} attachment from {card}',
+ 'action_delete_attachment_from_card@card'
+ => '{memberCreator} deleted the {attachment} attachment from this card',
+ 'action_delete_card'
+ => '{memberCreator} deleted card #{idCard} from {list}',
+ 'action_delete_custom_field'
+ => '{memberCreator} deleted the {customField} custom field from {board}',
+ 'action_delete_custom_field@board'
+ => '{memberCreator} deleted the {customField} custom field from this board',
+ 'action_deleted_account'
+ => '[deleted account]',
+ 'action_deleted_an_invitation_to_board'
+ => '{memberCreator} deleted an invitation to {board}',
+ 'action_deleted_an_invitation_to_board@board'
+ => '{memberCreator} deleted an invitation to this board',
+ 'action_deleted_an_invitation_to_organization'
+ => '{memberCreator} deleted an invitation to {organization}',
+ 'action_deleted_checkitem'
+ => '{memberCreator} deleted task {checkitem} on {checklist}',
+ 'action_disabled_calendar_feed'
+ => '{memberCreator} disabled the iCalendar feed on {board}',
+ 'action_disabled_calendar_feed@board'
+ => '{memberCreator} disabled the iCalendar feed on this board',
+ 'action_disabled_card_covers'
+ => '{memberCreator} disabled card cover images on {board}',
+ 'action_disabled_card_covers@board'
+ => '{memberCreator} disabled card cover images on this board',
+ 'action_disabled_commenting'
+ => '{memberCreator} disabled commenting on {board}',
+ 'action_disabled_commenting@board'
+ => '{memberCreator} disabled commenting on this board',
+ 'action_disabled_inviting'
+ => '{memberCreator} disabled inviting on {board}',
+ 'action_disabled_inviting@board'
+ => '{memberCreator} disabled inviting on this board',
+ 'action_disabled_plugin'
+ => '{memberCreator} disabled the {plugin} Power-Up',
+ 'action_disabled_powerup'
+ => '{memberCreator} disabled the {powerup} Power-Up',
+ 'action_disabled_self_join'
+ => '{memberCreator} disabled self join on {board}',
+ 'action_disabled_self_join@board'
+ => '{memberCreator} disabled self join on this board',
+ 'action_disabled_voting'
+ => '{memberCreator} disabled voting on {board}',
+ 'action_disabled_voting@board'
+ => '{memberCreator} disabled voting on this board',
+ 'action_due_date_change'
+ => '{memberCreator}',
+ 'action_email_card'
+ => '{memberCreator} emailed {card} to {list}',
+ 'action_email_card@card'
+ => '{memberCreator} emailed this card to {list}',
+ 'action_email_card_from'
+ => '{memberCreator} emailed {card} to {list} from {from}',
+ 'action_email_card_from@card'
+ => '{memberCreator} emailed this card to {list} from {from}',
+ 'action_enabled_calendar_feed'
+ => '{memberCreator} enabled the iCalendar feed on {board}',
+ 'action_enabled_calendar_feed@board'
+ => '{memberCreator} enabled the iCalendar feed on this board',
+ 'action_enabled_card_covers'
+ => '{memberCreator} enabled card cover images on {board}',
+ 'action_enabled_card_covers@board'
+ => '{memberCreator} enabled card cover images on this board',
+ 'action_enabled_plugin'
+ => '{memberCreator} enabled the {plugin} Power-Up',
+ 'action_enabled_powerup'
+ => '{memberCreator} enabled the {powerup} Power-Up',
+ 'action_enabled_self_join'
+ => '{memberCreator} enabled self join on {board}',
+ 'action_enabled_self_join@board'
+ => '{memberCreator} enabled self join on this board',
+ 'action_hid_board'
+ => '{memberCreator} hid {board}',
+ 'action_hid_board@board'
+ => '{memberCreator} hid this board',
+ 'action_invited_an_unconfirmed_member_to_board'
+ => '{memberCreator} invited an unconfirmed member to {board}',
+ 'action_invited_an_unconfirmed_member_to_board@board'
+ => '{memberCreator} invited an unconfirmed member to this board',
+ 'action_invited_an_unconfirmed_member_to_organization'
+ => '{memberCreator} invited an unconfirmed member to {organization}',
+ 'action_joined_board'
+ => '{memberCreator} joined {board}',
+ 'action_joined_board@board'
+ => '{memberCreator} joined this board',
+ 'action_joined_board_by_invitation_link'
+ => '{memberCreator} joined {board} with an invitation link from {memberInviter}',
+ 'action_joined_board_by_invitation_link@board'
+ => '{memberCreator} joined this board with an invitation link from {memberInviter}',
+ 'action_joined_organization'
+ => '{memberCreator} joined {organization}',
+ 'action_joined_organization_by_invitation_link'
+ => '{memberCreator} joined {organization} with an invitation link from {memberInviter}',
+ 'action_left_board'
+ => '{memberCreator} left {board}',
+ 'action_left_board@board'
+ => '{memberCreator} left this board',
+ 'action_left_organization'
+ => '{memberCreator} left {organization}',
+ 'action_made_a_normal_user_in_organization'
+ => '{memberCreator} made {member} a normal user in {organization}',
+ 'action_made_a_normal_user_on'
+ => '{memberCreator} made {member} a normal user on {board}',
+ 'action_made_a_normal_user_on@board'
+ => '{memberCreator} made {member} a normal user on this board',
+ 'action_made_admin_of_board'
+ => '{memberCreator} made {member} an admin of {board}',
+ 'action_made_admin_of_board@board'
+ => '{memberCreator} made {member} an admin of this board',
+ 'action_made_an_admin_of_organization'
+ => '{memberCreator} made {member} an admin of {organization}',
+ 'action_made_commenting_on'
+ => '{memberCreator} made commenting on {board} available to {level}',
+ 'action_made_commenting_on@board'
+ => '{memberCreator} made commenting on this board available to {level}',
+ 'action_made_inviting_on'
+ => '{memberCreator} made inviting on {board} available to {level}',
+ 'action_made_inviting_on@board'
+ => '{memberCreator} made inviting on this board available to {level}',
+ 'action_made_observer_of_board'
+ => '{memberCreator} made {member} an observer of {board}',
+ 'action_made_observer_of_board@board'
+ => '{memberCreator} made {member} an observer of this board',
+ 'action_made_self_admin_of_board'
+ => '{memberCreator} made themselves an admin of {board}',
+ 'action_made_self_admin_of_board@board'
+ => '{memberCreator} made themselves an admin of this board',
+ 'action_made_self_observer_of_board'
+ => '{memberCreator} became an observer of {board}',
+ 'action_made_self_observer_of_board@board'
+ => '{memberCreator} became an observer of this board',
+ 'action_made_voting_on'
+ => '{memberCreator} made voting on {board} available to {level}',
+ 'action_made_voting_on@board'
+ => '{memberCreator} made voting on this board available to {level}',
+ 'action_marked_checkitem_incomplete'
+ => '{memberCreator} marked {checkitem} incomplete on {card}',
+ 'action_marked_checkitem_incomplete@card'
+ => '{memberCreator} marked {checkitem} incomplete on this card',
+ 'action_marked_the_due_date_complete'
+ => '{memberCreator} marked the due date on {card} complete',
+ 'action_marked_the_due_date_complete@card'
+ => '{memberCreator} marked the due date complete',
+ 'action_marked_the_due_date_incomplete'
+ => '{memberCreator} marked the due date on {card} incomplete',
+ 'action_marked_the_due_date_incomplete@card'
+ => '{memberCreator} marked the due date incomplete',
+ 'action_member_joined_card'
+ => '{memberCreator} joined {card}',
+ 'action_member_joined_card@card'
+ => '{memberCreator} joined this card',
+ 'action_member_left_card'
+ => '{memberCreator} left {card}',
+ 'action_member_left_card@card'
+ => '{memberCreator} left this card',
+ 'action_members_visibility'
+ => 'its members',
+ 'action_move_card_from_board'
+ => '{memberCreator} transferred {card} to {board}',
+ 'action_move_card_from_board@card'
+ => '{memberCreator} transferred this card to {board}',
+ 'action_move_card_from_list_to_list'
+ => '{memberCreator} moved {card} from {listBefore} to {listAfter}',
+ 'action_move_card_from_list_to_list@card'
+ => '{memberCreator} moved this card from {listBefore} to {listAfter}',
+ 'action_move_card_to_board'
+ => '{memberCreator} transferred {card} from {board}',
+ 'action_move_card_to_board@card'
+ => '{memberCreator} transferred this card from {board}',
+ 'action_move_list_from_board'
+ => '{memberCreator} transferred {list} to {board}',
+ 'action_move_list_to_board'
+ => '{memberCreator} transferred {list} from {board}',
+ 'action_moved_card_higher'
+ => '{memberCreator} moved {card} higher',
+ 'action_moved_card_higher@card'
+ => '{memberCreator} moved this card higher',
+ 'action_moved_card_lower'
+ => '{memberCreator} moved {card} lower',
+ 'action_moved_card_lower@card'
+ => '{memberCreator} moved this card lower',
+ 'action_moved_checkitem_higher'
+ => '{memberCreator} moved {checkitem} higher in the checklist {checklist}',
+ 'action_moved_checkitem_lower'
+ => '{memberCreator} moved {checkitem} higher in the checklist {checklist}',
+ 'action_moved_list_left'
+ => '{memberCreator} moved list {list} left on {board}',
+ 'action_moved_list_left@board'
+ => '{memberCreator} moved {list} left on this board',
+ 'action_moved_list_right'
+ => '{memberCreator} moved list {list} right on {board}',
+ 'action_moved_list_right@board'
+ => '{memberCreator} moved {list} right on this board',
+ 'action_observers_visibility'
+ => 'members and observers',
+ 'action_on'
+ => 'on',
+ 'action_org_visibility'
+ => 'members of its team',
+ 'action_public_visibility'
+ => 'the public',
+ 'action_remove_checklist_from_card'
+ => '{memberCreator} removed {checklist} from {card}',
+ 'action_remove_checklist_from_card@card'
+ => '{memberCreator} removed {checklist} from this card',
+ 'action_remove_from_organization_board'
+ => '{memberCreator} removed {board} from {organization}',
+ 'action_remove_from_organization_board@board'
+ => '{memberCreator} removed this board from {organization}',
+ 'action_remove_label_from_card'
+ => '{memberCreator} removed the {label} label from {card}',
+ 'action_remove_label_from_card@card'
+ => '{memberCreator} removed the {label} label from this card',
+ 'action_remove_organization_from_enterprise'
+ => '{memberCreator} removed team {organization} from the enterprise {enterprise}',
+ 'action_removed_a_due_date'
+ => '{memberCreator} removed the due date from {card}',
+ 'action_removed_a_due_date@card'
+ => '{memberCreator} removed the due date from this card',
+ 'action_removed_from_board'
+ => '{memberCreator} removed {member} from {board}',
+ 'action_removed_from_board@board'
+ => '{memberCreator} removed {member} from this board',
+ 'action_removed_member_from_card'
+ => '{memberCreator} removed {member} from {card}',
+ 'action_removed_member_from_card@card'
+ => '{memberCreator} removed {member} from this card',
+ 'action_removed_member_from_organization'
+ => '{memberCreator} removed {member} from {organization}',
+ 'action_removed_vote_for_card'
+ => '{memberCreator} removed vote for {card}',
+ 'action_removed_vote_for_card@card'
+ => '{memberCreator} removed vote for this card',
+ 'action_rename_custom_field'
+ => '{memberCreator} renamed the {customField} custom field on {board} (from {name})',
+ 'action_rename_custom_field@board'
+ => '{memberCreator} renamed the {customField} custom field on this board (from {name})',
+ 'action_renamed_card'
+ => '{memberCreator} renamed {card} (from {name})',
+ 'action_renamed_card@card'
+ => '{memberCreator} renamed this card (from {name})',
+ 'action_renamed_checkitem'
+ => '{memberCreator} renamed {checkitem} (from {name})',
+ 'action_renamed_checklist'
+ => '{memberCreator} renamed {checklist} (from {name})',
+ 'action_renamed_list'
+ => '{memberCreator} renamed list {list} (from {name})',
+ 'action_reopened_board'
+ => '{memberCreator} re-opened {board}',
+ 'action_reopened_board@board'
+ => '{memberCreator} re-opened this board',
+ 'action_sent_card_to_board'
+ => '{memberCreator} sent {card} to the board',
+ 'action_sent_card_to_board@card'
+ => '{memberCreator} sent this card to the board',
+ 'action_sent_list_to_board'
+ => '{memberCreator} sent list {list} to the board',
+ 'action_set_card_aging_mode_pirate'
+ => '{memberCreator} changed card aging to pirate mode',
+ 'action_set_card_aging_mode_regular'
+ => '{memberCreator} changed card aging to regular mode',
+ 'action_update_board_desc'
+ => '{memberCreator} changed description of {board}',
+ 'action_update_board_desc@board'
+ => '{memberCreator} changed description of this board',
+ 'action_update_board_name'
+ => '{memberCreator} renamed {board} (from {name})',
+ 'action_update_board_name@board'
+ => '{memberCreator} renamed this board (from {name})',
+ 'action_update_custom_field'
+ => '{memberCreator} updated the {customField} custom field on {board}',
+ 'action_update_custom_field@board'
+ => '{memberCreator} updated the {customField} custom field on this board',
+ 'action_update_custom_field_item'
+ => '{memberCreator} updated the value for the {customFieldItem} custom field on {card}',
+ 'action_update_custom_field_item@card'
+ => '{memberCreator} updated the value for the {customFieldItem} custom field on this card',
+ 'action_updated_their_bio'
+ => '{memberCreator} updated their bio',
+ 'action_updated_their_display_name'
+ => '{memberCreator} updated their display name',
+ 'action_updated_their_initials'
+ => '{memberCreator} updated their initials',
+ 'action_updated_their_username'
+ => '{memberCreator} updated their username',
+ 'action_vote_on_card'
+ => '{memberCreator} voted for {card}',
+ 'action_vote_on_card@card'
+ => '{memberCreator} voted for this card',
+ 'action_voting'
+ => 'voting',
+ 'action_withdraw_enterprise_join_request'
+ => '{memberCreator} withdrew a request to add team {organization} to the enterprise {enterprise}'
+ ];
- const REQUEST_ACTIONS_CARDS = array(
- 'addAttachmentToCard',
- 'addChecklistToCard',
- 'addMemberToCard',
- 'commentCard',
- 'copyCommentCard',
- 'convertToCardFromCheckItem',
- 'createCard',
- 'copyCard',
- 'deleteAttachmentFromCard',
- 'emailCard',
- 'moveCardFromBoard',
- 'moveCardToBoard',
- 'removeChecklistFromCard',
- 'removeMemberFromCard',
- 'updateCard:idList',
- 'updateCard:closed',
- 'updateCard:due',
- 'updateCard:dueComplete',
- 'updateCheckItemStateOnCard',
- 'updateCustomFieldItem'
- );
+ const REQUEST_ACTIONS_BOARDS = [
+ 'addAttachmentToCard',
+ 'addChecklistToCard',
+ 'addMemberToCard',
+ 'commentCard',
+ 'copyCommentCard',
+ 'convertToCardFromCheckItem',
+ 'createCard',
+ 'copyCard',
+ 'deleteAttachmentFromCard',
+ 'emailCard',
+ 'moveCardFromBoard',
+ 'moveCardToBoard',
+ 'removeChecklistFromCard',
+ 'removeMemberFromCard',
+ 'updateCard:idList',
+ 'updateCard:closed',
+ 'updateCard:due',
+ 'updateCard:dueComplete',
+ 'updateCheckItemStateOnCard',
+ 'updateCustomFieldItem',
+ 'addMemberToBoard',
+ 'addToOrganizationBoard',
+ 'copyBoard',
+ 'createBoard',
+ 'createCustomField',
+ 'createList',
+ 'deleteCard',
+ 'deleteCustomField',
+ 'disablePlugin',
+ 'disablePowerUp',
+ 'enablePlugin',
+ 'enablePowerUp',
+ 'makeAdminOfBoard',
+ 'makeNormalMemberOfBoard',
+ 'makeObserverOfBoard',
+ 'moveListFromBoard',
+ 'moveListToBoard',
+ 'removeFromOrganizationBoard',
+ 'unconfirmedBoardInvitation',
+ 'unconfirmedOrganizationInvitation',
+ 'updateBoard',
+ 'updateCustomField',
+ 'updateList:closed'
+ ];
- private $feedName = '';
- private $feedURI = '';
+ const REQUEST_ACTIONS_CARDS = [
+ 'addAttachmentToCard',
+ 'addChecklistToCard',
+ 'addMemberToCard',
+ 'commentCard',
+ 'copyCommentCard',
+ 'convertToCardFromCheckItem',
+ 'createCard',
+ 'copyCard',
+ 'deleteAttachmentFromCard',
+ 'emailCard',
+ 'moveCardFromBoard',
+ 'moveCardToBoard',
+ 'removeChecklistFromCard',
+ 'removeMemberFromCard',
+ 'updateCard:idList',
+ 'updateCard:closed',
+ 'updateCard:due',
+ 'updateCard:dueComplete',
+ 'updateCheckItemStateOnCard',
+ 'updateCustomFieldItem'
+ ];
- private function queryAPI($path, $params = array()) {
- $data = json_decode(getContents('https://trello.com/1/'
- . $path
- . '?'
- . http_build_query($params)));
- return $data;
- }
+ private $feedName = '';
+ private $feedURI = '';
- private function renderAction($action, $textOnly = false) {
- if(!array_key_exists($action->display->translationKey, self::ACTION_TEXTS)) {
- return '';
- }
+ private function queryAPI($path, $params = [])
+ {
+ $data = json_decode(getContents('https://trello.com/1/'
+ . $path
+ . '?'
+ . http_build_query($params)));
+ return $data;
+ }
- $strings = array();
- $entities = (array)$action->display->entities;
+ private function renderAction($action, $textOnly = false)
+ {
+ if (!array_key_exists($action->display->translationKey, self::ACTION_TEXTS)) {
+ return '';
+ }
- foreach($entities as $entity_name => $entity) {
- $type = $entity->type;
- if($type === 'attachmentPreview'
- && !$textOnly
- && isset($entity->originalUrl)) {
- $string = '<p><a href="'
- . $entity->originalUrl
- . '"><img src="'
- . $entity->previewUrl
- . '"></a></p>';
- } elseif($type === 'card' && !$textOnly) {
- $string = '<a href="https://trello.com/c/'
- . $entity->shortLink
- . '">'
- . $entity->text
- . '</a>';
- } elseif($type === 'member' && !$textOnly) {
- $string = '<a href="https://trello.com/'
- . $entity->username
- . '">'
- . $entity->text
- . '</a>';
- } elseif($type === 'date') {
- $string = gmdate('M j, Y \a\t g:i A T', strtotime($entity->date));
- } elseif($type === 'translatable') {
- $string = self::ACTION_TEXTS[$entity->translationKey];
- } else {
- if(isset($entity->text)) {
- $string = $entity->text;
- } else {
- $string = '';
- }
- }
- $strings['{' . $entity_name . '}'] = $string;
- }
+ $strings = [];
+ $entities = (array)$action->display->entities;
- return str_replace(array_keys($strings),
- array_values($strings),
- self::ACTION_TEXTS[$action->display->translationKey]);
- }
+ foreach ($entities as $entity_name => $entity) {
+ $type = $entity->type;
+ if (
+ $type === 'attachmentPreview'
+ && !$textOnly
+ && isset($entity->originalUrl)
+ ) {
+ $string = '<p><a href="'
+ . $entity->originalUrl
+ . '"><img src="'
+ . $entity->previewUrl
+ . '"></a></p>';
+ } elseif ($type === 'card' && !$textOnly) {
+ $string = '<a href="https://trello.com/c/'
+ . $entity->shortLink
+ . '">'
+ . $entity->text
+ . '</a>';
+ } elseif ($type === 'member' && !$textOnly) {
+ $string = '<a href="https://trello.com/'
+ . $entity->username
+ . '">'
+ . $entity->text
+ . '</a>';
+ } elseif ($type === 'date') {
+ $string = gmdate('M j, Y \a\t g:i A T', strtotime($entity->date));
+ } elseif ($type === 'translatable') {
+ $string = self::ACTION_TEXTS[$entity->translationKey];
+ } else {
+ if (isset($entity->text)) {
+ $string = $entity->text;
+ } else {
+ $string = '';
+ }
+ }
+ $strings['{' . $entity_name . '}'] = $string;
+ }
- public function collectData() {
- $apiParams = array(
- 'actions_display' => 'true',
- 'fields' => 'name,url'
- );
- switch($this->queriedContext) {
- case 'Board':
- $apiParams['actions'] = implode(',', self::REQUEST_ACTIONS_BOARDS);
- $data = $this->queryAPI('boards/' . $this->getInput('b'), $apiParams);
- break;
- case 'Card':
- $apiParams['actions'] = implode(',', self::REQUEST_ACTIONS_CARDS);
- $data = $this->queryAPI('cards/' . $this->getInput('c'), $apiParams);
- break;
- default:
- returnClientError('Invalid context');
- }
+ return str_replace(
+ array_keys($strings),
+ array_values($strings),
+ self::ACTION_TEXTS[$action->display->translationKey]
+ );
+ }
- $this->feedName = $data->name;
- $this->feedURI = $data->url;
+ public function collectData()
+ {
+ $apiParams = [
+ 'actions_display' => 'true',
+ 'fields' => 'name,url'
+ ];
+ switch ($this->queriedContext) {
+ case 'Board':
+ $apiParams['actions'] = implode(',', self::REQUEST_ACTIONS_BOARDS);
+ $data = $this->queryAPI('boards/' . $this->getInput('b'), $apiParams);
+ break;
+ case 'Card':
+ $apiParams['actions'] = implode(',', self::REQUEST_ACTIONS_CARDS);
+ $data = $this->queryAPI('cards/' . $this->getInput('c'), $apiParams);
+ break;
+ default:
+ returnClientError('Invalid context');
+ }
- foreach($data->actions as $action) {
- $item = array();
+ $this->feedName = $data->name;
+ $this->feedURI = $data->url;
- $item['title'] = $this->renderAction($action, true);
- $item['timestamp'] = strtotime($action->date);
- $item['author'] = $action->memberCreator->fullName;
- $item['categories'] = array(
- 'trello',
- $action->data->board->name,
- $action->type
- );
- if(isset($action->data->card)) {
- $item['categories'][] = $action->data->card->name;
- $item['uri'] = 'https://trello.com/c/'
- . $action->data->card->shortLink
- . '#action-'
- . $action->id;
- } else {
- $item['uri'] = 'https://trello.com/b/'
- . $action->data->board->shortLink;
- }
- $item['content'] = $this->renderAction($action, false);
- if(isset($action->data->attachment->url)) {
- $item['enclosures'] = array($action->data->attachment->url);
- }
+ foreach ($data->actions as $action) {
+ $item = [];
- $this->items[] = $item;
- }
- }
+ $item['title'] = $this->renderAction($action, true);
+ $item['timestamp'] = strtotime($action->date);
+ $item['author'] = $action->memberCreator->fullName;
+ $item['categories'] = [
+ 'trello',
+ $action->data->board->name,
+ $action->type
+ ];
+ if (isset($action->data->card)) {
+ $item['categories'][] = $action->data->card->name;
+ $item['uri'] = 'https://trello.com/c/'
+ . $action->data->card->shortLink
+ . '#action-'
+ . $action->id;
+ } else {
+ $item['uri'] = 'https://trello.com/b/'
+ . $action->data->board->shortLink;
+ }
+ $item['content'] = $this->renderAction($action, false);
+ if (isset($action->data->attachment->url)) {
+ $item['enclosures'] = [$action->data->attachment->url];
+ }
- public function detectParameters($url) {
- $regex = '/^(https?:\/\/)?trello\.com\/([bc])\/([^\/?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- return array($matches[2] => $matches[3]);
- } else {
- return null;
- }
- }
+ $this->items[] = $item;
+ }
+ }
- public function getURI() {
- switch($this->queriedContext) {
- case 'Board':
- case 'Card':
- return $this->feedURI;
- default: return parent::getURI();
- }
- }
+ public function detectParameters($url)
+ {
+ $regex = '/^(https?:\/\/)?trello\.com\/([bc])\/([^\/?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ return [$matches[2] => $matches[3]];
+ } else {
+ return null;
+ }
+ }
- public function getName() {
- switch($this->queriedContext) {
- case 'Board':
- case 'Card':
- return $this->feedName;
- default: return parent::getName();
- }
- }
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'Board':
+ case 'Card':
+ return $this->feedURI;
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Board':
+ case 'Card':
+ return $this->feedName;
+ default:
+ return parent::getName();
+ }
+ }
}
diff --git a/bridges/TwitScoopBridge.php b/bridges/TwitScoopBridge.php
index 4a85dcfc..8865e06c 100644
--- a/bridges/TwitScoopBridge.php
+++ b/bridges/TwitScoopBridge.php
@@ -1,120 +1,123 @@
<?php
-class TwitScoopBridge extends BridgeAbstract {
- const NAME = 'TwitScoop Bridge';
- const URI = 'https://www.twitscoop.com';
- const DESCRIPTION = 'Returns trending Twitter topics by country';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(
- array(
- 'country' => array(
- 'name' => 'Country',
- 'type' => 'list',
- 'values' => array(
- 'Worldwide' => 'worldwide',
- 'Algeria' => 'algeria',
- 'Argentina' => 'argentina',
- 'Australia' => 'australia',
- 'Austria' => 'austria',
- 'Bahrain' => 'bahrain',
- 'Belarus' => 'belarus',
- 'Belgium' => 'belgium',
- 'Brazil' => 'brazil',
- 'Canada' => 'canada',
- 'Chile' => 'chile',
- 'Colombia' => 'colombia',
- 'Denmark' => 'denmark',
- 'Dominican Republic' => 'dominican-republic',
- 'Ecuador' => 'ecuador',
- 'Egypt' => 'egypt',
- 'France' => 'france',
- 'Germany' => 'germany',
- 'Ghana' => 'ghana',
- 'Greece' => 'greece',
- 'Guatemala' => 'guatemala',
- 'India' => 'india',
- 'Indonesia' => 'indonesia',
- 'Ireland' => 'ireland',
- 'Israel' => 'israel',
- 'Italy' => 'italy',
- 'Japan' => 'japan',
- 'Jordan' => 'jordan',
- 'Kenya' => 'kenya',
- 'Korea' => 'korea',
- 'Kuwait' => 'kuwait',
- 'Latvia' => 'latvia',
- 'Lebanon' => 'lebanon',
- 'Malaysia' => 'malaysia',
- 'Mexico' => 'mexico',
- 'Netherlands' => 'netherlands',
- 'New Zealand' => 'new-zealand',
- 'Nigeria' => 'nigeria',
- 'Norway' => 'norway',
- 'Oman' => 'oman',
- 'Pakistan' => 'pakistan',
- 'Panama' => 'panama',
- 'Peru' => 'peru',
- 'Philippines' => 'philippines',
- 'Poland' => 'poland',
- 'Portugal' => 'portugal',
- 'Puerto Rico' => 'puerto-rico',
- 'Qatar' => 'qatar',
- 'Russia' => 'russia',
- 'Saudi Arabia' => 'saudi-arabia',
- 'Singapore' => 'singapore',
- 'South Africa' => 'south-africa',
- 'Spain' => 'spain',
- 'Sweden' => 'sweden',
- 'Switzerland' => 'switzerland',
- 'Thailand' => 'thailand',
- 'Turkey' => 'turkey',
- 'Ukraine' => 'ukraine',
- 'United Arab Emirates' => 'united-arab-emirates',
- 'United Kingdom' => 'united-kingdom',
- 'United States' => 'united-states',
- 'Venezuela' => 'venezuela',
- 'Vietnam' => 'vietnam',
- )
- ),
- 'limit' => array(
- 'name' => 'Topics',
- 'type' => 'number',
- 'title' => 'Number of trending topics to return. Max 50',
- 'defaultValue' => 20,
- )
- )
- );
-
- const CACHE_TIMEOUT = 900; // 15 mins
-
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI());
-
- $updated = $html->find('time', 0)->datetime;
- $trends = $html->find('div.trends', 0);
-
- $limit = $this->getInput('limit');
-
- if ($limit > 50 || $limit < 1) {
- $limit = 50;
- }
-
- foreach($trends->find('ol.items > li') as $index => $li) {
- $number = $index + 1;
-
- $item = array();
-
- $name = rtrim($li->find('span.trend.name', 0)->plaintext, '&nbsp');
- $tweets = str_replace(' tweets', '', $li->find('span.tweets', 0)->plaintext);
- $tweets = str_replace('<', '', $tweets);
-
- $item['title'] = '#' . $number . ' - ' . $name . ' (' . $tweets . ' tweets)';
- $item['uri'] = 'https://twitter.com/search?q=' . rawurlencode($name);
-
- if ($tweets === '10K') {
- $tweets = 'less than 10K';
- }
-
- $item['content'] = <<<EOD
+
+class TwitScoopBridge extends BridgeAbstract
+{
+ const NAME = 'TwitScoop Bridge';
+ const URI = 'https://www.twitscoop.com';
+ const DESCRIPTION = 'Returns trending Twitter topics by country';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [
+ [
+ 'country' => [
+ 'name' => 'Country',
+ 'type' => 'list',
+ 'values' => [
+ 'Worldwide' => 'worldwide',
+ 'Algeria' => 'algeria',
+ 'Argentina' => 'argentina',
+ 'Australia' => 'australia',
+ 'Austria' => 'austria',
+ 'Bahrain' => 'bahrain',
+ 'Belarus' => 'belarus',
+ 'Belgium' => 'belgium',
+ 'Brazil' => 'brazil',
+ 'Canada' => 'canada',
+ 'Chile' => 'chile',
+ 'Colombia' => 'colombia',
+ 'Denmark' => 'denmark',
+ 'Dominican Republic' => 'dominican-republic',
+ 'Ecuador' => 'ecuador',
+ 'Egypt' => 'egypt',
+ 'France' => 'france',
+ 'Germany' => 'germany',
+ 'Ghana' => 'ghana',
+ 'Greece' => 'greece',
+ 'Guatemala' => 'guatemala',
+ 'India' => 'india',
+ 'Indonesia' => 'indonesia',
+ 'Ireland' => 'ireland',
+ 'Israel' => 'israel',
+ 'Italy' => 'italy',
+ 'Japan' => 'japan',
+ 'Jordan' => 'jordan',
+ 'Kenya' => 'kenya',
+ 'Korea' => 'korea',
+ 'Kuwait' => 'kuwait',
+ 'Latvia' => 'latvia',
+ 'Lebanon' => 'lebanon',
+ 'Malaysia' => 'malaysia',
+ 'Mexico' => 'mexico',
+ 'Netherlands' => 'netherlands',
+ 'New Zealand' => 'new-zealand',
+ 'Nigeria' => 'nigeria',
+ 'Norway' => 'norway',
+ 'Oman' => 'oman',
+ 'Pakistan' => 'pakistan',
+ 'Panama' => 'panama',
+ 'Peru' => 'peru',
+ 'Philippines' => 'philippines',
+ 'Poland' => 'poland',
+ 'Portugal' => 'portugal',
+ 'Puerto Rico' => 'puerto-rico',
+ 'Qatar' => 'qatar',
+ 'Russia' => 'russia',
+ 'Saudi Arabia' => 'saudi-arabia',
+ 'Singapore' => 'singapore',
+ 'South Africa' => 'south-africa',
+ 'Spain' => 'spain',
+ 'Sweden' => 'sweden',
+ 'Switzerland' => 'switzerland',
+ 'Thailand' => 'thailand',
+ 'Turkey' => 'turkey',
+ 'Ukraine' => 'ukraine',
+ 'United Arab Emirates' => 'united-arab-emirates',
+ 'United Kingdom' => 'united-kingdom',
+ 'United States' => 'united-states',
+ 'Venezuela' => 'venezuela',
+ 'Vietnam' => 'vietnam',
+ ]
+ ],
+ 'limit' => [
+ 'name' => 'Topics',
+ 'type' => 'number',
+ 'title' => 'Number of trending topics to return. Max 50',
+ 'defaultValue' => 20,
+ ]
+ ]
+ ];
+
+ const CACHE_TIMEOUT = 900; // 15 mins
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $updated = $html->find('time', 0)->datetime;
+ $trends = $html->find('div.trends', 0);
+
+ $limit = $this->getInput('limit');
+
+ if ($limit > 50 || $limit < 1) {
+ $limit = 50;
+ }
+
+ foreach ($trends->find('ol.items > li') as $index => $li) {
+ $number = $index + 1;
+
+ $item = [];
+
+ $name = rtrim($li->find('span.trend.name', 0)->plaintext, '&nbsp');
+ $tweets = str_replace(' tweets', '', $li->find('span.tweets', 0)->plaintext);
+ $tweets = str_replace('<', '', $tweets);
+
+ $item['title'] = '#' . $number . ' - ' . $name . ' (' . $tweets . ' tweets)';
+ $item['uri'] = 'https://twitter.com/search?q=' . rawurlencode($name);
+
+ if ($tweets === '10K') {
+ $tweets = 'less than 10K';
+ }
+
+ $item['content'] = <<<EOD
<strong>Rank</strong><br>
<p>{$number}</p>
<Strong>Topic</strong><br>
@@ -122,33 +125,34 @@ class TwitScoopBridge extends BridgeAbstract {
<Strong>Tweets</strong><br>
<p>{$tweets}</p>
EOD;
- $item['timestamp'] = $updated;
-
- $this->items[] = $item;
+ $item['timestamp'] = $updated;
- if (count($this->items) >= $limit) {
- break;
- }
- }
+ $this->items[] = $item;
- }
+ if (count($this->items) >= $limit) {
+ break;
+ }
+ }
+ }
- public function getURI() {
- if (!is_null($this->getInput('country'))) {
- return self::URI . '/' . $this->getInput('country');
- }
+ public function getURI()
+ {
+ if (!is_null($this->getInput('country'))) {
+ return self::URI . '/' . $this->getInput('country');
+ }
- return parent::getURI();
- }
+ return parent::getURI();
+ }
- public function getName() {
- if (!is_null($this->getInput('country'))) {
- $parameters = $this->getParameters();
- $values = array_flip($parameters[0]['country']['values']);
+ public function getName()
+ {
+ if (!is_null($this->getInput('country'))) {
+ $parameters = $this->getParameters();
+ $values = array_flip($parameters[0]['country']['values']);
- return $values[$this->getInput('country')] . ' - TwitScoop';
- }
+ return $values[$this->getInput('country')] . ' - TwitScoop';
+ }
- return parent::getName();
- }
+ return parent::getName();
+ }
}
diff --git a/bridges/TwitchBridge.php b/bridges/TwitchBridge.php
index a0e089dd..c26dafc6 100644
--- a/bridges/TwitchBridge.php
+++ b/bridges/TwitchBridge.php
@@ -1,58 +1,60 @@
<?php
-class TwitchBridge extends BridgeAbstract {
- const MAINTAINER = 'Roliga';
- const NAME = 'Twitch Bridge';
- const URI = 'https://twitch.tv/';
- const CACHE_TIMEOUT = 300; // 5min
- const DESCRIPTION = 'Twitch channel videos';
- const PARAMETERS = array( array(
- 'channel' => array(
- 'name' => 'Channel',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'criticalrole',
- 'title' => 'Lowercase channel name as seen in channel URL'
- ),
- 'type' => array(
- 'name' => 'Type',
- 'type' => 'list',
- 'values' => array(
- 'All' => 'all',
- 'Archive' => 'archive',
- 'Highlights' => 'highlight',
- 'Uploads' => 'upload',
- 'Past Premieres' => 'past_premiere',
- 'Premiere Uploads' => 'premiere_upload'
- ),
- 'defaultValue' => 'archive'
- )
- ));
+class TwitchBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'Roliga';
+ const NAME = 'Twitch Bridge';
+ const URI = 'https://twitch.tv/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Twitch channel videos';
+ const PARAMETERS = [ [
+ 'channel' => [
+ 'name' => 'Channel',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'criticalrole',
+ 'title' => 'Lowercase channel name as seen in channel URL'
+ ],
+ 'type' => [
+ 'name' => 'Type',
+ 'type' => 'list',
+ 'values' => [
+ 'All' => 'all',
+ 'Archive' => 'archive',
+ 'Highlights' => 'highlight',
+ 'Uploads' => 'upload',
+ 'Past Premieres' => 'past_premiere',
+ 'Premiere Uploads' => 'premiere_upload'
+ ],
+ 'defaultValue' => 'archive'
+ ]
+ ]];
- /*
- * Official instructions for obtaining your own client ID can be found here:
- * https://dev.twitch.tv/docs/v5/#getting-a-client-id
- */
- const CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
+ /*
+ * Official instructions for obtaining your own client ID can be found here:
+ * https://dev.twitch.tv/docs/v5/#getting-a-client-id
+ */
+ const CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
- const API_ENDPOINT = 'https://gql.twitch.tv/gql';
- const BROADCAST_TYPES = array(
- 'all' => array(
- 'ARCHIVE',
- 'HIGHLIGHT',
- 'UPLOAD',
- 'PAST_PREMIERE',
- 'PREMIERE_UPLOAD'
- ),
- 'archive' => 'ARCHIVE',
- 'highlight' => 'HIGHLIGHT',
- 'upload' => 'UPLOAD',
- 'past_premiere' => 'PAST_PREMIERE',
- 'premiere_upload' => 'PREMIERE_UPLOAD'
- );
+ const API_ENDPOINT = 'https://gql.twitch.tv/gql';
+ const BROADCAST_TYPES = [
+ 'all' => [
+ 'ARCHIVE',
+ 'HIGHLIGHT',
+ 'UPLOAD',
+ 'PAST_PREMIERE',
+ 'PREMIERE_UPLOAD'
+ ],
+ 'archive' => 'ARCHIVE',
+ 'highlight' => 'HIGHLIGHT',
+ 'upload' => 'UPLOAD',
+ 'past_premiere' => 'PAST_PREMIERE',
+ 'premiere_upload' => 'PREMIERE_UPLOAD'
+ ];
- public function collectData(){
- $query = <<<'EOD'
+ public function collectData()
+ {
+ $query = <<<'EOD'
query VODList($channel: String!, $types: [BroadcastType!]) {
user(login: $channel) {
displayName
@@ -89,176 +91,189 @@ query VODList($channel: String!, $types: [BroadcastType!]) {
}
}
EOD;
- $variables = array(
- 'channel' => $this->getInput('channel'),
- 'types' => self::BROADCAST_TYPES[$this->getInput('type')]
- );
- $data = $this->apiRequest($query, $variables);
+ $variables = [
+ 'channel' => $this->getInput('channel'),
+ 'types' => self::BROADCAST_TYPES[$this->getInput('type')]
+ ];
+ $data = $this->apiRequest($query, $variables);
- $user = $data->user;
- foreach($user->videos->edges as $edge) {
- $video = $edge->node;
+ $user = $data->user;
+ foreach ($user->videos->edges as $edge) {
+ $video = $edge->node;
- $url = 'https://www.twitch.tv/videos/' . $video->id;
+ $url = 'https://www.twitch.tv/videos/' . $video->id;
- $item = array(
- 'uri' => $url,
- 'title' => $video->title,
- 'timestamp' => $video->publishedAt,
- 'author' => $user->displayName,
- );
+ $item = [
+ 'uri' => $url,
+ 'title' => $video->title,
+ 'timestamp' => $video->publishedAt,
+ 'author' => $user->displayName,
+ ];
- // Add categories for tags and played game
- $item['categories'] = $video->tags;
- if(!is_null($video->game))
- $item['categories'][] = $video->game->displayName;
- foreach($video->contentTags as $tag)
- if(!$tag->isLanguageTag)
- $item['categories'][] = $tag->localizedName;
+ // Add categories for tags and played game
+ $item['categories'] = $video->tags;
+ if (!is_null($video->game)) {
+ $item['categories'][] = $video->game->displayName;
+ }
+ foreach ($video->contentTags as $tag) {
+ if (!$tag->isLanguageTag) {
+ $item['categories'][] = $tag->localizedName;
+ }
+ }
- // Add enclosures for thumbnails from a few points in the video
- // Thumbnail list has duplicate entries sometimes so remove those
- $item['enclosures'] = array_unique($video->thumbnailURLs);
+ // Add enclosures for thumbnails from a few points in the video
+ // Thumbnail list has duplicate entries sometimes so remove those
+ $item['enclosures'] = array_unique($video->thumbnailURLs);
- /*
- * Content format example:
- *
- * [Preview Image]
- *
- * Some optional video description.
- *
- * Duration: 1:23:45
- * Views: 123
- *
- * Played games:
- * * 00:00:00 Game 1
- * * 00:12:34 Game 2
- *
- */
- $item['content'] = '<p><a href="'
- . $url
- . '"><img src="'
- . $video->previewThumbnailURL
- . '" /></a></p><p>'
- . $video->description // in markdown format
- . '</p><p><b>Duration:</b> '
- . $this->formatTimestampTime($video->lengthSeconds)
- . '<br/><b>Views:</b> '
- . $video->viewCount
- . '</p>';
+ /*
+ * Content format example:
+ *
+ * [Preview Image]
+ *
+ * Some optional video description.
+ *
+ * Duration: 1:23:45
+ * Views: 123
+ *
+ * Played games:
+ * * 00:00:00 Game 1
+ * * 00:12:34 Game 2
+ *
+ */
+ $item['content'] = '<p><a href="'
+ . $url
+ . '"><img src="'
+ . $video->previewThumbnailURL
+ . '" /></a></p><p>'
+ . $video->description // in markdown format
+ . '</p><p><b>Duration:</b> '
+ . $this->formatTimestampTime($video->lengthSeconds)
+ . '<br/><b>Views:</b> '
+ . $video->viewCount
+ . '</p>';
- // Add played games list to content
- $item['content'] .= '<p><b>Played games:</b><ul>';
- if(count($video->moments->edges) > 0) {
- foreach($video->moments->edges as $edge) {
- $moment = $edge->node;
+ // Add played games list to content
+ $item['content'] .= '<p><b>Played games:</b><ul>';
+ if (count($video->moments->edges) > 0) {
+ foreach ($video->moments->edges as $edge) {
+ $moment = $edge->node;
- $item['categories'][] = $moment->description;
- $item['content'] .= '<li><a href="'
- . $url
- . '?t='
- . $this->formatQueryTime($moment->positionMilliseconds / 1000)
- . '">'
- . $this->formatTimestampTime($moment->positionMilliseconds / 1000)
- . '</a> - '
- . $moment->description
- . '</li>';
- }
- } else {
- $item['content'] .= '<li><a href="'
- . $url
- . '">00:00:00</a> - '
- . ($video->game ? $video->game->displayName : 'No Game')
- . '</li>';
- }
- $item['content'] .= '</ul></p>';
+ $item['categories'][] = $moment->description;
+ $item['content'] .= '<li><a href="'
+ . $url
+ . '?t='
+ . $this->formatQueryTime($moment->positionMilliseconds / 1000)
+ . '">'
+ . $this->formatTimestampTime($moment->positionMilliseconds / 1000)
+ . '</a> - '
+ . $moment->description
+ . '</li>';
+ }
+ } else {
+ $item['content'] .= '<li><a href="'
+ . $url
+ . '">00:00:00</a> - '
+ . ($video->game ? $video->game->displayName : 'No Game')
+ . '</li>';
+ }
+ $item['content'] .= '</ul></p>';
- $item['categories'] = array_unique($item['categories']);
+ $item['categories'] = array_unique($item['categories']);
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
- // e.g. 01:53:27
- private function formatTimestampTime($seconds) {
- return sprintf('%02d:%02d:%02d',
- floor($seconds / 3600),
- ($seconds / 60) % 60,
- $seconds % 60);
- }
+ // e.g. 01:53:27
+ private function formatTimestampTime($seconds)
+ {
+ return sprintf(
+ '%02d:%02d:%02d',
+ floor($seconds / 3600),
+ ($seconds / 60) % 60,
+ $seconds % 60
+ );
+ }
- // e.g. 01h53m27s
- private function formatQueryTime($seconds) {
- return sprintf('%02dh%02dm%02ds',
- floor($seconds / 3600),
- ($seconds / 60) % 60,
- $seconds % 60);
- }
+ // e.g. 01h53m27s
+ private function formatQueryTime($seconds)
+ {
+ return sprintf(
+ '%02dh%02dm%02ds',
+ floor($seconds / 3600),
+ ($seconds / 60) % 60,
+ $seconds % 60
+ );
+ }
- // GraphQL: https://graphql.org/
- // Tool for developing/testing queries: https://github.com/skevy/graphiql-app
- private function apiRequest($query, $variables) {
- $request = array(
- 'query' => $query,
- 'variables' => $variables
- );
- $header = array(
- 'Client-ID: ' . self::CLIENT_ID
- );
- $opts = array(
- CURLOPT_CUSTOMREQUEST => 'POST',
- CURLOPT_POSTFIELDS => json_encode($request)
- );
+ // GraphQL: https://graphql.org/
+ // Tool for developing/testing queries: https://github.com/skevy/graphiql-app
+ private function apiRequest($query, $variables)
+ {
+ $request = [
+ 'query' => $query,
+ 'variables' => $variables
+ ];
+ $header = [
+ 'Client-ID: ' . self::CLIENT_ID
+ ];
+ $opts = [
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_POSTFIELDS => json_encode($request)
+ ];
- Debug::log("Sending GraphQL query:\n" . $query);
- Debug::log("Sending GraphQL variables:\n"
- . json_encode($variables, JSON_PRETTY_PRINT));
+ Debug::log("Sending GraphQL query:\n" . $query);
+ Debug::log("Sending GraphQL variables:\n"
+ . json_encode($variables, JSON_PRETTY_PRINT));
- $response = json_decode(getContents(self::API_ENDPOINT, $header, $opts));
+ $response = json_decode(getContents(self::API_ENDPOINT, $header, $opts));
- Debug::log("Got GraphQL response:\n"
- . json_encode($response, JSON_PRETTY_PRINT));
+ Debug::log("Got GraphQL response:\n"
+ . json_encode($response, JSON_PRETTY_PRINT));
- if(isset($response->errors)) {
- $messages = array_column($response->errors, 'message');
- returnServerError('API error(s): ' . implode("\n", $messages));
- }
+ if (isset($response->errors)) {
+ $messages = array_column($response->errors, 'message');
+ returnServerError('API error(s): ' . implode("\n", $messages));
+ }
- return $response->data;
- }
+ return $response->data;
+ }
- public function getName(){
- if(!is_null($this->getInput('channel'))) {
- return $this->getInput('channel') . ' twitch videos';
- }
+ public function getName()
+ {
+ if (!is_null($this->getInput('channel'))) {
+ return $this->getInput('channel') . ' twitch videos';
+ }
- return parent::getName();
- }
+ return parent::getName();
+ }
- public function getURI(){
- if(!is_null($this->getInput('channel'))) {
- return self::URI . $this->getInput('channel');
- }
+ public function getURI()
+ {
+ if (!is_null($this->getInput('channel'))) {
+ return self::URI . $this->getInput('channel');
+ }
- return parent::getURI();
- }
+ return parent::getURI();
+ }
- public function detectParameters($url){
- $params = array();
+ public function detectParameters($url)
+ {
+ $params = [];
- // Matches e.g. https://www.twitch.tv/someuser/videos?filter=archives
- $regex = '/^(https?:\/\/)?
+ // Matches e.g. https://www.twitch.tv/someuser/videos?filter=archives
+ $regex = '/^(https?:\/\/)?
(www\.)?
twitch\.tv\/
([^\/&?\n]+)
\/videos\?.*filter=
(all|archive|highlight|upload)/x';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['channel'] = urldecode($matches[3]);
- $params['type'] = $matches[4];
- return $params;
- }
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['channel'] = urldecode($matches[3]);
+ $params['type'] = $matches[4];
+ return $params;
+ }
- return null;
- }
+ return null;
+ }
}
diff --git a/bridges/TwitterBridge.php b/bridges/TwitterBridge.php
index 71ac52ba..743843da 100644
--- a/bridges/TwitterBridge.php
+++ b/bridges/TwitterBridge.php
@@ -1,37 +1,39 @@
<?php
-class TwitterBridge extends BridgeAbstract {
- const NAME = 'Twitter Bridge';
- const URI = 'https://twitter.com/';
- const API_URI = 'https://api.twitter.com';
- const GUEST_TOKEN_USES = 100;
- const GUEST_TOKEN_EXPIRY = 10800; // 3hrs
- const CACHE_TIMEOUT = 300; // 5min
- const DESCRIPTION = 'returns tweets';
- const MAINTAINER = 'arnd-s';
- const PARAMETERS = array(
- 'global' => array(
- 'nopic' => array(
- 'name' => 'Hide profile pictures',
- 'type' => 'checkbox',
- 'title' => 'Activate to hide profile pictures in content'
- ),
- 'noimg' => array(
- 'name' => 'Hide images in tweets',
- 'type' => 'checkbox',
- 'title' => 'Activate to hide images in tweets'
- ),
- 'noimgscaling' => array(
- 'name' => 'Disable image scaling',
- 'type' => 'checkbox',
- 'title' => 'Activate to disable image scaling in tweets (keeps original image)'
- )
- ),
- 'By keyword or hashtag' => array(
- 'q' => array(
- 'name' => 'Keyword or #hashtag',
- 'required' => true,
- 'exampleValue' => 'rss-bridge OR rssbridge',
- 'title' => <<<EOD
+
+class TwitterBridge extends BridgeAbstract
+{
+ const NAME = 'Twitter Bridge';
+ const URI = 'https://twitter.com/';
+ const API_URI = 'https://api.twitter.com';
+ const GUEST_TOKEN_USES = 100;
+ const GUEST_TOKEN_EXPIRY = 10800; // 3hrs
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'returns tweets';
+ const MAINTAINER = 'arnd-s';
+ const PARAMETERS = [
+ 'global' => [
+ 'nopic' => [
+ 'name' => 'Hide profile pictures',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to hide profile pictures in content'
+ ],
+ 'noimg' => [
+ 'name' => 'Hide images in tweets',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to hide images in tweets'
+ ],
+ 'noimgscaling' => [
+ 'name' => 'Disable image scaling',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to disable image scaling in tweets (keeps original image)'
+ ]
+ ],
+ 'By keyword or hashtag' => [
+ 'q' => [
+ 'name' => 'Keyword or #hashtag',
+ 'required' => true,
+ 'exampleValue' => 'rss-bridge OR rssbridge',
+ 'title' => <<<EOD
* To search for multiple words (must contain all of these words), put a space between them.
Example: `rss-bridge release`.
@@ -56,328 +58,340 @@ Example: `#rss-bridge OR #rssbridge`
Example: `#rss-bridge OR #rssbridge -release`
EOD
- )
- ),
- 'By username' => array(
- 'u' => array(
- 'name' => 'username',
- 'required' => true,
- 'exampleValue' => 'sebsauvage',
- 'title' => 'Insert a user name'
- ),
- 'norep' => array(
- 'name' => 'Without replies',
- 'type' => 'checkbox',
- 'title' => 'Only return initial tweets'
- ),
- 'noretweet' => array(
- 'name' => 'Without retweets',
- 'required' => false,
- 'type' => 'checkbox',
- 'title' => 'Hide retweets'
- ),
- 'nopinned' => array(
- 'name' => 'Without pinned tweet',
- 'required' => false,
- 'type' => 'checkbox',
- 'title' => 'Hide pinned tweet'
- )
- ),
- 'By list' => array(
- 'user' => array(
- 'name' => 'User',
- 'required' => true,
- 'exampleValue' => 'Scobleizer',
- 'title' => 'Insert a user name'
- ),
- 'list' => array(
- 'name' => 'List',
- 'required' => true,
- 'exampleValue' => 'Tech-News',
- 'title' => 'Insert the list name'
- ),
- 'filter' => array(
- 'name' => 'Filter',
- 'exampleValue' => '#rss-bridge',
- 'required' => false,
- 'title' => 'Specify term to search for'
- )
- ),
- 'By list ID' => array(
- 'listid' => array(
- 'name' => 'List ID',
- 'exampleValue' => '31748',
- 'required' => true,
- 'title' => 'Insert the list id'
- ),
- 'filter' => array(
- 'name' => 'Filter',
- 'exampleValue' => '#rss-bridge',
- 'required' => false,
- 'title' => 'Specify term to search for'
- )
- )
- );
-
- private $apiKey = null;
- private $guestToken = null;
- private $authHeader = array();
-
- public function detectParameters($url){
- $params = array();
-
- // By keyword or hashtag (search)
- $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/search.*(\?|&)q=([^\/&?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['q'] = urldecode($matches[4]);
- return $params;
- }
-
- // By hashtag
- $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/hashtag\/([^\/?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['q'] = urldecode($matches[3]);
- return $params;
- }
-
- // By list
- $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)\/lists\/([^\/?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['user'] = urldecode($matches[3]);
- $params['list'] = urldecode($matches[4]);
- return $params;
- }
-
- // By username
- $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)/';
- if(preg_match($regex, $url, $matches) > 0) {
- $params['u'] = urldecode($matches[3]);
- return $params;
- }
-
- return null;
- }
-
- public function getName(){
- switch($this->queriedContext) {
- case 'By keyword or hashtag':
- $specific = 'search ';
- $param = 'q';
- break;
- case 'By username':
- $specific = '@';
- $param = 'u';
- break;
- case 'By list':
- return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user');
- case 'By list ID':
- return 'Twitter List #' . $this->getInput('listid');
- default: return parent::getName();
- }
- return 'Twitter ' . $specific . $this->getInput($param);
- }
-
- public function getURI(){
- switch($this->queriedContext) {
- case 'By keyword or hashtag':
- return self::URI
- . 'search?q='
- . urlencode($this->getInput('q'))
- . '&f=tweets';
- case 'By username':
- return self::URI
- . urlencode($this->getInput('u'));
- // Always return without replies!
- // . ($this->getInput('norep') ? '' : '/with_replies');
- case 'By list':
- return self::URI
- . urlencode($this->getInput('user'))
- . '/lists/'
- . str_replace(' ', '-', strtolower($this->getInput('list')));
- case 'By list ID':
- return self::URI
- . 'i/lists/'
- . urlencode($this->getInput('listid'));
- default: return parent::getURI();
- }
- }
-
- public function collectData(){
- // $data will contain an array of all found tweets (unfiltered)
- $data = null;
- // Contains user data (when in by username context)
- $user = null;
- // Array of all found tweets
- $tweets = array();
-
- // Get authentication information
- $this->getApiKey();
-
- // Try to get all tweets
- switch($this->queriedContext) {
- case 'By username':
- $user = $this->makeApiCall('/1.1/users/show.json', array('screen_name' => $this->getInput('u')));
- if (!$user) {
- returnServerError('Requested username can\'t be found.');
- }
-
- $params = array(
- 'user_id' => $user->id_str,
- 'tweet_mode' => 'extended'
- );
-
- $data = $this->makeApiCall('/1.1/statuses/user_timeline.json', $params);
- break;
-
- case 'By keyword or hashtag':
- $params = array(
- 'q' => urlencode($this->getInput('q')),
- 'tweet_mode' => 'extended',
- 'tweet_search_mode' => 'live',
- );
-
- $data = $this->makeApiCall('/1.1/search/tweets.json', $params)->statuses;
- break;
-
- case 'By list':
- $params = array(
- 'slug' => strtolower($this->getInput('list')),
- 'owner_screen_name' => strtolower($this->getInput('user')),
- 'tweet_mode' => 'extended',
- );
-
- $data = $this->makeApiCall('/1.1/lists/statuses.json', $params);
- break;
-
- case 'By list ID':
- $params = array(
- 'list_id' => $this->getInput('listid'),
- 'tweet_mode' => 'extended',
- );
-
- $data = $this->makeApiCall('/1.1/lists/statuses.json', $params);
- break;
-
- default:
- returnServerError('Invalid query context !');
- }
-
- if(!$data) {
- switch($this->queriedContext) {
- case 'By keyword or hashtag':
- returnServerError('No results for this query.');
- // fall-through
- case 'By username':
- returnServerError('Requested username can\'t be found.');
- // fall-through
- case 'By list':
- returnServerError('Requested username or list can\'t be found');
- }
- }
-
- // Filter out unwanted tweets
- foreach ($data as $tweet) {
- // Filter out retweets to remove possible duplicates of original tweet
- switch($this->queriedContext) {
- case 'By keyword or hashtag':
- if (isset($tweet->retweeted_status) && substr($tweet->full_text, 0, 4) === 'RT @') {
- continue 2;
- }
- break;
- }
- $tweets[] = $tweet;
- }
-
- $hidePictures = $this->getInput('nopic');
-
- $hidePinned = $this->getInput('nopinned');
- if ($hidePinned) {
- $pinnedTweetId = null;
- if ($user && $user->pinned_tweet_ids_str) {
- $pinnedTweetId = $user->pinned_tweet_ids_str;
- }
- }
-
- foreach($tweets as $tweet) {
-
- // Skip own Retweets...
- if (isset($tweet->retweeted_status) && $tweet->retweeted_status->user->id_str === $tweet->user->id_str) {
- continue;
- }
-
- // Skip pinned tweet
- if ($hidePinned && $tweet->id_str === $pinnedTweetId) {
- continue;
- }
-
- switch($this->queriedContext) {
- case 'By username':
- if ($this->getInput('norep') && isset($tweet->in_reply_to_status_id))
- continue 2;
- break;
- }
-
- $item = array();
-
- $realtweet = $tweet;
- if (isset($tweet->retweeted_status)) {
- // Tweet is a Retweet, so set author based on original tweet and set realtweet for reference to the right content
- $realtweet = $tweet->retweeted_status;
- }
-
- $item['username'] = $realtweet->user->screen_name;
- $item['fullname'] = $realtweet->user->name;
- $item['avatar'] = $realtweet->user->profile_image_url_https;
- $item['timestamp'] = $realtweet->created_at;
- $item['id'] = $realtweet->id_str;
- $item['uri'] = self::URI . $item['username'] . '/status/' . $item['id'];
- $item['author'] = (isset($tweet->retweeted_status) ? 'RT: ' : '' )
- . $item['fullname']
- . ' (@'
- . $item['username'] . ')';
-
- // Convert plain text URLs into HTML hyperlinks
- $fulltext = $realtweet->full_text;
- $cleanedTweet = $fulltext;
-
- $foundUrls = false;
-
- if (substr($cleanedTweet, 0, 4) === 'RT @') {
- $cleanedTweet = substr($cleanedTweet, 3);
- }
-
- if (isset($realtweet->entities->media)) {
- foreach($realtweet->entities->media as $media) {
- $cleanedTweet = str_replace($media->url,
- '<a href="' . $media->expanded_url . '">' . $media->display_url . '</a>',
- $cleanedTweet);
- $foundUrls = true;
- }
- }
- if (isset($realtweet->entities->urls)) {
- foreach($realtweet->entities->urls as $url) {
- $cleanedTweet = str_replace($url->url,
- '<a href="' . $url->expanded_url . '">' . $url->display_url . '</a>',
- $cleanedTweet);
- $foundUrls = true;
- }
- }
- if ($foundUrls === false) {
- // fallback to regex'es
- $reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
- if(preg_match($reg_ex, $realtweet->full_text, $url)) {
- $cleanedTweet = preg_replace($reg_ex,
- "<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ",
- $cleanedTweet);
- }
- }
- // generate the title
- $item['title'] = strip_tags($cleanedTweet);
-
- // Add avatar
- $picture_html = '';
- if(!$hidePictures) {
- $picture_html = <<<EOD
+ ]
+ ],
+ 'By username' => [
+ 'u' => [
+ 'name' => 'username',
+ 'required' => true,
+ 'exampleValue' => 'sebsauvage',
+ 'title' => 'Insert a user name'
+ ],
+ 'norep' => [
+ 'name' => 'Without replies',
+ 'type' => 'checkbox',
+ 'title' => 'Only return initial tweets'
+ ],
+ 'noretweet' => [
+ 'name' => 'Without retweets',
+ 'required' => false,
+ 'type' => 'checkbox',
+ 'title' => 'Hide retweets'
+ ],
+ 'nopinned' => [
+ 'name' => 'Without pinned tweet',
+ 'required' => false,
+ 'type' => 'checkbox',
+ 'title' => 'Hide pinned tweet'
+ ]
+ ],
+ 'By list' => [
+ 'user' => [
+ 'name' => 'User',
+ 'required' => true,
+ 'exampleValue' => 'Scobleizer',
+ 'title' => 'Insert a user name'
+ ],
+ 'list' => [
+ 'name' => 'List',
+ 'required' => true,
+ 'exampleValue' => 'Tech-News',
+ 'title' => 'Insert the list name'
+ ],
+ 'filter' => [
+ 'name' => 'Filter',
+ 'exampleValue' => '#rss-bridge',
+ 'required' => false,
+ 'title' => 'Specify term to search for'
+ ]
+ ],
+ 'By list ID' => [
+ 'listid' => [
+ 'name' => 'List ID',
+ 'exampleValue' => '31748',
+ 'required' => true,
+ 'title' => 'Insert the list id'
+ ],
+ 'filter' => [
+ 'name' => 'Filter',
+ 'exampleValue' => '#rss-bridge',
+ 'required' => false,
+ 'title' => 'Specify term to search for'
+ ]
+ ]
+ ];
+
+ private $apiKey = null;
+ private $guestToken = null;
+ private $authHeader = [];
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ // By keyword or hashtag (search)
+ $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/search.*(\?|&)q=([^\/&?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['q'] = urldecode($matches[4]);
+ return $params;
+ }
+
+ // By hashtag
+ $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/hashtag\/([^\/?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['q'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ // By list
+ $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)\/lists\/([^\/?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['user'] = urldecode($matches[3]);
+ $params['list'] = urldecode($matches[4]);
+ return $params;
+ }
+
+ // By username
+ $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)/';
+ if (preg_match($regex, $url, $matches) > 0) {
+ $params['u'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ return null;
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'By keyword or hashtag':
+ $specific = 'search ';
+ $param = 'q';
+ break;
+ case 'By username':
+ $specific = '@';
+ $param = 'u';
+ break;
+ case 'By list':
+ return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user');
+ case 'By list ID':
+ return 'Twitter List #' . $this->getInput('listid');
+ default:
+ return parent::getName();
+ }
+ return 'Twitter ' . $specific . $this->getInput($param);
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case 'By keyword or hashtag':
+ return self::URI
+ . 'search?q='
+ . urlencode($this->getInput('q'))
+ . '&f=tweets';
+ case 'By username':
+ return self::URI
+ . urlencode($this->getInput('u'));
+ // Always return without replies!
+ // . ($this->getInput('norep') ? '' : '/with_replies');
+ case 'By list':
+ return self::URI
+ . urlencode($this->getInput('user'))
+ . '/lists/'
+ . str_replace(' ', '-', strtolower($this->getInput('list')));
+ case 'By list ID':
+ return self::URI
+ . 'i/lists/'
+ . urlencode($this->getInput('listid'));
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function collectData()
+ {
+ // $data will contain an array of all found tweets (unfiltered)
+ $data = null;
+ // Contains user data (when in by username context)
+ $user = null;
+ // Array of all found tweets
+ $tweets = [];
+
+ // Get authentication information
+ $this->getApiKey();
+
+ // Try to get all tweets
+ switch ($this->queriedContext) {
+ case 'By username':
+ $user = $this->makeApiCall('/1.1/users/show.json', ['screen_name' => $this->getInput('u')]);
+ if (!$user) {
+ returnServerError('Requested username can\'t be found.');
+ }
+
+ $params = [
+ 'user_id' => $user->id_str,
+ 'tweet_mode' => 'extended'
+ ];
+
+ $data = $this->makeApiCall('/1.1/statuses/user_timeline.json', $params);
+ break;
+
+ case 'By keyword or hashtag':
+ $params = [
+ 'q' => urlencode($this->getInput('q')),
+ 'tweet_mode' => 'extended',
+ 'tweet_search_mode' => 'live',
+ ];
+
+ $data = $this->makeApiCall('/1.1/search/tweets.json', $params)->statuses;
+ break;
+
+ case 'By list':
+ $params = [
+ 'slug' => strtolower($this->getInput('list')),
+ 'owner_screen_name' => strtolower($this->getInput('user')),
+ 'tweet_mode' => 'extended',
+ ];
+
+ $data = $this->makeApiCall('/1.1/lists/statuses.json', $params);
+ break;
+
+ case 'By list ID':
+ $params = [
+ 'list_id' => $this->getInput('listid'),
+ 'tweet_mode' => 'extended',
+ ];
+
+ $data = $this->makeApiCall('/1.1/lists/statuses.json', $params);
+ break;
+
+ default:
+ returnServerError('Invalid query context !');
+ }
+
+ if (!$data) {
+ switch ($this->queriedContext) {
+ case 'By keyword or hashtag':
+ returnServerError('No results for this query.');
+ // fall-through
+ case 'By username':
+ returnServerError('Requested username can\'t be found.');
+ // fall-through
+ case 'By list':
+ returnServerError('Requested username or list can\'t be found');
+ }
+ }
+
+ // Filter out unwanted tweets
+ foreach ($data as $tweet) {
+ // Filter out retweets to remove possible duplicates of original tweet
+ switch ($this->queriedContext) {
+ case 'By keyword or hashtag':
+ if (isset($tweet->retweeted_status) && substr($tweet->full_text, 0, 4) === 'RT @') {
+ continue 2;
+ }
+ break;
+ }
+ $tweets[] = $tweet;
+ }
+
+ $hidePictures = $this->getInput('nopic');
+
+ $hidePinned = $this->getInput('nopinned');
+ if ($hidePinned) {
+ $pinnedTweetId = null;
+ if ($user && $user->pinned_tweet_ids_str) {
+ $pinnedTweetId = $user->pinned_tweet_ids_str;
+ }
+ }
+
+ foreach ($tweets as $tweet) {
+ // Skip own Retweets...
+ if (isset($tweet->retweeted_status) && $tweet->retweeted_status->user->id_str === $tweet->user->id_str) {
+ continue;
+ }
+
+ // Skip pinned tweet
+ if ($hidePinned && $tweet->id_str === $pinnedTweetId) {
+ continue;
+ }
+
+ switch ($this->queriedContext) {
+ case 'By username':
+ if ($this->getInput('norep') && isset($tweet->in_reply_to_status_id)) {
+ continue 2;
+ }
+ break;
+ }
+
+ $item = [];
+
+ $realtweet = $tweet;
+ if (isset($tweet->retweeted_status)) {
+ // Tweet is a Retweet, so set author based on original tweet and set realtweet for reference to the right content
+ $realtweet = $tweet->retweeted_status;
+ }
+
+ $item['username'] = $realtweet->user->screen_name;
+ $item['fullname'] = $realtweet->user->name;
+ $item['avatar'] = $realtweet->user->profile_image_url_https;
+ $item['timestamp'] = $realtweet->created_at;
+ $item['id'] = $realtweet->id_str;
+ $item['uri'] = self::URI . $item['username'] . '/status/' . $item['id'];
+ $item['author'] = (isset($tweet->retweeted_status) ? 'RT: ' : '' )
+ . $item['fullname']
+ . ' (@'
+ . $item['username'] . ')';
+
+ // Convert plain text URLs into HTML hyperlinks
+ $fulltext = $realtweet->full_text;
+ $cleanedTweet = $fulltext;
+
+ $foundUrls = false;
+
+ if (substr($cleanedTweet, 0, 4) === 'RT @') {
+ $cleanedTweet = substr($cleanedTweet, 3);
+ }
+
+ if (isset($realtweet->entities->media)) {
+ foreach ($realtweet->entities->media as $media) {
+ $cleanedTweet = str_replace(
+ $media->url,
+ '<a href="' . $media->expanded_url . '">' . $media->display_url . '</a>',
+ $cleanedTweet
+ );
+ $foundUrls = true;
+ }
+ }
+ if (isset($realtweet->entities->urls)) {
+ foreach ($realtweet->entities->urls as $url) {
+ $cleanedTweet = str_replace(
+ $url->url,
+ '<a href="' . $url->expanded_url . '">' . $url->display_url . '</a>',
+ $cleanedTweet
+ );
+ $foundUrls = true;
+ }
+ }
+ if ($foundUrls === false) {
+ // fallback to regex'es
+ $reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
+ if (preg_match($reg_ex, $realtweet->full_text, $url)) {
+ $cleanedTweet = preg_replace(
+ $reg_ex,
+ "<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ",
+ $cleanedTweet
+ );
+ }
+ }
+ // generate the title
+ $item['title'] = strip_tags($cleanedTweet);
+
+ // Add avatar
+ $picture_html = '';
+ if (!$hidePictures) {
+ $picture_html = <<<EOD
<a href="https://twitter.com/{$item['username']}">
<img
style="align:top; width:75px; border:1px solid black;"
@@ -386,20 +400,20 @@ EOD
title="{$item['fullname']}" />
</a>
EOD;
- }
-
- // Get images
- $media_html = '';
- if(isset($realtweet->extended_entities->media) && !$this->getInput('noimg')) {
- foreach($realtweet->extended_entities->media as $media) {
- switch($media->type) {
- case 'photo':
- $image = $media->media_url_https . '?name=orig';
- $display_image = $media->media_url_https;
- // add enclosures
- $item['enclosures'][] = $image;
-
- $media_html .= <<<EOD
+ }
+
+ // Get images
+ $media_html = '';
+ if (isset($realtweet->extended_entities->media) && !$this->getInput('noimg')) {
+ foreach ($realtweet->extended_entities->media as $media) {
+ switch ($media->type) {
+ case 'photo':
+ $image = $media->media_url_https . '?name=orig';
+ $display_image = $media->media_url_https;
+ // add enclosures
+ $item['enclosures'][] = $image;
+
+ $media_html .= <<<EOD
<a href="{$image}">
<img
style="align:top; max-width:558px; border:1px solid black;"
@@ -407,61 +421,61 @@ EOD;
src="{$display_image}" />
</a>
EOD;
- break;
- case 'video':
- case 'animated_gif':
- if(isset($media->video_info)) {
- $link = $media->expanded_url;
- $poster = $media->media_url_https;
- $video = null;
- $maxBitrate = -1;
- foreach($media->video_info->variants as $variant) {
- $bitRate = isset($variant->bitrate) ? $variant->bitrate : -100;
- if ($bitRate > $maxBitrate) {
- $maxBitrate = $bitRate;
- $video = $variant->url;
- }
- }
- if(!is_null($video)) {
- // add enclosures
- $item['enclosures'][] = $video;
- $item['enclosures'][] = $poster;
-
- $media_html .= <<<EOD
+ break;
+ case 'video':
+ case 'animated_gif':
+ if (isset($media->video_info)) {
+ $link = $media->expanded_url;
+ $poster = $media->media_url_https;
+ $video = null;
+ $maxBitrate = -1;
+ foreach ($media->video_info->variants as $variant) {
+ $bitRate = isset($variant->bitrate) ? $variant->bitrate : -100;
+ if ($bitRate > $maxBitrate) {
+ $maxBitrate = $bitRate;
+ $video = $variant->url;
+ }
+ }
+ if (!is_null($video)) {
+ // add enclosures
+ $item['enclosures'][] = $video;
+ $item['enclosures'][] = $poster;
+
+ $media_html .= <<<EOD
<a href="{$link}">Video</a>
<video
style="align:top; max-width:558px; border:1px solid black;"
referrerpolicy="no-referrer"
src="{$video}" poster="{$poster}" />
EOD;
- }
- }
- break;
- default:
- Debug::log('Missing support for media type: ' . $media->type);
- }
- }
- }
-
- switch($this->queriedContext) {
- case 'By list':
- case 'By list ID':
- // Check if filter applies to list (using raw content)
- if($this->getInput('filter')) {
- if(stripos($cleanedTweet, $this->getInput('filter')) === false) {
- continue 2; // switch + for-loop!
- }
- }
- break;
- case 'By username':
- if ($this->getInput('noretweet') && strtolower($item['username']) != strtolower($this->getInput('u'))) {
- continue 2; // switch + for-loop!
- }
- break;
- default:
- }
-
- $item['content'] = <<<EOD
+ }
+ }
+ break;
+ default:
+ Debug::log('Missing support for media type: ' . $media->type);
+ }
+ }
+ }
+
+ switch ($this->queriedContext) {
+ case 'By list':
+ case 'By list ID':
+ // Check if filter applies to list (using raw content)
+ if ($this->getInput('filter')) {
+ if (stripos($cleanedTweet, $this->getInput('filter')) === false) {
+ continue 2; // switch + for-loop!
+ }
+ }
+ break;
+ case 'By username':
+ if ($this->getInput('noretweet') && strtolower($item['username']) != strtolower($this->getInput('u'))) {
+ continue 2; // switch + for-loop!
+ }
+ break;
+ default:
+ }
+
+ $item['content'] = <<<EOD
<div style="display: inline-block; vertical-align: top;">
{$picture_html}
</div>
@@ -473,173 +487,178 @@ EOD;
</div>
EOD;
- // put out
- $this->items[] = $item;
- }
-
- usort($this->items, array('TwitterBridge', 'compareTweetId'));
- }
-
- private static function compareTweetId($tweet1, $tweet2) {
- return (intval($tweet1['id']) < intval($tweet2['id']) ? 1 : -1);
- }
-
- //The aim of this function is to get an API key and a guest token
- //This function takes 2 requests, and therefore is cached
- private function getApiKey($forceNew = 0) {
-
- $cacheFac = new CacheFactory();
-
- $r_cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $r_cache->setScope(get_called_class());
- $r_cache->setKey(array('refresh'));
- $data = $r_cache->loadData();
-
- $refresh = null;
- if($data === null) {
- $refresh = time();
- $r_cache->saveData($refresh);
- } else {
- $refresh = $data;
- }
-
- $cacheFac = new CacheFactory();
-
- $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $cache->setScope(get_called_class());
- $cache->setKey(array('api_key'));
- $data = $cache->loadData();
-
- $apiKey = null;
- if($forceNew || $data === null || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY) {
- $twitterPage = getContents('https://twitter.com');
-
- $jsLink = false;
- $jsMainRegexArray = array(
- '/(https:\/\/abs\.twimg\.com\/responsive-web\/web\/main\.[^\.]+\.js)/m',
- '/(https:\/\/abs\.twimg\.com\/responsive-web\/web_legacy\/main\.[^\.]+\.js)/m',
- '/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web\/main\.[^\.]+\.js)/m',
- '/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web-legacy\/main\.[^\.]+\.js)/m',
- );
- foreach ($jsMainRegexArray as $jsMainRegex) {
- if (preg_match_all($jsMainRegex, $twitterPage, $jsMainMatches, PREG_SET_ORDER, 0)) {
- $jsLink = $jsMainMatches[0][0];
- break;
- }
- }
- if (!$jsLink) {
- returnServerError('Could not locate main.js link');
- }
-
- $jsContent = getContents($jsLink);
- $apiKeyRegex = '/([a-zA-Z0-9]{59}%[a-zA-Z0-9]{44})/m';
- preg_match_all($apiKeyRegex, $jsContent, $apiKeyMatches, PREG_SET_ORDER, 0);
- $apiKey = $apiKeyMatches[0][0];
- $cache->saveData($apiKey);
- } else {
- $apiKey = $data;
- }
-
- $cacheFac2 = new CacheFactory();
-
- $gt_cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $gt_cache->setScope(get_called_class());
- $gt_cache->setKey(array('guest_token'));
- $guestTokenUses = $gt_cache->loadData();
-
- $guestToken = null;
- if($forceNew || $guestTokenUses === null || !is_array($guestTokenUses) || count($guestTokenUses) != 2
- || $guestTokenUses[0] <= 0 || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY) {
- $guestToken = $this->getGuestToken($apiKey);
- if ($guestToken === null) {
- if($guestTokenUses === null) {
- returnServerError('Could not parse guest token');
- } else {
- $guestToken = $guestTokenUses[1];
- }
- } else {
- $gt_cache->saveData(array(self::GUEST_TOKEN_USES, $guestToken));
- $r_cache->saveData(time());
- }
- } else {
- $guestTokenUses[0] -= 1;
- $gt_cache->saveData($guestTokenUses);
- $guestToken = $guestTokenUses[1];
- }
-
- $this->apiKey = $apiKey;
- $this->guestToken = $guestToken;
- $this->authHeaders = array(
- 'authorization: Bearer ' . $apiKey,
- 'x-guest-token: ' . $guestToken,
- );
-
- return array($apiKey, $guestToken);
- }
-
- // Get a guest token. This is different to an API key,
- // and it seems to change more regularly than the API key.
- private function getGuestToken($apiKey) {
- $headers = array(
- 'authorization: Bearer ' . $apiKey,
- );
- $opts = array(
- CURLOPT_POST => 1,
- );
-
- try {
- $pageContent = getContents('https://api.twitter.com/1.1/guest/activate.json', $headers, $opts, true);
- $guestToken = json_decode($pageContent['content'])->guest_token;
- } catch (Exception $e) {
- $guestToken = null;
- }
- return $guestToken;
- }
-
- /**
- * Tries to make an API call to twitter.
- * @param $api string API entry point
- * @param $params array additional URI parmaeters
- * @return object json data
- */
- private function makeApiCall($api, $params) {
- $uri = self::API_URI . $api . '?' . http_build_query($params);
-
- $retries = 1;
- $retry = 0;
- do {
- $retry = 0;
-
- try {
- $result = getContents($uri, $this->authHeaders, array(), true);
- } catch (HttpException $e) {
- switch ($e->getCode()) {
- case 401:
- // fall-through
- case 403:
- if ($retries) {
- $retries--;
- $retry = 1;
- $this->getApiKey(1);
- continue 2;
- }
- // fall-through
- default:
- $code = $e->getCode();
- $data = $e->getMessage();
- returnServerError(<<<EOD
+ // put out
+ $this->items[] = $item;
+ }
+
+ usort($this->items, ['TwitterBridge', 'compareTweetId']);
+ }
+
+ private static function compareTweetId($tweet1, $tweet2)
+ {
+ return (intval($tweet1['id']) < intval($tweet2['id']) ? 1 : -1);
+ }
+
+ //The aim of this function is to get an API key and a guest token
+ //This function takes 2 requests, and therefore is cached
+ private function getApiKey($forceNew = 0)
+ {
+ $cacheFac = new CacheFactory();
+
+ $r_cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $r_cache->setScope(get_called_class());
+ $r_cache->setKey(['refresh']);
+ $data = $r_cache->loadData();
+
+ $refresh = null;
+ if ($data === null) {
+ $refresh = time();
+ $r_cache->saveData($refresh);
+ } else {
+ $refresh = $data;
+ }
+
+ $cacheFac = new CacheFactory();
+
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope(get_called_class());
+ $cache->setKey(['api_key']);
+ $data = $cache->loadData();
+
+ $apiKey = null;
+ if ($forceNew || $data === null || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY) {
+ $twitterPage = getContents('https://twitter.com');
+
+ $jsLink = false;
+ $jsMainRegexArray = [
+ '/(https:\/\/abs\.twimg\.com\/responsive-web\/web\/main\.[^\.]+\.js)/m',
+ '/(https:\/\/abs\.twimg\.com\/responsive-web\/web_legacy\/main\.[^\.]+\.js)/m',
+ '/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web\/main\.[^\.]+\.js)/m',
+ '/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web-legacy\/main\.[^\.]+\.js)/m',
+ ];
+ foreach ($jsMainRegexArray as $jsMainRegex) {
+ if (preg_match_all($jsMainRegex, $twitterPage, $jsMainMatches, PREG_SET_ORDER, 0)) {
+ $jsLink = $jsMainMatches[0][0];
+ break;
+ }
+ }
+ if (!$jsLink) {
+ returnServerError('Could not locate main.js link');
+ }
+
+ $jsContent = getContents($jsLink);
+ $apiKeyRegex = '/([a-zA-Z0-9]{59}%[a-zA-Z0-9]{44})/m';
+ preg_match_all($apiKeyRegex, $jsContent, $apiKeyMatches, PREG_SET_ORDER, 0);
+ $apiKey = $apiKeyMatches[0][0];
+ $cache->saveData($apiKey);
+ } else {
+ $apiKey = $data;
+ }
+
+ $cacheFac2 = new CacheFactory();
+
+ $gt_cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $gt_cache->setScope(get_called_class());
+ $gt_cache->setKey(['guest_token']);
+ $guestTokenUses = $gt_cache->loadData();
+
+ $guestToken = null;
+ if (
+ $forceNew || $guestTokenUses === null || !is_array($guestTokenUses) || count($guestTokenUses) != 2
+ || $guestTokenUses[0] <= 0 || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY
+ ) {
+ $guestToken = $this->getGuestToken($apiKey);
+ if ($guestToken === null) {
+ if ($guestTokenUses === null) {
+ returnServerError('Could not parse guest token');
+ } else {
+ $guestToken = $guestTokenUses[1];
+ }
+ } else {
+ $gt_cache->saveData([self::GUEST_TOKEN_USES, $guestToken]);
+ $r_cache->saveData(time());
+ }
+ } else {
+ $guestTokenUses[0] -= 1;
+ $gt_cache->saveData($guestTokenUses);
+ $guestToken = $guestTokenUses[1];
+ }
+
+ $this->apiKey = $apiKey;
+ $this->guestToken = $guestToken;
+ $this->authHeaders = [
+ 'authorization: Bearer ' . $apiKey,
+ 'x-guest-token: ' . $guestToken,
+ ];
+
+ return [$apiKey, $guestToken];
+ }
+
+ // Get a guest token. This is different to an API key,
+ // and it seems to change more regularly than the API key.
+ private function getGuestToken($apiKey)
+ {
+ $headers = [
+ 'authorization: Bearer ' . $apiKey,
+ ];
+ $opts = [
+ CURLOPT_POST => 1,
+ ];
+
+ try {
+ $pageContent = getContents('https://api.twitter.com/1.1/guest/activate.json', $headers, $opts, true);
+ $guestToken = json_decode($pageContent['content'])->guest_token;
+ } catch (Exception $e) {
+ $guestToken = null;
+ }
+ return $guestToken;
+ }
+
+ /**
+ * Tries to make an API call to twitter.
+ * @param $api string API entry point
+ * @param $params array additional URI parmaeters
+ * @return object json data
+ */
+ private function makeApiCall($api, $params)
+ {
+ $uri = self::API_URI . $api . '?' . http_build_query($params);
+
+ $retries = 1;
+ $retry = 0;
+ do {
+ $retry = 0;
+
+ try {
+ $result = getContents($uri, $this->authHeaders, [], true);
+ } catch (HttpException $e) {
+ switch ($e->getCode()) {
+ case 401:
+ // fall-through
+ case 403:
+ if ($retries) {
+ $retries--;
+ $retry = 1;
+ $this->getApiKey(1);
+ continue 2;
+ }
+ // fall-through
+ default:
+ $code = $e->getCode();
+ $data = $e->getMessage();
+ returnServerError(<<<EOD
Failed to make api call: $api
HTTP Status: $code
Errormessage: $data
EOD
- );
- break;
- }
- }
- } while ($retry);
+ );
+ break;
+ }
+ }
+ } while ($retry);
- $data = json_decode($result['content']);
+ $data = json_decode($result['content']);
- return $data;
- }
+ return $data;
+ }
}
diff --git a/bridges/TwitterEngineeringBridge.php b/bridges/TwitterEngineeringBridge.php
index f11caaa1..7c450013 100644
--- a/bridges/TwitterEngineeringBridge.php
+++ b/bridges/TwitterEngineeringBridge.php
@@ -1,62 +1,67 @@
<?php
-class TwitterEngineeringBridge extends FeedExpander {
-
- const MAINTAINER = 'corenting';
- const NAME = 'Twitter Engineering Blog';
- const URI = 'https://blog.twitter.com/engineering/';
- const DESCRIPTION = 'Returns the newest articles.';
- const CACHE_TIMEOUT = 21600; // 6h
-
- protected function parseItem($item){
- $item = parent::parseItem($item);
-
- $article_html = getSimpleHTMLDOMCached($item['uri']);
- if(!$article_html) {
- $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>';
- return $item;
- }
- $article_html = defaultLinkTo($article_html, $this->getURI());
-
- $article_body = $article_html->find('div.column.column-6', 0);
-
- // Remove elements that are not part of article content
- $unwanted_selector = 'div.bl02-blog-post-text-masthead, div.tweet-error-text, div.bl13-tweet-template';
- foreach($article_body->find($unwanted_selector) as $found) {
- $found->outertext = '';
- }
-
- // Set src for images
- foreach($article_body->find('img') as $found) {
- $found->setAttribute('src', $found->getAttribute('data-src'));
- }
-
- $item['content'] = $article_body;
- $item['timestamp'] = strtotime($article_html->find('span.b02-blog-post-no-masthead__date', 0)->innertext);
- $item['categories'] = self::getCategoriesFromTags($article_html);
-
- return $item;
- }
-
- private function getCategoriesFromTags($article_html){
- $tags_list_items = array($article_html->find('.post__tags > ul > li'));
- $categories = array();
-
- foreach($tags_list_items as $tag_list_item) {
- foreach($tag_list_item as $tag) {
- $categories[] = trim($tag->plaintext);
- }
- }
-
- return $categories;
- }
-
- public function collectData(){
- $feed = static::URI . 'en_us/blog.rss';
- $this->collectExpandableDatas($feed);
- }
-
- public function getName(){
- // Else the original feed returns "English (US)" as the title
- return 'Twitter Engineering Blog';
- }
+
+class TwitterEngineeringBridge extends FeedExpander
+{
+ const MAINTAINER = 'corenting';
+ const NAME = 'Twitter Engineering Blog';
+ const URI = 'https://blog.twitter.com/engineering/';
+ const DESCRIPTION = 'Returns the newest articles.';
+ const CACHE_TIMEOUT = 21600; // 6h
+
+ protected function parseItem($item)
+ {
+ $item = parent::parseItem($item);
+
+ $article_html = getSimpleHTMLDOMCached($item['uri']);
+ if (!$article_html) {
+ $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>';
+ return $item;
+ }
+ $article_html = defaultLinkTo($article_html, $this->getURI());
+
+ $article_body = $article_html->find('div.column.column-6', 0);
+
+ // Remove elements that are not part of article content
+ $unwanted_selector = 'div.bl02-blog-post-text-masthead, div.tweet-error-text, div.bl13-tweet-template';
+ foreach ($article_body->find($unwanted_selector) as $found) {
+ $found->outertext = '';
+ }
+
+ // Set src for images
+ foreach ($article_body->find('img') as $found) {
+ $found->setAttribute('src', $found->getAttribute('data-src'));
+ }
+
+ $item['content'] = $article_body;
+ $item['timestamp'] = strtotime($article_html->find('span.b02-blog-post-no-masthead__date', 0)->innertext);
+ $item['categories'] = self::getCategoriesFromTags($article_html);
+
+ return $item;
+ }
+
+ private function getCategoriesFromTags($article_html)
+ {
+ $tags_list_items = [$article_html->find('.post__tags > ul > li')];
+ $categories = [];
+
+ foreach ($tags_list_items as $tag_list_item) {
+ foreach ($tag_list_item as $tag) {
+ $categories[] = trim($tag->plaintext);
+ }
+ }
+
+ return $categories;
+ }
+
+ public function collectData()
+ {
+ $feed = static::URI . 'en_us/blog.rss';
+ $this->collectExpandableDatas($feed);
+ }
+
+ public function getName()
+ {
+ // Else the original feed returns "English (US)" as the title
+ return 'Twitter Engineering Blog';
+ }
}
diff --git a/bridges/TwitterV2Bridge.php b/bridges/TwitterV2Bridge.php
index 1636139d..cad88598 100644
--- a/bridges/TwitterV2Bridge.php
+++ b/bridges/TwitterV2Bridge.php
@@ -1,93 +1,95 @@
<?php
+
/**
* TwitterV2Bridge leverages Twitter API v2, and requires
* a unique API Bearer Token, which requires creation of
* a Twitter Dev account. Link to instructions in DESCRIPTION.
*/
-class TwitterV2Bridge extends BridgeAbstract {
- const NAME = 'Twitter V2 Bridge';
- const URI = 'https://twitter.com/';
- const API_URI = 'https://api.twitter.com/2';
- const DESCRIPTION = 'Returns tweets (using Twitter API v2). See the
+class TwitterV2Bridge extends BridgeAbstract
+{
+ const NAME = 'Twitter V2 Bridge';
+ const URI = 'https://twitter.com/';
+ const API_URI = 'https://api.twitter.com/2';
+ const DESCRIPTION = 'Returns tweets (using Twitter API v2). See the
<a href="https://rss-bridge.github.io/rss-bridge/Bridge_Specific/TwitterV2.html">
Configuration Instructions</a>.';
- const MAINTAINER = 'quickwick';
- const CONFIGURATION = array(
- 'twitterv2apitoken' => array(
- 'required' => true,
- )
- );
- const PARAMETERS = array(
- 'global' => array(
- 'filter' => array(
- 'name' => 'Filter',
- 'exampleValue' => 'rss-bridge',
- 'required' => false,
- 'title' => 'Specify a single term to search for'
- ),
- 'norep' => array(
- 'name' => 'Without replies',
- 'type' => 'checkbox',
- 'title' => 'Activate to exclude reply tweets'
- ),
- 'noretweet' => array(
- 'name' => 'Without retweets',
- 'required' => false,
- 'type' => 'checkbox',
- 'title' => 'Activate to exclude retweets'
- ),
- 'nopinned' => array(
- 'name' => 'Without pinned tweet',
- 'required' => false,
- 'type' => 'checkbox',
- 'title' => 'Activate to exclude pinned tweets'
- ),
- 'maxresults' => array(
- 'name' => 'Maximum results',
- 'required' => false,
- 'exampleValue' => '20',
- 'title' => 'Maximum number of tweets to retrieve (limit is 100)'
- ),
- 'imgonly' => array(
- 'name' => 'Only media tweets',
- 'type' => 'checkbox',
- 'title' => 'Activate to show only tweets with media (photo/video)'
- ),
- 'nopic' => array(
- 'name' => 'Hide profile pictures',
- 'type' => 'checkbox',
- 'title' => 'Activate to hide profile pictures in content'
- ),
- 'noimg' => array(
- 'name' => 'Hide images in tweets',
- 'type' => 'checkbox',
- 'title' => 'Activate to hide images in tweets'
- ),
- 'noimgscaling' => array(
- 'name' => 'Disable image scaling',
- 'type' => 'checkbox',
- 'title' => 'Activate to display original sized images (no thumbnails)'
- ),
- 'idastitle' => array(
- 'name' => 'Use tweet id as title',
- 'type' => 'checkbox',
- 'title' => 'Activate to use tweet id as title (instead of tweet text)'
- )
- ),
- 'By username' => array(
- 'u' => array(
- 'name' => 'username',
- 'required' => true,
- 'exampleValue' => 'sebsauvage',
- 'title' => 'Insert a user name'
- )
- ),
- 'By keyword or hashtag' => array(
- 'query' => array(
- 'name' => 'Keyword or #hashtag',
- 'required' => true,
- 'exampleValue' => 'rss-bridge OR #rss-bridge',
- 'title' => <<<EOD
+ const MAINTAINER = 'quickwick';
+ const CONFIGURATION = [
+ 'twitterv2apitoken' => [
+ 'required' => true,
+ ]
+ ];
+ const PARAMETERS = [
+ 'global' => [
+ 'filter' => [
+ 'name' => 'Filter',
+ 'exampleValue' => 'rss-bridge',
+ 'required' => false,
+ 'title' => 'Specify a single term to search for'
+ ],
+ 'norep' => [
+ 'name' => 'Without replies',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to exclude reply tweets'
+ ],
+ 'noretweet' => [
+ 'name' => 'Without retweets',
+ 'required' => false,
+ 'type' => 'checkbox',
+ 'title' => 'Activate to exclude retweets'
+ ],
+ 'nopinned' => [
+ 'name' => 'Without pinned tweet',
+ 'required' => false,
+ 'type' => 'checkbox',
+ 'title' => 'Activate to exclude pinned tweets'
+ ],
+ 'maxresults' => [
+ 'name' => 'Maximum results',
+ 'required' => false,
+ 'exampleValue' => '20',
+ 'title' => 'Maximum number of tweets to retrieve (limit is 100)'
+ ],
+ 'imgonly' => [
+ 'name' => 'Only media tweets',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to show only tweets with media (photo/video)'
+ ],
+ 'nopic' => [
+ 'name' => 'Hide profile pictures',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to hide profile pictures in content'
+ ],
+ 'noimg' => [
+ 'name' => 'Hide images in tweets',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to hide images in tweets'
+ ],
+ 'noimgscaling' => [
+ 'name' => 'Disable image scaling',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to display original sized images (no thumbnails)'
+ ],
+ 'idastitle' => [
+ 'name' => 'Use tweet id as title',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to use tweet id as title (instead of tweet text)'
+ ]
+ ],
+ 'By username' => [
+ 'u' => [
+ 'name' => 'username',
+ 'required' => true,
+ 'exampleValue' => 'sebsauvage',
+ 'title' => 'Insert a user name'
+ ]
+ ],
+ 'By keyword or hashtag' => [
+ 'query' => [
+ 'name' => 'Keyword or #hashtag',
+ 'required' => true,
+ 'exampleValue' => 'rss-bridge OR #rss-bridge',
+ 'title' => <<<EOD
* To search for multiple words (must contain all of these words), put a space between them.
Example: `rss-bridge release`.
@@ -112,353 +114,362 @@ Example: `#rss-bridge OR #rssbridge`
Example: `#rss-bridge OR #rssbridge -release`
EOD
- )
- ),
- 'By list ID' => array(
- 'listid' => array(
- 'name' => 'List ID',
- 'exampleValue' => '31748',
- 'required' => true,
- 'title' => 'Enter a list id'
- )
- )
- );
-
- // $Item variable needs to be accessible from multiple functions without passing
- private $item = array();
-
- public function getName() {
- switch($this->queriedContext) {
- case 'By keyword or hashtag':
- $specific = 'search ';
- $param = 'query';
- break;
- case 'By username':
- $specific = '@';
- $param = 'u';
- break;
- case 'By list ID':
- return 'Twitter List #' . $this->getInput('listid');
- default:
- return parent::getName();
- }
- return 'Twitter ' . $specific . $this->getInput($param);
- }
-
- public function collectData() {
- // $data will contain an array of all found tweets
- $data = null;
- // Contains user data (when in by username context)
- $user = null;
- // Array of all found tweets
- $tweets = array();
-
- $hideProfilePic = $this->getInput('nopic');
- $hideImages = $this->getInput('noimg');
- $hideReplies = $this->getInput('norep');
- $hideRetweets = $this->getInput('noretweet');
- $hidePinned = $this->getInput('nopinned');
- $tweetFilter = $this->getInput('filter');
- $maxResults = $this->getInput('maxresults');
- if ($maxResults > 100) {
- $maxResults = 100;
- }
- $idAsTitle = $this->getInput('idastitle');
- $onlyMediaTweets = $this->getInput('imgonly');
-
- // Read API token from config.ini.php, put into Header
- $apiToken = $this->getOption('twitterv2apitoken');
- $authHeaders = array(
- 'authorization: Bearer ' . $apiToken,
- );
-
- // Try to get all tweets
- switch($this->queriedContext) {
- case 'By username':
- //Get id from username
- $params = array(
- 'user.fields' => 'pinned_tweet_id,profile_image_url'
- );
- $user = $this->makeApiCall('/users/by/username/'
- . $this->getInput('u'), $authHeaders, $params);
-
- if(isset($user->errors)) {
- Debug::log('User JSON: ' . json_encode($user));
- returnServerError('Requested username can\'t be found.');
- }
-
- // Set default params
- $params = array(
- 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
- 'tweet.fields'
- => 'created_at,referenced_tweets,entities,attachments',
- 'user.fields' => 'pinned_tweet_id',
- 'expansions'
- => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
- 'media.fields' => 'type,url,preview_image_url'
- );
-
- // Set params to filter out replies and/or retweets
- if($hideReplies && $hideRetweets) {
- $params['exclude'] = 'replies,retweets';
- } elseif($hideReplies) {
- $params['exclude'] = 'replies';
- } elseif($hideRetweets) {
- $params['exclude'] = 'retweets';
- }
-
- // Get the tweets
- $data = $this->makeApiCall('/users/' . $user->data->id
- . '/tweets', $authHeaders, $params);
- break;
-
- case 'By keyword or hashtag':
- $params = array(
- 'query' => $this->getInput('query'),
- 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
- 'tweet.fields'
- => 'created_at,referenced_tweets,entities,attachments',
- 'expansions'
- => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
- 'media.fields' => 'type,url,preview_image_url'
- );
-
- // Set params to filter out replies and/or retweets
- if($hideReplies) {
- $params['query'] = $params['query'] . ' -is:reply';
- }
- if($hideRetweets) {
- $params['query'] = $params['query'] . ' -is:retweet';
- }
-
- $data = $this->makeApiCall('/tweets/search/recent', $authHeaders, $params);
- break;
-
- case 'By list ID':
- // Set default params
- $params = array(
- 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
- 'tweet.fields'
- => 'created_at,referenced_tweets,entities,attachments',
- 'expansions'
- => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
- 'media.fields' => 'type,url,preview_image_url'
- );
-
- $data = $this->makeApiCall('/lists/' . $this->getInput('listid') .
- '/tweets', $authHeaders, $params);
- break;
-
- default:
- returnServerError('Invalid query context !');
- }
-
- if((isset($data->errors) && !isset($data->data)) ||
- (isset($data->meta) && $data->meta->result_count === 0)) {
- Debug::log('Data JSON: ' . json_encode($data));
- switch($this->queriedContext) {
- case 'By keyword or hashtag':
- returnServerError('No results for this query.');
- // fall-through
- case 'By username':
- returnServerError('Requested username cannnot be found.');
- // fall-through
- case 'By list ID':
- returnServerError('Requested list cannnot be found');
- // fall-through
- }
- }
-
- // figure out the Pinned Tweet Id
- if($hidePinned) {
- $pinnedTweetId = null;
- if(isset($user) && isset($user->data->pinned_tweet_id)) {
- $pinnedTweetId = $user->data->pinned_tweet_id;
- }
- }
-
- // Extract Media data into array
- isset($data->includes->media) ? $includesMedia = $data->includes->media : $includesMedia = null;
-
- // Extract additional Users data into array
- isset($data->includes->users) ? $includesUsers = $data->includes->users : $includesUsers = null;
-
- // Extract additional Tweets data into array
- isset($data->includes->tweets) ? $includesTweets = $data->includes->tweets : $includesTweets = null;
-
- // Extract main Tweets data into array
- $tweets = $data->data;
-
- // Make another API call to get user and media info for retweets
- // Is there some way to get this info included in original API call?
- $retweetedData = null;
- $retweetedMedia = null;
- $retweetedUsers = null;
- if(!$hideImages && isset($includesTweets)) {
- // There has to be a better PHP way to extract the tweet Ids?
- $includesTweetsIds = array();
- foreach($includesTweets as $includesTweet) {
- $includesTweetsIds[] = $includesTweet->id;
- }
- Debug::log('includesTweetsIds: ' . join(',', $includesTweetsIds));
-
- // Set default params for API query
- $params = array(
- 'ids' => join(',', $includesTweetsIds),
- 'tweet.fields' => 'entities,attachments',
- 'expansions' => 'author_id,attachments.media_keys',
- 'media.fields' => 'type,url,preview_image_url',
- 'user.fields' => 'id,profile_image_url'
- );
-
- // Get the retweeted tweets
- $retweetedData = $this->makeApiCall('/tweets', $authHeaders, $params);
-
- // Extract retweets Media data into array
- isset($retweetedData->includes->media) ? $retweetedMedia
- = $retweetedData->includes->media : $retweetedMedia = null;
-
- // Extract retweets additional Users data into array
- isset($retweetedData->includes->users) ? $retweetedUsers
- = $retweetedData->includes->users : $retweetedUsers = null;
- }
-
- // Create output array with all required elements for each tweet
- foreach($tweets as $tweet) {
- //Debug::log('Tweet JSON: ' . json_encode($tweet));
-
- // Skip pinned tweet (if selected)
- if($hidePinned && $tweet->id === $pinnedTweetId) {
- continue;
- }
-
- // Check if tweet is Retweet, Quote or Reply
- $isRetweet = false;
- $isReply = false;
- $isQuote = false;
-
- if(isset($tweet->referenced_tweets)) {
- switch($tweet->referenced_tweets[0]->type) {
- case 'retweeted':
- $isRetweet = true; break;
- case 'quoted':
- $isQuote = true; break;
- case 'replied_to':
- $isReply = true; break;
- }
- }
-
- // Skip replies and/or retweets (if selected). This check is primarily for lists
- // These should already be pre-filtered for username and keyword queries
- if (($hideRetweets && $isRetweet) || ($hideReplies && $isReply)) {
- continue;
- }
-
- $cleanedTweet = nl2br($tweet->text);
- //Debug::log('cleanedTweet: ' . $cleanedTweet);
-
- // Perform optional keyword filtering (only keep tweet if keyword is found)
- if (! empty($tweetFilter)) {
- if(stripos($cleanedTweet, $this->getInput('filter')) === false) {
- continue;
- }
- }
-
- // Initialize empty array to hold feed item values
- $this->item = array();
-
- // Start getting and setting values needed for HTML output
- $quotedTweet = null;
- $cleanedQuotedTweet = null;
- $quotedUser = null;
- if ($isQuote) {
- Debug::log('Tweet is quote');
- foreach($includesTweets as $includesTweet) {
- if($includesTweet->id === $tweet->referenced_tweets[0]->id) {
- $quotedTweet = $includesTweet;
- $cleanedQuotedTweet = nl2br($quotedTweet->text);
- //Debug::log('Found quoted tweet');
- break;
- }
- }
-
- $quotedUser = $this->getTweetUser($quotedTweet, $retweetedUsers, $includesUsers);
- }
- if($isRetweet || is_null($user)) {
- Debug::log('Tweet is retweet, or $user is null');
- // Replace tweet object with original retweeted object
- if($isRetweet) {
- foreach($includesTweets as $includesTweet) {
- if($includesTweet->id === $tweet->referenced_tweets[0]->id) {
- $tweet = $includesTweet;
- break;
- }
- }
- }
-
- // Skip self-Retweets (can cause duplicate entries in output)
- if(isset($user) && $tweet->author_id === $user->data->id) {
- continue;
- }
-
- // Get user object for retweeted tweet
- $originalUser = $this->getTweetUser($tweet, $retweetedUsers, $includesUsers);
-
- $this->item['username'] = $originalUser->username;
- $this->item['fullname'] = $originalUser->name;
- if(isset($originalUser->profile_image_url)) {
- $this->item['avatar'] = $originalUser->profile_image_url;
- } else{
- $this->item['avatar'] = null;
- }
- } else{
- $this->item['username'] = $user->data->username;
- $this->item['fullname'] = $user->data->name;
- $this->item['avatar'] = $user->data->profile_image_url;
- }
- $this->item['id'] = $tweet->id;
- $this->item['timestamp'] = $tweet->created_at;
- $this->item['uri']
- = self::URI . $this->item['username'] . '/status/' . $this->item['id'];
- $this->item['author'] = ($isRetweet ? 'RT: ' : '' )
- . $this->item['fullname']
- . ' (@'
- . $this->item['username'] . ')';
-
- // (Optional) Skip non-media tweet
- // This check must wait until after retweets are identified
- if ($onlyMediaTweets && !isset($tweet->attachments->media_keys) &&
- (($isQuote && !isset($quotedTweet->attachments->media_keys)) || !$isQuote)) {
- // There is no media in current tweet or quoted tweet, skip to next
- continue;
- }
-
- // Search for and replace URLs in Tweet text
- $cleanedTweet = $this->replaceTweetURLs($tweet, $cleanedTweet);
- if (isset($cleanedQuotedTweet)) {
- Debug::log('Replacing URLs in Quoted Tweet text');
- $cleanedQuotedTweet = $this->replaceTweetURLs($quotedTweet, $cleanedQuotedTweet);
- }
-
- // Generate Title text
- if ($idAsTitle) {
- $titleText = $tweet->id;
- } else{
- $titleText = strip_tags($cleanedTweet);
- }
-
- if($isRetweet && substr($titleText, 0, 4) === 'RT @') {
- $titleText = substr_replace($titleText, ':', 2, 0 );
- } elseif ($isReply && !$idAsTitle) {
- $titleText = 'R: ' . $titleText;
- }
-
- $this->item['title'] = $titleText;
-
- // Generate Avatar HTML block
- $picture_html = '';
- if(!$hideProfilePic && isset($this->item['avatar'])) {
- $picture_html = <<<EOD
+ ]
+ ],
+ 'By list ID' => [
+ 'listid' => [
+ 'name' => 'List ID',
+ 'exampleValue' => '31748',
+ 'required' => true,
+ 'title' => 'Enter a list id'
+ ]
+ ]
+ ];
+
+ // $Item variable needs to be accessible from multiple functions without passing
+ private $item = [];
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'By keyword or hashtag':
+ $specific = 'search ';
+ $param = 'query';
+ break;
+ case 'By username':
+ $specific = '@';
+ $param = 'u';
+ break;
+ case 'By list ID':
+ return 'Twitter List #' . $this->getInput('listid');
+ default:
+ return parent::getName();
+ }
+ return 'Twitter ' . $specific . $this->getInput($param);
+ }
+
+ public function collectData()
+ {
+ // $data will contain an array of all found tweets
+ $data = null;
+ // Contains user data (when in by username context)
+ $user = null;
+ // Array of all found tweets
+ $tweets = [];
+
+ $hideProfilePic = $this->getInput('nopic');
+ $hideImages = $this->getInput('noimg');
+ $hideReplies = $this->getInput('norep');
+ $hideRetweets = $this->getInput('noretweet');
+ $hidePinned = $this->getInput('nopinned');
+ $tweetFilter = $this->getInput('filter');
+ $maxResults = $this->getInput('maxresults');
+ if ($maxResults > 100) {
+ $maxResults = 100;
+ }
+ $idAsTitle = $this->getInput('idastitle');
+ $onlyMediaTweets = $this->getInput('imgonly');
+
+ // Read API token from config.ini.php, put into Header
+ $apiToken = $this->getOption('twitterv2apitoken');
+ $authHeaders = [
+ 'authorization: Bearer ' . $apiToken,
+ ];
+
+ // Try to get all tweets
+ switch ($this->queriedContext) {
+ case 'By username':
+ //Get id from username
+ $params = [
+ 'user.fields' => 'pinned_tweet_id,profile_image_url'
+ ];
+ $user = $this->makeApiCall('/users/by/username/'
+ . $this->getInput('u'), $authHeaders, $params);
+
+ if (isset($user->errors)) {
+ Debug::log('User JSON: ' . json_encode($user));
+ returnServerError('Requested username can\'t be found.');
+ }
+
+ // Set default params
+ $params = [
+ 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
+ 'tweet.fields'
+ => 'created_at,referenced_tweets,entities,attachments',
+ 'user.fields' => 'pinned_tweet_id',
+ 'expansions'
+ => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
+ 'media.fields' => 'type,url,preview_image_url'
+ ];
+
+ // Set params to filter out replies and/or retweets
+ if ($hideReplies && $hideRetweets) {
+ $params['exclude'] = 'replies,retweets';
+ } elseif ($hideReplies) {
+ $params['exclude'] = 'replies';
+ } elseif ($hideRetweets) {
+ $params['exclude'] = 'retweets';
+ }
+
+ // Get the tweets
+ $data = $this->makeApiCall('/users/' . $user->data->id
+ . '/tweets', $authHeaders, $params);
+ break;
+
+ case 'By keyword or hashtag':
+ $params = [
+ 'query' => $this->getInput('query'),
+ 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
+ 'tweet.fields'
+ => 'created_at,referenced_tweets,entities,attachments',
+ 'expansions'
+ => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
+ 'media.fields' => 'type,url,preview_image_url'
+ ];
+
+ // Set params to filter out replies and/or retweets
+ if ($hideReplies) {
+ $params['query'] = $params['query'] . ' -is:reply';
+ }
+ if ($hideRetweets) {
+ $params['query'] = $params['query'] . ' -is:retweet';
+ }
+
+ $data = $this->makeApiCall('/tweets/search/recent', $authHeaders, $params);
+ break;
+
+ case 'By list ID':
+ // Set default params
+ $params = [
+ 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
+ 'tweet.fields'
+ => 'created_at,referenced_tweets,entities,attachments',
+ 'expansions'
+ => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
+ 'media.fields' => 'type,url,preview_image_url'
+ ];
+
+ $data = $this->makeApiCall('/lists/' . $this->getInput('listid') .
+ '/tweets', $authHeaders, $params);
+ break;
+
+ default:
+ returnServerError('Invalid query context !');
+ }
+
+ if (
+ (isset($data->errors) && !isset($data->data)) ||
+ (isset($data->meta) && $data->meta->result_count === 0)
+ ) {
+ Debug::log('Data JSON: ' . json_encode($data));
+ switch ($this->queriedContext) {
+ case 'By keyword or hashtag':
+ returnServerError('No results for this query.');
+ // fall-through
+ case 'By username':
+ returnServerError('Requested username cannnot be found.');
+ // fall-through
+ case 'By list ID':
+ returnServerError('Requested list cannnot be found');
+ // fall-through
+ }
+ }
+
+ // figure out the Pinned Tweet Id
+ if ($hidePinned) {
+ $pinnedTweetId = null;
+ if (isset($user) && isset($user->data->pinned_tweet_id)) {
+ $pinnedTweetId = $user->data->pinned_tweet_id;
+ }
+ }
+
+ // Extract Media data into array
+ isset($data->includes->media) ? $includesMedia = $data->includes->media : $includesMedia = null;
+
+ // Extract additional Users data into array
+ isset($data->includes->users) ? $includesUsers = $data->includes->users : $includesUsers = null;
+
+ // Extract additional Tweets data into array
+ isset($data->includes->tweets) ? $includesTweets = $data->includes->tweets : $includesTweets = null;
+
+ // Extract main Tweets data into array
+ $tweets = $data->data;
+
+ // Make another API call to get user and media info for retweets
+ // Is there some way to get this info included in original API call?
+ $retweetedData = null;
+ $retweetedMedia = null;
+ $retweetedUsers = null;
+ if (!$hideImages && isset($includesTweets)) {
+ // There has to be a better PHP way to extract the tweet Ids?
+ $includesTweetsIds = [];
+ foreach ($includesTweets as $includesTweet) {
+ $includesTweetsIds[] = $includesTweet->id;
+ }
+ Debug::log('includesTweetsIds: ' . join(',', $includesTweetsIds));
+
+ // Set default params for API query
+ $params = [
+ 'ids' => join(',', $includesTweetsIds),
+ 'tweet.fields' => 'entities,attachments',
+ 'expansions' => 'author_id,attachments.media_keys',
+ 'media.fields' => 'type,url,preview_image_url',
+ 'user.fields' => 'id,profile_image_url'
+ ];
+
+ // Get the retweeted tweets
+ $retweetedData = $this->makeApiCall('/tweets', $authHeaders, $params);
+
+ // Extract retweets Media data into array
+ isset($retweetedData->includes->media) ? $retweetedMedia
+ = $retweetedData->includes->media : $retweetedMedia = null;
+
+ // Extract retweets additional Users data into array
+ isset($retweetedData->includes->users) ? $retweetedUsers
+ = $retweetedData->includes->users : $retweetedUsers = null;
+ }
+
+ // Create output array with all required elements for each tweet
+ foreach ($tweets as $tweet) {
+ //Debug::log('Tweet JSON: ' . json_encode($tweet));
+
+ // Skip pinned tweet (if selected)
+ if ($hidePinned && $tweet->id === $pinnedTweetId) {
+ continue;
+ }
+
+ // Check if tweet is Retweet, Quote or Reply
+ $isRetweet = false;
+ $isReply = false;
+ $isQuote = false;
+
+ if (isset($tweet->referenced_tweets)) {
+ switch ($tweet->referenced_tweets[0]->type) {
+ case 'retweeted':
+ $isRetweet = true;
+ break;
+ case 'quoted':
+ $isQuote = true;
+ break;
+ case 'replied_to':
+ $isReply = true;
+ break;
+ }
+ }
+
+ // Skip replies and/or retweets (if selected). This check is primarily for lists
+ // These should already be pre-filtered for username and keyword queries
+ if (($hideRetweets && $isRetweet) || ($hideReplies && $isReply)) {
+ continue;
+ }
+
+ $cleanedTweet = nl2br($tweet->text);
+ //Debug::log('cleanedTweet: ' . $cleanedTweet);
+
+ // Perform optional keyword filtering (only keep tweet if keyword is found)
+ if (! empty($tweetFilter)) {
+ if (stripos($cleanedTweet, $this->getInput('filter')) === false) {
+ continue;
+ }
+ }
+
+ // Initialize empty array to hold feed item values
+ $this->item = [];
+
+ // Start getting and setting values needed for HTML output
+ $quotedTweet = null;
+ $cleanedQuotedTweet = null;
+ $quotedUser = null;
+ if ($isQuote) {
+ Debug::log('Tweet is quote');
+ foreach ($includesTweets as $includesTweet) {
+ if ($includesTweet->id === $tweet->referenced_tweets[0]->id) {
+ $quotedTweet = $includesTweet;
+ $cleanedQuotedTweet = nl2br($quotedTweet->text);
+ //Debug::log('Found quoted tweet');
+ break;
+ }
+ }
+
+ $quotedUser = $this->getTweetUser($quotedTweet, $retweetedUsers, $includesUsers);
+ }
+ if ($isRetweet || is_null($user)) {
+ Debug::log('Tweet is retweet, or $user is null');
+ // Replace tweet object with original retweeted object
+ if ($isRetweet) {
+ foreach ($includesTweets as $includesTweet) {
+ if ($includesTweet->id === $tweet->referenced_tweets[0]->id) {
+ $tweet = $includesTweet;
+ break;
+ }
+ }
+ }
+
+ // Skip self-Retweets (can cause duplicate entries in output)
+ if (isset($user) && $tweet->author_id === $user->data->id) {
+ continue;
+ }
+
+ // Get user object for retweeted tweet
+ $originalUser = $this->getTweetUser($tweet, $retweetedUsers, $includesUsers);
+
+ $this->item['username'] = $originalUser->username;
+ $this->item['fullname'] = $originalUser->name;
+ if (isset($originalUser->profile_image_url)) {
+ $this->item['avatar'] = $originalUser->profile_image_url;
+ } else {
+ $this->item['avatar'] = null;
+ }
+ } else {
+ $this->item['username'] = $user->data->username;
+ $this->item['fullname'] = $user->data->name;
+ $this->item['avatar'] = $user->data->profile_image_url;
+ }
+ $this->item['id'] = $tweet->id;
+ $this->item['timestamp'] = $tweet->created_at;
+ $this->item['uri']
+ = self::URI . $this->item['username'] . '/status/' . $this->item['id'];
+ $this->item['author'] = ($isRetweet ? 'RT: ' : '' )
+ . $this->item['fullname']
+ . ' (@'
+ . $this->item['username'] . ')';
+
+ // (Optional) Skip non-media tweet
+ // This check must wait until after retweets are identified
+ if (
+ $onlyMediaTweets && !isset($tweet->attachments->media_keys) &&
+ (($isQuote && !isset($quotedTweet->attachments->media_keys)) || !$isQuote)
+ ) {
+ // There is no media in current tweet or quoted tweet, skip to next
+ continue;
+ }
+
+ // Search for and replace URLs in Tweet text
+ $cleanedTweet = $this->replaceTweetURLs($tweet, $cleanedTweet);
+ if (isset($cleanedQuotedTweet)) {
+ Debug::log('Replacing URLs in Quoted Tweet text');
+ $cleanedQuotedTweet = $this->replaceTweetURLs($quotedTweet, $cleanedQuotedTweet);
+ }
+
+ // Generate Title text
+ if ($idAsTitle) {
+ $titleText = $tweet->id;
+ } else {
+ $titleText = strip_tags($cleanedTweet);
+ }
+
+ if ($isRetweet && substr($titleText, 0, 4) === 'RT @') {
+ $titleText = substr_replace($titleText, ':', 2, 0);
+ } elseif ($isReply && !$idAsTitle) {
+ $titleText = 'R: ' . $titleText;
+ }
+
+ $this->item['title'] = $titleText;
+
+ // Generate Avatar HTML block
+ $picture_html = '';
+ if (!$hideProfilePic && isset($this->item['avatar'])) {
+ $picture_html = <<<EOD
<a href="https://twitter.com/{$this->item['username']}">
<img
style="margin-right: 10px; margin-bottom: 10px;"
@@ -467,24 +478,24 @@ EOD
title="{$this->item['fullname']}" />
</a>
EOD;
- }
-
- // Generate media HTML block
- $media_html = '';
- $quoted_media_html = '';
- if(!$hideImages) {
- if (isset($tweet->attachments->media_keys)) {
- Debug::log('Generating HTML for tweet media');
- $media_html = $this->createTweetMediaHTML($tweet, $includesMedia, $retweetedMedia);
- }
- if (isset($quotedTweet->attachments->media_keys)) {
- Debug::log('Generating HTML for quoted tweet media');
- $quoted_media_html = $this->createTweetMediaHTML($quotedTweet, $includesMedia, $retweetedMedia);
- }
- }
-
- // Generate the HTML for Item content
- $this->item['content'] = <<<EOD
+ }
+
+ // Generate media HTML block
+ $media_html = '';
+ $quoted_media_html = '';
+ if (!$hideImages) {
+ if (isset($tweet->attachments->media_keys)) {
+ Debug::log('Generating HTML for tweet media');
+ $media_html = $this->createTweetMediaHTML($tweet, $includesMedia, $retweetedMedia);
+ }
+ if (isset($quotedTweet->attachments->media_keys)) {
+ Debug::log('Generating HTML for quoted tweet media');
+ $quoted_media_html = $this->createTweetMediaHTML($quotedTweet, $includesMedia, $retweetedMedia);
+ }
+ }
+
+ // Generate the HTML for Item content
+ $this->item['content'] = <<<EOD
<div style="float: left;">
{$picture_html}
</div>
@@ -495,10 +506,10 @@ EOD;
{$media_html}
EOD;
- // Add Quoted Tweet HTML, if relevant
- if (isset($quotedTweet)) {
- $quotedTweetURI = self::URI . $quotedUser->username . '/status/' . $quotedTweet->id;
- $quote_html = <<<QUOTE
+ // Add Quoted Tweet HTML, if relevant
+ if (isset($quotedTweet)) {
+ $quotedTweetURI = self::URI . $quotedUser->username . '/status/' . $quotedTweet->id;
+ $quote_html = <<<QUOTE
<div style="display: table; border-style: solid; border-width: 1px;
border-radius: 5px; padding: 5px;">
<p><b>$quotedUser->name</b> @$quotedUser->username ·
@@ -507,183 +518,200 @@ EOD;
$quoted_media_html
</div>
QUOTE;
- $this->item['content'] .= $quote_html;
- }
-
- $this->item['content'] = htmlspecialchars_decode($this->item['content'], ENT_QUOTES);
-
- // Add current Item to Items array
- $this->items[] = $this->item;
- }
-
- // Sort all tweets in array by date
- usort($this->items, array('TwitterV2Bridge', 'compareTweetDate'));
- }
-
- private static function compareTweetDate($tweet1, $tweet2) {
- return (strtotime($tweet1['timestamp']) < strtotime($tweet2['timestamp']) ? 1 : -1);
- }
-
- /**
- * Tries to make an API call to Twitter.
- * @param $api string API entry point
- * @param $params array additional URI parmaeters
- * @return object json data
- */
- private function makeApiCall($api, $authHeaders, $params) {
- $uri = self::API_URI . $api . '?' . http_build_query($params);
- $result = getContents($uri, $authHeaders, array(), false);
- $data = json_decode($result);
- return $data;
- }
-
- /**
- * Change format of URLs in tweet text
- * @param $tweetObject object current Tweet JSON
- * @param $tweetText string current Tweet text
- * @return string modified tweet text
- */
- private function replaceTweetURLs($tweetObject, $tweetText) {
- $foundUrls = false;
- // Rewrite URL links, based on URL list in tweet object
- if(isset($tweetObject->entities->urls)) {
- foreach($tweetObject->entities->urls as $url) {
- $tweetText = str_replace($url->url,
- '<a href="' . $url->expanded_url
- . '">' . $url->display_url . '</a>',
- $tweetText);
- }
- $foundUrls = true;
- }
- // Regex fallback for rewriting URL links. Should never trigger?
- if($foundUrls === false) {
- $reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
- if(preg_match($reg_ex, $tweetText, $url)) {
- $tweetText = preg_replace($reg_ex,
- "<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ",
- $tweetText);
- }
- }
- // Fix back-to-back URLs by adding a <br>
- $reg_ex = '/\/a>\s*<a/';
- $tweetText = preg_replace($reg_ex, '/a><br><a', $tweetText);
-
- return $tweetText;
- }
-
- /**
- * Find User object for Retweeted/Quoted tweet
- * @param $tweetObject object current Tweet JSON
- * @param $retweetedUsers
- * @param $includesUsers
- * @return object found User
- */
- private function getTweetUser($tweetObject, $retweetedUsers, $includesUsers) {
- $originalUser = new stdClass(); // make the linters stop complaining
- if(isset($retweetedUsers)) {
- Debug::log('Searching for tweet author_id in $retweetedUsers');
- foreach($retweetedUsers as $retweetedUser) {
- if($retweetedUser->id === $tweetObject->author_id) {
- $matchedUser = $retweetedUser;
- Debug::log('Found author_id match in $retweetedUsers');
- break;
- }
- }
- }
- if(!isset($matchedUser->username) && isset($includesUsers)) {
- Debug::log('Searching for tweet author_id in $includesUsers');
- foreach($includesUsers as $includesUser) {
- if($includesUser->id === $tweetObject->author_id) {
- $matchedUser = $includesUser;
- Debug::log('Found author_id match in $includesUsers');
- break;
- }
- }
- }
- return $matchedUser;
- }
-
- /**
- * Generates HTML for embedded media
- * @param $tweetObject object current Tweet JSON
- * @param $includesMedia
- * @param $retweetedMedia
- * @return string modified tweet text
- */
- private function createTweetMediaHTML($tweetObject, $includesMedia, $retweetedMedia){
- $media_html = '';
- // Match media_keys in tweet to media list from, put matches into new array
- $tweetMedia = array();
- // Start by checking the original list of tweet Media includes
- if(isset($includesMedia)) {
- Debug::log('Searching for media_key in $includesMedia');
- foreach($includesMedia as $includesMedium) {
- if(in_array ($includesMedium->media_key,
- $tweetObject->attachments->media_keys)) {
- Debug::log('Found media_key in $includesMedia');
- $tweetMedia[] = $includesMedium;
- }
- }
- }
- // If no matches found, check the retweet Media includes
- if(empty($tweetMedia) && isset($retweetedMedia)) {
- Debug::log('Searching for media_key in $retweetedMedia');
- foreach($retweetedMedia as $retweetedMedium) {
- if(in_array ($retweetedMedium->media_key,
- $tweetObject->attachments->media_keys)) {
- Debug::log('Found media_key in $retweetedMedia');
- $tweetMedia[] = $retweetedMedium;
- }
- }
- }
-
- foreach($tweetMedia as $media) {
- switch($media->type) {
- case 'photo':
- if ($this->getInput('noimgscaling')) {
- $image = $media->url;
- $display_image = $media->url;
- } else{
- $image = $media->url . '?name=orig';
- $display_image = $media->url;
- }
- // add enclosures
- $this->item['enclosures'][] = $image;
-
- $media_html .= <<<EOD
+ $this->item['content'] .= $quote_html;
+ }
+
+ $this->item['content'] = htmlspecialchars_decode($this->item['content'], ENT_QUOTES);
+
+ // Add current Item to Items array
+ $this->items[] = $this->item;
+ }
+
+ // Sort all tweets in array by date
+ usort($this->items, ['TwitterV2Bridge', 'compareTweetDate']);
+ }
+
+ private static function compareTweetDate($tweet1, $tweet2)
+ {
+ return (strtotime($tweet1['timestamp']) < strtotime($tweet2['timestamp']) ? 1 : -1);
+ }
+
+ /**
+ * Tries to make an API call to Twitter.
+ * @param $api string API entry point
+ * @param $params array additional URI parmaeters
+ * @return object json data
+ */
+ private function makeApiCall($api, $authHeaders, $params)
+ {
+ $uri = self::API_URI . $api . '?' . http_build_query($params);
+ $result = getContents($uri, $authHeaders, [], false);
+ $data = json_decode($result);
+ return $data;
+ }
+
+ /**
+ * Change format of URLs in tweet text
+ * @param $tweetObject object current Tweet JSON
+ * @param $tweetText string current Tweet text
+ * @return string modified tweet text
+ */
+ private function replaceTweetURLs($tweetObject, $tweetText)
+ {
+ $foundUrls = false;
+ // Rewrite URL links, based on URL list in tweet object
+ if (isset($tweetObject->entities->urls)) {
+ foreach ($tweetObject->entities->urls as $url) {
+ $tweetText = str_replace(
+ $url->url,
+ '<a href="' . $url->expanded_url
+ . '">' . $url->display_url . '</a>',
+ $tweetText
+ );
+ }
+ $foundUrls = true;
+ }
+ // Regex fallback for rewriting URL links. Should never trigger?
+ if ($foundUrls === false) {
+ $reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
+ if (preg_match($reg_ex, $tweetText, $url)) {
+ $tweetText = preg_replace(
+ $reg_ex,
+ "<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ",
+ $tweetText
+ );
+ }
+ }
+ // Fix back-to-back URLs by adding a <br>
+ $reg_ex = '/\/a>\s*<a/';
+ $tweetText = preg_replace($reg_ex, '/a><br><a', $tweetText);
+
+ return $tweetText;
+ }
+
+ /**
+ * Find User object for Retweeted/Quoted tweet
+ * @param $tweetObject object current Tweet JSON
+ * @param $retweetedUsers
+ * @param $includesUsers
+ * @return object found User
+ */
+ private function getTweetUser($tweetObject, $retweetedUsers, $includesUsers)
+ {
+ $originalUser = new stdClass(); // make the linters stop complaining
+ if (isset($retweetedUsers)) {
+ Debug::log('Searching for tweet author_id in $retweetedUsers');
+ foreach ($retweetedUsers as $retweetedUser) {
+ if ($retweetedUser->id === $tweetObject->author_id) {
+ $matchedUser = $retweetedUser;
+ Debug::log('Found author_id match in $retweetedUsers');
+ break;
+ }
+ }
+ }
+ if (!isset($matchedUser->username) && isset($includesUsers)) {
+ Debug::log('Searching for tweet author_id in $includesUsers');
+ foreach ($includesUsers as $includesUser) {
+ if ($includesUser->id === $tweetObject->author_id) {
+ $matchedUser = $includesUser;
+ Debug::log('Found author_id match in $includesUsers');
+ break;
+ }
+ }
+ }
+ return $matchedUser;
+ }
+
+ /**
+ * Generates HTML for embedded media
+ * @param $tweetObject object current Tweet JSON
+ * @param $includesMedia
+ * @param $retweetedMedia
+ * @return string modified tweet text
+ */
+ private function createTweetMediaHTML($tweetObject, $includesMedia, $retweetedMedia)
+ {
+ $media_html = '';
+ // Match media_keys in tweet to media list from, put matches into new array
+ $tweetMedia = [];
+ // Start by checking the original list of tweet Media includes
+ if (isset($includesMedia)) {
+ Debug::log('Searching for media_key in $includesMedia');
+ foreach ($includesMedia as $includesMedium) {
+ if (
+ in_array(
+ $includesMedium->media_key,
+ $tweetObject->attachments->media_keys
+ )
+ ) {
+ Debug::log('Found media_key in $includesMedia');
+ $tweetMedia[] = $includesMedium;
+ }
+ }
+ }
+ // If no matches found, check the retweet Media includes
+ if (empty($tweetMedia) && isset($retweetedMedia)) {
+ Debug::log('Searching for media_key in $retweetedMedia');
+ foreach ($retweetedMedia as $retweetedMedium) {
+ if (
+ in_array(
+ $retweetedMedium->media_key,
+ $tweetObject->attachments->media_keys
+ )
+ ) {
+ Debug::log('Found media_key in $retweetedMedia');
+ $tweetMedia[] = $retweetedMedium;
+ }
+ }
+ }
+
+ foreach ($tweetMedia as $media) {
+ switch ($media->type) {
+ case 'photo':
+ if ($this->getInput('noimgscaling')) {
+ $image = $media->url;
+ $display_image = $media->url;
+ } else {
+ $image = $media->url . '?name=orig';
+ $display_image = $media->url;
+ }
+ // add enclosures
+ $this->item['enclosures'][] = $image;
+
+ $media_html .= <<<EOD
<a href="{$image}">
<img
referrerpolicy="no-referrer"
src="{$display_image}" />
</a>
EOD;
- break;
- case 'video':
- // To Do: Is there a way to easily match this
- // to a direct Video URL?
- $display_image = $media->preview_image_url;
+ break;
+ case 'video':
+ // To Do: Is there a way to easily match this
+ // to a direct Video URL?
+ $display_image = $media->preview_image_url;
- $media_html .= <<<EOD
+ $media_html .= <<<EOD
<p>Video:</p><a href="{$this->item['uri']}">
<img referrerpolicy="no-referrer" src="{$display_image}" /></a>
EOD;
- break;
- case 'animated_gif':
- // To Do: Is there a way to easily match this to a
- // direct animated Gif URL?
- $display_image = $media->preview_image_url;
+ break;
+ case 'animated_gif':
+ // To Do: Is there a way to easily match this to a
+ // direct animated Gif URL?
+ $display_image = $media->preview_image_url;
- $media_html .= <<<EOD
+ $media_html .= <<<EOD
<p>Animated Gif:</p><a href="{$this->item['uri']}">
<img referrerpolicy="no-referrer" src="{$display_image}" /></a>
EOD;
- break;
- default:
- Debug::log('Missing support for media type: '
- . $media->type);
- }
- }
-
- return $media_html;
- }
+ break;
+ default:
+ Debug::log('Missing support for media type: '
+ . $media->type);
+ }
+ }
+
+ return $media_html;
+ }
}
diff --git a/bridges/UberNewsroomBridge.php b/bridges/UberNewsroomBridge.php
index 560998cd..333200cd 100644
--- a/bridges/UberNewsroomBridge.php
+++ b/bridges/UberNewsroomBridge.php
@@ -1,179 +1,185 @@
<?php
-class UberNewsroomBridge extends BridgeAbstract {
- const NAME = 'Uber Newsroom Bridge';
- const URI = 'https://www.uber.com';
- const URI_API_DATA = 'https://newsroomapi.uber.com/wp-json/newsroom/v1/data?locale=';
- const URI_API_POST = 'https://newsroomapi.uber.com/wp-json/wp/v2/posts/';
- const DESCRIPTION = 'Returns news posts';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(array(
- 'region' => array(
- 'name' => 'Region',
- 'type' => 'list',
- 'values' => array(
- 'Africa' => array(
- 'Egypt' => 'ar-EG',
- 'Ghana' => 'en-GH',
- 'Kenya' => 'en-KE',
- 'Morocco' => 'fr-MA',
- 'Nigeria' => 'en-NG',
- 'South Africa' => 'en-ZA',
- 'Tanzania' => 'en-TZ',
- 'Uganda' => 'en-UG',
- ),
- 'Asia' => array(
- 'Bangladesh' => 'en-BD',
- 'Cambodia' => 'km-KH',
- 'China' => 'zh-CN',
- 'Hong Kong' => 'zh-HK',
- 'India' => 'en-IN',
- 'Indonesia' => 'en-ID',
- 'Japan' => 'ja-JP',
- 'Korea' => 'ko-KR',
- 'Macau' => 'zh-MO',
- 'Malaysia' => 'en-MY',
- 'Myanmar' => 'en-MM',
- 'Philippines' => 'en-PH',
- 'Singapore' => 'en-SG',
- 'Sri Lanka' => 'en-LK',
- 'Taiwan' => 'zh-TW',
- 'Thailand' => 'th-TH',
- 'Vietnam' => 'vi-VN',
- ),
- 'Central America' => array(
- 'Costa Rica' => 'es-CR',
- 'Dominican Republic' => 'es-DO',
- 'El Salvador' => 'es-SV',
- 'Guatemala' => 'es-GT',
- 'Honduras' => 'es-HN',
- 'Mexico' => 'es-MX',
- 'Nicaragua' => 'es-NI',
- 'Panama' => 'es-PA',
- 'Puerto Rico' => 'es-PR',
- ),
- 'Europe' => array(
- 'Austria' => 'de-AT',
- 'Azerbaijan' => 'az',
- 'Belarus' => 'ru-BY',
- 'Belgium' => 'fr-BE',
- 'Bulgaria' => 'bg',
- 'Croatia' => 'hr',
- 'Czech Republic' => 'cs-CZ',
- 'Denmark' => 'da-DK',
- 'Estonia' => 'et-EE',
- 'Finland' => 'fi',
- 'France' => 'fr',
- 'Germany' => 'de',
- 'Greece' => 'el-GR',
- 'Hungary' => 'hu',
- 'Ireland' => 'en-IE',
- 'Italy' => 'it',
- 'Kazakhstan' => 'ru-KZ',
- 'Lithuania' => 'lt',
- 'Netherlands' => 'nl',
- 'Norway' => 'nb-NO',
- 'Poland' => 'pl',
- 'Portugal' => 'pt',
- 'Romania' => 'ro',
- 'Russia' => 'ru',
- 'Slovakia' => 'sk',
- 'Spain' => 'es-ES',
- 'Sweden' => 'sv-SE',
- 'Switzerland' => 'fr-CH',
- 'Turkey' => 'tr',
- 'Ukraine' => 'uk-UA',
- 'United Kingdom' => 'en-GB',
- ),
- 'Middle East' => array(
- 'Bahrain' => 'en-BH',
- 'Israel' => 'he-IL',
- 'Jordan' => 'en-JO',
- 'Kuwait' => 'en-KW',
- 'Lebanon' => 'en-LB',
- 'Pakistan' => 'en-PK',
- 'Qatar' => 'en-QA',
- 'Saudi Arabia' => 'ar-SA',
- 'United Arab Emirates' => 'en-AE',
- ),
- 'North America' => array(
- 'Canada' => 'en-CA',
- 'United States' => 'en-US',
- ),
- 'Pacific' => array(
- 'Australia' => 'en-AU',
- 'New Zealand' => 'en-NZ',
- ),
- 'South America' => array(
- 'Argentina' => 'es-AR',
- 'Bolivia' => 'es-BO',
- 'Brazil' => 'pt-BR',
- 'Chile' => 'es-CL',
- 'Colombia' => 'es-CO',
- 'Ecuador' => 'es-EC',
- 'Paraguay' => 'es-PY',
- 'Peru' => 'es-PE',
- 'Trinidad & Tobago' => 'en-TT',
- 'Uruguay' => 'es-UY',
- 'Venezuela' => 'es-VE',
- ),
- ),
- 'defaultValue' => 'en-US',
- )
- ));
-
- const CACHE_TIMEOUT = 3600;
-
- private $regionName = '';
-
- public function collectData() {
- $json = getContents(self::URI_API_DATA . $this->getInput('region'));
- $data = json_decode($json);
-
- $this->regionName = $data->region->name;
-
- foreach ($data->articles as $article) {
- $json = getContents(self::URI_API_POST . $article->id);
- $post = json_decode($json);
-
- $item = array();
- $item['title'] = $post->title->rendered;
- $item['timestamp'] = $post->date;
- $item['uri'] = $post->link;
- $item['content'] = $this->formatContent($post->content->rendered);
- $item['enclosures'][] = $article->image_full;
-
- $this->items[] = $item;
- }
- }
-
- public function getURI() {
- if (is_null($this->getInput('region')) === false) {
- return self::URI . '/' . $this->getInput('region') . '/newsroom';
- }
-
- return parent::getURI() . '/newsroom';
- }
-
- public function getName() {
- if (is_null($this->getInput('region')) === false) {
- return $this->regionName . ' - Uber Newsroom';
- }
-
- return parent::getName();
- }
-
- private function formatContent($html) {
- $html = str_get_html($html);
-
- foreach ($html->find('div.wp-video') as $div) {
- $div->style = '';
- }
-
- foreach ($html->find('video') as $video) {
- $video->width = '100%';
- $video->height = '';
- }
-
- return $html;
- }
+
+class UberNewsroomBridge extends BridgeAbstract
+{
+ const NAME = 'Uber Newsroom Bridge';
+ const URI = 'https://www.uber.com';
+ const URI_API_DATA = 'https://newsroomapi.uber.com/wp-json/newsroom/v1/data?locale=';
+ const URI_API_POST = 'https://newsroomapi.uber.com/wp-json/wp/v2/posts/';
+ const DESCRIPTION = 'Returns news posts';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [[
+ 'region' => [
+ 'name' => 'Region',
+ 'type' => 'list',
+ 'values' => [
+ 'Africa' => [
+ 'Egypt' => 'ar-EG',
+ 'Ghana' => 'en-GH',
+ 'Kenya' => 'en-KE',
+ 'Morocco' => 'fr-MA',
+ 'Nigeria' => 'en-NG',
+ 'South Africa' => 'en-ZA',
+ 'Tanzania' => 'en-TZ',
+ 'Uganda' => 'en-UG',
+ ],
+ 'Asia' => [
+ 'Bangladesh' => 'en-BD',
+ 'Cambodia' => 'km-KH',
+ 'China' => 'zh-CN',
+ 'Hong Kong' => 'zh-HK',
+ 'India' => 'en-IN',
+ 'Indonesia' => 'en-ID',
+ 'Japan' => 'ja-JP',
+ 'Korea' => 'ko-KR',
+ 'Macau' => 'zh-MO',
+ 'Malaysia' => 'en-MY',
+ 'Myanmar' => 'en-MM',
+ 'Philippines' => 'en-PH',
+ 'Singapore' => 'en-SG',
+ 'Sri Lanka' => 'en-LK',
+ 'Taiwan' => 'zh-TW',
+ 'Thailand' => 'th-TH',
+ 'Vietnam' => 'vi-VN',
+ ],
+ 'Central America' => [
+ 'Costa Rica' => 'es-CR',
+ 'Dominican Republic' => 'es-DO',
+ 'El Salvador' => 'es-SV',
+ 'Guatemala' => 'es-GT',
+ 'Honduras' => 'es-HN',
+ 'Mexico' => 'es-MX',
+ 'Nicaragua' => 'es-NI',
+ 'Panama' => 'es-PA',
+ 'Puerto Rico' => 'es-PR',
+ ],
+ 'Europe' => [
+ 'Austria' => 'de-AT',
+ 'Azerbaijan' => 'az',
+ 'Belarus' => 'ru-BY',
+ 'Belgium' => 'fr-BE',
+ 'Bulgaria' => 'bg',
+ 'Croatia' => 'hr',
+ 'Czech Republic' => 'cs-CZ',
+ 'Denmark' => 'da-DK',
+ 'Estonia' => 'et-EE',
+ 'Finland' => 'fi',
+ 'France' => 'fr',
+ 'Germany' => 'de',
+ 'Greece' => 'el-GR',
+ 'Hungary' => 'hu',
+ 'Ireland' => 'en-IE',
+ 'Italy' => 'it',
+ 'Kazakhstan' => 'ru-KZ',
+ 'Lithuania' => 'lt',
+ 'Netherlands' => 'nl',
+ 'Norway' => 'nb-NO',
+ 'Poland' => 'pl',
+ 'Portugal' => 'pt',
+ 'Romania' => 'ro',
+ 'Russia' => 'ru',
+ 'Slovakia' => 'sk',
+ 'Spain' => 'es-ES',
+ 'Sweden' => 'sv-SE',
+ 'Switzerland' => 'fr-CH',
+ 'Turkey' => 'tr',
+ 'Ukraine' => 'uk-UA',
+ 'United Kingdom' => 'en-GB',
+ ],
+ 'Middle East' => [
+ 'Bahrain' => 'en-BH',
+ 'Israel' => 'he-IL',
+ 'Jordan' => 'en-JO',
+ 'Kuwait' => 'en-KW',
+ 'Lebanon' => 'en-LB',
+ 'Pakistan' => 'en-PK',
+ 'Qatar' => 'en-QA',
+ 'Saudi Arabia' => 'ar-SA',
+ 'United Arab Emirates' => 'en-AE',
+ ],
+ 'North America' => [
+ 'Canada' => 'en-CA',
+ 'United States' => 'en-US',
+ ],
+ 'Pacific' => [
+ 'Australia' => 'en-AU',
+ 'New Zealand' => 'en-NZ',
+ ],
+ 'South America' => [
+ 'Argentina' => 'es-AR',
+ 'Bolivia' => 'es-BO',
+ 'Brazil' => 'pt-BR',
+ 'Chile' => 'es-CL',
+ 'Colombia' => 'es-CO',
+ 'Ecuador' => 'es-EC',
+ 'Paraguay' => 'es-PY',
+ 'Peru' => 'es-PE',
+ 'Trinidad & Tobago' => 'en-TT',
+ 'Uruguay' => 'es-UY',
+ 'Venezuela' => 'es-VE',
+ ],
+ ],
+ 'defaultValue' => 'en-US',
+ ]
+ ]];
+
+ const CACHE_TIMEOUT = 3600;
+
+ private $regionName = '';
+
+ public function collectData()
+ {
+ $json = getContents(self::URI_API_DATA . $this->getInput('region'));
+ $data = json_decode($json);
+
+ $this->regionName = $data->region->name;
+
+ foreach ($data->articles as $article) {
+ $json = getContents(self::URI_API_POST . $article->id);
+ $post = json_decode($json);
+
+ $item = [];
+ $item['title'] = $post->title->rendered;
+ $item['timestamp'] = $post->date;
+ $item['uri'] = $post->link;
+ $item['content'] = $this->formatContent($post->content->rendered);
+ $item['enclosures'][] = $article->image_full;
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ if (is_null($this->getInput('region')) === false) {
+ return self::URI . '/' . $this->getInput('region') . '/newsroom';
+ }
+
+ return parent::getURI() . '/newsroom';
+ }
+
+ public function getName()
+ {
+ if (is_null($this->getInput('region')) === false) {
+ return $this->regionName . ' - Uber Newsroom';
+ }
+
+ return parent::getName();
+ }
+
+ private function formatContent($html)
+ {
+ $html = str_get_html($html);
+
+ foreach ($html->find('div.wp-video') as $div) {
+ $div->style = '';
+ }
+
+ foreach ($html->find('video') as $video) {
+ $video->width = '100%';
+ $video->height = '';
+ }
+
+ return $html;
+ }
}
diff --git a/bridges/UnogsBridge.php b/bridges/UnogsBridge.php
index f03555b4..021b75ed 100644
--- a/bridges/UnogsBridge.php
+++ b/bridges/UnogsBridge.php
@@ -1,191 +1,196 @@
<?php
-class UnogsBridge extends BridgeAbstract {
-
- const MAINTAINER = 'csisoap';
- const NAME = 'uNoGS Bridge';
- const URI = 'https://unogs.com';
- const DESCRIPTION = 'Return what\'s new or removal on Netflix';
-
- const PARAMETERS = array(
- 'global' => array(
- 'feed' => array(
- 'name' => 'feed',
- 'type' => 'list',
- 'title' => 'Choose whether you want latest movies or removal on Netflix',
- 'values' => array(
- 'What\'s New' => 'new last 7 days',
- 'Expiring' => 'expiring'
- )
- ),
- 'limit' => self::LIMIT,
- ),
- 'Global' => array(),
- 'Country' => array(
- 'country_code' => array(
- 'name' => 'Country',
- 'type' => 'list',
- 'title' => 'Choose your preferred country',
- 'values' => array(
- 'Argentina' => 21,
- 'Australia' => 23,
- 'Belgium' => 26,
- 'Brazil' => 29,
- 'Canada' => 33,
- 'Colombia' => 36,
- 'Czech Republic' => 307,
- 'France' => 45,
- 'Germany' => 39,
- 'Greece' => 327,
- 'Hong Kong' => 331,
- 'Hungary' => 334,
- 'Iceland' => 265,
- 'India' => 337,
- 'Israel' => 336,
- 'Italy' => 269,
- 'Japan' => 267,
- 'Lithuania' => 357,
- 'Malaysia' => 378,
- 'Mexico' => 65,
- 'Netherlands' => 67,
- 'Philippines' => 390,
- 'Poland' => 392,
- 'Portugal' => 268,
- 'Romania' => 400,
- 'Russia' => 402,
- 'Singapore' => 408,
- 'Slovakia' => 412,
- 'South Africa' => 447,
- 'South Korea' => 348,
- 'Spain' => 270,
- 'Sweden' => 73,
- 'Switzerland' => 34,
- 'Thailand' => 425,
- 'Turkey' => 432,
- 'Ukraine' => 436,
- 'United Kingdom' => 46,
- 'United States' => 78
- )
- )
- )
- );
-
- public function getName() {
- $feedName = '';
- if($this->queriedContext == 'Global') {
- $feedName .= 'Netflix Global - ';
- } elseif($this->queriedContext == 'Country') {
- $feedName .= 'Netflix ' . $this->getParametersKey('country_code') . ' - ';
- }
- if($this->getInput('feed') == 'expiring') {
- $feedName .= 'Expiring title';
- } elseif($this->getInput('feed') == 'new last 7 days') {
- $feedName .= 'What\'s New';
- } else {
- $feedName = self::NAME;
- }
- return $feedName;
- }
-
- private function getParametersKey($input = '') {
- $params = $this->getParameters();
- $tab = 'Country';
- if (!isset($params[$tab][$input])) {
- return '';
- }
-
- return array_search(
- $this->getInput($input),
- $params[$tab][$input]['values']
- );
-
- }
-
- private function getJSON($url) {
- $header = array(
- 'Referer: https://unogs.com/',
- 'referrer: http://unogs.com'
- );
-
- $raw = getContents($url, $header);
- return json_decode($raw, true);
- }
-
- private function getImage($nfid) {
- $url = self::URI . '/api/title/bgimages?netflixid=' . $nfid;
- $json = $this->getJSON($url);
- $image_wrapper = '';
- if(isset($json['bo1280x448'])) {
- $image_wrapper = 'bo1280x448';
- } else {
- $image_wrapper = 'bo665x375';
- }
- end($json[$image_wrapper]);
- $position = key($json[$image_wrapper]);
- $image_link = $json[$image_wrapper][$position]['url'];
- return $image_link;
- }
-
- private function handleData($data) {
- $item = array();
- $item['title'] = $data['title'] . ' - ' . $data['year'];
- $item['timestamp'] = $data['titledate'];
- $netflix_id = $data['nfid'];
- $item['uri'] = 'https://www.netflix.com/title/' . $netflix_id;
- $image_url = $this->getImage($netflix_id);
- $netflix_synopsis = $data['synopsis'];
- $expired_warning = '';
- if(isset($data['expires'])) {
- $expired_warning .= '<p><b>Expired on: ' . $data['expires'] . '</b></p>';
- $item['timestamp'] = $data['expires'];
- }
- $unogs_url = self::URI . '/title/' . $netflix_id;
-
- $item['content'] = <<<EOD
+class UnogsBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'csisoap';
+ const NAME = 'uNoGS Bridge';
+ const URI = 'https://unogs.com';
+ const DESCRIPTION = 'Return what\'s new or removal on Netflix';
+
+ const PARAMETERS = [
+ 'global' => [
+ 'feed' => [
+ 'name' => 'feed',
+ 'type' => 'list',
+ 'title' => 'Choose whether you want latest movies or removal on Netflix',
+ 'values' => [
+ 'What\'s New' => 'new last 7 days',
+ 'Expiring' => 'expiring'
+ ]
+ ],
+ 'limit' => self::LIMIT,
+ ],
+ 'Global' => [],
+ 'Country' => [
+ 'country_code' => [
+ 'name' => 'Country',
+ 'type' => 'list',
+ 'title' => 'Choose your preferred country',
+ 'values' => [
+ 'Argentina' => 21,
+ 'Australia' => 23,
+ 'Belgium' => 26,
+ 'Brazil' => 29,
+ 'Canada' => 33,
+ 'Colombia' => 36,
+ 'Czech Republic' => 307,
+ 'France' => 45,
+ 'Germany' => 39,
+ 'Greece' => 327,
+ 'Hong Kong' => 331,
+ 'Hungary' => 334,
+ 'Iceland' => 265,
+ 'India' => 337,
+ 'Israel' => 336,
+ 'Italy' => 269,
+ 'Japan' => 267,
+ 'Lithuania' => 357,
+ 'Malaysia' => 378,
+ 'Mexico' => 65,
+ 'Netherlands' => 67,
+ 'Philippines' => 390,
+ 'Poland' => 392,
+ 'Portugal' => 268,
+ 'Romania' => 400,
+ 'Russia' => 402,
+ 'Singapore' => 408,
+ 'Slovakia' => 412,
+ 'South Africa' => 447,
+ 'South Korea' => 348,
+ 'Spain' => 270,
+ 'Sweden' => 73,
+ 'Switzerland' => 34,
+ 'Thailand' => 425,
+ 'Turkey' => 432,
+ 'Ukraine' => 436,
+ 'United Kingdom' => 46,
+ 'United States' => 78
+ ]
+ ]
+ ]
+ ];
+
+ public function getName()
+ {
+ $feedName = '';
+ if ($this->queriedContext == 'Global') {
+ $feedName .= 'Netflix Global - ';
+ } elseif ($this->queriedContext == 'Country') {
+ $feedName .= 'Netflix ' . $this->getParametersKey('country_code') . ' - ';
+ }
+ if ($this->getInput('feed') == 'expiring') {
+ $feedName .= 'Expiring title';
+ } elseif ($this->getInput('feed') == 'new last 7 days') {
+ $feedName .= 'What\'s New';
+ } else {
+ $feedName = self::NAME;
+ }
+ return $feedName;
+ }
+
+ private function getParametersKey($input = '')
+ {
+ $params = $this->getParameters();
+ $tab = 'Country';
+ if (!isset($params[$tab][$input])) {
+ return '';
+ }
+
+ return array_search(
+ $this->getInput($input),
+ $params[$tab][$input]['values']
+ );
+ }
+
+ private function getJSON($url)
+ {
+ $header = [
+ 'Referer: https://unogs.com/',
+ 'referrer: http://unogs.com'
+ ];
+
+ $raw = getContents($url, $header);
+ return json_decode($raw, true);
+ }
+
+ private function getImage($nfid)
+ {
+ $url = self::URI . '/api/title/bgimages?netflixid=' . $nfid;
+ $json = $this->getJSON($url);
+ $image_wrapper = '';
+ if (isset($json['bo1280x448'])) {
+ $image_wrapper = 'bo1280x448';
+ } else {
+ $image_wrapper = 'bo665x375';
+ }
+ end($json[$image_wrapper]);
+ $position = key($json[$image_wrapper]);
+ $image_link = $json[$image_wrapper][$position]['url'];
+ return $image_link;
+ }
+
+ private function handleData($data)
+ {
+ $item = [];
+ $item['title'] = $data['title'] . ' - ' . $data['year'];
+ $item['timestamp'] = $data['titledate'];
+ $netflix_id = $data['nfid'];
+ $item['uri'] = 'https://www.netflix.com/title/' . $netflix_id;
+ $image_url = $this->getImage($netflix_id);
+ $netflix_synopsis = $data['synopsis'];
+ $expired_warning = '';
+ if (isset($data['expires'])) {
+ $expired_warning .= '<p><b>Expired on: ' . $data['expires'] . '</b></p>';
+ $item['timestamp'] = $data['expires'];
+ }
+ $unogs_url = self::URI . '/title/' . $netflix_id;
+
+ $item['content'] = <<<EOD
<img src={$image_url}>
$expired_warning
<p>$netflix_synopsis</p>
<p>Details: <a href={$unogs_url}>$unogs_url</a></p>
EOD;
- $this->items[] = $item;
- }
-
- public function collectData() {
- $feed = $this->getInput('feed');
- $is_global = false;
- $country_code = '';
-
- switch ($this->queriedContext) {
- case 'Country':
- $country_code = $this->getInput('country_code');
- break;
- }
-
- $limit = $this->getInput('limit') ?? 30;
-
- // https://rapidapi.com/unogs/api/unogsng/details
- $api_url = sprintf(
- '%s/api/search?query=%s%s&limit=%s',
- self::URI,
- urlencode($feed),
- $country_code ? '&countrylist=' . $country_code : '',
- $limit
- );
-
- $json_data = $this->getJSON($api_url);
- $movies = $json_data['results'];
-
- if($this->getInput('feed') == 'expiring') {
- /* uNoGS API returns movies/series that going to remove
- * today according to the day you fetch the data.
- * They put items that going to remove in the future on the last
- * so I reverse this to get those items, not to bothers those that already removed today.
- */
- $movies = array_reverse($movies);
- }
-
- foreach($movies as $movie) {
- $this->handleData($movie);
- }
- }
+ $this->items[] = $item;
+ }
+
+ public function collectData()
+ {
+ $feed = $this->getInput('feed');
+ $is_global = false;
+ $country_code = '';
+
+ switch ($this->queriedContext) {
+ case 'Country':
+ $country_code = $this->getInput('country_code');
+ break;
+ }
+
+ $limit = $this->getInput('limit') ?? 30;
+
+ // https://rapidapi.com/unogs/api/unogsng/details
+ $api_url = sprintf(
+ '%s/api/search?query=%s%s&limit=%s',
+ self::URI,
+ urlencode($feed),
+ $country_code ? '&countrylist=' . $country_code : '',
+ $limit
+ );
+
+ $json_data = $this->getJSON($api_url);
+ $movies = $json_data['results'];
+
+ if ($this->getInput('feed') == 'expiring') {
+ /* uNoGS API returns movies/series that going to remove
+ * today according to the day you fetch the data.
+ * They put items that going to remove in the future on the last
+ * so I reverse this to get those items, not to bothers those that already removed today.
+ */
+ $movies = array_reverse($movies);
+ }
+
+ foreach ($movies as $movie) {
+ $this->handleData($movie);
+ }
+ }
}
diff --git a/bridges/UnraidCommunityApplicationsBridge.php b/bridges/UnraidCommunityApplicationsBridge.php
index c2cb3ace..5acd5049 100644
--- a/bridges/UnraidCommunityApplicationsBridge.php
+++ b/bridges/UnraidCommunityApplicationsBridge.php
@@ -1,70 +1,81 @@
<?php
-class UnraidCommunityApplicationsBridge extends BridgeAbstract {
- const NAME = 'Unraid Community Applications';
- const URI = 'https://forums.unraid.net/topic/38582-plug-in-community-applications/';
- const DESCRIPTION = 'Fetches the latest fifteen new apps/plugins from Unraid Community Applications';
- const MAINTAINER = 'Paroleen';
- const CACHE_TIMEOUT = 3600;
- const APPSURI = 'https://raw.githubusercontent.com/Squidly271/AppFeed/master/applicationFeed.json';
+class UnraidCommunityApplicationsBridge extends BridgeAbstract
+{
+ const NAME = 'Unraid Community Applications';
+ const URI = 'https://forums.unraid.net/topic/38582-plug-in-community-applications/';
+ const DESCRIPTION = 'Fetches the latest fifteen new apps/plugins from Unraid Community Applications';
+ const MAINTAINER = 'Paroleen';
+ const CACHE_TIMEOUT = 3600;
- private $apps = array();
+ const APPSURI = 'https://raw.githubusercontent.com/Squidly271/AppFeed/master/applicationFeed.json';
- private function fetchApps() {
- Debug::log('Fetching all applications/plugins');
- $this->apps = getContents(self::APPSURI);
- $this->apps = json_decode($this->apps, true)['applist'];
- }
+ private $apps = [];
- private function sortApps() {
- Debug::log('Sorting applications/plugins');
- usort($this->apps, function($app1, $app2) {
- return $app1['FirstSeen'] < $app2['FirstSeen'] ? 1 : -1;
- });
- }
+ private function fetchApps()
+ {
+ Debug::log('Fetching all applications/plugins');
+ $this->apps = getContents(self::APPSURI);
+ $this->apps = json_decode($this->apps, true)['applist'];
+ }
- public function collectData() {
- $this->fetchApps();
- $this->sortApps();
+ private function sortApps()
+ {
+ Debug::log('Sorting applications/plugins');
+ usort($this->apps, function ($app1, $app2) {
+ return $app1['FirstSeen'] < $app2['FirstSeen'] ? 1 : -1;
+ });
+ }
- Debug::log('Building RSS feed');
- foreach($this->apps as $app) {
- if(!array_key_exists('Language', $app)) {
- $item = array();
- $item['title'] = $app['Name'];
- $item['timestamp'] = $app['FirstSeen'];
- $item['author'] = explode('\'', $app['Repo'])[0];
- $item['categories'] = explode(' ', $app['Category']);
- $item['content'] = '';
+ public function collectData()
+ {
+ $this->fetchApps();
+ $this->sortApps();
- if(array_key_exists('Icon', $app))
- $item['content'] .= '<img style="width: 64px" src="'
- . $app['Icon']
- . '">';
+ Debug::log('Building RSS feed');
+ foreach ($this->apps as $app) {
+ if (!array_key_exists('Language', $app)) {
+ $item = [];
+ $item['title'] = $app['Name'];
+ $item['timestamp'] = $app['FirstSeen'];
+ $item['author'] = explode('\'', $app['Repo'])[0];
+ $item['categories'] = explode(' ', $app['Category']);
+ $item['content'] = '';
- if(array_key_exists('Overview', $app))
- $item['content'] .= '<p>'
- . $app['Overview']
- . '</p>';
+ if (array_key_exists('Icon', $app)) {
+ $item['content'] .= '<img style="width: 64px" src="'
+ . $app['Icon']
+ . '">';
+ }
- if(array_key_exists('Project', $app))
- $item['uri'] = $app['Project'];
+ if (array_key_exists('Overview', $app)) {
+ $item['content'] .= '<p>'
+ . $app['Overview']
+ . '</p>';
+ }
- if(array_key_exists('Registry', $app))
- $item['content'] .= '<br><a href="'
- . $app['Registry']
- . '">Docker Hub</a>';
+ if (array_key_exists('Project', $app)) {
+ $item['uri'] = $app['Project'];
+ }
- if(array_key_exists('Support', $app))
- $item['content'] .= '<br><a href="'
- . $app['Support']
- . '">Support</a>';
+ if (array_key_exists('Registry', $app)) {
+ $item['content'] .= '<br><a href="'
+ . $app['Registry']
+ . '">Docker Hub</a>';
+ }
- $this->items[] = $item;
+ if (array_key_exists('Support', $app)) {
+ $item['content'] .= '<br><a href="'
+ . $app['Support']
+ . '">Support</a>';
+ }
- if(count($this->items) >= 15)
- break;
- }
- }
- }
+ $this->items[] = $item;
+
+ if (count($this->items) >= 15) {
+ break;
+ }
+ }
+ }
+ }
}
diff --git a/bridges/UnsplashBridge.php b/bridges/UnsplashBridge.php
index 876dfe9d..590d16ab 100644
--- a/bridges/UnsplashBridge.php
+++ b/bridges/UnsplashBridge.php
@@ -2,112 +2,118 @@
class UnsplashBridge extends BridgeAbstract
{
- const MAINTAINER = 'nel50n, langfingaz';
- const NAME = 'Unsplash Bridge';
- const URI = 'https://unsplash.com/';
- const CACHE_TIMEOUT = 43200; // 12h
- const DESCRIPTION = 'Returns the latest photos from Unsplash';
+ const MAINTAINER = 'nel50n, langfingaz';
+ const NAME = 'Unsplash Bridge';
+ const URI = 'https://unsplash.com/';
+ const CACHE_TIMEOUT = 43200; // 12h
+ const DESCRIPTION = 'Returns the latest photos from Unsplash';
- const PARAMETERS = array(array(
- 'u' => array(
- 'name' => 'Filter by username (optional)',
- 'type' => 'text',
- 'defaultValue' => 'unsplash'
- ),
- 'm' => array(
- 'name' => 'Max number of photos',
- 'type' => 'number',
- 'defaultValue' => 20,
- 'required' => true
- ),
- 'prev_q' => array(
- 'name' => 'Preview quality',
- 'type' => 'list',
- 'values' => array(
- 'full' => 'full',
- 'regular' => 'regular',
- 'small' => 'small',
- 'thumb' => 'thumb',
- ),
- 'defaultValue' => 'regular'
- ),
- 'w' => array(
- 'name' => 'Max download width (optional)',
- 'exampleValue' => 1920,
- 'type' => 'number',
- 'defaultValue' => 1920,
- ),
- 'jpg_q' => array(
- 'name' => 'Max JPEG quality (optional)',
- 'exampleValue' => 75,
- 'type' => 'number',
- 'defaultValue' => 75,
- )
- ));
+ const PARAMETERS = [[
+ 'u' => [
+ 'name' => 'Filter by username (optional)',
+ 'type' => 'text',
+ 'defaultValue' => 'unsplash'
+ ],
+ 'm' => [
+ 'name' => 'Max number of photos',
+ 'type' => 'number',
+ 'defaultValue' => 20,
+ 'required' => true
+ ],
+ 'prev_q' => [
+ 'name' => 'Preview quality',
+ 'type' => 'list',
+ 'values' => [
+ 'full' => 'full',
+ 'regular' => 'regular',
+ 'small' => 'small',
+ 'thumb' => 'thumb',
+ ],
+ 'defaultValue' => 'regular'
+ ],
+ 'w' => [
+ 'name' => 'Max download width (optional)',
+ 'exampleValue' => 1920,
+ 'type' => 'number',
+ 'defaultValue' => 1920,
+ ],
+ 'jpg_q' => [
+ 'name' => 'Max JPEG quality (optional)',
+ 'exampleValue' => 75,
+ 'type' => 'number',
+ 'defaultValue' => 75,
+ ]
+ ]];
- public function collectData()
- {
- $filteredUser = $this->getInput('u');
- $width = $this->getInput('w');
- $max = $this->getInput('m');
- $previewQuality = $this->getInput('prev_q');
- $jpgQuality = $this->getInput('jpg_q');
+ public function collectData()
+ {
+ $filteredUser = $this->getInput('u');
+ $width = $this->getInput('w');
+ $max = $this->getInput('m');
+ $previewQuality = $this->getInput('prev_q');
+ $jpgQuality = $this->getInput('jpg_q');
- $url = 'https://unsplash.com/napi';
- if (strlen($filteredUser) > 0) $url .= '/users/' . $filteredUser;
- $url .= '/photos?page=1&per_page=' . $max;
- $api_response = getContents($url);
+ $url = 'https://unsplash.com/napi';
+ if (strlen($filteredUser) > 0) {
+ $url .= '/users/' . $filteredUser;
+ }
+ $url .= '/photos?page=1&per_page=' . $max;
+ $api_response = getContents($url);
- $json = json_decode($api_response, true);
+ $json = json_decode($api_response, true);
- foreach ($json as $json_item) {
- $item = array();
+ foreach ($json as $json_item) {
+ $item = [];
- // Get image URI
- $uri = $json_item['urls']['raw'] . '&fm=jpg';
- if ($jpgQuality > 0) $uri .= '&q=' . $jpgQuality;
- if ($width > 0) $uri .= '&w=' . $width . '&fit=max';
- $uri .= '.jpg'; // only for format hint
- $item['uri'] = $uri;
+ // Get image URI
+ $uri = $json_item['urls']['raw'] . '&fm=jpg';
+ if ($jpgQuality > 0) {
+ $uri .= '&q=' . $jpgQuality;
+ }
+ if ($width > 0) {
+ $uri .= '&w=' . $width . '&fit=max';
+ }
+ $uri .= '.jpg'; // only for format hint
+ $item['uri'] = $uri;
- // Get title from description
- if (is_null($json_item['description'])) {
- $item['title'] = 'Unsplash picture from ' . $json_item['user']['name'];
- } else {
- $item['title'] = $json_item['description'];
- }
+ // Get title from description
+ if (is_null($json_item['description'])) {
+ $item['title'] = 'Unsplash picture from ' . $json_item['user']['name'];
+ } else {
+ $item['title'] = $json_item['description'];
+ }
- $item['timestamp'] = $json_item['created_at'];
- $content = 'User: <a href="'
- . $json_item['user']['links']['html']
- . '">@'
- . $json_item['user']['username']
- . '</a>';
- if (isset($json_item['location']['name'])) {
- $content .= ' | Location: ' . $json_item['location']['name'];
- }
- $content .= ' | Image on <a href="'
- . $json_item['links']['html']
- . '">Unsplash</a><br><a href="'
- . $uri
- . '"><img src="'
- . $json_item['urls'][$previewQuality]
- . '" alt="Image from '
- . $filteredUser
- . '" /></a>';
- $item['content'] = $content;
+ $item['timestamp'] = $json_item['created_at'];
+ $content = 'User: <a href="'
+ . $json_item['user']['links']['html']
+ . '">@'
+ . $json_item['user']['username']
+ . '</a>';
+ if (isset($json_item['location']['name'])) {
+ $content .= ' | Location: ' . $json_item['location']['name'];
+ }
+ $content .= ' | Image on <a href="'
+ . $json_item['links']['html']
+ . '">Unsplash</a><br><a href="'
+ . $uri
+ . '"><img src="'
+ . $json_item['urls'][$previewQuality]
+ . '" alt="Image from '
+ . $filteredUser
+ . '" /></a>';
+ $item['content'] = $content;
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+ }
+ }
- public function getName()
- {
- $filteredUser = $this->getInput('u') ?? '';
- if (strlen($filteredUser) > 0) {
- return $filteredUser . ' - ' . self::NAME;
- } else {
- return self::NAME;
- }
- }
+ public function getName()
+ {
+ $filteredUser = $this->getInput('u') ?? '';
+ if (strlen($filteredUser) > 0) {
+ return $filteredUser . ' - ' . self::NAME;
+ } else {
+ return self::NAME;
+ }
+ }
}
diff --git a/bridges/UrlebirdBridge.php b/bridges/UrlebirdBridge.php
index 98a16aae..429e93f5 100644
--- a/bridges/UrlebirdBridge.php
+++ b/bridges/UrlebirdBridge.php
@@ -1,72 +1,77 @@
<?php
-class UrlebirdBridge extends BridgeAbstract {
- const MAINTAINER = 'dotter-ak';
- const NAME = 'urlebird.com';
- const URI = 'https://urlebird.com/';
- const DESCRIPTION = 'Bridge for urlebird.com';
- const CACHE_TIMEOUT = 10;
- const PARAMETERS = array(
- array(
- 'query' => array(
- 'name' => '@username or #hashtag',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => '@willsmith',
- 'title' => '@username or #hashtag'
- )
- )
- );
+class UrlebirdBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'dotter-ak';
+ const NAME = 'urlebird.com';
+ const URI = 'https://urlebird.com/';
+ const DESCRIPTION = 'Bridge for urlebird.com';
+ const CACHE_TIMEOUT = 10;
+ const PARAMETERS = [
+ [
+ 'query' => [
+ 'name' => '@username or #hashtag',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => '@willsmith',
+ 'title' => '@username or #hashtag'
+ ]
+ ]
+ ];
- private $title;
+ private $title;
- private function fixURI($uri) {
- $path = parse_url($uri, PHP_URL_PATH);
- $encoded_path = array_map('urlencode', explode('/', $path));
- return str_replace($path, implode('/', $encoded_path), $uri);
- }
+ private function fixURI($uri)
+ {
+ $path = parse_url($uri, PHP_URL_PATH);
+ $encoded_path = array_map('urlencode', explode('/', $path));
+ return str_replace($path, implode('/', $encoded_path), $uri);
+ }
- public function collectData() {
- switch($this->getInput('query')[0]) {
- default:
- returnServerError('Please, enter valid username or hashtag!');
- break;
- case '@':
- $url = 'https://urlebird.com/user/' . substr($this->getInput('query'), 1) . '/';
- break;
- case '#':
- $url = 'https://urlebird.com/hash/' . substr($this->getInput('query'), 1) . '/';
- break;
- }
+ public function collectData()
+ {
+ switch ($this->getInput('query')[0]) {
+ default:
+ returnServerError('Please, enter valid username or hashtag!');
+ break;
+ case '@':
+ $url = 'https://urlebird.com/user/' . substr($this->getInput('query'), 1) . '/';
+ break;
+ case '#':
+ $url = 'https://urlebird.com/hash/' . substr($this->getInput('query'), 1) . '/';
+ break;
+ }
- $html = getSimpleHTMLDOM($url);
- $this->title = $html->find('title', 0)->innertext;
- $articles = $html->find('div.thumb');
- foreach ($articles as $article) {
- $item = array();
- $item['uri'] = $this->fixURI($article->find('a', 2)->href);
- $article_content = getSimpleHTMLDOM($item['uri']);
- $item['author'] = $article->find('img', 0)->alt . ' (' .
- $article_content->find('a.user-video', 1)->innertext . ')';
- $item['title'] = $article_content->find('title', 0)->innertext;
- $item['enclosures'][] = $article_content->find('video', 0)->poster;
- $video = $article_content->find('video', 0);
- $video->autoplay = null;
- $item['content'] = $video->outertext . '<br>' .
- $article_content->find('div.music', 0) . '<br>' .
- $article_content->find('div.info2', 0)->innertext .
- '<br><br><a href="' . $article_content->find('video', 0)->src .
- '">Direct video link</a><br><br><a href="' . $item['uri'] .
- '">Post link</a><br><br>';
- $this->items[] = $item;
- }
- }
+ $html = getSimpleHTMLDOM($url);
+ $this->title = $html->find('title', 0)->innertext;
+ $articles = $html->find('div.thumb');
+ foreach ($articles as $article) {
+ $item = [];
+ $item['uri'] = $this->fixURI($article->find('a', 2)->href);
+ $article_content = getSimpleHTMLDOM($item['uri']);
+ $item['author'] = $article->find('img', 0)->alt . ' (' .
+ $article_content->find('a.user-video', 1)->innertext . ')';
+ $item['title'] = $article_content->find('title', 0)->innertext;
+ $item['enclosures'][] = $article_content->find('video', 0)->poster;
+ $video = $article_content->find('video', 0);
+ $video->autoplay = null;
+ $item['content'] = $video->outertext . '<br>' .
+ $article_content->find('div.music', 0) . '<br>' .
+ $article_content->find('div.info2', 0)->innertext .
+ '<br><br><a href="' . $article_content->find('video', 0)->src .
+ '">Direct video link</a><br><br><a href="' . $item['uri'] .
+ '">Post link</a><br><br>';
+ $this->items[] = $item;
+ }
+ }
- public function getName() {
- return $this->title ?: parent::getName();
- }
+ public function getName()
+ {
+ return $this->title ?: parent::getName();
+ }
- public function getIcon() {
- return 'https://urlebird.com/favicon.ico';
- }
+ public function getIcon()
+ {
+ return 'https://urlebird.com/favicon.ico';
+ }
}
diff --git a/bridges/UsbekEtRicaBridge.php b/bridges/UsbekEtRicaBridge.php
index d5fd507a..3dd432f0 100644
--- a/bridges/UsbekEtRicaBridge.php
+++ b/bridges/UsbekEtRicaBridge.php
@@ -1,111 +1,115 @@
<?php
-class UsbekEtRicaBridge extends BridgeAbstract {
-
- const MAINTAINER = 'logmanoriginal';
- const NAME = 'Usbek & Rica Bridge';
- const URI = 'https://usbeketrica.com';
- const DESCRIPTION = 'Returns latest articles from the front page';
-
- const PARAMETERS = array(
- array(
- 'limit' => array(
- 'name' => 'Number of articles to return',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Specifies the maximum number of articles to return',
- 'defaultValue' => -1
- ),
- 'fullarticle' => array(
- 'name' => 'Load full article',
- 'type' => 'checkbox',
- 'required' => false,
- 'title' => 'Activate to load full articles',
- )
- )
- );
-
- public function collectData(){
- $limit = $this->getInput('limit');
- $fullarticle = $this->getInput('fullarticle');
- $html = getSimpleHTMLDOM($this->getURI());
-
- $articles = $html->find('article');
-
- foreach($articles as $article) {
- $item = array();
-
- $title = $article->find('h2', 0);
- if($title) {
- $item['title'] = $title->plaintext;
- } else {
- // Sometimes we get rubbish, ignore.
- continue;
- }
-
- $author = $article->find('div.author span', 0);
- if($author) {
- $item['author'] = $author->plaintext;
- }
-
- $u = $article->find('a.card-img', 0);
-
- $uri = $u->href;
- if(substr($uri, 0, 1) === 'h') { // absolute uri
- $item['uri'] = $uri;
- } else { // relative uri
- $item['uri'] = $this->getURI() . $uri;
- }
-
- if($fullarticle) {
- $content = $this->loadFullArticle($item['uri']);
- }
-
- if($fullarticle && !is_null($content)) {
- $item['content'] = $content;
- } else {
- $excerpt = $article->find('div.card-excerpt', 0);
- if($excerpt) {
- $item['content'] = $excerpt->plaintext;
- }
- }
-
- $image = $article->find('div.card-img img', 0);
- if($image) {
- $item['enclosures'] = array(
- $image->src
- );
- }
-
- $this->items[] = $item;
-
- if($limit > 0 && count($this->items) >= $limit) {
- break;
- }
- }
- }
-
- /**
- * Loads the full article and returns the contents
- * @param $uri The article URI
- * @return The article content
- */
- private function loadFullArticle($uri){
- $html = getSimpleHTMLDOMCached($uri);
-
- $content = $html->find('div.rich-text', 1);
- if($content) {
- return $this->replaceUriInHtmlElement($content);
- }
-
- return null;
- }
-
- /**
- * Replaces all relative URIs with absolute ones
- * @param $element A simplehtmldom element
- * @return The $element->innertext with all URIs replaced
- */
- private function replaceUriInHtmlElement($element){
- return str_replace('href="/', 'href="' . $this->getURI() . '/', $element->innertext);
- }
+
+class UsbekEtRicaBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Usbek & Rica Bridge';
+ const URI = 'https://usbeketrica.com';
+ const DESCRIPTION = 'Returns latest articles from the front page';
+
+ const PARAMETERS = [
+ [
+ 'limit' => [
+ 'name' => 'Number of articles to return',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Specifies the maximum number of articles to return',
+ 'defaultValue' => -1
+ ],
+ 'fullarticle' => [
+ 'name' => 'Load full article',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'title' => 'Activate to load full articles',
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $limit = $this->getInput('limit');
+ $fullarticle = $this->getInput('fullarticle');
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $articles = $html->find('article');
+
+ foreach ($articles as $article) {
+ $item = [];
+
+ $title = $article->find('h2', 0);
+ if ($title) {
+ $item['title'] = $title->plaintext;
+ } else {
+ // Sometimes we get rubbish, ignore.
+ continue;
+ }
+
+ $author = $article->find('div.author span', 0);
+ if ($author) {
+ $item['author'] = $author->plaintext;
+ }
+
+ $u = $article->find('a.card-img', 0);
+
+ $uri = $u->href;
+ if (substr($uri, 0, 1) === 'h') { // absolute uri
+ $item['uri'] = $uri;
+ } else { // relative uri
+ $item['uri'] = $this->getURI() . $uri;
+ }
+
+ if ($fullarticle) {
+ $content = $this->loadFullArticle($item['uri']);
+ }
+
+ if ($fullarticle && !is_null($content)) {
+ $item['content'] = $content;
+ } else {
+ $excerpt = $article->find('div.card-excerpt', 0);
+ if ($excerpt) {
+ $item['content'] = $excerpt->plaintext;
+ }
+ }
+
+ $image = $article->find('div.card-img img', 0);
+ if ($image) {
+ $item['enclosures'] = [
+ $image->src
+ ];
+ }
+
+ $this->items[] = $item;
+
+ if ($limit > 0 && count($this->items) >= $limit) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Loads the full article and returns the contents
+ * @param $uri The article URI
+ * @return The article content
+ */
+ private function loadFullArticle($uri)
+ {
+ $html = getSimpleHTMLDOMCached($uri);
+
+ $content = $html->find('div.rich-text', 1);
+ if ($content) {
+ return $this->replaceUriInHtmlElement($content);
+ }
+
+ return null;
+ }
+
+ /**
+ * Replaces all relative URIs with absolute ones
+ * @param $element A simplehtmldom element
+ * @return The $element->innertext with all URIs replaced
+ */
+ private function replaceUriInHtmlElement($element)
+ {
+ return str_replace('href="/', 'href="' . $this->getURI() . '/', $element->innertext);
+ }
}
diff --git a/bridges/UsenixBridge.php b/bridges/UsenixBridge.php
index 4f785a0e..659f012d 100644
--- a/bridges/UsenixBridge.php
+++ b/bridges/UsenixBridge.php
@@ -1,68 +1,69 @@
<?php
+
declare(strict_types=1);
final class UsenixBridge extends BridgeAbstract
{
- const NAME = 'USENIX';
- const URI = 'https://www.usenix.org/publications';
- const DESCRIPTION = 'Digital publications from USENIX (usenix.org)';
- const MAINTAINER = 'dvikan';
- const PARAMETERS = [
- 'USENIX ;login:' => [
- ],
- ];
+ const NAME = 'USENIX';
+ const URI = 'https://www.usenix.org/publications';
+ const DESCRIPTION = 'Digital publications from USENIX (usenix.org)';
+ const MAINTAINER = 'dvikan';
+ const PARAMETERS = [
+ 'USENIX ;login:' => [
+ ],
+ ];
- public function collectData()
- {
- if ($this->queriedContext === 'USENIX ;login:') {
- $this->collectLoginOnlineItems();
- return;
- }
- returnClientError('Illegal Context');
- }
+ public function collectData()
+ {
+ if ($this->queriedContext === 'USENIX ;login:') {
+ $this->collectLoginOnlineItems();
+ return;
+ }
+ returnClientError('Illegal Context');
+ }
- private function collectLoginOnlineItems(): void
- {
- $url = 'https://www.usenix.org/publications/loginonline';
- $dom = getSimpleHTMLDOMCached($url);
- $items = $dom->find('div.view-content > div');
+ private function collectLoginOnlineItems(): void
+ {
+ $url = 'https://www.usenix.org/publications/loginonline';
+ $dom = getSimpleHTMLDOMCached($url);
+ $items = $dom->find('div.view-content > div');
- foreach ($items as $item) {
- $title = $item->find('.views-field-title > span', 0);
- $author = $item->find('.views-field-pseudo-author-list > span.field-content', 0);
- $relativeUrl = $item->find('.views-field-nothing-1 > span > a', 0);
- $uri = sprintf('https://www.usenix.org%s', $relativeUrl->href);
- // June 2, 2022
- $createdAt = $item->find('div.views-field-field-lv2-publication-date > div > span', 0);
+ foreach ($items as $item) {
+ $title = $item->find('.views-field-title > span', 0);
+ $author = $item->find('.views-field-pseudo-author-list > span.field-content', 0);
+ $relativeUrl = $item->find('.views-field-nothing-1 > span > a', 0);
+ $uri = sprintf('https://www.usenix.org%s', $relativeUrl->href);
+ // June 2, 2022
+ $createdAt = $item->find('div.views-field-field-lv2-publication-date > div > span', 0);
- $item = [
- 'title' => $title->innertext,
- 'author' => strstr($author->plaintext, ',', true) ?: $author->plaintext,
- 'uri' => $uri,
- 'timestamp' => $createdAt->innertext,
- ];
+ $item = [
+ 'title' => $title->innertext,
+ 'author' => strstr($author->plaintext, ',', true) ?: $author->plaintext,
+ 'uri' => $uri,
+ 'timestamp' => $createdAt->innertext,
+ ];
- $this->items[] = array_merge($item, $this->getItemContent($uri));
- }
- }
+ $this->items[] = array_merge($item, $this->getItemContent($uri));
+ }
+ }
- private function getItemContent(string $uri) : array
- {
- $html = getSimpleHTMLDOMCached($uri);
- $content = $html->find('.paragraphs-items-full', 0)->innertext;
- $extra = $html->find('fieldset', 0);
- if (!empty($extra)) {
- $content .= $extra->innertext;
- }
+ private function getItemContent(string $uri): array
+ {
+ $html = getSimpleHTMLDOMCached($uri);
+ $content = $html->find('.paragraphs-items-full', 0)->innertext;
+ $extra = $html->find('fieldset', 0);
+ if (!empty($extra)) {
+ $content .= $extra->innertext;
+ }
- $tags = [];
- foreach($html->find('.field-name-field-lv2-tags div.field-item') as $tag) {
- $tags[] = $tag->plaintext;
- }
+ $tags = [];
+ foreach ($html->find('.field-name-field-lv2-tags div.field-item') as $tag) {
+ $tags[] = $tag->plaintext;
+ }
- return [
- 'content' => $content,
- 'categories' => $tags
- ];
- }
+ return [
+ 'content' => $content,
+ 'categories' => $tags
+ ];
+ }
}
diff --git a/bridges/VarietyBridge.php b/bridges/VarietyBridge.php
index 8bc48f46..23d1df3f 100644
--- a/bridges/VarietyBridge.php
+++ b/bridges/VarietyBridge.php
@@ -1,30 +1,33 @@
<?php
-class VarietyBridge extends FeedExpander {
- const MAINTAINER = 'IceWreck';
- const NAME = 'Variety Bridge';
- const URI = 'https://variety.com';
- const CACHE_TIMEOUT = 3600;
- const DESCRIPTION = 'RSS feed for Variety';
+class VarietyBridge extends FeedExpander
+{
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'Variety Bridge';
+ const URI = 'https://variety.com';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'RSS feed for Variety';
- public function collectData(){
- $this->collectExpandableDatas('https://feeds.feedburner.com/variety/headlines', 15);
- }
+ public function collectData()
+ {
+ $this->collectExpandableDatas('https://feeds.feedburner.com/variety/headlines', 15);
+ }
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- // $articlePage gets the entire page's contents
- $articlePage = getSimpleHTMLDOM($newsItem->link);
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
- // Remove Script tags
- foreach($articlePage->find('script') as $script_tag) {
- $script_tag->remove();
- }
- $article = $articlePage->find('div.c-featured-media', 0);
- $article = $article . $articlePage->find('.c-content', 0);
+ // Remove Script tags
+ foreach ($articlePage->find('script') as $script_tag) {
+ $script_tag->remove();
+ }
+ $article = $articlePage->find('div.c-featured-media', 0);
+ $article = $article . $articlePage->find('.c-content', 0);
- $item['content'] = $article;
+ $item['content'] = $article;
- return $item;
- }
+ return $item;
+ }
}
diff --git a/bridges/ViadeoCompanyBridge.php b/bridges/ViadeoCompanyBridge.php
index fd1a29b6..3b147c41 100644
--- a/bridges/ViadeoCompanyBridge.php
+++ b/bridges/ViadeoCompanyBridge.php
@@ -1,40 +1,43 @@
<?php
-class ViadeoCompanyBridge extends BridgeAbstract {
- const MAINTAINER = 'regisenguehard';
- const NAME = 'Viadeo Company';
- const URI = 'https://www.viadeo.com/';
- const CACHE_TIMEOUT = 21600; // 6h
- const DESCRIPTION = 'Returns most recent actus from Company on Viadeo.
+class ViadeoCompanyBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'regisenguehard';
+ const NAME = 'Viadeo Company';
+ const URI = 'https://www.viadeo.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns most recent actus from Company on Viadeo.
(http://www.viadeo.com/fr/company/<strong style="font-weight:bold;">apple</strong>)';
- const PARAMETERS = array( array(
- 'c' => array(
- 'name' => 'Company name',
- 'exampleValue' => 'apple',
- 'required' => true
- )
- ));
+ const PARAMETERS = [ [
+ 'c' => [
+ 'name' => 'Company name',
+ 'exampleValue' => 'apple',
+ 'required' => true
+ ]
+ ]];
- public function collectData(){
- // Redirects to https://emploi.lefigaro.fr/recherche/entreprises
- $url = sprintf('%sfr/company/%s', self::URI, $this->getInput('c'));
+ public function collectData()
+ {
+ // Redirects to https://emploi.lefigaro.fr/recherche/entreprises
+ $url = sprintf('%sfr/company/%s', self::URI, $this->getInput('c'));
- $html = getSimpleHTMLDOM($url);
+ $html = getSimpleHTMLDOM($url);
- // TODO: Fix broken xpath selector
- $elements = $html->find('//*[@id="company-newsfeed"]/ul/li');
+ // TODO: Fix broken xpath selector
+ $elements = $html->find('//*[@id="company-newsfeed"]/ul/li');
- foreach($elements as $element) {
- $title = $element->find('p', 0)->innertext;
- if(!$title) {
- continue;
- }
- $item = array();
- $item['uri'] = $url;
- $item['title'] = mb_substr($element->find('p', 0)->innertext, 0, 100);
- $item['content'] = $element->find('p', 0)->innertext;;
- $this->items[] = $item;
- }
- }
+ foreach ($elements as $element) {
+ $title = $element->find('p', 0)->innertext;
+ if (!$title) {
+ continue;
+ }
+ $item = [];
+ $item['uri'] = $url;
+ $item['title'] = mb_substr($element->find('p', 0)->innertext, 0, 100);
+ $item['content'] = $element->find('p', 0)->innertext;
+ ;
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/ViceBridge.php b/bridges/ViceBridge.php
index 4dccb8ef..14272517 100644
--- a/bridges/ViceBridge.php
+++ b/bridges/ViceBridge.php
@@ -1,38 +1,42 @@
<?php
-class ViceBridge extends FeedExpander {
- const MAINTAINER = 'IceWreck';
- const NAME = 'Vice Bridge';
- const URI = 'https://www.vice.com/';
- const CACHE_TIMEOUT = 3600; // This is a news site, so don't cache for more than 10 mins
- const DESCRIPTION = 'RSS feed for vice publications like Vice News, Munchies, Motherboard, etc.';
- const PARAMETERS = array( array(
- 'feed' => array(
- 'name' => 'Feed',
- 'type' => 'list',
- 'values' => array(
- 'Vice News' => 'rss',
- 'Motherboard - Tech' => 'en_us/rss/topic/tech',
- 'Entertainment' => 'en_us/rss/topic/entertainment',
- 'Noisey - Music' => 'en_us/rss/topic/music',
- 'Munchies - Food' => 'en_us/rss/topic/food'
- )
- )
- ));
- public function collectData(){
- $feed = $this->getInput('feed');
- $feedURL = 'https://www.vice.com/' . $feed;
- $this->collectExpandableDatas($feedURL, 10);
- }
+class ViceBridge extends FeedExpander
+{
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'Vice Bridge';
+ const URI = 'https://www.vice.com/';
+ const CACHE_TIMEOUT = 3600; // This is a news site, so don't cache for more than 10 mins
+ const DESCRIPTION = 'RSS feed for vice publications like Vice News, Munchies, Motherboard, etc.';
+ const PARAMETERS = [ [
+ 'feed' => [
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'values' => [
+ 'Vice News' => 'rss',
+ 'Motherboard - Tech' => 'en_us/rss/topic/tech',
+ 'Entertainment' => 'en_us/rss/topic/entertainment',
+ 'Noisey - Music' => 'en_us/rss/topic/music',
+ 'Munchies - Food' => 'en_us/rss/topic/food'
+ ]
+ ]
+ ]];
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- // $articlePage gets the entire page's contents
- $articlePage = getSimpleHTMLDOM($newsItem->link);
- // text and embedded content
- $article = $article . $articlePage->find('.article__body', 0);
- $item['content'] = $article;
+ public function collectData()
+ {
+ $feed = $this->getInput('feed');
+ $feedURL = 'https://www.vice.com/' . $feed;
+ $this->collectExpandableDatas($feedURL, 10);
+ }
- return $item;
- }
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+ // text and embedded content
+ $article = $article . $articlePage->find('.article__body', 0);
+ $item['content'] = $article;
+
+ return $item;
+ }
}
diff --git a/bridges/VieDeMerdeBridge.php b/bridges/VieDeMerdeBridge.php
index d9c906e5..9e6166fb 100644
--- a/bridges/VieDeMerdeBridge.php
+++ b/bridges/VieDeMerdeBridge.php
@@ -1,56 +1,58 @@
<?php
-class VieDeMerdeBridge extends BridgeAbstract {
-
- const MAINTAINER = 'floviolleau';
- const NAME = 'VieDeMerde Bridge';
- const URI = 'https://www.viedemerde.fr';
- const DESCRIPTION = 'Returns latest quotes from VieDeMerde.';
- const CACHE_TIMEOUT = 7200;
-
- const PARAMETERS = array(array(
- 'item_limit' => array(
- 'name' => 'Limit number of returned items',
- 'type' => 'number',
- 'defaultValue' => 20
- )
- ));
-
- public function collectData() {
- $limit = $this->getInput('item_limit');
-
- if ($limit < 1) {
- $limit = 20;
- }
-
- $html = getSimpleHTMLDOM(self::URI, array());
- $quotes = $html->find('article.bg-white');
- if(sizeof($quotes) === 0) {
- return;
- }
-
- foreach($quotes as $quote) {
- $item = array();
- $item['uri'] = self::URI . $quote->find('a', 0)->href;
- $titleContent = $quote->find('h2', 0);
-
- if($titleContent) {
- $item['title'] = html_entity_decode($titleContent->plaintext, ENT_QUOTES);
- } else {
- continue;
- }
-
- $quoteText = $quote->find('a', 1)->plaintext;
- $isAVDM = $quote->find('.vote-btn', 0)->plaintext;
- $isNotAVDM = $quote->find('.vote-btn', 1)->plaintext;
- $item['content'] = $quoteText . '<br>' . $isAVDM . '<br>' . $isNotAVDM;
- $item['author'] = $quote->find('p', 0)->plaintext;
- $item['uid'] = hash('sha256', $item['title']);
-
- $this->items[] = $item;
-
- if (count($this->items) >= $limit) {
- break;
- }
- }
- }
+
+class VieDeMerdeBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'floviolleau';
+ const NAME = 'VieDeMerde Bridge';
+ const URI = 'https://www.viedemerde.fr';
+ const DESCRIPTION = 'Returns latest quotes from VieDeMerde.';
+ const CACHE_TIMEOUT = 7200;
+
+ const PARAMETERS = [[
+ 'item_limit' => [
+ 'name' => 'Limit number of returned items',
+ 'type' => 'number',
+ 'defaultValue' => 20
+ ]
+ ]];
+
+ public function collectData()
+ {
+ $limit = $this->getInput('item_limit');
+
+ if ($limit < 1) {
+ $limit = 20;
+ }
+
+ $html = getSimpleHTMLDOM(self::URI, []);
+ $quotes = $html->find('article.bg-white');
+ if (sizeof($quotes) === 0) {
+ return;
+ }
+
+ foreach ($quotes as $quote) {
+ $item = [];
+ $item['uri'] = self::URI . $quote->find('a', 0)->href;
+ $titleContent = $quote->find('h2', 0);
+
+ if ($titleContent) {
+ $item['title'] = html_entity_decode($titleContent->plaintext, ENT_QUOTES);
+ } else {
+ continue;
+ }
+
+ $quoteText = $quote->find('a', 1)->plaintext;
+ $isAVDM = $quote->find('.vote-btn', 0)->plaintext;
+ $isNotAVDM = $quote->find('.vote-btn', 1)->plaintext;
+ $item['content'] = $quoteText . '<br>' . $isAVDM . '<br>' . $isNotAVDM;
+ $item['author'] = $quote->find('p', 0)->plaintext;
+ $item['uid'] = hash('sha256', $item['title']);
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= $limit) {
+ break;
+ }
+ }
+ }
}
diff --git a/bridges/VimeoBridge.php b/bridges/VimeoBridge.php
index d97026d6..80bfb8ac 100644
--- a/bridges/VimeoBridge.php
+++ b/bridges/VimeoBridge.php
@@ -1,175 +1,197 @@
<?php
-class VimeoBridge extends BridgeAbstract {
-
- const NAME = 'Vimeo Bridge';
- const URI = 'https://vimeo.com/';
- const DESCRIPTION = 'Returns search results from Vimeo';
- const MAINTAINER = 'logmanoriginal';
-
- const PARAMETERS = array(
- array(
- 'q' => array(
- 'name' => 'Search Query',
- 'type' => 'text',
- 'exampleValue' => 'birds',
- 'required' => true
- ),
- 'type' => array(
- 'name' => 'Show results for',
- 'type' => 'list',
- 'defaultValue' => 'Videos',
- 'values' => array(
- 'Videos' => 'search',
- 'On Demand' => 'search/ondemand',
- 'People' => 'search/people',
- 'Channels' => 'search/channels',
- 'Groups' => 'search/groups'
- )
- )
- )
- );
-
- public function getURI() {
- if(($query = $this->getInput('q'))
- && ($type = $this->getInput('type'))) {
- return self::URI . $type . '/sort:latest?q=' . $query;
- }
-
- return parent::getURI();
- }
-
- public function collectData() {
-
- $html = getSimpleHTMLDOM($this->getURI(),
- $header = array(),
- $opts = array(),
- $lowercase = true,
- $forceTagsClosed = true,
- $target_charset = DEFAULT_TARGET_CHARSET,
- $stripRN = false, // We want to keep newline characters
- $defaultBRText = DEFAULT_BR_TEXT,
- $defaultSpanText = DEFAULT_SPAN_TEXT);
-
- $json = null; // Holds the JSON data
-
- /**
- * Search results are included as JSON formatted string inside a script
- * tag that has the variable 'vimeo.config'. The data is condensed into
- * a single line of code, so we can just search for the newline.
- *
- * Everything after "vimeo.config = _extend((vimeo.config || {}), " is
- * the JSON formatted string.
- */
- foreach($html->find('script') as $script) {
- foreach(explode("\n", $script) as $line) {
- $line = trim($line);
-
- if(strpos($line, 'vimeo.config') !== 0)
- continue;
-
- // 45 = strlen("vimeo.config = _extend((vimeo.config || {}), ");
- // 47 = 45 + 2, because we don't want the final ");"
- $json = json_decode(substr($line, 45, strlen($line) - 47));
- }
- }
-
- if(is_null($json)) {
- returnClientError('No results for this query!');
- }
-
- foreach($json->api->initial_json->data as $element) {
- switch($element->type) {
- case 'clip': $this->addClip($element); break;
- case 'ondemand': $this->addOnDemand($element); break;
- case 'people': $this->addPeople($element); break;
- case 'channel': $this->addChannel($element); break;
- case 'group': $this->addGroup($element); break;
-
- default: returnServerError('Unknown type: ' . $element->type);
- }
- }
-
- }
-
- private function addClip($element) {
- $item = array();
-
- $item['uri'] = $element->clip->link;
- $item['title'] = $element->clip->name;
- $item['author'] = $element->clip->user->name;
- $item['timestamp'] = strtotime($element->clip->created_time);
-
- $item['enclosures'] = array(
- end($element->clip->pictures->sizes)->link
- );
-
- $item['content'] = "<img src={$item['enclosures'][0]} />";
-
- $this->items[] = $item;
- }
-
- private function addOnDemand($element) {
- $item = array();
-
- $item['uri'] = $element->ondemand->link;
- $item['title'] = $element->ondemand->name;
-
- // Only for films
- if(isset($element->ondemand->film))
- $item['timestamp'] = strtotime($element->ondemand->film->release_time);
+class VimeoBridge extends BridgeAbstract
+{
+ const NAME = 'Vimeo Bridge';
+ const URI = 'https://vimeo.com/';
+ const DESCRIPTION = 'Returns search results from Vimeo';
+ const MAINTAINER = 'logmanoriginal';
+
+ const PARAMETERS = [
+ [
+ 'q' => [
+ 'name' => 'Search Query',
+ 'type' => 'text',
+ 'exampleValue' => 'birds',
+ 'required' => true
+ ],
+ 'type' => [
+ 'name' => 'Show results for',
+ 'type' => 'list',
+ 'defaultValue' => 'Videos',
+ 'values' => [
+ 'Videos' => 'search',
+ 'On Demand' => 'search/ondemand',
+ 'People' => 'search/people',
+ 'Channels' => 'search/channels',
+ 'Groups' => 'search/groups'
+ ]
+ ]
+ ]
+ ];
+
+ public function getURI()
+ {
+ if (
+ ($query = $this->getInput('q'))
+ && ($type = $this->getInput('type'))
+ ) {
+ return self::URI . $type . '/sort:latest?q=' . $query;
+ }
+
+ return parent::getURI();
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM(
+ $this->getURI(),
+ $header = [],
+ $opts = [],
+ $lowercase = true,
+ $forceTagsClosed = true,
+ $target_charset = DEFAULT_TARGET_CHARSET,
+ $stripRN = false, // We want to keep newline characters
+ $defaultBRText = DEFAULT_BR_TEXT,
+ $defaultSpanText = DEFAULT_SPAN_TEXT
+ );
+
+ $json = null; // Holds the JSON data
+
+ /**
+ * Search results are included as JSON formatted string inside a script
+ * tag that has the variable 'vimeo.config'. The data is condensed into
+ * a single line of code, so we can just search for the newline.
+ *
+ * Everything after "vimeo.config = _extend((vimeo.config || {}), " is
+ * the JSON formatted string.
+ */
+ foreach ($html->find('script') as $script) {
+ foreach (explode("\n", $script) as $line) {
+ $line = trim($line);
+
+ if (strpos($line, 'vimeo.config') !== 0) {
+ continue;
+ }
+
+ // 45 = strlen("vimeo.config = _extend((vimeo.config || {}), ");
+ // 47 = 45 + 2, because we don't want the final ");"
+ $json = json_decode(substr($line, 45, strlen($line) - 47));
+ }
+ }
+
+ if (is_null($json)) {
+ returnClientError('No results for this query!');
+ }
+
+ foreach ($json->api->initial_json->data as $element) {
+ switch ($element->type) {
+ case 'clip':
+ $this->addClip($element);
+ break;
+ case 'ondemand':
+ $this->addOnDemand($element);
+ break;
+ case 'people':
+ $this->addPeople($element);
+ break;
+ case 'channel':
+ $this->addChannel($element);
+ break;
+ case 'group':
+ $this->addGroup($element);
+ break;
+
+ default:
+ returnServerError('Unknown type: ' . $element->type);
+ }
+ }
+ }
+
+ private function addClip($element)
+ {
+ $item = [];
+
+ $item['uri'] = $element->clip->link;
+ $item['title'] = $element->clip->name;
+ $item['author'] = $element->clip->user->name;
+ $item['timestamp'] = strtotime($element->clip->created_time);
+
+ $item['enclosures'] = [
+ end($element->clip->pictures->sizes)->link
+ ];
+
+ $item['content'] = "<img src={$item['enclosures'][0]} />";
+
+ $this->items[] = $item;
+ }
+
+ private function addOnDemand($element)
+ {
+ $item = [];
+
+ $item['uri'] = $element->ondemand->link;
+ $item['title'] = $element->ondemand->name;
+
+ // Only for films
+ if (isset($element->ondemand->film)) {
+ $item['timestamp'] = strtotime($element->ondemand->film->release_time);
+ }
+
+ $item['enclosures'] = [
+ end($element->ondemand->pictures->sizes)->link
+ ];
+
+ $item['content'] = "<img src={$item['enclosures'][0]} />";
+
+ $this->items[] = $item;
+ }
+
+ private function addPeople($element)
+ {
+ $item = [];
+
+ $item['uri'] = $element->people->link;
+ $item['title'] = $element->people->name;
+
+ $item['enclosures'] = [
+ end($element->people->pictures->sizes)->link
+ ];
+
+ $item['content'] = "<img src={$item['enclosures'][0]} />";
+
+ $this->items[] = $item;
+ }
+
+ private function addChannel($element)
+ {
+ $item = [];
+
+ $item['uri'] = $element->channel->link;
+ $item['title'] = $element->channel->name;
- $item['enclosures'] = array(
- end($element->ondemand->pictures->sizes)->link
- );
+ $item['enclosures'] = [
+ end($element->channel->pictures->sizes)->link
+ ];
- $item['content'] = "<img src={$item['enclosures'][0]} />";
+ $item['content'] = "<img src={$item['enclosures'][0]} />";
- $this->items[] = $item;
- }
+ $this->items[] = $item;
+ }
- private function addPeople($element) {
- $item = array();
+ private function addGroup($element)
+ {
+ $item = [];
- $item['uri'] = $element->people->link;
- $item['title'] = $element->people->name;
+ $item['uri'] = $element->group->link;
+ $item['title'] = $element->group->name;
- $item['enclosures'] = array(
- end($element->people->pictures->sizes)->link
- );
+ $item['enclosures'] = [
+ end($element->group->pictures->sizes)->link
+ ];
- $item['content'] = "<img src={$item['enclosures'][0]} />";
+ $item['content'] = "<img src={$item['enclosures'][0]} />";
- $this->items[] = $item;
- }
-
- private function addChannel($element) {
- $item = array();
-
- $item['uri'] = $element->channel->link;
- $item['title'] = $element->channel->name;
-
- $item['enclosures'] = array(
- end($element->channel->pictures->sizes)->link
- );
-
- $item['content'] = "<img src={$item['enclosures'][0]} />";
-
- $this->items[] = $item;
- }
-
- private function addGroup($element) {
- $item = array();
-
- $item['uri'] = $element->group->link;
- $item['title'] = $element->group->name;
-
- $item['enclosures'] = array(
- end($element->group->pictures->sizes)->link
- );
-
- $item['content'] = "<img src={$item['enclosures'][0]} />";
-
- $this->items[] = $item;
- }
+ $this->items[] = $item;
+ }
}
diff --git a/bridges/VixenBridge.php b/bridges/VixenBridge.php
index 721524e9..048b9a7b 100644
--- a/bridges/VixenBridge.php
+++ b/bridges/VixenBridge.php
@@ -1,99 +1,111 @@
<?php
-class VixenBridge extends BridgeAbstract {
- const NAME = 'Vixen Network Bridge';
- const URI = 'https://www.vixen.com';
- const DESCRIPTION = 'Latest videos from Vixen Network sites';
- const MAINTAINER = 'pubak42';
- /**
- * The pictures on the pages are referenced with temporary links with
- * limited validity. Greater cache timeout results in invalid links in
- * the feed
- */
- const CACHE_TIMEOUT = 60;
+class VixenBridge extends BridgeAbstract
+{
+ const NAME = 'Vixen Network Bridge';
+ const URI = 'https://www.vixen.com';
+ const DESCRIPTION = 'Latest videos from Vixen Network sites';
+ const MAINTAINER = 'pubak42';
- const PARAMETERS = array(
- array(
- 'site' => array(
- 'type' => 'list',
- 'name' => 'Site',
- 'title' => 'Choose site of interest',
- 'values' => array(
- 'Blacked' => 'Blacked',
- 'BlackedRaw' => 'BlackedRaw',
- 'Tushy' => 'Tushy',
- 'TushyRaw' => 'TushyRaw',
- 'Vixen' => 'Vixen',
- 'Slayed' => 'Slayed',
- 'Deeper' => 'Deeper'
- ),
- )
- )
- );
+ /**
+ * The pictures on the pages are referenced with temporary links with
+ * limited validity. Greater cache timeout results in invalid links in
+ * the feed
+ */
+ const CACHE_TIMEOUT = 60;
- public function collectData() {
- $videosURL = $this->getURI() . '/videos';
+ const PARAMETERS = [
+ [
+ 'site' => [
+ 'type' => 'list',
+ 'name' => 'Site',
+ 'title' => 'Choose site of interest',
+ 'values' => [
+ 'Blacked' => 'Blacked',
+ 'BlackedRaw' => 'BlackedRaw',
+ 'Tushy' => 'Tushy',
+ 'TushyRaw' => 'TushyRaw',
+ 'Vixen' => 'Vixen',
+ 'Slayed' => 'Slayed',
+ 'Deeper' => 'Deeper'
+ ],
+ ]
+ ]
+ ];
- $website = getSimpleHTMLDOM($videosURL);
- $json = $website->getElementById('__NEXT_DATA__');
- $data = json_decode($json->innertext(), true);
- $nodes = array_column($data['props']['pageProps']['edges'], 'node');
+ public function collectData()
+ {
+ $videosURL = $this->getURI() . '/videos';
- foreach($nodes as $n) {
- $imageURL = $n['images']['listing'][2]['highdpi']['triple'];
+ $website = getSimpleHTMLDOM($videosURL);
+ $json = $website->getElementById('__NEXT_DATA__');
+ $data = json_decode($json->innertext(), true);
+ $nodes = array_column($data['props']['pageProps']['edges'], 'node');
- $item = [
- 'title' => $n['title'],
- 'uri' => "$videosURL/$n[slug]",
- 'uid' => $n['videoId'],
- 'timestamp' => strtotime($n['releaseDate']),
- 'enclosures' => [ $imageURL ],
- 'author' => implode(' & ', array_column($n['modelsSlugged'], 'name')),
- ];
+ foreach ($nodes as $n) {
+ $imageURL = $n['images']['listing'][2]['highdpi']['triple'];
- /*
- * No images retrieved from here. Should be cached for as long as
- * possible to avoid rate throttling
- */
- $target = getSimpleHtmlDOMCached($item['uri'], 86400);
- $item['content'] = $this->generateContent($imageURL,
- $target->find('meta[name=description]', 0)->content,
- $n['modelsSlugged']);
+ $item = [
+ 'title' => $n['title'],
+ 'uri' => "$videosURL/$n[slug]",
+ 'uid' => $n['videoId'],
+ 'timestamp' => strtotime($n['releaseDate']),
+ 'enclosures' => [ $imageURL ],
+ 'author' => implode(' & ', array_column($n['modelsSlugged'], 'name')),
+ ];
- $item['categories'] = array_map('ucwords',
- explode(',', $target->find('meta[name=keywords]', 0)->content));
+ /*
+ * No images retrieved from here. Should be cached for as long as
+ * possible to avoid rate throttling
+ */
+ $target = getSimpleHtmlDOMCached($item['uri'], 86400);
+ $item['content'] = $this->generateContent(
+ $imageURL,
+ $target->find('meta[name=description]', 0)->content,
+ $n['modelsSlugged']
+ );
- $this->items[] = $item;
- }
- }
+ $item['categories'] = array_map(
+ 'ucwords',
+ explode(',', $target->find('meta[name=keywords]', 0)->content)
+ );
- public function getURI() {
- $param = $this->getInput('site');
- return $param ? "https://www.$param.com" : self::URI;
- }
+ $this->items[] = $item;
+ }
+ }
- /**
- * Return name of the bridge. Default is needed for bridge index list
- */
- public function getName() {
- $param = $this->getInput('site');
- return $param ? "$param Bridge" : self::NAME;
- }
+ public function getURI()
+ {
+ $param = $this->getInput('site');
+ return $param ? "https://www.$param.com" : self::URI;
+ }
- private static function makeLink($URI, $text) {
- return "<a href=\"$URI\">$text</a>";
- }
+ /**
+ * Return name of the bridge. Default is needed for bridge index list
+ */
+ public function getName()
+ {
+ $param = $this->getInput('site');
+ return $param ? "$param Bridge" : self::NAME;
+ }
- private function generateContent($imageURI, $description, $models) {
- $content = "<img src=\"$imageURI\" referrerpolicy=\"no-referrer\"/><p>$description</p>";
- $modelLinks = array_map(
- function($model) {
- return self::makeLink(
- $this->getURI() . "/models/$model[slugged]",
- $model['name']);
- },
- $models
- );
- return $content . '<p>Starring: ' . implode(' & ', $modelLinks) . '</p>';
- }
+ private static function makeLink($URI, $text)
+ {
+ return "<a href=\"$URI\">$text</a>";
+ }
+
+ private function generateContent($imageURI, $description, $models)
+ {
+ $content = "<img src=\"$imageURI\" referrerpolicy=\"no-referrer\"/><p>$description</p>";
+ $modelLinks = array_map(
+ function ($model) {
+ return self::makeLink(
+ $this->getURI() . "/models/$model[slugged]",
+ $model['name']
+ );
+ },
+ $models
+ );
+ return $content . '<p>Starring: ' . implode(' & ', $modelLinks) . '</p>';
+ }
}
diff --git a/bridges/VkBridge.php b/bridges/VkBridge.php
index 1d0f65b0..bb554bc1 100644
--- a/bridges/VkBridge.php
+++ b/bridges/VkBridge.php
@@ -2,463 +2,476 @@
class VkBridge extends BridgeAbstract
{
-
- const MAINTAINER = 'em92';
- // const MAINTAINER = 'pmaziere';
- // const MAINTAINER = 'ahiles3005';
- const NAME = 'VK.com';
- const URI = 'https://vk.com/';
- const CACHE_TIMEOUT = 300; // 5min
- const DESCRIPTION = 'Working with open pages';
- const PARAMETERS = array(
- array(
- 'u' => array(
- 'name' => 'Group or user name',
- 'exampleValue' => 'elonmusk_tech',
- 'required' => true
- ),
- 'hide_reposts' => array(
- 'name' => 'Hide reposts',
- 'type' => 'checkbox',
- )
- )
- );
-
- protected $videos = array();
- protected $pageName;
-
- protected function getAccessToken()
- {
- return 'e69b2db9f6cd4a97c0716893232587165c18be85bc1af1834560125c1d3c8ec281eb407a78cca0ae16776';
- }
-
- public function getURI()
- {
- if (!is_null($this->getInput('u'))) {
- return static::URI . urlencode($this->getInput('u'));
- }
-
- return parent::getURI();
- }
-
- public function getName()
- {
- if ($this->pageName) {
- return $this->pageName;
- }
-
- return parent::getName();
- }
-
- public function collectData()
- {
- $text_html = $this->getContents();
-
- $text_html = iconv('windows-1251', 'utf-8//ignore', $text_html);
- // makes album link generating work correctly
- $text_html = str_replace('"class="page_album_link">', '" class="page_album_link">', $text_html);
- $html = str_get_html($text_html);
- $pageName = $html->find('.page_name', 0);
- if (is_object($pageName)) {
- $pageName = $pageName->plaintext;
- $this->pageName = htmlspecialchars_decode($pageName);
- }
- foreach ($html->find('div.replies') as $comment_block) {
- $comment_block->outertext = '';
- }
- $html->load($html->save());
-
- $pinned_post_item = null;
- $last_post_id = 0;
-
- foreach ($html->find('.post') as $post) {
-
- if ($post->find('.wall_post_text_deleted')) {
- // repost of deleted post
- continue;
- }
-
- defaultLinkTo($post, self::URI);
-
- $post_videos = array();
-
- $is_pinned_post = false;
- if (strpos($post->getAttribute('class'), 'post_fixed') !== false) {
- $is_pinned_post = true;
- }
-
- if (is_object($post->find('a.wall_post_more', 0))) {
- //delete link "show full" in content
- $post->find('a.wall_post_more', 0)->outertext = '';
- }
-
- $content_suffix = '';
-
- // looking for external links
- $external_link_selectors = array(
- 'a.page_media_link_title',
- 'div.page_media_link_title > a',
- 'div.media_desc > a.lnk',
- );
-
- foreach($external_link_selectors as $sel) {
- if (is_object($post->find($sel, 0))) {
- $a = $post->find($sel, 0);
- $innertext = $a->innertext;
- $parsed_url = parse_url($a->getAttribute('href'));
- if (strpos($parsed_url['path'], '/away.php') !== 0) continue;
- parse_str($parsed_url['query'], $parsed_query);
- $content_suffix .= "<br>External link: <a href='" . $parsed_query['to'] . "'>$innertext</a>";
- }
- }
-
- // remove external link from content
- $external_link_selectors_to_remove = array(
- 'div.page_media_thumbed_link',
- 'div.page_media_link_desc_wrap',
- 'div.media_desc > a.lnk',
- );
-
- foreach($external_link_selectors_to_remove as $sel) {
- if (is_object($post->find($sel, 0))) {
- $post->find($sel, 0)->outertext = '';
- }
- }
-
- // looking for article
- $article = $post->find('a.article_snippet', 0);
- if (is_object($article)) {
- if (strpos($article->getAttribute('class'), 'article_snippet_mini') !== false) {
- $article_title_selector = 'div.article_snippet_mini_title';
- $article_author_selector = 'div.article_snippet_mini_info > .mem_link,
+ const MAINTAINER = 'em92';
+ // const MAINTAINER = 'pmaziere';
+ // const MAINTAINER = 'ahiles3005';
+ const NAME = 'VK.com';
+ const URI = 'https://vk.com/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Working with open pages';
+ const PARAMETERS = [
+ [
+ 'u' => [
+ 'name' => 'Group or user name',
+ 'exampleValue' => 'elonmusk_tech',
+ 'required' => true
+ ],
+ 'hide_reposts' => [
+ 'name' => 'Hide reposts',
+ 'type' => 'checkbox',
+ ]
+ ]
+ ];
+
+ protected $videos = [];
+ protected $pageName;
+
+ protected function getAccessToken()
+ {
+ return 'e69b2db9f6cd4a97c0716893232587165c18be85bc1af1834560125c1d3c8ec281eb407a78cca0ae16776';
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('u'))) {
+ return static::URI . urlencode($this->getInput('u'));
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if ($this->pageName) {
+ return $this->pageName;
+ }
+
+ return parent::getName();
+ }
+
+ public function collectData()
+ {
+ $text_html = $this->getContents();
+
+ $text_html = iconv('windows-1251', 'utf-8//ignore', $text_html);
+ // makes album link generating work correctly
+ $text_html = str_replace('"class="page_album_link">', '" class="page_album_link">', $text_html);
+ $html = str_get_html($text_html);
+ $pageName = $html->find('.page_name', 0);
+ if (is_object($pageName)) {
+ $pageName = $pageName->plaintext;
+ $this->pageName = htmlspecialchars_decode($pageName);
+ }
+ foreach ($html->find('div.replies') as $comment_block) {
+ $comment_block->outertext = '';
+ }
+ $html->load($html->save());
+
+ $pinned_post_item = null;
+ $last_post_id = 0;
+
+ foreach ($html->find('.post') as $post) {
+ if ($post->find('.wall_post_text_deleted')) {
+ // repost of deleted post
+ continue;
+ }
+
+ defaultLinkTo($post, self::URI);
+
+ $post_videos = [];
+
+ $is_pinned_post = false;
+ if (strpos($post->getAttribute('class'), 'post_fixed') !== false) {
+ $is_pinned_post = true;
+ }
+
+ if (is_object($post->find('a.wall_post_more', 0))) {
+ //delete link "show full" in content
+ $post->find('a.wall_post_more', 0)->outertext = '';
+ }
+
+ $content_suffix = '';
+
+ // looking for external links
+ $external_link_selectors = [
+ 'a.page_media_link_title',
+ 'div.page_media_link_title > a',
+ 'div.media_desc > a.lnk',
+ ];
+
+ foreach ($external_link_selectors as $sel) {
+ if (is_object($post->find($sel, 0))) {
+ $a = $post->find($sel, 0);
+ $innertext = $a->innertext;
+ $parsed_url = parse_url($a->getAttribute('href'));
+ if (strpos($parsed_url['path'], '/away.php') !== 0) {
+ continue;
+ }
+ parse_str($parsed_url['query'], $parsed_query);
+ $content_suffix .= "<br>External link: <a href='" . $parsed_query['to'] . "'>$innertext</a>";
+ }
+ }
+
+ // remove external link from content
+ $external_link_selectors_to_remove = [
+ 'div.page_media_thumbed_link',
+ 'div.page_media_link_desc_wrap',
+ 'div.media_desc > a.lnk',
+ ];
+
+ foreach ($external_link_selectors_to_remove as $sel) {
+ if (is_object($post->find($sel, 0))) {
+ $post->find($sel, 0)->outertext = '';
+ }
+ }
+
+ // looking for article
+ $article = $post->find('a.article_snippet', 0);
+ if (is_object($article)) {
+ if (strpos($article->getAttribute('class'), 'article_snippet_mini') !== false) {
+ $article_title_selector = 'div.article_snippet_mini_title';
+ $article_author_selector = 'div.article_snippet_mini_info > .mem_link,
div.article_snippet_mini_info > .group_link';
- $article_thumb_selector = 'div.article_snippet_mini_thumb';
- } else {
- $article_title_selector = 'div.article_snippet__title';
- $article_author_selector = 'div.article_snippet__author';
- $article_thumb_selector = 'div.article_snippet__image';
- }
- $article_title = $article->find($article_title_selector, 0)->innertext;
- $article_author = $article->find($article_author_selector, 0)->innertext;
- $article_link = $article->getAttribute('href');
- $article_img_element_style = $article->find($article_thumb_selector, 0)->getAttribute('style');
- preg_match('/background-image: url\((.*)\)/', $article_img_element_style, $matches);
- if (count($matches) > 0) {
- $content_suffix .= "<br><img src='" . $matches[1] . "'>";
- }
- $content_suffix .= "<br>Article: <a href='$article_link'>$article_title ($article_author)</a>";
- $article->outertext = '';
- }
-
- // get video on post
- $video = $post->find('div.post_video_desc', 0);
- $main_video_link = '';
- if (is_object($video)) {
- $video_title = $video->find('div.post_video_title', 0)->plaintext;
- $video_link = $video->find('a.lnk', 0)->getAttribute('href');
- $this->appendVideo($video_title, $video_link, $content_suffix, $post_videos);
- $video->outertext = '';
- $main_video_link = $video_link;
- }
-
- // get all other videos
- foreach($post->find('a.page_post_thumb_video') as $a) {
- $video_title = htmlspecialchars_decode($a->getAttribute('aria-label'));
- $video_link = $a->getAttribute('href');
- if ($video_link != $main_video_link) $this->appendVideo($video_title, $video_link, $content_suffix, $post_videos);
- $a->outertext = '';
- }
-
- // get all photos
- foreach($post->find('div.wall_text a.page_post_thumb_wrap') as $a) {
- $result = $this->getPhoto($a);
- if ($result == null) continue;
- $a->outertext = '';
- $content_suffix .= "<br>$result";
- }
-
- // get albums
- foreach($post->find('.page_album_wrap') as $el) {
- $a = $el->find('.page_album_link', 0);
- $album_title = $a->find('.page_album_title_text', 0)->getAttribute('title');
- $album_link = $a->getAttribute('href');
- $el->outertext = '';
- $content_suffix .= "<br>Album: <a href='$album_link'>$album_title</a>";
- }
-
- // get photo documents
- foreach($post->find('a.page_doc_photo_href') as $a) {
- $doc_link = $a->getAttribute('href');
- $doc_gif_label_element = $a->find('.page_gif_label', 0);
- $doc_title_element = $a->find('.doc_label', 0);
-
- if (is_object($doc_gif_label_element)) {
- $gif_preview_img = backgroundToImg($a->find('.page_doc_photo', 0));
- $content_suffix .= "<br>Gif: <a href='$doc_link'>$gif_preview_img</a>";
-
- } else if (is_object($doc_title_element)) {
- $doc_title = $doc_title_element->innertext;
- $content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>";
-
- } else {
- continue;
-
- }
-
- $a->outertext = '';
- }
-
- // get other documents
- foreach($post->find('div.page_doc_row') as $div) {
- $doc_title_element = $div->find('a.page_doc_title', 0);
-
- if (is_object($doc_title_element)) {
- $doc_title = $doc_title_element->innertext;
- $doc_link = $doc_title_element->getAttribute('href');
- $content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>";
-
- } else {
- continue;
-
- }
-
- $div->outertext = '';
- }
-
- // get polls
- foreach($post->find('div.page_media_poll_wrap') as $div) {
- $poll_title = $div->find('.page_media_poll_title', 0)->innertext;
- $content_suffix .= "<br>Poll: $poll_title";
- foreach($div->find('div.page_poll_text') as $poll_stat_title) {
- $content_suffix .= '<br>- ' . $poll_stat_title->innertext;
- }
- $div->outertext = '';
- }
-
- // get sign / post author
- $post_author = $pageName;
- $author_selectors = array('a.wall_signed_by', 'a.author');
- foreach($author_selectors as $author_selector) {
- $a = $post->find($author_selector, 0);
- if (is_object($a)) {
- $post_author = $a->innertext;
- $a->outertext = '';
- break;
- }
- }
-
- // fix links and get post hashtags
- $hashtags = array();
- foreach($post->find('a') as $a) {
- $href = $a->getAttribute('href');
- $innertext = $a->innertext;
-
- $hashtag_prefix = '/feed?section=search&q=%23';
- $hashtag = null;
-
- if ($href && substr($href, 0, strlen($hashtag_prefix)) === $hashtag_prefix) {
- $hashtag = urldecode(substr($href, strlen($hashtag_prefix)));
- } else if (substr($innertext, 0, 1) == '#') {
- $hashtag = $innertext;
- }
-
- if ($hashtag) {
- $a->outertext = $innertext;
- $hashtags[] = $hashtag;
- continue;
- }
-
- $parsed_url = parse_url($href);
-
- if (array_key_exists('path', $parsed_url) === false) continue;
-
- if (strpos($parsed_url['path'], '/away.php') === 0) {
- parse_str($parsed_url['query'], $parsed_query);
- $a->setAttribute('href', iconv(
- 'windows-1251',
- 'utf-8//ignore',
- $parsed_query['to']
- ));
- }
- }
-
- $copy_quote = $post->find('div.copy_quote', 0);
- if (is_object($copy_quote)) {
- if ($this->getInput('hide_reposts') === true) {
- continue;
- }
- if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) {
- $copy_post_header->outertext = '';
- }
-
- $second_copy_quote = $copy_quote->find('div.published_sec_quote', 0);
- if (is_object($second_copy_quote)) {
- $second_copy_quote_author = $second_copy_quote->find('a.copy_author', 0)->outertext;
- $second_copy_quote_content = $second_copy_quote->find('div.copy_post_date', 0)->outertext;
- $second_copy_quote->outertext = "<br>Reposted ($second_copy_quote_author): $second_copy_quote_content";
- }
- $copy_quote_author = $copy_quote->find('a.copy_author', 0)->outertext;
- $copy_quote_content = $copy_quote->innertext;
- $copy_quote->outertext = "<br>Reposted ($copy_quote_author): <br>$copy_quote_content";
- }
-
- $item = array();
- $item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<a><br><img>');
- $item['content'] .= $content_suffix;
- $item['categories'] = $hashtags;
-
- // get post link
- $post_link = $post->find('a.post_link', 0)->getAttribute('href');
- preg_match('/wall-?\d+_(\d+)/', $post_link, $preg_match_result);
- $item['post_id'] = intval($preg_match_result[1]);
- $item['uri'] = $post_link;
- $item['timestamp'] = $this->getTime($post);
- $item['title'] = $this->getTitle($item['content']);
- $item['author'] = $post_author;
- $item['videos'] = $post_videos;
- if ($is_pinned_post) {
- // do not append it now
- $pinned_post_item = $item;
- } else {
- $last_post_id = $item['post_id'];
- $this->items[] = $item;
- }
-
- }
-
- if (!is_null($pinned_post_item)) {
- if (count($this->items) == 0) {
- $this->items[] = $pinned_post_item;
- } else if ($last_post_id < $pinned_post_item['post_id']) {
- $this->items[] = $pinned_post_item;
- usort($this->items, function ($item1, $item2) {
- return $item2['post_id'] - $item1['post_id'];
- });
- }
- }
-
- $this->getCleanVideoLinks();
- }
-
- private function getPhoto($a) {
- $onclick = $a->getAttribute('onclick');
- preg_match('/return showPhoto\(.+?({.*})/', $onclick, $preg_match_result);
- if (count($preg_match_result) == 0) return;
-
- $arg = htmlspecialchars_decode( str_replace('queue:1', '"queue":1', $preg_match_result[1]) );
- $data = json_decode($arg, true);
- if ($data == null) return;
-
- $thumb = $data['temp']['base'] . $data['temp']['x_'][0];
- $original = '';
- foreach(array('y_', 'z_', 'w_') as $key) {
- if (!isset($data['temp'][$key])) continue;
- if (!isset($data['temp'][$key][0])) continue;
- if (substr($data['temp'][$key][0], 0, 4) == 'http') {
- $base = '';
- } else {
- $base = $data['temp']['base'];
- }
- $original = $base . $data['temp'][$key][0];
- }
-
- if ($original) {
- return "<a href='$original'><img src='$thumb'></a>";
- } else {
- return "<img src='$thumb'>";
- }
- }
-
- private function getTitle($content)
- {
- preg_match('/^["\w\ \p{L}\(\)\?#«»-]+/mu', htmlspecialchars_decode($content), $result);
- if (count($result) == 0) return 'untitled';
- return $result[0];
- }
-
- private function getTime($post)
- {
- if ($time = $post->find('span.rel_date', 0)->getAttribute('time')) {
- return $time;
- } else {
- $strdate = $post->find('span.rel_date', 0)->plaintext;
- $strdate = preg_replace('/[\x00-\x1F\x7F-\xFF]/', ' ', $strdate);
-
- $date = date_parse($strdate);
- if (!$date['year']) {
- if (strstr($strdate, 'today') !== false) {
- $strdate = date('d-m-Y') . ' ' . $strdate;
- } elseif (strstr($strdate, 'yesterday ') !== false) {
- $time = time() - 60 * 60 * 24;
- $strdate = date('d-m-Y', $time) . ' ' . $strdate;
- } elseif ($date['month'] && intval(date('m')) < $date['month']) {
- $strdate = $strdate . ' ' . (date('Y') - 1);
- } else {
- $strdate = $strdate . ' ' . date('Y');
- }
-
- $date = date_parse($strdate);
- } elseif ($date['hour'] === false) {
- $date['hour'] = $date['minute'] = '00';
- }
- return strtotime($date['day'] . '-' . $date['month'] . '-' . $date['year'] . ' ' .
- $date['hour'] . ':' . $date['minute']);
- }
-
- }
-
- private function getContents()
- {
- $header = array('Accept-language: en', 'Cookie: remixlang=3');
-
- return getContents($this->getURI(), $header);
- }
-
- protected function appendVideo($video_title, $video_link, &$content_suffix, array &$post_videos)
- {
- if (!$video_title) $video_title = '(empty)';
-
- preg_match('/video([0-9-]+_[0-9]+)/', $video_link, $preg_match_result);
-
- if (count($preg_match_result) > 1) {
- $video_id = $preg_match_result[1];
- $this->videos[ $video_id ] = array(
- 'url' => $video_link,
- 'title' => $video_title,
- );
- $post_videos[] = $video_id;
- } else {
- $content_suffix .= '<br>Video: <a href="' . htmlspecialchars($video_link) . '">' . $video_title . '</a>';
- }
- }
-
- protected function getCleanVideoLinks() {
- $result = $this->api('video.get', array(
- 'videos' => implode(',', array_keys($this->videos)),
- 'count' => 200
- ));
-
- if (!isset($result['error'])) {
- foreach($result['response']['items'] as $item) {
- $video_id = strval($item['owner_id']) . '_' . strval($item['id']);
- $this->videos[$video_id]['url'] = $item['player'];
- }
- }
-
- foreach($this->items as &$item) {
- foreach($item['videos'] as $video_id) {
- $video_link = $this->videos[$video_id]['url'];
- $video_title = $this->videos[$video_id]['title'];
- $item['content'] .= '<br>Video: <a href="' . htmlspecialchars($video_link) . '">' . $video_title . '</a>';
- }
- unset($item['videos']);
- }
- }
-
- protected function api($method, array $params)
- {
- $params['v'] = '5.80';
- $params['access_token'] = $this->getAccessToken();
- return json_decode( getContents('https://api.vk.com/method/' . $method . '?' . http_build_query($params)), true );
- }
+ $article_thumb_selector = 'div.article_snippet_mini_thumb';
+ } else {
+ $article_title_selector = 'div.article_snippet__title';
+ $article_author_selector = 'div.article_snippet__author';
+ $article_thumb_selector = 'div.article_snippet__image';
+ }
+ $article_title = $article->find($article_title_selector, 0)->innertext;
+ $article_author = $article->find($article_author_selector, 0)->innertext;
+ $article_link = $article->getAttribute('href');
+ $article_img_element_style = $article->find($article_thumb_selector, 0)->getAttribute('style');
+ preg_match('/background-image: url\((.*)\)/', $article_img_element_style, $matches);
+ if (count($matches) > 0) {
+ $content_suffix .= "<br><img src='" . $matches[1] . "'>";
+ }
+ $content_suffix .= "<br>Article: <a href='$article_link'>$article_title ($article_author)</a>";
+ $article->outertext = '';
+ }
+
+ // get video on post
+ $video = $post->find('div.post_video_desc', 0);
+ $main_video_link = '';
+ if (is_object($video)) {
+ $video_title = $video->find('div.post_video_title', 0)->plaintext;
+ $video_link = $video->find('a.lnk', 0)->getAttribute('href');
+ $this->appendVideo($video_title, $video_link, $content_suffix, $post_videos);
+ $video->outertext = '';
+ $main_video_link = $video_link;
+ }
+
+ // get all other videos
+ foreach ($post->find('a.page_post_thumb_video') as $a) {
+ $video_title = htmlspecialchars_decode($a->getAttribute('aria-label'));
+ $video_link = $a->getAttribute('href');
+ if ($video_link != $main_video_link) {
+ $this->appendVideo($video_title, $video_link, $content_suffix, $post_videos);
+ }
+ $a->outertext = '';
+ }
+
+ // get all photos
+ foreach ($post->find('div.wall_text a.page_post_thumb_wrap') as $a) {
+ $result = $this->getPhoto($a);
+ if ($result == null) {
+ continue;
+ }
+ $a->outertext = '';
+ $content_suffix .= "<br>$result";
+ }
+
+ // get albums
+ foreach ($post->find('.page_album_wrap') as $el) {
+ $a = $el->find('.page_album_link', 0);
+ $album_title = $a->find('.page_album_title_text', 0)->getAttribute('title');
+ $album_link = $a->getAttribute('href');
+ $el->outertext = '';
+ $content_suffix .= "<br>Album: <a href='$album_link'>$album_title</a>";
+ }
+
+ // get photo documents
+ foreach ($post->find('a.page_doc_photo_href') as $a) {
+ $doc_link = $a->getAttribute('href');
+ $doc_gif_label_element = $a->find('.page_gif_label', 0);
+ $doc_title_element = $a->find('.doc_label', 0);
+
+ if (is_object($doc_gif_label_element)) {
+ $gif_preview_img = backgroundToImg($a->find('.page_doc_photo', 0));
+ $content_suffix .= "<br>Gif: <a href='$doc_link'>$gif_preview_img</a>";
+ } elseif (is_object($doc_title_element)) {
+ $doc_title = $doc_title_element->innertext;
+ $content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>";
+ } else {
+ continue;
+ }
+
+ $a->outertext = '';
+ }
+
+ // get other documents
+ foreach ($post->find('div.page_doc_row') as $div) {
+ $doc_title_element = $div->find('a.page_doc_title', 0);
+
+ if (is_object($doc_title_element)) {
+ $doc_title = $doc_title_element->innertext;
+ $doc_link = $doc_title_element->getAttribute('href');
+ $content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>";
+ } else {
+ continue;
+ }
+
+ $div->outertext = '';
+ }
+
+ // get polls
+ foreach ($post->find('div.page_media_poll_wrap') as $div) {
+ $poll_title = $div->find('.page_media_poll_title', 0)->innertext;
+ $content_suffix .= "<br>Poll: $poll_title";
+ foreach ($div->find('div.page_poll_text') as $poll_stat_title) {
+ $content_suffix .= '<br>- ' . $poll_stat_title->innertext;
+ }
+ $div->outertext = '';
+ }
+
+ // get sign / post author
+ $post_author = $pageName;
+ $author_selectors = ['a.wall_signed_by', 'a.author'];
+ foreach ($author_selectors as $author_selector) {
+ $a = $post->find($author_selector, 0);
+ if (is_object($a)) {
+ $post_author = $a->innertext;
+ $a->outertext = '';
+ break;
+ }
+ }
+
+ // fix links and get post hashtags
+ $hashtags = [];
+ foreach ($post->find('a') as $a) {
+ $href = $a->getAttribute('href');
+ $innertext = $a->innertext;
+
+ $hashtag_prefix = '/feed?section=search&q=%23';
+ $hashtag = null;
+
+ if ($href && substr($href, 0, strlen($hashtag_prefix)) === $hashtag_prefix) {
+ $hashtag = urldecode(substr($href, strlen($hashtag_prefix)));
+ } elseif (substr($innertext, 0, 1) == '#') {
+ $hashtag = $innertext;
+ }
+
+ if ($hashtag) {
+ $a->outertext = $innertext;
+ $hashtags[] = $hashtag;
+ continue;
+ }
+
+ $parsed_url = parse_url($href);
+
+ if (array_key_exists('path', $parsed_url) === false) {
+ continue;
+ }
+
+ if (strpos($parsed_url['path'], '/away.php') === 0) {
+ parse_str($parsed_url['query'], $parsed_query);
+ $a->setAttribute('href', iconv(
+ 'windows-1251',
+ 'utf-8//ignore',
+ $parsed_query['to']
+ ));
+ }
+ }
+
+ $copy_quote = $post->find('div.copy_quote', 0);
+ if (is_object($copy_quote)) {
+ if ($this->getInput('hide_reposts') === true) {
+ continue;
+ }
+ if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) {
+ $copy_post_header->outertext = '';
+ }
+
+ $second_copy_quote = $copy_quote->find('div.published_sec_quote', 0);
+ if (is_object($second_copy_quote)) {
+ $second_copy_quote_author = $second_copy_quote->find('a.copy_author', 0)->outertext;
+ $second_copy_quote_content = $second_copy_quote->find('div.copy_post_date', 0)->outertext;
+ $second_copy_quote->outertext = "<br>Reposted ($second_copy_quote_author): $second_copy_quote_content";
+ }
+ $copy_quote_author = $copy_quote->find('a.copy_author', 0)->outertext;
+ $copy_quote_content = $copy_quote->innertext;
+ $copy_quote->outertext = "<br>Reposted ($copy_quote_author): <br>$copy_quote_content";
+ }
+
+ $item = [];
+ $item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<a><br><img>');
+ $item['content'] .= $content_suffix;
+ $item['categories'] = $hashtags;
+
+ // get post link
+ $post_link = $post->find('a.post_link', 0)->getAttribute('href');
+ preg_match('/wall-?\d+_(\d+)/', $post_link, $preg_match_result);
+ $item['post_id'] = intval($preg_match_result[1]);
+ $item['uri'] = $post_link;
+ $item['timestamp'] = $this->getTime($post);
+ $item['title'] = $this->getTitle($item['content']);
+ $item['author'] = $post_author;
+ $item['videos'] = $post_videos;
+ if ($is_pinned_post) {
+ // do not append it now
+ $pinned_post_item = $item;
+ } else {
+ $last_post_id = $item['post_id'];
+ $this->items[] = $item;
+ }
+ }
+
+ if (!is_null($pinned_post_item)) {
+ if (count($this->items) == 0) {
+ $this->items[] = $pinned_post_item;
+ } elseif ($last_post_id < $pinned_post_item['post_id']) {
+ $this->items[] = $pinned_post_item;
+ usort($this->items, function ($item1, $item2) {
+ return $item2['post_id'] - $item1['post_id'];
+ });
+ }
+ }
+
+ $this->getCleanVideoLinks();
+ }
+
+ private function getPhoto($a)
+ {
+ $onclick = $a->getAttribute('onclick');
+ preg_match('/return showPhoto\(.+?({.*})/', $onclick, $preg_match_result);
+ if (count($preg_match_result) == 0) {
+ return;
+ }
+
+ $arg = htmlspecialchars_decode(str_replace('queue:1', '"queue":1', $preg_match_result[1]));
+ $data = json_decode($arg, true);
+ if ($data == null) {
+ return;
+ }
+
+ $thumb = $data['temp']['base'] . $data['temp']['x_'][0];
+ $original = '';
+ foreach (['y_', 'z_', 'w_'] as $key) {
+ if (!isset($data['temp'][$key])) {
+ continue;
+ }
+ if (!isset($data['temp'][$key][0])) {
+ continue;
+ }
+ if (substr($data['temp'][$key][0], 0, 4) == 'http') {
+ $base = '';
+ } else {
+ $base = $data['temp']['base'];
+ }
+ $original = $base . $data['temp'][$key][0];
+ }
+
+ if ($original) {
+ return "<a href='$original'><img src='$thumb'></a>";
+ } else {
+ return "<img src='$thumb'>";
+ }
+ }
+
+ private function getTitle($content)
+ {
+ preg_match('/^["\w\ \p{L}\(\)\?#«»-]+/mu', htmlspecialchars_decode($content), $result);
+ if (count($result) == 0) {
+ return 'untitled';
+ }
+ return $result[0];
+ }
+
+ private function getTime($post)
+ {
+ if ($time = $post->find('span.rel_date', 0)->getAttribute('time')) {
+ return $time;
+ } else {
+ $strdate = $post->find('span.rel_date', 0)->plaintext;
+ $strdate = preg_replace('/[\x00-\x1F\x7F-\xFF]/', ' ', $strdate);
+
+ $date = date_parse($strdate);
+ if (!$date['year']) {
+ if (strstr($strdate, 'today') !== false) {
+ $strdate = date('d-m-Y') . ' ' . $strdate;
+ } elseif (strstr($strdate, 'yesterday ') !== false) {
+ $time = time() - 60 * 60 * 24;
+ $strdate = date('d-m-Y', $time) . ' ' . $strdate;
+ } elseif ($date['month'] && intval(date('m')) < $date['month']) {
+ $strdate = $strdate . ' ' . (date('Y') - 1);
+ } else {
+ $strdate = $strdate . ' ' . date('Y');
+ }
+
+ $date = date_parse($strdate);
+ } elseif ($date['hour'] === false) {
+ $date['hour'] = $date['minute'] = '00';
+ }
+ return strtotime($date['day'] . '-' . $date['month'] . '-' . $date['year'] . ' ' .
+ $date['hour'] . ':' . $date['minute']);
+ }
+ }
+
+ private function getContents()
+ {
+ $header = ['Accept-language: en', 'Cookie: remixlang=3'];
+
+ return getContents($this->getURI(), $header);
+ }
+
+ protected function appendVideo($video_title, $video_link, &$content_suffix, array &$post_videos)
+ {
+ if (!$video_title) {
+ $video_title = '(empty)';
+ }
+
+ preg_match('/video([0-9-]+_[0-9]+)/', $video_link, $preg_match_result);
+
+ if (count($preg_match_result) > 1) {
+ $video_id = $preg_match_result[1];
+ $this->videos[ $video_id ] = [
+ 'url' => $video_link,
+ 'title' => $video_title,
+ ];
+ $post_videos[] = $video_id;
+ } else {
+ $content_suffix .= '<br>Video: <a href="' . htmlspecialchars($video_link) . '">' . $video_title . '</a>';
+ }
+ }
+
+ protected function getCleanVideoLinks()
+ {
+ $result = $this->api('video.get', [
+ 'videos' => implode(',', array_keys($this->videos)),
+ 'count' => 200
+ ]);
+
+ if (!isset($result['error'])) {
+ foreach ($result['response']['items'] as $item) {
+ $video_id = strval($item['owner_id']) . '_' . strval($item['id']);
+ $this->videos[$video_id]['url'] = $item['player'];
+ }
+ }
+
+ foreach ($this->items as &$item) {
+ foreach ($item['videos'] as $video_id) {
+ $video_link = $this->videos[$video_id]['url'];
+ $video_title = $this->videos[$video_id]['title'];
+ $item['content'] .= '<br>Video: <a href="' . htmlspecialchars($video_link) . '">' . $video_title . '</a>';
+ }
+ unset($item['videos']);
+ }
+ }
+
+ protected function api($method, array $params)
+ {
+ $params['v'] = '5.80';
+ $params['access_token'] = $this->getAccessToken();
+ return json_decode(getContents('https://api.vk.com/method/' . $method . '?' . http_build_query($params)), true);
+ }
}
diff --git a/bridges/WallmineNewsBridge.php b/bridges/WallmineNewsBridge.php
index f21627a0..c5009172 100644
--- a/bridges/WallmineNewsBridge.php
+++ b/bridges/WallmineNewsBridge.php
@@ -1,48 +1,50 @@
<?php
-class WallmineNewsBridge extends BridgeAbstract {
- const NAME = 'Wallmine News Bridge';
- const URI = 'https://wallmine.com';
- const DESCRIPTION = 'Returns financial news';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array();
- const CACHE_TIMEOUT = 900; // 15 mins
+class WallmineNewsBridge extends BridgeAbstract
+{
+ const NAME = 'Wallmine News Bridge';
+ const URI = 'https://wallmine.com';
+ const DESCRIPTION = 'Returns financial news';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [];
- public function collectData() {
- $html = getSimpleHTMLDOM($this->getURI() . '/news/');
+ const CACHE_TIMEOUT = 900; // 15 mins
- $html = defaultLinkTo($html, self::URI);
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI() . '/news/');
- foreach($html->find('div.container.news-card') as $div) {
- $item = array();
- $item['uri'] = $div->find('a', 0)->href;
+ $html = defaultLinkTo($html, self::URI);
- $image = $div->find('img.img-fluid', 0)->src;
+ foreach ($html->find('div.container.news-card') as $div) {
+ $item = [];
+ $item['uri'] = $div->find('a', 0)->href;
- $page = getSimpleHTMLDOMCached($item['uri'], 7200);
+ $image = $div->find('img.img-fluid', 0)->src;
- $article = $page->find('div.container.article-container', 0);
+ $page = getSimpleHTMLDOMCached($item['uri'], 7200);
- $item['title'] = $article->find('h1', 0)->plaintext;
+ $article = $page->find('div.container.article-container', 0);
- $article->find('p.published-on', 0)->children(0)->outertext = '';
- $article->find('p.published-on', 0)->children(1)->outertext = '';
- $date = str_replace('at', '', $article->find('p.published-on', 0)->innertext);
+ $item['title'] = $article->find('h1', 0)->plaintext;
- $item['timestamp'] = $date;
+ $article->find('p.published-on', 0)->children(0)->outertext = '';
+ $article->find('p.published-on', 0)->children(1)->outertext = '';
+ $date = str_replace('at', '', $article->find('p.published-on', 0)->innertext);
- $article->find('h1', 0)->outertext = '';
- $article->find('p.published-on', 0)->outertext = '';
+ $item['timestamp'] = $date;
- $item['content'] = $article->innertext;
- $item['enclosures'][] = $image;
+ $article->find('h1', 0)->outertext = '';
+ $article->find('p.published-on', 0)->outertext = '';
- $this->items[] = $item;
+ $item['content'] = $article->innertext;
+ $item['enclosures'][] = $image;
- if (count($this->items) >= 10) {
- break;
- }
- }
+ $this->items[] = $item;
- }
+ if (count($this->items) >= 10) {
+ break;
+ }
+ }
+ }
}
diff --git a/bridges/WallpaperflareBridge.php b/bridges/WallpaperflareBridge.php
index 60486368..907288d0 100644
--- a/bridges/WallpaperflareBridge.php
+++ b/bridges/WallpaperflareBridge.php
@@ -1,41 +1,46 @@
<?php
-class WallpaperflareBridge extends XPathAbstract {
- const NAME = 'Wallpaperflare';
- const URI = 'https://wallpaperflare.com';
- const DESCRIPTION = 'Wallpaperflare is a provider for Wallpapers on nearly every topic, especially for Anime';
- const MAINTAINER = 'dhuschde';
- const PARAMETERS = array(
- '' => array(
- 'search' => array(
- 'name' => 'Search',
- 'exampleValue' => 'birds',
- 'required' => true
- )
- ));
- const CACHE_TIMEOUT = 3600; //1 hour
- const XPATH_EXPRESSION_ITEM = './/figure';
- const XPATH_EXPRESSION_ITEM_TITLE = './/img/@title';
- const XPATH_EXPRESSION_ITEM_CONTENT = '';
- const XPATH_EXPRESSION_ITEM_URI = './/a[@itemprop="url"]/@href';
- const XPATH_EXPRESSION_ITEM_AUTHOR = '/html[1]/body[1]/main[1]/section[1]/h1[1]';
- const XPATH_EXPRESSION_ITEM_TIMESTAMP = '';
- const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/img/@data-src';
- const XPATH_EXPRESSION_ITEM_CATEGORIES = './/figcaption[@itemprop="caption description"]';
- const SETTING_FIX_ENCODING = false;
- protected function getSourceUrl(){
- return 'https://www.wallpaperflare.com/search?wallpaper=' . $this->getInput('search');
- }
+class WallpaperflareBridge extends XPathAbstract
+{
+ const NAME = 'Wallpaperflare';
+ const URI = 'https://wallpaperflare.com';
+ const DESCRIPTION = 'Wallpaperflare is a provider for Wallpapers on nearly every topic, especially for Anime';
+ const MAINTAINER = 'dhuschde';
+ const PARAMETERS = [
+ '' => [
+ 'search' => [
+ 'name' => 'Search',
+ 'exampleValue' => 'birds',
+ 'required' => true
+ ]
+ ]];
+ const CACHE_TIMEOUT = 3600; //1 hour
+ const XPATH_EXPRESSION_ITEM = './/figure';
+ const XPATH_EXPRESSION_ITEM_TITLE = './/img/@title';
+ const XPATH_EXPRESSION_ITEM_CONTENT = '';
+ const XPATH_EXPRESSION_ITEM_URI = './/a[@itemprop="url"]/@href';
+ const XPATH_EXPRESSION_ITEM_AUTHOR = '/html[1]/body[1]/main[1]/section[1]/h1[1]';
+ const XPATH_EXPRESSION_ITEM_TIMESTAMP = '';
+ const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/img/@data-src';
+ const XPATH_EXPRESSION_ITEM_CATEGORIES = './/figcaption[@itemprop="caption description"]';
+ const SETTING_FIX_ENCODING = false;
- public function getIcon() {
- return 'https://www.google.com/s2/favicons?domain=wallpaperflare.com/';
- }
+ protected function getSourceUrl()
+ {
+ return 'https://www.wallpaperflare.com/search?wallpaper=' . $this->getInput('search');
+ }
- public function getName() {
- if(!is_null($this->getInput('search'))) {
- return 'Wallpaperflare - ' . $this->getInput('search');
- } else {
- return 'Wallpaperflare';
- }
- }
+ public function getIcon()
+ {
+ return 'https://www.google.com/s2/favicons?domain=wallpaperflare.com/';
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('search'))) {
+ return 'Wallpaperflare - ' . $this->getInput('search');
+ } else {
+ return 'Wallpaperflare';
+ }
+ }
}
diff --git a/bridges/WeLiveSecurityBridge.php b/bridges/WeLiveSecurityBridge.php
index 14af1ab3..6434a13a 100644
--- a/bridges/WeLiveSecurityBridge.php
+++ b/bridges/WeLiveSecurityBridge.php
@@ -1,38 +1,41 @@
<?php
-class WeLiveSecurityBridge extends FeedExpander {
- const MAINTAINER = 'ORelio';
- const NAME = 'We Live Security';
- const URI = 'https://www.welivesecurity.com/';
- const DESCRIPTION = 'Returns the newest articles.';
- const PARAMETERS = [
- [
- 'limit' => self::LIMIT,
- ],
- ];
+class WeLiveSecurityBridge extends FeedExpander
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = 'We Live Security';
+ const URI = 'https://www.welivesecurity.com/';
+ const DESCRIPTION = 'Returns the newest articles.';
+ const PARAMETERS = [
+ [
+ 'limit' => self::LIMIT,
+ ],
+ ];
- 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.formatted', 0)->innertext;
- $article_content = stripWithDelimiters($article_content, '<script', '</script>');
- $article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="comments');
- $article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="similar-articles');
- $article_content = stripRecursiveHTMLSection($article_content, 'span', '<span class="meta');
- $item['content'] = trim($article_content);
+ $article_content = $article_html->find('div.formatted', 0)->innertext;
+ $article_content = stripWithDelimiters($article_content, '<script', '</script>');
+ $article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="comments');
+ $article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="similar-articles');
+ $article_content = stripRecursiveHTMLSection($article_content, 'span', '<span class="meta');
+ $item['content'] = trim($article_content);
- return $item;
- }
+ return $item;
+ }
- public function collectData(){
- $feed = static::URI . 'feed/';
- $limit = $this->getInput('limit') ?? 10;
- $this->collectExpandableDatas($feed, $limit);
- }
+ public function collectData()
+ {
+ $feed = static::URI . 'feed/';
+ $limit = $this->getInput('limit') ?? 10;
+ $this->collectExpandableDatas($feed, $limit);
+ }
}
diff --git a/bridges/WebfailBridge.php b/bridges/WebfailBridge.php
index fefd539a..e55988da 100644
--- a/bridges/WebfailBridge.php
+++ b/bridges/WebfailBridge.php
@@ -1,156 +1,169 @@
<?php
-class WebfailBridge extends BridgeAbstract {
- const MAINTAINER = 'logmanoriginal';
- const URI = 'https://webfail.com';
- const NAME = 'Webfail';
- const DESCRIPTION = 'Returns the latest fails';
- const PARAMETERS = array(
- 'By content type' => array(
- 'language' => array(
- 'name' => 'Language',
- 'type' => 'list',
- 'title' => 'Select your language',
- 'values' => array(
- 'English' => 'en',
- 'German' => 'de'
- ),
- 'defaultValue' => 'English'
- ),
- 'type' => array(
- 'name' => 'Type',
- 'type' => 'list',
- 'title' => 'Select your content type',
- 'values' => array(
- 'None' => '/',
- 'Facebook' => '/ffdts',
- 'Images' => '/images',
- 'Videos' => '/videos',
- 'Gifs' => '/gifs'
- ),
- 'defaultValue' => 'None'
- )
- )
- );
-
- public function getURI(){
- if(is_null($this->getInput('language')))
- return parent::getURI();
-
- // e.g.: https://en.webfail.com
- return 'https://' . $this->getInput('language') . '.webfail.com';
- }
-
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI() . $this->getInput('type'));
-
- $type = array_search($this->getInput('type'),
- self::PARAMETERS[$this->queriedContext]['type']['values']);
-
- switch(strtolower($type)) {
- case 'facebook':
- case 'videos':
- $this->extractNews($html, $type);
- break;
- case 'none':
- case 'images':
- case 'gifs':
- $this->extractArticle($html);
- break;
- default: returnClientError('Unknown type: ' . $type);
- }
- }
-
- private function extractNews($html, $type){
- $news = $html->find('#main', 0)->find('a.wf-list-news');
- foreach($news as $element) {
- $item = array();
- $item['title'] = $this->fixTitle($element->find('div.wf-news-title', 0)->innertext);
- $item['uri'] = $this->getURI() . $element->href;
-
- $img = $element->find('img.wf-image', 0)->src;
- // Load high resolution image for 'facebook'
- switch(strtolower($type)) {
- case 'facebook':
- $img = $this->getImageHiResUri($item['uri']);
- break;
- default:
- }
-
- $description = '';
- if(!is_null($element->find('div.wf-news-description', 0))) {
- $description = $element->find('div.wf-news-description', 0)->innertext;
- }
-
- $infoElement = $element->find('div.wf-small', 0);
- if (!is_null($infoElement)) {
- if (preg_match('/(\d{2}\.\d{2}\.\d{4})/m', $infoElement->innertext, $matches) === 1 && count($matches) == 2) {
- $dt = DateTime::createFromFormat('!d.m.Y', $matches[1]);
- if ($dt !== false) {
- $item['timestamp'] = $dt->getTimestamp();
- }
- }
- }
-
- $item['content'] = '<p>'
- . $description
- . '</p><br><a href="'
- . $item['uri']
- . '"><img src="'
- . $img
- . '"></a>';
-
- $this->items[] = $item;
- }
- }
-
- private function extractArticle($html){
- $articles = $html->find('article');
- foreach($articles as $article) {
- $item = array();
- $item['title'] = $this->fixTitle($article->find('a', 1)->innertext);
-
- // Images, videos and gifs are provided in their own unique way
- if(!is_null($article->find('img.wf-image', 0))) { // Image type
- $item['uri'] = $this->getURI() . $article->find('a', 2)->href;
- $item['content'] = '<a href="'
- . $item['uri']
- . '"><img src="'
- . $article->find('img.wf-image', 0)->src
- . '"></a>';
- } elseif(!is_null($article->find('div.wf-video', 0))) { // Video type
- $videoId = $this->getVideoId($article->find('div.wf-play', 0)->onclick);
- $item['uri'] = 'https://youtube.com/watch?v=' . $videoId;
- $item['content'] = '<a href="'
- . $item['uri']
- . '"><img src="http://img.youtube.com/vi/'
- . $videoId
- . '/0.jpg"></a>';
- } elseif(!is_null($article->find('video[id*=gif-]', 0))) { // Gif type
- $item['uri'] = $this->getURI() . $article->find('a', 2)->href;
- $item['content'] = '<video controls src="'
- . $article->find('video[id*=gif-]', 0)->src
- . '" poster="'
- . $article->find('video[id*=gif-]', 0)->poster
- . '"></video>';
- }
-
- $this->items[] = $item;
- }
- }
-
- private function fixTitle($title){
- // This fixes titles that include umlauts (in German language)
- return html_entity_decode($title, ENT_QUOTES | ENT_HTML401, 'UTF-8');
- }
-
- private function getVideoId($onclick){
- return substr($onclick, 21, 11);
- }
-
- private function getImageHiResUri($url){
- // https://de.webfail.com/ef524fae509?tag=ffdt
- // http://cdn.webfail.com/upl/img/ef524fae509/post2.jpg
- $id = substr($url, strrpos($url, '/') + 1, strlen($url) - strrpos($url, '?') + 2);
- return 'http://cdn.webfail.com/upl/img/' . $id . '/post2.jpg';
- }
+
+class WebfailBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'logmanoriginal';
+ const URI = 'https://webfail.com';
+ const NAME = 'Webfail';
+ const DESCRIPTION = 'Returns the latest fails';
+ const PARAMETERS = [
+ 'By content type' => [
+ 'language' => [
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'title' => 'Select your language',
+ 'values' => [
+ 'English' => 'en',
+ 'German' => 'de'
+ ],
+ 'defaultValue' => 'English'
+ ],
+ 'type' => [
+ 'name' => 'Type',
+ 'type' => 'list',
+ 'title' => 'Select your content type',
+ 'values' => [
+ 'None' => '/',
+ 'Facebook' => '/ffdts',
+ 'Images' => '/images',
+ 'Videos' => '/videos',
+ 'Gifs' => '/gifs'
+ ],
+ 'defaultValue' => 'None'
+ ]
+ ]
+ ];
+
+ public function getURI()
+ {
+ if (is_null($this->getInput('language'))) {
+ return parent::getURI();
+ }
+
+ // e.g.: https://en.webfail.com
+ return 'https://' . $this->getInput('language') . '.webfail.com';
+ }
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI() . $this->getInput('type'));
+
+ $type = array_search(
+ $this->getInput('type'),
+ self::PARAMETERS[$this->queriedContext]['type']['values']
+ );
+
+ switch (strtolower($type)) {
+ case 'facebook':
+ case 'videos':
+ $this->extractNews($html, $type);
+ break;
+ case 'none':
+ case 'images':
+ case 'gifs':
+ $this->extractArticle($html);
+ break;
+ default:
+ returnClientError('Unknown type: ' . $type);
+ }
+ }
+
+ private function extractNews($html, $type)
+ {
+ $news = $html->find('#main', 0)->find('a.wf-list-news');
+ foreach ($news as $element) {
+ $item = [];
+ $item['title'] = $this->fixTitle($element->find('div.wf-news-title', 0)->innertext);
+ $item['uri'] = $this->getURI() . $element->href;
+
+ $img = $element->find('img.wf-image', 0)->src;
+ // Load high resolution image for 'facebook'
+ switch (strtolower($type)) {
+ case 'facebook':
+ $img = $this->getImageHiResUri($item['uri']);
+ break;
+ default:
+ }
+
+ $description = '';
+ if (!is_null($element->find('div.wf-news-description', 0))) {
+ $description = $element->find('div.wf-news-description', 0)->innertext;
+ }
+
+ $infoElement = $element->find('div.wf-small', 0);
+ if (!is_null($infoElement)) {
+ if (preg_match('/(\d{2}\.\d{2}\.\d{4})/m', $infoElement->innertext, $matches) === 1 && count($matches) == 2) {
+ $dt = DateTime::createFromFormat('!d.m.Y', $matches[1]);
+ if ($dt !== false) {
+ $item['timestamp'] = $dt->getTimestamp();
+ }
+ }
+ }
+
+ $item['content'] = '<p>'
+ . $description
+ . '</p><br><a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $img
+ . '"></a>';
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function extractArticle($html)
+ {
+ $articles = $html->find('article');
+ foreach ($articles as $article) {
+ $item = [];
+ $item['title'] = $this->fixTitle($article->find('a', 1)->innertext);
+
+ // Images, videos and gifs are provided in their own unique way
+ if (!is_null($article->find('img.wf-image', 0))) { // Image type
+ $item['uri'] = $this->getURI() . $article->find('a', 2)->href;
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $article->find('img.wf-image', 0)->src
+ . '"></a>';
+ } elseif (!is_null($article->find('div.wf-video', 0))) { // Video type
+ $videoId = $this->getVideoId($article->find('div.wf-play', 0)->onclick);
+ $item['uri'] = 'https://youtube.com/watch?v=' . $videoId;
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="http://img.youtube.com/vi/'
+ . $videoId
+ . '/0.jpg"></a>';
+ } elseif (!is_null($article->find('video[id*=gif-]', 0))) { // Gif type
+ $item['uri'] = $this->getURI() . $article->find('a', 2)->href;
+ $item['content'] = '<video controls src="'
+ . $article->find('video[id*=gif-]', 0)->src
+ . '" poster="'
+ . $article->find('video[id*=gif-]', 0)->poster
+ . '"></video>';
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function fixTitle($title)
+ {
+ // This fixes titles that include umlauts (in German language)
+ return html_entity_decode($title, ENT_QUOTES | ENT_HTML401, 'UTF-8');
+ }
+
+ private function getVideoId($onclick)
+ {
+ return substr($onclick, 21, 11);
+ }
+
+ private function getImageHiResUri($url)
+ {
+ // https://de.webfail.com/ef524fae509?tag=ffdt
+ // http://cdn.webfail.com/upl/img/ef524fae509/post2.jpg
+ $id = substr($url, strrpos($url, '/') + 1, strlen($url) - strrpos($url, '?') + 2);
+ return 'http://cdn.webfail.com/upl/img/' . $id . '/post2.jpg';
+ }
}
diff --git a/bridges/WikiLeaksBridge.php b/bridges/WikiLeaksBridge.php
index cf44b066..512b1c30 100644
--- a/bridges/WikiLeaksBridge.php
+++ b/bridges/WikiLeaksBridge.php
@@ -1,127 +1,134 @@
<?php
-class WikiLeaksBridge extends BridgeAbstract {
- const NAME = 'WikiLeaks';
- const URI = 'https://wikileaks.org';
- const DESCRIPTION = 'Returns the latest news or articles from WikiLeaks';
- const MAINTAINER = 'logmanoriginal';
- const PARAMETERS = array(
- array(
- 'category' => array(
- 'name' => 'Category',
- 'type' => 'list',
- 'title' => 'Select your category',
- 'values' => array(
- 'News' => '-News-',
- 'Leaks' => array(
- 'All' => '-Leaks-',
- 'Intelligence' => '+-Intelligence-+',
- 'Global Economy' => '+-Global-Economy-+',
- 'International Politics' => '+-International-Politics-+',
- 'Corporations' => '+-Corporations-+',
- 'Government' => '+-Government-+',
- 'War & Military' => '+-War-Military-+'
- )
- ),
- 'defaultValue' => 'news'
- ),
- 'teaser' => array(
- 'name' => 'Show teaser',
- 'type' => 'checkbox',
- 'title' => 'If checked feeds will display the teaser',
- 'defaultValue' => 'checked'
- )
- )
- );
-
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI());
-
- // News are presented differently
- switch($this->getInput('category')) {
- case '-News-':
- $this->loadNewsItems($html);
- break;
- default:
- $this->loadLeakItems($html);
- }
- }
-
- public function getURI(){
- if(!is_null($this->getInput('category'))) {
- return static::URI . '/' . $this->getInput('category') . '.html';
- }
-
- return parent::getURI();
- }
-
- public function getName(){
- if(!is_null($this->getInput('category'))) {
- $category = array_search(
- $this->getInput('category'),
- static::PARAMETERS[0]['category']['values']
- );
-
- if($category === false) {
- $category = array_search(
- $this->getInput('category'),
- static::PARAMETERS[0]['category']['values']['Leaks']
- );
- }
-
- return $category . ' - ' . static::NAME;
- }
-
- return parent::getName();
- }
-
- private function loadNewsItems($html){
- $articles = $html->find('div.news-articles ul li');
-
- if(is_null($articles) || count($articles) === 0) {
- return;
- }
-
- foreach($articles as $article) {
- $item = array();
-
- $item['title'] = $article->find('h3', 0)->plaintext;
- $item['uri'] = static::URI . $article->find('h3 a', 0)->href;
- $item['content'] = $article->find('div.introduction', 0)->plaintext;
- $item['timestamp'] = strtotime($article->find('div.timestamp', 0)->plaintext);
-
- $this->items[] = $item;
- }
- }
-
- private function loadLeakItems($html){
- $articles = $html->find('li.tile');
-
- if(is_null($articles) || count($articles) === 0) {
- return;
- }
-
- foreach($articles as $article) {
- $item = array();
-
- $item['title'] = $article->find('h2', 0)->plaintext;
- $item['uri'] = static::URI . $article->find('a', 0)->href;
-
- $teaser = static::URI . '/' . $article->find('div.teaser img', 0)->src;
-
- if($this->getInput('teaser')) {
- $item['content'] = '<img src="'
- . $teaser
- . '" /><p>'
- . $article->find('div.intro', 0)->plaintext
- . '</p>';
- } else {
- $item['content'] = $article->find('div.intro', 0)->plaintext;
- }
-
- $item['timestamp'] = strtotime($article->find('div.timestamp', 0)->plaintext);
- $item['enclosures'] = array($teaser);
-
- $this->items[] = $item;
- }
- }
+
+class WikiLeaksBridge extends BridgeAbstract
+{
+ const NAME = 'WikiLeaks';
+ const URI = 'https://wikileaks.org';
+ const DESCRIPTION = 'Returns the latest news or articles from WikiLeaks';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = [
+ [
+ 'category' => [
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'title' => 'Select your category',
+ 'values' => [
+ 'News' => '-News-',
+ 'Leaks' => [
+ 'All' => '-Leaks-',
+ 'Intelligence' => '+-Intelligence-+',
+ 'Global Economy' => '+-Global-Economy-+',
+ 'International Politics' => '+-International-Politics-+',
+ 'Corporations' => '+-Corporations-+',
+ 'Government' => '+-Government-+',
+ 'War & Military' => '+-War-Military-+'
+ ]
+ ],
+ 'defaultValue' => 'news'
+ ],
+ 'teaser' => [
+ 'name' => 'Show teaser',
+ 'type' => 'checkbox',
+ 'title' => 'If checked feeds will display the teaser',
+ 'defaultValue' => 'checked'
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ // News are presented differently
+ switch ($this->getInput('category')) {
+ case '-News-':
+ $this->loadNewsItems($html);
+ break;
+ default:
+ $this->loadLeakItems($html);
+ }
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('category'))) {
+ return static::URI . '/' . $this->getInput('category') . '.html';
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if (!is_null($this->getInput('category'))) {
+ $category = array_search(
+ $this->getInput('category'),
+ static::PARAMETERS[0]['category']['values']
+ );
+
+ if ($category === false) {
+ $category = array_search(
+ $this->getInput('category'),
+ static::PARAMETERS[0]['category']['values']['Leaks']
+ );
+ }
+
+ return $category . ' - ' . static::NAME;
+ }
+
+ return parent::getName();
+ }
+
+ private function loadNewsItems($html)
+ {
+ $articles = $html->find('div.news-articles ul li');
+
+ if (is_null($articles) || count($articles) === 0) {
+ return;
+ }
+
+ foreach ($articles as $article) {
+ $item = [];
+
+ $item['title'] = $article->find('h3', 0)->plaintext;
+ $item['uri'] = static::URI . $article->find('h3 a', 0)->href;
+ $item['content'] = $article->find('div.introduction', 0)->plaintext;
+ $item['timestamp'] = strtotime($article->find('div.timestamp', 0)->plaintext);
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function loadLeakItems($html)
+ {
+ $articles = $html->find('li.tile');
+
+ if (is_null($articles) || count($articles) === 0) {
+ return;
+ }
+
+ foreach ($articles as $article) {
+ $item = [];
+
+ $item['title'] = $article->find('h2', 0)->plaintext;
+ $item['uri'] = static::URI . $article->find('a', 0)->href;
+
+ $teaser = static::URI . '/' . $article->find('div.teaser img', 0)->src;
+
+ if ($this->getInput('teaser')) {
+ $item['content'] = '<img src="'
+ . $teaser
+ . '" /><p>'
+ . $article->find('div.intro', 0)->plaintext
+ . '</p>';
+ } else {
+ $item['content'] = $article->find('div.intro', 0)->plaintext;
+ }
+
+ $item['timestamp'] = strtotime($article->find('div.timestamp', 0)->plaintext);
+ $item['enclosures'] = [$teaser];
+
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/bridges/WikipediaBridge.php b/bridges/WikipediaBridge.php
index 1bdf2ddc..30e551ed 100644
--- a/bridges/WikipediaBridge.php
+++ b/bridges/WikipediaBridge.php
@@ -3,324 +3,347 @@
define('WIKIPEDIA_SUBJECT_TFA', 0); // Today's featured article
define('WIKIPEDIA_SUBJECT_DYK', 1); // Did you know...
-class WikipediaBridge extends BridgeAbstract {
- const MAINTAINER = 'logmanoriginal';
- const NAME = 'Wikipedia bridge for many languages';
- const URI = 'https://www.wikipedia.org/';
- const DESCRIPTION = 'Returns articles for a language of your choice';
-
- const PARAMETERS = array( array(
- 'language' => array(
- 'name' => 'Language',
- 'type' => 'list',
- 'title' => 'Select your language',
- 'exampleValue' => 'English',
- 'values' => array(
- 'English' => 'en',
- 'Русский' => 'ru',
- 'Dutch' => 'nl',
- 'Esperanto' => 'eo',
- 'French' => 'fr',
- 'German' => 'de',
- )
- ),
- 'subject' => array(
- 'name' => 'Subject',
- 'type' => 'list',
- 'title' => 'What subject are you interested in?',
- 'exampleValue' => 'Today\'s featured article',
- 'values' => array(
- 'Today\'s featured article' => 'tfa',
- 'Did you know…' => 'dyk'
- )
- ),
- 'fullarticle' => array(
- 'name' => 'Load full article',
- 'type' => 'checkbox',
- 'title' => 'Activate to always load the full article'
- )
- ));
-
- public function getURI(){
- if(!is_null($this->getInput('language'))) {
- return 'https://'
- . strtolower($this->getInput('language'))
- . '.wikipedia.org';
- }
-
- return parent::getURI();
- }
-
- public function getName(){
- switch($this->getInput('subject')) {
- case 'tfa':
- $subject = WIKIPEDIA_SUBJECT_TFA;
- break;
- case 'dyk':
- $subject = WIKIPEDIA_SUBJECT_DYK;
- break;
- default: return parent::getName();
- }
-
- switch($subject) {
- case WIKIPEDIA_SUBJECT_TFA:
- $name = 'Today\'s featured article from '
- . strtolower($this->getInput('language'))
- . '.wikipedia.org';
- break;
- case WIKIPEDIA_SUBJECT_DYK:
- $name = 'Did you know? - articles from '
- . strtolower($this->getInput('language'))
- . '.wikipedia.org';
- break;
- default:
- $name = 'Articles from '
- . strtolower($this->getInput('language'))
- . '.wikipedia.org';
- break;
- }
- return $name;
- }
-
- public function collectData(){
-
- switch($this->getInput('subject')) {
- case 'tfa':
- $subject = WIKIPEDIA_SUBJECT_TFA;
- break;
- case 'dyk':
- $subject = WIKIPEDIA_SUBJECT_DYK;
- break;
- default:
- $subject = WIKIPEDIA_SUBJECT_TFA;
- break;
- }
-
- $fullArticle = $this->getInput('fullarticle');
-
- // This will automatically send us to the correct main page in any language (try it!)
- $html = getSimpleHTMLDOM($this->getURI() . '/wiki');
-
- if(!$html)
- returnServerError('Could not load site: ' . $this->getURI() . '!');
-
- /*
- * Now read content depending on the language (make sure to create one function per language!)
- * We build the function name automatically, just make sure you create a private function ending
- * with your desired language code, where the language code is upper case! (en -> getContentsEN).
- */
- $function = 'getContents' . ucfirst(strtolower($this->getInput('language')));
-
- if(!method_exists($this, $function))
- returnServerError('A function to get the contents for your language is missing (\'' . $function . '\')!');
-
- /*
- * The method takes care of creating all items.
- */
- $this->$function($html, $subject, $fullArticle);
- }
-
- /**
- * Replaces all relative URIs with absolute ones
- * @param $element A simplehtmldom element
- * @return The $element->innertext with all URIs replaced
- */
- private function replaceUriInHtmlElement($element){
- return str_replace('href="/', 'href="' . $this->getURI() . '/', $element->innertext);
- }
-
- /*
- * Adds a new item to $items using a generic operation (should work for most
- * (all?) wikis) $anchorText can be specified if the wiki in question doesn't
- * use '...' (like Dutch, French and Italian) $anchorFallbackIndex can be
- * used to specify a different fallback link than the first
- * (e.g., -1 for the last)
- */
- private function addTodaysFeaturedArticleGeneric($element,
- $fullArticle,
- $anchorText = '...',
- $anchorFallbackIndex = 0){
- // Clean the bottom of the featured article
- if ($element->find('ul', -1))
- $element->find('ul', -1)->outertext = '';
- elseif ($element->find('div', -1)) {
- $element->find('div', -1)->outertext = '';
- }
-
- // The title and URI of the article can be found in an anchor containing
- // the string '...' in most wikis ('full article ...')
- $target = $element->find('p a', $anchorFallbackIndex);
- foreach($element->find('//a') as $anchor) {
- if(strpos($anchor->innertext, $anchorText) !== false) {
- $target = $anchor;
- break;
- }
- }
-
- $item = array();
- $item['uri'] = $this->getURI() . $target->href;
- $item['title'] = $target->title;
-
- if(!$fullArticle)
- $item['content'] = strip_tags($this->replaceUriInHtmlElement($element), '<a><p><br><img>');
- else
- $item['content'] = $this->loadFullArticle($item['uri']);
-
- $this->items[] = $item;
- }
-
- /*
- * Adds a new item to $items using a generic operation (should work for most (all?) wikis)
- */
- private function addDidYouKnowGeneric($element, $fullArticle){
- foreach($element->find('ul', 0)->find('li') as $entry) {
- $item = array();
-
- // We can only use the first anchor, there is no way of finding the 'correct' one if there are multiple
- $item['uri'] = $this->getURI() . $entry->find('a', 0)->href;
- $item['title'] = strip_tags($entry->innertext);
-
- if(!$fullArticle)
- $item['content'] = $this->replaceUriInHtmlElement($entry);
- else
- $item['content'] = $this->loadFullArticle($item['uri']);
-
- $this->items[] = $item;
- }
- }
-
- /**
- * Loads the full article from a given URI
- */
- private function loadFullArticle($uri){
- $content_html = getSimpleHTMLDOMCached($uri);
-
- if(!$content_html)
- returnServerError('Could not load site: ' . $uri . '!');
-
- $content = $content_html->find('#mw-content-text', 0);
-
- if(!$content)
- returnServerError('Could not find content in page: ' . $uri . '!');
-
- // Let's remove a couple of things from the article
- $table = $content->find('#toc', 0); // Table of contents
- if(!$table === false)
- $table->outertext = '';
-
- foreach($content->find('ol.references') as $reference) // References
- $reference->outertext = '';
-
- return str_replace('href="/', 'href="' . $this->getURI() . '/', $content->innertext);
- }
-
- /**
- * Implementation for de.wikipedia.org
- */
- private function getContentsDe($html, $subject, $fullArticle){
- switch($subject) {
- case WIKIPEDIA_SUBJECT_TFA:
- $element = $html->find('div[id=artikel] div.hauptseite-box-content', 0);
- $this->addTodaysFeaturedArticleGeneric($element, $fullArticle);
- break;
- case WIKIPEDIA_SUBJECT_DYK:
- $element = $html->find('div[id=wissenswertes]', 0);
- $this->addDidYouKnowGeneric($element, $fullArticle);
- break;
- default:
- break;
- }
- }
-
- /**
- * Implementation for fr.wikipedia.org
- */
- private function getContentsFr($html, $subject, $fullArticle){
- switch($subject) {
- case WIKIPEDIA_SUBJECT_TFA:
- $element = $html->find('div[class=accueil_2017_cadre]', 0);
- $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, 'Lire la suite');
- break;
- case WIKIPEDIA_SUBJECT_DYK:
- $element = $html->find('div[class=accueil_2017_cadre]', 2);
- $this->addDidYouKnowGeneric($element, $fullArticle);
- break;
- default:
- break;
- }
- }
-
- /**
- * Implementation for en.wikipedia.org
- */
- private function getContentsEn($html, $subject, $fullArticle){
- switch($subject) {
- case WIKIPEDIA_SUBJECT_TFA:
- $element = $html->find('div[id=mp-tfa]', 0);
- $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, -1);
- break;
- case WIKIPEDIA_SUBJECT_DYK:
- $element = $html->find('div[id=mp-dyk]', 0);
- $this->addDidYouKnowGeneric($element, $fullArticle);
- break;
- default:
- break;
- }
- }
-
- /**
- * Implementation for ru.wikipedia.org
- */
- private function getContentsRu($html, $subject, $fullArticle){
- switch($subject) {
- case WIKIPEDIA_SUBJECT_TFA:
- $element = $html->find('div[id=main-tfa]', 0);
- $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, -1);
- break;
- case WIKIPEDIA_SUBJECT_DYK:
- $element = $html->find('div[id=main-dyk]', 0);
- $this->addDidYouKnowGeneric($element, $fullArticle);
- break;
- default:
- break;
- }
- }
-
- /**
- * Implementation for eo.wikipedia.org
- */
- private function getContentsEo($html, $subject, $fullArticle){
- switch($subject) {
- case WIKIPEDIA_SUBJECT_TFA:
- $element = $html->find('div[id=mf-artikolo-de-la-monato]', 0);
- $element->find('div', -2)->outertext = '';
- $this->addTodaysFeaturedArticleGeneric($element, $fullArticle);
- break;
- case WIKIPEDIA_SUBJECT_DYK:
- $element = $html->find('div.hp', 1)->find('table', 4)->find('td', -1);
- $this->addDidYouKnowGeneric($element, $fullArticle);
- break;
- default:
- break;
- }
- }
-
- /**
- * Implementation for nl.wikipedia.org
- */
- private function getContentsNl($html, $subject, $fullArticle){
- switch($subject) {
- case WIKIPEDIA_SUBJECT_TFA:
- $element = $html->find('td[id=segment-Uitgelicht] div', 0);
- $element->find('p', 1)->outertext = '';
- $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, 'Lees verder');
- break;
- case WIKIPEDIA_SUBJECT_DYK:
- $element = $html->find('td[id=segment-Wist_je_dat] div', 0);
- $this->addDidYouKnowGeneric($element, $fullArticle);
- break;
- default:
- break;
- }
- }
+class WikipediaBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Wikipedia bridge for many languages';
+ const URI = 'https://www.wikipedia.org/';
+ const DESCRIPTION = 'Returns articles for a language of your choice';
+
+ const PARAMETERS = [ [
+ 'language' => [
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'title' => 'Select your language',
+ 'exampleValue' => 'English',
+ 'values' => [
+ 'English' => 'en',
+ 'Русский' => 'ru',
+ 'Dutch' => 'nl',
+ 'Esperanto' => 'eo',
+ 'French' => 'fr',
+ 'German' => 'de',
+ ]
+ ],
+ 'subject' => [
+ 'name' => 'Subject',
+ 'type' => 'list',
+ 'title' => 'What subject are you interested in?',
+ 'exampleValue' => 'Today\'s featured article',
+ 'values' => [
+ 'Today\'s featured article' => 'tfa',
+ 'Did you know…' => 'dyk'
+ ]
+ ],
+ 'fullarticle' => [
+ 'name' => 'Load full article',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to always load the full article'
+ ]
+ ]];
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('language'))) {
+ return 'https://'
+ . strtolower($this->getInput('language'))
+ . '.wikipedia.org';
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ switch ($this->getInput('subject')) {
+ case 'tfa':
+ $subject = WIKIPEDIA_SUBJECT_TFA;
+ break;
+ case 'dyk':
+ $subject = WIKIPEDIA_SUBJECT_DYK;
+ break;
+ default:
+ return parent::getName();
+ }
+
+ switch ($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $name = 'Today\'s featured article from '
+ . strtolower($this->getInput('language'))
+ . '.wikipedia.org';
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $name = 'Did you know? - articles from '
+ . strtolower($this->getInput('language'))
+ . '.wikipedia.org';
+ break;
+ default:
+ $name = 'Articles from '
+ . strtolower($this->getInput('language'))
+ . '.wikipedia.org';
+ break;
+ }
+ return $name;
+ }
+
+ public function collectData()
+ {
+ switch ($this->getInput('subject')) {
+ case 'tfa':
+ $subject = WIKIPEDIA_SUBJECT_TFA;
+ break;
+ case 'dyk':
+ $subject = WIKIPEDIA_SUBJECT_DYK;
+ break;
+ default:
+ $subject = WIKIPEDIA_SUBJECT_TFA;
+ break;
+ }
+
+ $fullArticle = $this->getInput('fullarticle');
+
+ // This will automatically send us to the correct main page in any language (try it!)
+ $html = getSimpleHTMLDOM($this->getURI() . '/wiki');
+
+ if (!$html) {
+ returnServerError('Could not load site: ' . $this->getURI() . '!');
+ }
+
+ /*
+ * Now read content depending on the language (make sure to create one function per language!)
+ * We build the function name automatically, just make sure you create a private function ending
+ * with your desired language code, where the language code is upper case! (en -> getContentsEN).
+ */
+ $function = 'getContents' . ucfirst(strtolower($this->getInput('language')));
+
+ if (!method_exists($this, $function)) {
+ returnServerError('A function to get the contents for your language is missing (\'' . $function . '\')!');
+ }
+
+ /*
+ * The method takes care of creating all items.
+ */
+ $this->$function($html, $subject, $fullArticle);
+ }
+
+ /**
+ * Replaces all relative URIs with absolute ones
+ * @param $element A simplehtmldom element
+ * @return The $element->innertext with all URIs replaced
+ */
+ private function replaceUriInHtmlElement($element)
+ {
+ return str_replace('href="/', 'href="' . $this->getURI() . '/', $element->innertext);
+ }
+
+ /*
+ * Adds a new item to $items using a generic operation (should work for most
+ * (all?) wikis) $anchorText can be specified if the wiki in question doesn't
+ * use '...' (like Dutch, French and Italian) $anchorFallbackIndex can be
+ * used to specify a different fallback link than the first
+ * (e.g., -1 for the last)
+ */
+ private function addTodaysFeaturedArticleGeneric(
+ $element,
+ $fullArticle,
+ $anchorText = '...',
+ $anchorFallbackIndex = 0
+ ) {
+ // Clean the bottom of the featured article
+ if ($element->find('ul', -1)) {
+ $element->find('ul', -1)->outertext = '';
+ } elseif ($element->find('div', -1)) {
+ $element->find('div', -1)->outertext = '';
+ }
+
+ // The title and URI of the article can be found in an anchor containing
+ // the string '...' in most wikis ('full article ...')
+ $target = $element->find('p a', $anchorFallbackIndex);
+ foreach ($element->find('//a') as $anchor) {
+ if (strpos($anchor->innertext, $anchorText) !== false) {
+ $target = $anchor;
+ break;
+ }
+ }
+
+ $item = [];
+ $item['uri'] = $this->getURI() . $target->href;
+ $item['title'] = $target->title;
+
+ if (!$fullArticle) {
+ $item['content'] = strip_tags($this->replaceUriInHtmlElement($element), '<a><p><br><img>');
+ } else {
+ $item['content'] = $this->loadFullArticle($item['uri']);
+ }
+
+ $this->items[] = $item;
+ }
+
+ /*
+ * Adds a new item to $items using a generic operation (should work for most (all?) wikis)
+ */
+ private function addDidYouKnowGeneric($element, $fullArticle)
+ {
+ foreach ($element->find('ul', 0)->find('li') as $entry) {
+ $item = [];
+
+ // We can only use the first anchor, there is no way of finding the 'correct' one if there are multiple
+ $item['uri'] = $this->getURI() . $entry->find('a', 0)->href;
+ $item['title'] = strip_tags($entry->innertext);
+
+ if (!$fullArticle) {
+ $item['content'] = $this->replaceUriInHtmlElement($entry);
+ } else {
+ $item['content'] = $this->loadFullArticle($item['uri']);
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Loads the full article from a given URI
+ */
+ private function loadFullArticle($uri)
+ {
+ $content_html = getSimpleHTMLDOMCached($uri);
+
+ if (!$content_html) {
+ returnServerError('Could not load site: ' . $uri . '!');
+ }
+
+ $content = $content_html->find('#mw-content-text', 0);
+
+ if (!$content) {
+ returnServerError('Could not find content in page: ' . $uri . '!');
+ }
+
+ // Let's remove a couple of things from the article
+ $table = $content->find('#toc', 0); // Table of contents
+ if (!$table === false) {
+ $table->outertext = '';
+ }
+
+ foreach ($content->find('ol.references') as $reference) { // References
+ $reference->outertext = '';
+ }
+
+ return str_replace('href="/', 'href="' . $this->getURI() . '/', $content->innertext);
+ }
+
+ /**
+ * Implementation for de.wikipedia.org
+ */
+ private function getContentsDe($html, $subject, $fullArticle)
+ {
+ switch ($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $element = $html->find('div[id=artikel] div.hauptseite-box-content', 0);
+ $this->addTodaysFeaturedArticleGeneric($element, $fullArticle);
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $element = $html->find('div[id=wissenswertes]', 0);
+ $this->addDidYouKnowGeneric($element, $fullArticle);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Implementation for fr.wikipedia.org
+ */
+ private function getContentsFr($html, $subject, $fullArticle)
+ {
+ switch ($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $element = $html->find('div[class=accueil_2017_cadre]', 0);
+ $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, 'Lire la suite');
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $element = $html->find('div[class=accueil_2017_cadre]', 2);
+ $this->addDidYouKnowGeneric($element, $fullArticle);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Implementation for en.wikipedia.org
+ */
+ private function getContentsEn($html, $subject, $fullArticle)
+ {
+ switch ($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $element = $html->find('div[id=mp-tfa]', 0);
+ $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, -1);
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $element = $html->find('div[id=mp-dyk]', 0);
+ $this->addDidYouKnowGeneric($element, $fullArticle);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Implementation for ru.wikipedia.org
+ */
+ private function getContentsRu($html, $subject, $fullArticle)
+ {
+ switch ($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $element = $html->find('div[id=main-tfa]', 0);
+ $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, -1);
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $element = $html->find('div[id=main-dyk]', 0);
+ $this->addDidYouKnowGeneric($element, $fullArticle);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Implementation for eo.wikipedia.org
+ */
+ private function getContentsEo($html, $subject, $fullArticle)
+ {
+ switch ($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $element = $html->find('div[id=mf-artikolo-de-la-monato]', 0);
+ $element->find('div', -2)->outertext = '';
+ $this->addTodaysFeaturedArticleGeneric($element, $fullArticle);
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $element = $html->find('div.hp', 1)->find('table', 4)->find('td', -1);
+ $this->addDidYouKnowGeneric($element, $fullArticle);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Implementation for nl.wikipedia.org
+ */
+ private function getContentsNl($html, $subject, $fullArticle)
+ {
+ switch ($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $element = $html->find('td[id=segment-Uitgelicht] div', 0);
+ $element->find('p', 1)->outertext = '';
+ $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, 'Lees verder');
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $element = $html->find('td[id=segment-Wist_je_dat] div', 0);
+ $this->addDidYouKnowGeneric($element, $fullArticle);
+ break;
+ default:
+ break;
+ }
+ }
}
diff --git a/bridges/WiredBridge.php b/bridges/WiredBridge.php
index b15f781f..d4c7cbbb 100644
--- a/bridges/WiredBridge.php
+++ b/bridges/WiredBridge.php
@@ -1,103 +1,110 @@
<?php
-class WiredBridge extends FeedExpander {
- const MAINTAINER = 'ORelio';
- const NAME = 'WIRED Bridge';
- const URI = 'https://www.wired.com/';
- const DESCRIPTION = 'Returns the newest articles from WIRED';
-
- const PARAMETERS = array( array(
- 'feed' => array(
- 'name' => 'Feed',
- 'type' => 'list',
- 'values' => array(
- 'WIRED Top Stories' => 'rss', // /feed/rss
- 'Business' => 'business', // /feed/category/business/latest/rss
- 'Culture' => 'culture', // /feed/category/culture/latest/rss
- 'Gear' => 'gear', // /feed/category/gear/latest/rss
- 'Ideas' => 'ideas', // /feed/category/ideas/latest/rss
- 'Science' => 'science', // /feed/category/science/latest/rss
- 'Security' => 'security', // /feed/category/security/latest/rss
- 'Transportation' => 'transportation', // /feed/category/transportation/latest/rss
- 'Backchannel' => 'backchannel', // /feed/category/backchannel/latest/rss
- 'WIRED Guides' => 'wired-guide', // /feed/tag/wired-guide/latest/rss
- 'Photo' => 'photo' // /feed/category/photo/latest/rss
- )
- ),
- 'limit' => self::LIMIT,
- ));
-
- public function collectData(){
- $feed = $this->getInput('feed');
- if(empty($feed) || !ctype_alpha(str_replace('-', '', $feed))) {
- returnClientError('Invalid feed, please check the "feed" parameter.');
- }
-
- $feed_url = $this->getURI() . 'feed/';
- if ($feed != 'rss') {
- if ($feed != 'wired-guide') {
- $feed_url .= 'category/';
- } else {
- $feed_url .= 'tag/';
- }
- $feed_url .= "$feed/latest/";
- }
- $feed_url .= 'rss';
-
- $limit = $this->getInput('limit') ?? -1;
- $this->collectExpandableDatas($feed_url, $limit);
- }
-
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- $article = getSimpleHTMLDOMCached($item['uri']);
- $item['content'] = $this->extractArticleContent($article);
-
- $headline = strval($newsItem->description);
- if(!empty($headline)) {
- $item['content'] = '<p><b>' . $headline . '</b></p>' . $item['content'];
- }
-
- $item_image = $article->find('meta[property="og:image"]', 0);
- if(!empty($item_image)) {
- $item['enclosures'] = array($item_image->content);
- $item['content'] = '<p><img src="' . $item_image->content . '" /></p>' . $item['content'];
- }
-
- return $item;
- }
-
- private function extractArticleContent($article){
- $content = $article->find('article', 0);
- $truncate = true;
-
- if (empty($content)) {
- $content = $article->find('div.listicle-main-component__container', 0);
- $truncate = false;
- }
-
- if (!empty($content)) {
- $content = $content->innertext;
- }
-
- foreach (array(
- '<div class="content-header',
- '<div class="mid-banner-wrap',
- '<div class="related',
- '<div class="social-icons',
- '<div class="recirc-most-popular',
- '<div class="grid--item article-related-video',
- '<div class="row full-bleed-ad',
- ) as $div_start) {
- $content = stripRecursiveHTMLSection($content, 'div', $div_start);
- }
-
- if ($truncate) {
- //Clutter after standard article is too hard to clean properly
- $content = trim(explode('<hr', $content)[0]);
- }
-
- $content = str_replace('href="/', 'href="' . $this->getURI() . '/', $content);
-
- return $content;
- }
+
+class WiredBridge extends FeedExpander
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = 'WIRED Bridge';
+ const URI = 'https://www.wired.com/';
+ const DESCRIPTION = 'Returns the newest articles from WIRED';
+
+ const PARAMETERS = [ [
+ 'feed' => [
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'values' => [
+ 'WIRED Top Stories' => 'rss', // /feed/rss
+ 'Business' => 'business', // /feed/category/business/latest/rss
+ 'Culture' => 'culture', // /feed/category/culture/latest/rss
+ 'Gear' => 'gear', // /feed/category/gear/latest/rss
+ 'Ideas' => 'ideas', // /feed/category/ideas/latest/rss
+ 'Science' => 'science', // /feed/category/science/latest/rss
+ 'Security' => 'security', // /feed/category/security/latest/rss
+ 'Transportation' => 'transportation', // /feed/category/transportation/latest/rss
+ 'Backchannel' => 'backchannel', // /feed/category/backchannel/latest/rss
+ 'WIRED Guides' => 'wired-guide', // /feed/tag/wired-guide/latest/rss
+ 'Photo' => 'photo' // /feed/category/photo/latest/rss
+ ]
+ ],
+ 'limit' => self::LIMIT,
+ ]];
+
+ public function collectData()
+ {
+ $feed = $this->getInput('feed');
+ if (empty($feed) || !ctype_alpha(str_replace('-', '', $feed))) {
+ returnClientError('Invalid feed, please check the "feed" parameter.');
+ }
+
+ $feed_url = $this->getURI() . 'feed/';
+ if ($feed != 'rss') {
+ if ($feed != 'wired-guide') {
+ $feed_url .= 'category/';
+ } else {
+ $feed_url .= 'tag/';
+ }
+ $feed_url .= "$feed/latest/";
+ }
+ $feed_url .= 'rss';
+
+ $limit = $this->getInput('limit') ?? -1;
+ $this->collectExpandableDatas($feed_url, $limit);
+ }
+
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ $article = getSimpleHTMLDOMCached($item['uri']);
+ $item['content'] = $this->extractArticleContent($article);
+
+ $headline = strval($newsItem->description);
+ if (!empty($headline)) {
+ $item['content'] = '<p><b>' . $headline . '</b></p>' . $item['content'];
+ }
+
+ $item_image = $article->find('meta[property="og:image"]', 0);
+ if (!empty($item_image)) {
+ $item['enclosures'] = [$item_image->content];
+ $item['content'] = '<p><img src="' . $item_image->content . '" /></p>' . $item['content'];
+ }
+
+ return $item;
+ }
+
+ private function extractArticleContent($article)
+ {
+ $content = $article->find('article', 0);
+ $truncate = true;
+
+ if (empty($content)) {
+ $content = $article->find('div.listicle-main-component__container', 0);
+ $truncate = false;
+ }
+
+ if (!empty($content)) {
+ $content = $content->innertext;
+ }
+
+ foreach (
+ [
+ '<div class="content-header',
+ '<div class="mid-banner-wrap',
+ '<div class="related',
+ '<div class="social-icons',
+ '<div class="recirc-most-popular',
+ '<div class="grid--item article-related-video',
+ '<div class="row full-bleed-ad',
+ ] as $div_start
+ ) {
+ $content = stripRecursiveHTMLSection($content, 'div', $div_start);
+ }
+
+ if ($truncate) {
+ //Clutter after standard article is too hard to clean properly
+ $content = trim(explode('<hr', $content)[0]);
+ }
+
+ $content = str_replace('href="/', 'href="' . $this->getURI() . '/', $content);
+
+ return $content;
+ }
}
diff --git a/bridges/WordPressBridge.php b/bridges/WordPressBridge.php
index 0371c834..5a80c398 100644
--- a/bridges/WordPressBridge.php
+++ b/bridges/WordPressBridge.php
@@ -1,107 +1,115 @@
<?php
-class WordPressBridge extends FeedExpander {
- const NAME = 'Wordpress Bridge';
- const URI = 'https://wordpress.org/';
- const DESCRIPTION = 'Returns the newest full posts of a WordPress powered website';
- const PARAMETERS = array( array(
- 'url' => array(
- 'name' => 'Blog URL',
- 'exampleValue' => 'https://www.wpbeginner.com/',
- 'required' => true
- )
- ));
+class WordPressBridge extends FeedExpander
+{
+ const NAME = 'Wordpress Bridge';
+ const URI = 'https://wordpress.org/';
+ const DESCRIPTION = 'Returns the newest full posts of a WordPress powered website';
- private function cleanContent($content){
- $content = stripWithDelimiters($content, '<script', '</script>');
- $content = preg_replace('/<div class="wpa".*/', '', $content);
- $content = preg_replace('/<form.*\/form>/', '', $content);
- return $content;
- }
+ const PARAMETERS = [ [
+ 'url' => [
+ 'name' => 'Blog URL',
+ 'exampleValue' => 'https://www.wpbeginner.com/',
+ 'required' => true
+ ]
+ ]];
- protected function parseItem($newItem){
- $item = parent::parseItem($newItem);
+ private function cleanContent($content)
+ {
+ $content = stripWithDelimiters($content, '<script', '</script>');
+ $content = preg_replace('/<div class="wpa".*/', '', $content);
+ $content = preg_replace('/<form.*\/form>/', '', $content);
+ return $content;
+ }
- $article_html = getSimpleHTMLDOMCached($item['uri']);
+ protected function parseItem($newItem)
+ {
+ $item = parent::parseItem($newItem);
- $article = null;
- switch(true) {
+ $article_html = getSimpleHTMLDOMCached($item['uri']);
- // Custom fix for theme in https://jungefreiheit.de/politik/deutschland/2022/wahl-im-saarland/
- case !is_null($article_html->find('div[data-widget_type="theme-post-content.default"]', 0)):
- $article = $article_html->find('div[data-widget_type="theme-post-content.default"]', 0);
- break;
- case !is_null($article_html->find('[itemprop=articleBody]', 0)):
- // highest priority content div
- $article = $article_html->find('[itemprop=articleBody]', 0);
- break;
- case !is_null($article_html->find('article', 0)):
- // most common content div
- $article = $article_html->find('article', 0);
- break;
- case !is_null($article_html->find('.single-content', 0)):
- // another common content div
- $article = $article_html->find('.single-content', 0);
- break;
- case !is_null($article_html->find('.post-content', 0)):
- // another common content div
- $article = $article_html->find('.post-content', 0);
- break;
- case !is_null($article_html->find('.post', 0)):
- // for old WordPress themes without HTML5
- $article = $article_html->find('.post', 0);
- break;
- }
+ $article = null;
+ switch (true) {
+ // Custom fix for theme in https://jungefreiheit.de/politik/deutschland/2022/wahl-im-saarland/
+ case !is_null($article_html->find('div[data-widget_type="theme-post-content.default"]', 0)):
+ $article = $article_html->find('div[data-widget_type="theme-post-content.default"]', 0);
+ break;
+ case !is_null($article_html->find('[itemprop=articleBody]', 0)):
+ // highest priority content div
+ $article = $article_html->find('[itemprop=articleBody]', 0);
+ break;
+ case !is_null($article_html->find('article', 0)):
+ // most common content div
+ $article = $article_html->find('article', 0);
+ break;
+ case !is_null($article_html->find('.single-content', 0)):
+ // another common content div
+ $article = $article_html->find('.single-content', 0);
+ break;
+ case !is_null($article_html->find('.post-content', 0)):
+ // another common content div
+ $article = $article_html->find('.post-content', 0);
+ break;
+ case !is_null($article_html->find('.post', 0)):
+ // for old WordPress themes without HTML5
+ $article = $article_html->find('.post', 0);
+ break;
+ }
- foreach ($article->find('h1.entry-title') as $title)
- if ($title->plaintext == $item['title'])
- $title->outertext = '';
+ foreach ($article->find('h1.entry-title') as $title) {
+ if ($title->plaintext == $item['title']) {
+ $title->outertext = '';
+ }
+ }
- $article_image = $article_html->find('img.wp-post-image', 0);
- if(!empty($item['content']) && (!is_object($article_image) || empty($article_image->src))) {
- $article_image = str_get_html($item['content'])->find('img.wp-post-image', 0);
- }
- if(is_object($article_image) && !empty($article_image->src)) {
- if(empty($article_image->getAttribute('data-lazy-src'))) {
- $article_image = $article_image->src;
- } else {
- $article_image = $article_image->getAttribute('data-lazy-src');
- }
- $mime_type = getMimeType($article_image);
- if (strpos($mime_type, 'image') === false)
- $article_image .= '#.image'; // force image
- if (empty($item['enclosures']))
- $item['enclosures'] = array($article_image);
- else
- $item['enclosures'] = array_merge($item['enclosures'], $article_image);
- }
+ $article_image = $article_html->find('img.wp-post-image', 0);
+ if (!empty($item['content']) && (!is_object($article_image) || empty($article_image->src))) {
+ $article_image = str_get_html($item['content'])->find('img.wp-post-image', 0);
+ }
+ if (is_object($article_image) && !empty($article_image->src)) {
+ if (empty($article_image->getAttribute('data-lazy-src'))) {
+ $article_image = $article_image->src;
+ } else {
+ $article_image = $article_image->getAttribute('data-lazy-src');
+ }
+ $mime_type = getMimeType($article_image);
+ if (strpos($mime_type, 'image') === false) {
+ $article_image .= '#.image'; // force image
+ }
+ if (empty($item['enclosures'])) {
+ $item['enclosures'] = [$article_image];
+ } else {
+ $item['enclosures'] = array_merge($item['enclosures'], $article_image);
+ }
+ }
- if(!is_null($article)) {
- $item['content'] = $this->cleanContent($article->innertext);
- $item['content'] = defaultLinkTo($item['content'], $item['uri']);
- }
+ if (!is_null($article)) {
+ $item['content'] = $this->cleanContent($article->innertext);
+ $item['content'] = defaultLinkTo($item['content'], $item['uri']);
+ }
- return $item;
- }
+ return $item;
+ }
- public function getURI(){
- $url = $this->getInput('url');
- if(empty($url)) {
- $url = parent::getURI();
- }
- return $url;
- }
+ public function getURI()
+ {
+ $url = $this->getInput('url');
+ if (empty($url)) {
+ $url = parent::getURI();
+ }
+ return $url;
+ }
- public function collectData(){
- if($this->getInput('url') && substr($this->getInput('url'), 0, strlen('http')) !== 'http') {
- // just in case someone find a way to access local files by playing with the url
- returnClientError('The url parameter must either refer to http or https protocol.');
- }
- try{
- $this->collectExpandableDatas($this->getURI() . '/feed/atom/', 20);
- } catch (Exception $e) {
- $this->collectExpandableDatas($this->getURI() . '/?feed=atom', 20);
- }
-
- }
+ public function collectData()
+ {
+ if ($this->getInput('url') && substr($this->getInput('url'), 0, strlen('http')) !== 'http') {
+ // just in case someone find a way to access local files by playing with the url
+ returnClientError('The url parameter must either refer to http or https protocol.');
+ }
+ try {
+ $this->collectExpandableDatas($this->getURI() . '/feed/atom/', 20);
+ } catch (Exception $e) {
+ $this->collectExpandableDatas($this->getURI() . '/?feed=atom', 20);
+ }
+ }
}
diff --git a/bridges/WordPressMadaraBridge.php b/bridges/WordPressMadaraBridge.php
index 3170e119..4325075c 100644
--- a/bridges/WordPressMadaraBridge.php
+++ b/bridges/WordPressMadaraBridge.php
@@ -1,132 +1,145 @@
<?php
+
/**
* This bridge currently parses only chapter lists, but it can be further
* extended to extract a list of manga titles using the implementation in this
* project as a reference: https://github.com/manga-download/hakuneko
*/
-class WordPressMadaraBridge extends BridgeAbstract {
- const URI = 'https://live.mangabooth.com/';
- const NAME = 'WordPress Madara';
- const DESCRIPTION = 'Returns latest chapters published through the Madara Manga theme.
+class WordPressMadaraBridge extends BridgeAbstract
+{
+ const URI = 'https://live.mangabooth.com/';
+ const NAME = 'WordPress Madara';
+ const DESCRIPTION = 'Returns latest chapters published through the Madara Manga theme.
The default URI shows the Madara demo page.';
- const PARAMETERS = array(
- 'Manga Chapters' => array(
- 'url' => array(
- 'name' => 'Manga URL',
- 'exampleValue' => 'https://live.mangabooth.com/manga/manga-text-chapter/',
- 'required' => true
- )
- )
- );
-
- public function getName() {
- switch($this->queriedContext) {
- case 'Manga Chapters':
- $mangaInfo = $this->getMangaInfo($this->getInput('url'));
- return $mangaInfo['title'];
- default:
- return parent::getName();
- }
- }
-
- public function getURI() {
- return $this->getInput('url') ?? self::URI;
- }
-
- public function collectData() {
- $html = $this->queryAjaxChapters();
-
- // Check if the list subcategorizes by volume
- $volumes = $html->find('ul.volumns', 0);
- if ($volumes) {
- $this->parseVolumes($volumes);
- } else {
- $this->parseChapterList($html, null);
- }
- }
-
- protected function queryAjaxChaptersNew() {
- $uri = rtrim($this->getInput('url'), '/') . '/ajax/chapters/';
- $headers = array();
- $opts = array(CURLOPT_POST => 1);
- return str_get_html(getContents($uri, $headers, $opts));
- }
-
- protected function queryAjaxChaptersOld() {
- $mangaInfo = $this->getMangaInfo($this->getInput('url'));
- $uri = rtrim($mangaInfo['root'], '/') . '/wp-admin/admin-ajax.php';
- $headers = array();
- $opts = array(CURLOPT_POSTFIELDS => array(
- 'action' => 'manga_get_chapters',
- 'manga' => $mangaInfo['id']
- ));
- return str_get_html(getContents($uri, $headers, $opts));
- }
-
- protected function queryAjaxChapters() {
- $new = $this->queryAjaxChaptersNew();
- if ($new->find('.wp-manga-chapter')) {
- return $new;
- } else {
- return $this->queryAjaxChaptersOld();
- }
- }
-
- protected function parseVolumes($volumes) {
- foreach($volumes->children(-1) as $volume) {
- $volume_name = trim($volume->find('a.has-child', 0)->plaintext);
- $this->parseChapterList($volume->find('ul', -1), $volume_name);
- }
- }
-
- protected function parseChapterList($chapters, $volume) {
- $mangaInfo = $this->getMangaInfo($this->getInput('url'));
- foreach($chapters->find('li.wp-manga-chapter') as $chap) {
- $link = $chap->find('a', 0);
-
- $item = array();
- $item['title'] = ($volume ?? '') . ' ' . trim($link->plaintext);
- $item['uri'] = $link->href;
- $item['uid'] = $link->href;
- $item['timestamp'] = $chap->find('span.chapter-release-date', 0)->plaintext;
- $item['author'] = $mangaInfo['author'] ?? null;
- $item['categories'] = $mangaInfo['categories'] ?? null;
- $this->items[] = $item;
- }
- }
-
- /**
- * Retrieves manga info from cache or title page.
- * The returned array contains 'title', 'author', and 'categories' keys for use in feed items.
- * The 'id' key contains the manga title id, used for the old ajax api.
- * The 'root' key contains the website root.
- *
- * @param $url
- * @return array
- */
- protected function getMangaInfo($url) {
- $url_cache = 'TitleInfo_' . preg_replace('/[^\w]/', '.', rtrim($url, '/'));
- $cache = $this->loadCacheValue($url_cache);
- if (isset($cache)) {
- return $cache;
- }
-
- $info = array();
- $html = getSimpleHTMLDOMCached($url);
-
- $info['title'] = html_entity_decode($html->find('*[property=og:title]', 0)->content);
- $author = $html->find('.author-content', 0);
- if (!is_null($author))
- $info['author'] = trim($author->plaintext);
- $cats = $html->find('.genres-content', 0);
- if (!is_null($cats))
- $info['categories'] = explode(', ', trim($cats->plaintext));
-
- $info['id'] = $html->find('#manga-chapters-holder', 0)->getAttribute('data-id');
- // It's possible to find this from the input parameters, but it is already available here.
- $info['root'] = $html->find('a.logo', 0)->href;
-
- $this->saveCacheValue($url_cache, $info);
- return $info;
- }
+ const PARAMETERS = [
+ 'Manga Chapters' => [
+ 'url' => [
+ 'name' => 'Manga URL',
+ 'exampleValue' => 'https://live.mangabooth.com/manga/manga-text-chapter/',
+ 'required' => true
+ ]
+ ]
+ ];
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Manga Chapters':
+ $mangaInfo = $this->getMangaInfo($this->getInput('url'));
+ return $mangaInfo['title'];
+ default:
+ return parent::getName();
+ }
+ }
+
+ public function getURI()
+ {
+ return $this->getInput('url') ?? self::URI;
+ }
+
+ public function collectData()
+ {
+ $html = $this->queryAjaxChapters();
+
+ // Check if the list subcategorizes by volume
+ $volumes = $html->find('ul.volumns', 0);
+ if ($volumes) {
+ $this->parseVolumes($volumes);
+ } else {
+ $this->parseChapterList($html, null);
+ }
+ }
+
+ protected function queryAjaxChaptersNew()
+ {
+ $uri = rtrim($this->getInput('url'), '/') . '/ajax/chapters/';
+ $headers = [];
+ $opts = [CURLOPT_POST => 1];
+ return str_get_html(getContents($uri, $headers, $opts));
+ }
+
+ protected function queryAjaxChaptersOld()
+ {
+ $mangaInfo = $this->getMangaInfo($this->getInput('url'));
+ $uri = rtrim($mangaInfo['root'], '/') . '/wp-admin/admin-ajax.php';
+ $headers = [];
+ $opts = [CURLOPT_POSTFIELDS => [
+ 'action' => 'manga_get_chapters',
+ 'manga' => $mangaInfo['id']
+ ]];
+ return str_get_html(getContents($uri, $headers, $opts));
+ }
+
+ protected function queryAjaxChapters()
+ {
+ $new = $this->queryAjaxChaptersNew();
+ if ($new->find('.wp-manga-chapter')) {
+ return $new;
+ } else {
+ return $this->queryAjaxChaptersOld();
+ }
+ }
+
+ protected function parseVolumes($volumes)
+ {
+ foreach ($volumes->children(-1) as $volume) {
+ $volume_name = trim($volume->find('a.has-child', 0)->plaintext);
+ $this->parseChapterList($volume->find('ul', -1), $volume_name);
+ }
+ }
+
+ protected function parseChapterList($chapters, $volume)
+ {
+ $mangaInfo = $this->getMangaInfo($this->getInput('url'));
+ foreach ($chapters->find('li.wp-manga-chapter') as $chap) {
+ $link = $chap->find('a', 0);
+
+ $item = [];
+ $item['title'] = ($volume ?? '') . ' ' . trim($link->plaintext);
+ $item['uri'] = $link->href;
+ $item['uid'] = $link->href;
+ $item['timestamp'] = $chap->find('span.chapter-release-date', 0)->plaintext;
+ $item['author'] = $mangaInfo['author'] ?? null;
+ $item['categories'] = $mangaInfo['categories'] ?? null;
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Retrieves manga info from cache or title page.
+ * The returned array contains 'title', 'author', and 'categories' keys for use in feed items.
+ * The 'id' key contains the manga title id, used for the old ajax api.
+ * The 'root' key contains the website root.
+ *
+ * @param $url
+ * @return array
+ */
+ protected function getMangaInfo($url)
+ {
+ $url_cache = 'TitleInfo_' . preg_replace('/[^\w]/', '.', rtrim($url, '/'));
+ $cache = $this->loadCacheValue($url_cache);
+ if (isset($cache)) {
+ return $cache;
+ }
+
+ $info = [];
+ $html = getSimpleHTMLDOMCached($url);
+
+ $info['title'] = html_entity_decode($html->find('*[property=og:title]', 0)->content);
+ $author = $html->find('.author-content', 0);
+ if (!is_null($author)) {
+ $info['author'] = trim($author->plaintext);
+ }
+ $cats = $html->find('.genres-content', 0);
+ if (!is_null($cats)) {
+ $info['categories'] = explode(', ', trim($cats->plaintext));
+ }
+
+ $info['id'] = $html->find('#manga-chapters-holder', 0)->getAttribute('data-id');
+ // It's possible to find this from the input parameters, but it is already available here.
+ $info['root'] = $html->find('a.logo', 0)->href;
+
+ $this->saveCacheValue($url_cache, $info);
+ return $info;
+ }
}
diff --git a/bridges/WordPressPluginUpdateBridge.php b/bridges/WordPressPluginUpdateBridge.php
index 272022dd..a092d72f 100644
--- a/bridges/WordPressPluginUpdateBridge.php
+++ b/bridges/WordPressPluginUpdateBridge.php
@@ -1,62 +1,64 @@
<?php
-final class WordPressPluginUpdateBridge extends BridgeAbstract {
- const MAINTAINER = 'dvikan';
- const NAME = 'WordPress Plugins Update Bridge';
- const URI = 'https://wordpress.org/plugins/';
- const DESCRIPTION = 'Returns latest updates of wordpress.org plugins.';
-
- const PARAMETERS = [
- [
- // The incorrectly named pluginUrl is kept for BC
- 'pluginUrl' => [
- 'name' => 'Plugin slug',
- 'exampleValue' => 'akismet',
- 'required' => true,
- 'title' => 'Slug or url',
- ]
- ]
- ];
-
- public function collectData() {
- $input = trim($this->getInput('pluginUrl'));
- if (preg_match('#https://wordpress\.org/plugins/([\w-]+)#', $input, $m)) {
- $slug = $m[1];
- } else {
- $slug = str_replace(['/'], '', $input);
- }
-
- $pluginData = self::fetchPluginData($slug);
-
- if ($pluginData->versions === []) {
- throw new \Exception('This plugin does not have versioning data');
- }
-
- // We don't need trunk. I think it's the latest commit.
- unset($pluginData->versions->trunk);
-
- foreach ($pluginData->versions as $version => $downloadUrl) {
- $this->items[] = [
- 'title' => $version,
- 'uri' => sprintf('https://wordpress.org/plugins/%s/#developers', $slug),
- 'uid' => $downloadUrl,
- ];
- }
-
- usort($this->items, function($a, $b) {
- return version_compare($b['title'], $a['title']);
- });
- }
-
- /**
- * Fetch plugin data from wordpress.org json api
- *
- * https://codex.wordpress.org/WordPress.org_API#Plugins
- * https://wordpress.org/support/topic/using-the-wordpress-org-api/
- */
- private static function fetchPluginData(string $slug): \stdClass
- {
- $api = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=%s';
- return json_decode(getContents(sprintf($api, $slug)));
- }
+final class WordPressPluginUpdateBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'dvikan';
+ const NAME = 'WordPress Plugins Update Bridge';
+ const URI = 'https://wordpress.org/plugins/';
+ const DESCRIPTION = 'Returns latest updates of wordpress.org plugins.';
+
+ const PARAMETERS = [
+ [
+ // The incorrectly named pluginUrl is kept for BC
+ 'pluginUrl' => [
+ 'name' => 'Plugin slug',
+ 'exampleValue' => 'akismet',
+ 'required' => true,
+ 'title' => 'Slug or url',
+ ]
+ ]
+ ];
+
+ public function collectData()
+ {
+ $input = trim($this->getInput('pluginUrl'));
+ if (preg_match('#https://wordpress\.org/plugins/([\w-]+)#', $input, $m)) {
+ $slug = $m[1];
+ } else {
+ $slug = str_replace(['/'], '', $input);
+ }
+
+ $pluginData = self::fetchPluginData($slug);
+
+ if ($pluginData->versions === []) {
+ throw new \Exception('This plugin does not have versioning data');
+ }
+
+ // We don't need trunk. I think it's the latest commit.
+ unset($pluginData->versions->trunk);
+
+ foreach ($pluginData->versions as $version => $downloadUrl) {
+ $this->items[] = [
+ 'title' => $version,
+ 'uri' => sprintf('https://wordpress.org/plugins/%s/#developers', $slug),
+ 'uid' => $downloadUrl,
+ ];
+ }
+
+ usort($this->items, function ($a, $b) {
+ return version_compare($b['title'], $a['title']);
+ });
+ }
+
+ /**
+ * Fetch plugin data from wordpress.org json api
+ *
+ * https://codex.wordpress.org/WordPress.org_API#Plugins
+ * https://wordpress.org/support/topic/using-the-wordpress-org-api/
+ */
+ private static function fetchPluginData(string $slug): \stdClass
+ {
+ $api = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=%s';
+ return json_decode(getContents(sprintf($api, $slug)));
+ }
}
diff --git a/bridges/WorldCosplayBridge.php b/bridges/WorldCosplayBridge.php
index b60d7948..cb28eee2 100644
--- a/bridges/WorldCosplayBridge.php
+++ b/bridges/WorldCosplayBridge.php
@@ -1,142 +1,146 @@
<?php
-class WorldCosplayBridge extends BridgeAbstract {
- const NAME = 'WorldCosplay Bridge';
- const URI = 'https://worldcosplay.net/';
- const DESCRIPTION = 'Returns WorldCosplay photos';
- const MAINTAINER = 'AxorPL';
- const API_CHARACTER = 'api/photo/list.json?character_id=%u&limit=%u';
- const API_COSPLAYER = 'api/member/photos.json?member_id=%u&limit=%u';
- const API_SERIES = 'api/photo/list.json?title_id=%u&limit=%u';
- const API_TAG = 'api/tag/photo_list.json?id=%u&limit=%u';
+class WorldCosplayBridge extends BridgeAbstract
+{
+ const NAME = 'WorldCosplay Bridge';
+ const URI = 'https://worldcosplay.net/';
+ const DESCRIPTION = 'Returns WorldCosplay photos';
+ const MAINTAINER = 'AxorPL';
- const CONTENT_HTML
- = '<a href="%s" target="_blank"><img src="%s" alt="%s" title="%s"></a>';
+ const API_CHARACTER = 'api/photo/list.json?character_id=%u&limit=%u';
+ const API_COSPLAYER = 'api/member/photos.json?member_id=%u&limit=%u';
+ const API_SERIES = 'api/photo/list.json?title_id=%u&limit=%u';
+ const API_TAG = 'api/tag/photo_list.json?id=%u&limit=%u';
- const ERR_CONTEXT = 'No context provided';
- const ERR_QUERY = 'Unable to query: %s';
+ const CONTENT_HTML
+ = '<a href="%s" target="_blank"><img src="%s" alt="%s" title="%s"></a>';
- const LIMIT_MIN = 1;
- const LIMIT_MAX = 24;
+ const ERR_CONTEXT = 'No context provided';
+ const ERR_QUERY = 'Unable to query: %s';
- const PARAMETERS = array(
- 'Character' => array(
- 'cid' => array(
- 'name' => 'Character ID',
- 'type' => 'number',
- 'required' => true,
- 'title' => 'WorldCosplay character ID',
- 'exampleValue' => 18204
- )
- ),
- 'Cosplayer' => array(
- 'uid' => array(
- 'name' => 'Cosplayer ID',
- 'type' => 'number',
- 'required' => true,
- 'title' => 'Cosplayer\'s WorldCosplay profile ID',
- 'exampleValue' => 406782
- )
- ),
- 'Series' => array(
- 'sid' => array(
- 'name' => 'Series ID',
- 'type' => 'number',
- 'required' => true,
- 'title' => 'WorldCosplay series ID',
- 'exampleValue' => 3139
- )
- ),
- 'Tag' => array(
- 'tid' => array(
- 'name' => 'Tag ID',
- 'type' => 'number',
- 'required' => true,
- 'title' => 'WorldCosplay tag ID',
- 'exampleValue' => 33643
- )
- ),
- 'global' => array(
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Maximum number of photos to return',
- 'exampleValue' => 5,
- 'defaultValue' => 5
- )
- )
- );
+ const LIMIT_MIN = 1;
+ const LIMIT_MAX = 24;
- public function collectData() {
- $limit = $this->getInput('limit');
- $limit = min(self::LIMIT_MAX, max(self::LIMIT_MIN, $limit));
- switch($this->queriedContext) {
- case 'Character':
- $id = $this->getInput('cid');
- $url = self::API_CHARACTER;
- break;
- case 'Cosplayer':
- $id = $this->getInput('uid');
- $url = self::API_COSPLAYER;
- break;
- case 'Series':
- $id = $this->getInput('sid');
- $url = self::API_SERIES;
- break;
- case 'Tag':
- $id = $this->getInput('tid');
- $url = self::API_TAG;
- break;
- default:
- returnClientError(self::ERR_CONTEXT);
- }
- $url = self::URI . sprintf($url, $id, $limit);
+ const PARAMETERS = [
+ 'Character' => [
+ 'cid' => [
+ 'name' => 'Character ID',
+ 'type' => 'number',
+ 'required' => true,
+ 'title' => 'WorldCosplay character ID',
+ 'exampleValue' => 18204
+ ]
+ ],
+ 'Cosplayer' => [
+ 'uid' => [
+ 'name' => 'Cosplayer ID',
+ 'type' => 'number',
+ 'required' => true,
+ 'title' => 'Cosplayer\'s WorldCosplay profile ID',
+ 'exampleValue' => 406782
+ ]
+ ],
+ 'Series' => [
+ 'sid' => [
+ 'name' => 'Series ID',
+ 'type' => 'number',
+ 'required' => true,
+ 'title' => 'WorldCosplay series ID',
+ 'exampleValue' => 3139
+ ]
+ ],
+ 'Tag' => [
+ 'tid' => [
+ 'name' => 'Tag ID',
+ 'type' => 'number',
+ 'required' => true,
+ 'title' => 'WorldCosplay tag ID',
+ 'exampleValue' => 33643
+ ]
+ ],
+ 'global' => [
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Maximum number of photos to return',
+ 'exampleValue' => 5,
+ 'defaultValue' => 5
+ ]
+ ]
+ ];
- $json = json_decode(getContents($url));
- if($json->has_error) {
- returnServerError($json->message);
- }
- $list = $json->list;
+ public function collectData()
+ {
+ $limit = $this->getInput('limit');
+ $limit = min(self::LIMIT_MAX, max(self::LIMIT_MIN, $limit));
+ switch ($this->queriedContext) {
+ case 'Character':
+ $id = $this->getInput('cid');
+ $url = self::API_CHARACTER;
+ break;
+ case 'Cosplayer':
+ $id = $this->getInput('uid');
+ $url = self::API_COSPLAYER;
+ break;
+ case 'Series':
+ $id = $this->getInput('sid');
+ $url = self::API_SERIES;
+ break;
+ case 'Tag':
+ $id = $this->getInput('tid');
+ $url = self::API_TAG;
+ break;
+ default:
+ returnClientError(self::ERR_CONTEXT);
+ }
+ $url = self::URI . sprintf($url, $id, $limit);
- foreach($list as $img) {
- $image = isset($img->photo) ? $img->photo : $img;
- $item = array(
- 'uri' => self::URI . substr($image->url, 1),
- 'title' => $image->subject,
- 'timestamp' => $image->created_at,
- 'author' => $img->member->global_name,
- 'enclosures' => array($image->large_url),
- 'uid' => $image->id,
- );
- $item['content'] = sprintf(
- self::CONTENT_HTML,
- $item['uri'],
- $item['enclosures'][0],
- $item['title'],
- $item['title']
- );
- $this->items[] = $item;
- }
- }
+ $json = json_decode(getContents($url));
+ if ($json->has_error) {
+ returnServerError($json->message);
+ }
+ $list = $json->list;
- public function getName() {
- switch($this->queriedContext) {
- case 'Character':
- $id = $this->getInput('cid');
- break;
- case 'Cosplayer':
- $id = $this->getInput('uid');
- break;
- case 'Series':
- $id = $this->getInput('sid');
- break;
- case 'Tag':
- $id = $this->getInput('tid');
- break;
- default:
- return parent::getName();
- }
- return sprintf('%s %u - ', $this->queriedContext, $id) . self::NAME;
- }
+ foreach ($list as $img) {
+ $image = isset($img->photo) ? $img->photo : $img;
+ $item = [
+ 'uri' => self::URI . substr($image->url, 1),
+ 'title' => $image->subject,
+ 'timestamp' => $image->created_at,
+ 'author' => $img->member->global_name,
+ 'enclosures' => [$image->large_url],
+ 'uid' => $image->id,
+ ];
+ $item['content'] = sprintf(
+ self::CONTENT_HTML,
+ $item['uri'],
+ $item['enclosures'][0],
+ $item['title'],
+ $item['title']
+ );
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'Character':
+ $id = $this->getInput('cid');
+ break;
+ case 'Cosplayer':
+ $id = $this->getInput('uid');
+ break;
+ case 'Series':
+ $id = $this->getInput('sid');
+ break;
+ case 'Tag':
+ $id = $this->getInput('tid');
+ break;
+ default:
+ return parent::getName();
+ }
+ return sprintf('%s %u - ', $this->queriedContext, $id) . self::NAME;
+ }
}
diff --git a/bridges/WorldOfTanksBridge.php b/bridges/WorldOfTanksBridge.php
index d48b2d6c..6e7a594b 100644
--- a/bridges/WorldOfTanksBridge.php
+++ b/bridges/WorldOfTanksBridge.php
@@ -1,58 +1,62 @@
<?php
-class WorldOfTanksBridge extends FeedExpander {
-
- const MAINTAINER = 'Riduidel';
- const NAME = 'World of Tanks';
- const URI = 'https://worldoftanks.eu/';
- const DESCRIPTION = 'News about the tank slaughter game.';
-
- const PARAMETERS = array( array(
- 'lang' => array(
- 'name' => 'Langue',
- 'type' => 'list',
- 'values' => array(
- 'Français' => 'fr',
- 'English' => 'en',
- 'Español' => 'es',
- 'Deutsch' => 'de',
- 'Čeština' => 'cs',
- 'Polski' => 'pl',
- 'Türkçe' => 'tr'
- )
- )
- ));
-
- const POSSIBLE_ARTICLES = array('article', 'rich-article');
-
- public function collectData() {
- $this->collectExpandableDatas(sprintf('https://worldoftanks.eu/%s/rss/news/', $this->getInput('lang')));
- }
-
- protected function parseItem($newsItem){
- $item = parent::parseItem($newsItem);
- $item['content'] = $this->loadFullArticle($item['uri']);
- return $item;
- }
-
- /**
- * Loads the full article and returns the contents
- * @param $uri The article URI
- * @return The article content
- */
- private function loadFullArticle($uri){
- $html = getSimpleHTMLDOMCached($uri);
-
- foreach(self::POSSIBLE_ARTICLES as $article_class) {
- $content = $html->find('article', 0);
-
- if($content !== null) {
- // Remove the scripts, please
- foreach($content->find('script') as $script) {
- $script->outertext = '';
- }
- return $content->innertext;
- }
- }
- return null;
- }
+
+class WorldOfTanksBridge extends FeedExpander
+{
+ const MAINTAINER = 'Riduidel';
+ const NAME = 'World of Tanks';
+ const URI = 'https://worldoftanks.eu/';
+ const DESCRIPTION = 'News about the tank slaughter game.';
+
+ const PARAMETERS = [ [
+ 'lang' => [
+ 'name' => 'Langue',
+ 'type' => 'list',
+ 'values' => [
+ 'Français' => 'fr',
+ 'English' => 'en',
+ 'Español' => 'es',
+ 'Deutsch' => 'de',
+ 'Čeština' => 'cs',
+ 'Polski' => 'pl',
+ 'Türkçe' => 'tr'
+ ]
+ ]
+ ]];
+
+ const POSSIBLE_ARTICLES = ['article', 'rich-article'];
+
+ public function collectData()
+ {
+ $this->collectExpandableDatas(sprintf('https://worldoftanks.eu/%s/rss/news/', $this->getInput('lang')));
+ }
+
+ protected function parseItem($newsItem)
+ {
+ $item = parent::parseItem($newsItem);
+ $item['content'] = $this->loadFullArticle($item['uri']);
+ return $item;
+ }
+
+ /**
+ * Loads the full article and returns the contents
+ * @param $uri The article URI
+ * @return The article content
+ */
+ private function loadFullArticle($uri)
+ {
+ $html = getSimpleHTMLDOMCached($uri);
+
+ foreach (self::POSSIBLE_ARTICLES as $article_class) {
+ $content = $html->find('article', 0);
+
+ if ($content !== null) {
+ // Remove the scripts, please
+ foreach ($content->find('script') as $script) {
+ $script->outertext = '';
+ }
+ return $content->innertext;
+ }
+ }
+ return null;
+ }
}
diff --git a/bridges/XPathBridge.php b/bridges/XPathBridge.php
index 5aa280e0..98defddc 100644
--- a/bridges/XPathBridge.php
+++ b/bridges/XPathBridge.php
@@ -1,127 +1,128 @@
<?php
-class XPathBridge extends XPathAbstract {
- const NAME = 'XPathBridge';
- const URI = 'https://github.com/rss-bridge/rss-bridge';
- const DESCRIPTION
- = 'Parse any webpage using <a href="https://devhints.io/xpath" target="_blank">XPath expressions</a>';
- const MAINTAINER = 'Niehztog';
- const PARAMETERS = array(
- '' => array(
-
- 'url' => array(
- 'name' => 'Enter web page URL',
- 'title' => <<<"EOL"
+class XPathBridge extends XPathAbstract
+{
+ const NAME = 'XPathBridge';
+ const URI = 'https://github.com/rss-bridge/rss-bridge';
+ const DESCRIPTION
+ = 'Parse any webpage using <a href="https://devhints.io/xpath" target="_blank">XPath expressions</a>';
+ const MAINTAINER = 'Niehztog';
+ const PARAMETERS = [
+ '' => [
+
+ 'url' => [
+ 'name' => 'Enter web page URL',
+ 'title' => <<<"EOL"
You can specify any website URL which serves data suited for display in RSS feeds
(for example a news blog).
EOL
- , 'type' => 'text',
- 'exampleValue' => 'https://news.blizzard.com/en-en',
- 'defaultValue' => 'https://news.blizzard.com/en-en',
- 'required' => true
- ),
-
- 'item' => array(
- 'name' => 'Item selector',
- 'title' => <<<"EOL"
+ , 'type' => 'text',
+ 'exampleValue' => 'https://news.blizzard.com/en-en',
+ 'defaultValue' => 'https://news.blizzard.com/en-en',
+ 'required' => true
+ ],
+
+ 'item' => [
+ 'name' => 'Item selector',
+ 'title' => <<<"EOL"
Enter an XPath expression matching a list of dom nodes, each node containing one
feed article item in total (usually a surrounding &lt;div&gt; or &lt;span&gt; tag). This will
be the context nodes for all of the following expressions. This expression usually
starts with a single forward slash.
EOL
- , 'type' => 'text',
- 'exampleValue' => '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article',
- 'defaultValue' => '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article',
- 'required' => true
- ),
-
- 'title' => array(
- 'name' => 'Item title selector',
- 'title' => <<<"EOL"
+ , 'type' => 'text',
+ 'exampleValue' => '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article',
+ 'defaultValue' => '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article',
+ 'required' => true
+ ],
+
+ 'title' => [
+ 'name' => 'Item title selector',
+ 'title' => <<<"EOL"
This expression should match a node contained within each article item node
containing the article headline. It should start with a dot followed by two
forward slashes, referring to any descendant nodes of the article item node.
EOL
- , 'type' => 'text',
- 'exampleValue' => './/div/div[2]/h2',
- 'defaultValue' => './/div/div[2]/h2',
- 'required' => true
- ),
-
- 'content' => array(
- 'name' => 'Item description selector',
- 'title' => <<<"EOL"
+ , 'type' => 'text',
+ 'exampleValue' => './/div/div[2]/h2',
+ 'defaultValue' => './/div/div[2]/h2',
+ 'required' => true
+ ],
+
+ 'content' => [
+ 'name' => 'Item description selector',
+ 'title' => <<<"EOL"
This expression should match a node contained within each article item node
containing the article content or description. It should start with a dot
followed by two forward slashes, referring to any descendant nodes of the
article item node.
EOL
- , 'type' => 'text',
- 'exampleValue' => './/div[@class="ArticleListItem-description"]/div[@class="h6"]',
- 'defaultValue' => './/div[@class="ArticleListItem-description"]/div[@class="h6"]',
- 'required' => false
- ),
-
- 'uri' => array(
- 'name' => 'Item URL selector',
- 'title' => <<<"EOL"
+ , 'type' => 'text',
+ 'exampleValue' => './/div[@class="ArticleListItem-description"]/div[@class="h6"]',
+ 'defaultValue' => './/div[@class="ArticleListItem-description"]/div[@class="h6"]',
+ 'required' => false
+ ],
+
+ 'uri' => [
+ 'name' => 'Item URL selector',
+ 'title' => <<<"EOL"
This expression should match a node's attribute containing the article URL
(usually the href attribute of an &lt;a&gt; tag). It should start with a dot
followed by two forward slashes, referring to any descendant nodes of
the article item node. Attributes can be selected by prepending an @ char
before the attributes name.
EOL
- , 'type' => 'text',
- 'exampleValue' => './/a[@class="ArticleLink ArticleLink"]/@href',
- 'defaultValue' => './/a[@class="ArticleLink ArticleLink"]/@href',
- 'required' => false
- ),
-
- 'author' => array(
- 'name' => 'Item author selector',
- 'title' => <<<"EOL"
+ , 'type' => 'text',
+ 'exampleValue' => './/a[@class="ArticleLink ArticleLink"]/@href',
+ 'defaultValue' => './/a[@class="ArticleLink ArticleLink"]/@href',
+ 'required' => false
+ ],
+
+ 'author' => [
+ 'name' => 'Item author selector',
+ 'title' => <<<"EOL"
This expression should match a node contained within each article item
node containing the article author's name. It should start with a dot
followed by two forward slashes, referring to any descendant nodes of
the article item node.
EOL
- , 'type' => 'text',
- 'required' => false
- ),
+ , 'type' => 'text',
+ 'required' => false
+ ],
- 'timestamp' => array(
- 'name' => 'Item date selector',
- 'title' => <<<"EOL"
+ 'timestamp' => [
+ 'name' => 'Item date selector',
+ 'title' => <<<"EOL"
This expression should match a node or node's attribute containing the
article timestamp or date (parsable by PHP's strtotime function). It
should start with a dot followed by two forward slashes, referring to
any descendant nodes of the article item node. Attributes can be
selected by prepending an @ char before the attributes name.
EOL
- , 'type' => 'text',
- 'exampleValue' => './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp',
- 'defaultValue' => './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp',
- 'required' => false
- ),
-
- 'enclosures' => array(
- 'name' => 'Item image selector',
- 'title' => <<<"EOL"
+ , 'type' => 'text',
+ 'exampleValue' => './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp',
+ 'defaultValue' => './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp',
+ 'required' => false
+ ],
+
+ 'enclosures' => [
+ 'name' => 'Item image selector',
+ 'title' => <<<"EOL"
This expression should match a node's attribute containing an article
image URL (usually the src attribute of an &lt;img&gt; tag or a style
attribute). It should start with a dot followed by two forward slashes,
referring to any descendant nodes of the article item node. Attributes
can be selected by prepending an @ char before the attributes name.
EOL
- , 'type' => 'text',
- 'exampleValue' => './/div[@class="ArticleListItem-image"]/@style',
- 'defaultValue' => './/div[@class="ArticleListItem-image"]/@style',
- 'required' => false
- ),
-
- 'categories' => array(
- 'name' => 'Item category selector',
- 'title' => <<<"EOL"
+ , 'type' => 'text',
+ 'exampleValue' => './/div[@class="ArticleListItem-image"]/@style',
+ 'defaultValue' => './/div[@class="ArticleListItem-image"]/@style',
+ 'required' => false
+ ],
+
+ 'categories' => [
+ 'name' => 'Item category selector',
+ 'title' => <<<"EOL"
This expression should match a node or node's attribute contained
within each article item node containing the article category. This
could be inside &lt;div&gt; or &lt;span&gt; tags or sometimes be hidden
@@ -130,122 +131,134 @@ forward slashes, referring to any descendant nodes of the article
item node. Attributes can be selected by prepending an @ char
before the attributes name.
EOL
- , 'type' => 'text',
- 'exampleValue' => './/div[@class="ArticleListItem-label"]',
- 'defaultValue' => './/div[@class="ArticleListItem-label"]',
- 'required' => false
- ),
-
- 'fix_encoding' => array(
- 'name' => 'Fix encoding',
- 'title' => <<<"EOL"
+ , 'type' => 'text',
+ 'exampleValue' => './/div[@class="ArticleListItem-label"]',
+ 'defaultValue' => './/div[@class="ArticleListItem-label"]',
+ 'required' => false
+ ],
+
+ 'fix_encoding' => [
+ 'name' => 'Fix encoding',
+ 'title' => <<<"EOL"
Check this to fix feed encoding by invoking PHP's utf8_decode
function on all extracted texts. Try this in case you see "broken" or
"weird" characters in your feed where you'd normally expect umlauts
or any other non-ascii characters.
EOL
- , 'type' => 'checkbox',
- 'required' => false
- ),
-
- )
- );
-
- /**
- * Source Web page URL (should provide either HTML or XML content)
- * @return string
- */
- protected function getSourceUrl(){
- return $this->encodeUri($this->getInput('url'));
- }
-
- /**
- * XPath expression for extracting the feed items from the source page
- * @return string
- */
- protected function getExpressionItem(){
- return urldecode($this->getInput('item'));
- }
-
- /**
- * XPath expression for extracting an item title from the item context
- * @return string
- */
- protected function getExpressionItemTitle(){
- return urldecode($this->getInput('title'));
- }
-
- /**
- * XPath expression for extracting an item's content from the item context
- * @return string
- */
- protected function getExpressionItemContent(){
- return urldecode($this->getInput('content'));
- }
-
- /**
- * XPath expression for extracting an item link from the item context
- * @return string
- */
- protected function getExpressionItemUri(){
- return urldecode($this->getInput('uri'));
- }
-
- /**
- * XPath expression for extracting an item author from the item context
- * @return string
- */
- protected function getExpressionItemAuthor(){
- return urldecode($this->getInput('author'));
- }
-
- /**
- * XPath expression for extracting an item timestamp from the item context
- * @return string
- */
- protected function getExpressionItemTimestamp(){
- return urldecode($this->getInput('timestamp'));
- }
-
- /**
- * XPath expression for extracting item enclosures (media content like
- * images or movies) from the item context
- * @return string
- */
- protected function getExpressionItemEnclosures(){
- return urldecode($this->getInput('enclosures'));
- }
-
- /**
- * XPath expression for extracting an item category from the item context
- * @return string
- */
- protected function getExpressionItemCategories(){
- return urldecode($this->getInput('categories'));
- }
-
- /**
- * Fix encoding
- * @return string
- */
- protected function getSettingFixEncoding(){
- return $this->getInput('fix_encoding');
- }
-
- /**
- * Fixes URL encoding issues in input URL's
- * @param $uri
- * @return string|string[]
- */
- private function encodeUri($uri)
- {
- if (strpos($uri, 'https%3A%2F%2F') === 0
- || strpos($uri, 'http%3A%2F%2F') === 0) {
- $uri = urldecode($uri);
- }
-
- $uri = str_replace('|', '%7C', $uri);
-
- return $uri;
- }
+ , 'type' => 'checkbox',
+ 'required' => false
+ ],
+
+ ]
+ ];
+
+ /**
+ * Source Web page URL (should provide either HTML or XML content)
+ * @return string
+ */
+ protected function getSourceUrl()
+ {
+ return $this->encodeUri($this->getInput('url'));
+ }
+
+ /**
+ * XPath expression for extracting the feed items from the source page
+ * @return string
+ */
+ protected function getExpressionItem()
+ {
+ return urldecode($this->getInput('item'));
+ }
+
+ /**
+ * XPath expression for extracting an item title from the item context
+ * @return string
+ */
+ protected function getExpressionItemTitle()
+ {
+ return urldecode($this->getInput('title'));
+ }
+
+ /**
+ * XPath expression for extracting an item's content from the item context
+ * @return string
+ */
+ protected function getExpressionItemContent()
+ {
+ return urldecode($this->getInput('content'));
+ }
+
+ /**
+ * XPath expression for extracting an item link from the item context
+ * @return string
+ */
+ protected function getExpressionItemUri()
+ {
+ return urldecode($this->getInput('uri'));
+ }
+
+ /**
+ * XPath expression for extracting an item author from the item context
+ * @return string
+ */
+ protected function getExpressionItemAuthor()
+ {
+ return urldecode($this->getInput('author'));
+ }
+
+ /**
+ * XPath expression for extracting an item timestamp from the item context
+ * @return string
+ */
+ protected function getExpressionItemTimestamp()
+ {
+ return urldecode($this->getInput('timestamp'));
+ }
+
+ /**
+ * XPath expression for extracting item enclosures (media content like
+ * images or movies) from the item context
+ * @return string
+ */
+ protected function getExpressionItemEnclosures()
+ {
+ return urldecode($this->getInput('enclosures'));
+ }
+
+ /**
+ * XPath expression for extracting an item category from the item context
+ * @return string
+ */
+ protected function getExpressionItemCategories()
+ {
+ return urldecode($this->getInput('categories'));
+ }
+
+ /**
+ * Fix encoding
+ * @return string
+ */
+ protected function getSettingFixEncoding()
+ {
+ return $this->getInput('fix_encoding');
+ }
+
+ /**
+ * Fixes URL encoding issues in input URL's
+ * @param $uri
+ * @return string|string[]
+ */
+ private function encodeUri($uri)
+ {
+ if (
+ strpos($uri, 'https%3A%2F%2F') === 0
+ || strpos($uri, 'http%3A%2F%2F') === 0
+ ) {
+ $uri = urldecode($uri);
+ }
+
+ $uri = str_replace('|', '%7C', $uri);
+
+ return $uri;
+ }
}
diff --git a/bridges/XbooruBridge.php b/bridges/XbooruBridge.php
index 2df4f4e1..d4a132e2 100644
--- a/bridges/XbooruBridge.php
+++ b/bridges/XbooruBridge.php
@@ -1,14 +1,15 @@
<?php
-class XbooruBridge extends GelbooruBridge {
+class XbooruBridge extends GelbooruBridge
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Xbooru';
+ const URI = 'https://xbooru.com/';
+ const DESCRIPTION = 'Returns images from given page';
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Xbooru';
- const URI = 'https://xbooru.com/';
- const DESCRIPTION = 'Returns images from given page';
-
- protected function buildThumbnailURI($element){
- return $this->getURI() . 'thumbnails/' . $element->directory
- . '/thumbnail_' . $element->hash . '.jpg';
- }
+ protected function buildThumbnailURI($element)
+ {
+ return $this->getURI() . 'thumbnails/' . $element->directory
+ . '/thumbnail_' . $element->hash . '.jpg';
+ }
}
diff --git a/bridges/XenForoBridge.php b/bridges/XenForoBridge.php
index 4904f6cf..1ecb1d74 100644
--- a/bridges/XenForoBridge.php
+++ b/bridges/XenForoBridge.php
@@ -1,4 +1,5 @@
<?php
+
/**
* This bridge generates feeds for threads from forums running XenForo version 2
*
@@ -13,452 +14,430 @@
* - https://xenforo.com/
* - https://en.wikipedia.org/wiki/XenForo
*/
-class XenForoBridge extends BridgeAbstract {
-
- // Bridge specific constants
- const CONTEXT_THREAD = 'Thread';
- const XENFORO_VERSION_1 = '1.0';
- const XENFORO_VERSION_2 = '2.0';
-
- // RSS-Bridge constants
- const NAME = 'XenForo Bridge';
- const URI = 'https://xenforo.com/';
- const DESCRIPTION = 'Generates feeds for threads in forums powered by XenForo';
- const MAINTAINER = 'logmanoriginal';
- const PARAMETERS = array(
- self::CONTEXT_THREAD => array(
- 'url' => array(
- 'name' => 'Thread URL',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Insert URL to the thread for which the feed should be generated',
- 'exampleValue' => 'https://xenforo.com/community/threads/guide-to-suggestions.2285/'
- )
- ),
- 'global' => array(
- 'limit' => array(
- 'name' => 'Limit',
- 'type' => 'number',
- 'required' => false,
- 'title' => 'Specify maximum number of elements to return in the feed',
- 'defaultValue' => 10
- )
- )
- );
- const CACHE_TIMEOUT = 7200; // 10 minutes
-
- private $title = '';
- private $threadurl = '';
- private $version; // Holds the XenForo version
-
- public function getName() {
-
- switch($this->queriedContext) {
- case self::CONTEXT_THREAD: return $this->title . ' - ' . static::NAME;
- }
-
- return parent::getName();
-
- }
-
- public function getURI() {
-
- switch($this->queriedContext) {
- case self::CONTEXT_THREAD: return $this->threadurl;
- }
-
- return parent::getURI();
-
- }
-
- public function collectData() {
-
- $this->threadurl = filter_var(
- $this->getInput('url'),
- FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED);
-
- if($this->threadurl === false) {
- returnClientError('The URL you provided is invalid!');
- }
-
- $urlparts = parse_url($this->threadurl, PHP_URL_SCHEME);
-
- // Scheme must be "http" or "https"
- if(preg_match('/http[s]{0,1}/', parse_url($this->threadurl, PHP_URL_SCHEME)) == false) {
- returnClientError('The URL you provided doesn\'t specify a valid scheme (http or https)!');
- }
-
- // Path cannot be root (../)
- if(parse_url($this->threadurl, PHP_URL_PATH) === '/') {
- returnClientError('The URL you provided doesn\'t link to a valid thread (root path)!');
- }
-
- // XenForo adds a thread ID to the URL, like "...-thread.454934283". It must be present
- if(preg_match('/.+\.\d+[\/]{0,1}/', parse_URL($this->threadurl, PHP_URL_PATH)) == false) {
- returnClientError('The URL you provided doesn\'t link to a valid thread (ID missing)!');
- }
-
- // We want to start at the first page in the thread. XenForo uses "../page-n" syntax
- // to identify pages (except for the first page).
- // Notice: XenForo uses the concept of "sentinels" to find and replace parts in the
- // URL. Technically forum hosts can change the syntax!
- if(preg_match('/.+\/(page-\d+.*)$/', $this->threadurl, $matches) != false) {
-
- // before: https://xenforo.com/community/threads/guide-to-suggestions.2285/page-5
- // after : https://xenforo.com/community/threads/guide-to-suggestions.2285/
- $this->threadurl = str_replace($matches[1], '', $this->threadurl);
-
- }
-
- $html = getSimpleHTMLDOMCached($this->threadurl);
-
- $html = defaultLinkTo($html, $this->threadurl);
-
- // Notice: The DOM structure changes depending on the XenForo version used
- if($mainContent = $html->find('div.mainContent', 0)) {
- $this->version = self::XENFORO_VERSION_1;
- } elseif ($mainContent = $html->find('div[class~="p-body"]', 0)) {
- $this->version = self::XENFORO_VERSION_2;
- } else {
- returnServerError('This forum is currently not supported!');
- }
-
- switch($this->version) {
- case self::XENFORO_VERSION_1:
-
- $titleBar = $mainContent->find('div.titleBar > h1', 0)
- or returnServerError('Error finding title bar!');
-
- $this->title = $titleBar->plaintext;
-
- // Store items from current page (we'll use $this->items as LIFO buffer)
- $this->extractThreadPostsV1($html, $this->threadurl);
- $this->extractPagesV1($html);
-
- break;
-
- case self::XENFORO_VERSION_2:
-
- $titleBar = $mainContent->find('div[class~="p-title"] h1', 0)
- or returnServerError('Error finding title bar!');
-
- $this->title = $titleBar->plaintext;
- $this->extractThreadPostsV2($html, $this->threadurl);
- $this->extractPagesV2($html);
-
- break;
- }
-
- usort($this->items, function($a, $b) {
- return $b['timestamp'] <=> $a['timestamp'];
- });
-
- $this->items = array_slice($this->items, 0, $this->getInput('limit'));
- }
-
- /**
- * Extracts thread posts
- * @param $html A simplehtmldom object
- * @param $url The url from which $html was loaded
- */
- private function extractThreadPostsV1($html, $url) {
-
- $lang = $html->find('html', 0)->lang;
-
- // Posts are contained in an "ol"
- $messageList = $html->find('#messageList > li')
- or returnServerError('Error finding message list!');
-
- foreach($messageList as $post) {
-
- if(!isset($post->attr['id'])) { // Skip ads
- continue;
- }
-
- $item = array();
-
- $item['uri'] = $url . '#' . $post->getAttribute('id');
-
- $content = $post->find('.messageContent > article', 0);
-
- // Add some style to quotes
- foreach($content->find('.bbCodeQuote') as $quote) {
- $quote->style = '
+class XenForoBridge extends BridgeAbstract
+{
+ // Bridge specific constants
+ const CONTEXT_THREAD = 'Thread';
+ const XENFORO_VERSION_1 = '1.0';
+ const XENFORO_VERSION_2 = '2.0';
+
+ // RSS-Bridge constants
+ const NAME = 'XenForo Bridge';
+ const URI = 'https://xenforo.com/';
+ const DESCRIPTION = 'Generates feeds for threads in forums powered by XenForo';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = [
+ self::CONTEXT_THREAD => [
+ 'url' => [
+ 'name' => 'Thread URL',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert URL to the thread for which the feed should be generated',
+ 'exampleValue' => 'https://xenforo.com/community/threads/guide-to-suggestions.2285/'
+ ]
+ ],
+ 'global' => [
+ 'limit' => [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Specify maximum number of elements to return in the feed',
+ 'defaultValue' => 10
+ ]
+ ]
+ ];
+ const CACHE_TIMEOUT = 7200; // 10 minutes
+
+ private $title = '';
+ private $threadurl = '';
+ private $version; // Holds the XenForo version
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case self::CONTEXT_THREAD:
+ return $this->title . ' - ' . static::NAME;
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI()
+ {
+ switch ($this->queriedContext) {
+ case self::CONTEXT_THREAD:
+ return $this->threadurl;
+ }
+
+ return parent::getURI();
+ }
+
+ public function collectData()
+ {
+ $this->threadurl = filter_var(
+ $this->getInput('url'),
+ FILTER_VALIDATE_URL,
+ FILTER_FLAG_PATH_REQUIRED
+ );
+
+ if ($this->threadurl === false) {
+ returnClientError('The URL you provided is invalid!');
+ }
+
+ $urlparts = parse_url($this->threadurl, PHP_URL_SCHEME);
+
+ // Scheme must be "http" or "https"
+ if (preg_match('/http[s]{0,1}/', parse_url($this->threadurl, PHP_URL_SCHEME)) == false) {
+ returnClientError('The URL you provided doesn\'t specify a valid scheme (http or https)!');
+ }
+
+ // Path cannot be root (../)
+ if (parse_url($this->threadurl, PHP_URL_PATH) === '/') {
+ returnClientError('The URL you provided doesn\'t link to a valid thread (root path)!');
+ }
+
+ // XenForo adds a thread ID to the URL, like "...-thread.454934283". It must be present
+ if (preg_match('/.+\.\d+[\/]{0,1}/', parse_URL($this->threadurl, PHP_URL_PATH)) == false) {
+ returnClientError('The URL you provided doesn\'t link to a valid thread (ID missing)!');
+ }
+
+ // We want to start at the first page in the thread. XenForo uses "../page-n" syntax
+ // to identify pages (except for the first page).
+ // Notice: XenForo uses the concept of "sentinels" to find and replace parts in the
+ // URL. Technically forum hosts can change the syntax!
+ if (preg_match('/.+\/(page-\d+.*)$/', $this->threadurl, $matches) != false) {
+ // before: https://xenforo.com/community/threads/guide-to-suggestions.2285/page-5
+ // after : https://xenforo.com/community/threads/guide-to-suggestions.2285/
+ $this->threadurl = str_replace($matches[1], '', $this->threadurl);
+ }
+
+ $html = getSimpleHTMLDOMCached($this->threadurl);
+
+ $html = defaultLinkTo($html, $this->threadurl);
+
+ // Notice: The DOM structure changes depending on the XenForo version used
+ if ($mainContent = $html->find('div.mainContent', 0)) {
+ $this->version = self::XENFORO_VERSION_1;
+ } elseif ($mainContent = $html->find('div[class~="p-body"]', 0)) {
+ $this->version = self::XENFORO_VERSION_2;
+ } else {
+ returnServerError('This forum is currently not supported!');
+ }
+
+ switch ($this->version) {
+ case self::XENFORO_VERSION_1:
+ $titleBar = $mainContent->find('div.titleBar > h1', 0)
+ or returnServerError('Error finding title bar!');
+
+ $this->title = $titleBar->plaintext;
+
+ // Store items from current page (we'll use $this->items as LIFO buffer)
+ $this->extractThreadPostsV1($html, $this->threadurl);
+ $this->extractPagesV1($html);
+
+ break;
+
+ case self::XENFORO_VERSION_2:
+ $titleBar = $mainContent->find('div[class~="p-title"] h1', 0)
+ or returnServerError('Error finding title bar!');
+
+ $this->title = $titleBar->plaintext;
+ $this->extractThreadPostsV2($html, $this->threadurl);
+ $this->extractPagesV2($html);
+
+ break;
+ }
+
+ usort($this->items, function ($a, $b) {
+ return $b['timestamp'] <=> $a['timestamp'];
+ });
+
+ $this->items = array_slice($this->items, 0, $this->getInput('limit'));
+ }
+
+ /**
+ * Extracts thread posts
+ * @param $html A simplehtmldom object
+ * @param $url The url from which $html was loaded
+ */
+ private function extractThreadPostsV1($html, $url)
+ {
+ $lang = $html->find('html', 0)->lang;
+
+ // Posts are contained in an "ol"
+ $messageList = $html->find('#messageList > li')
+ or returnServerError('Error finding message list!');
+
+ foreach ($messageList as $post) {
+ if (!isset($post->attr['id'])) { // Skip ads
+ continue;
+ }
+
+ $item = [];
+
+ $item['uri'] = $url . '#' . $post->getAttribute('id');
+
+ $content = $post->find('.messageContent > article', 0);
+
+ // Add some style to quotes
+ foreach ($content->find('.bbCodeQuote') as $quote) {
+ $quote->style = '
color: #495566;
background-color: rgb(248,251,253);
border: 1px solid rgb(111, 140, 180);
border-color: rgb(111, 140, 180);
font-style: italic;';
- }
-
- // Remove script tags
- foreach($content->find('script') as $script) {
- $script->outertext = '';
- }
-
- $item['content'] = $content->innertext;
-
- // Remove quotes (for the title)
- foreach($content->find('.bbCodeQuote') as $quote) {
- $quote->innertext = '';
- }
-
- $title = trim($content->plaintext);
-
- if(strlen($title) > 70) {
- $item['title'] = substr($title, 0, strpos($title, ' ', 70)) . '...';
- } else {
- $item['title'] = $title;
- }
-
- /**
- * Timestamps are presented in two forms:
- *
- * 1) short version (for older posts?)
- * <span
- * class="DateTime"
- * title="22 Oct. 2018 at 23:47"
- * >22 Oct. 2018</span>
- *
- * This form has to be interpreted depending on the current language.
- *
- * 2) long version (for newer posts?)
- * <abbr
- * class="DateTime"
- * data-time="1541008785"
- * data-diff="310694"
- * data-datestring="31 Oct. 2018"
- * data-timestring="18:59"
- * title="31 Oct. 2018 at 18:59"
- * >Wednesday at 18:59</abbr>
- *
- * This form has the timestamp embedded (data-time)
- */
- if($timestamp = $post->find('abbr.DateTime', 0)) { // long version (preffered)
- $item['timestamp'] = $timestamp->{'data-time'};
- } elseif($timestamp = $post->find('span.DateTime', 0)) { // short version
- $item['timestamp'] = $this->fixDate($timestamp->title, $lang);
- }
-
- $item['author'] = $post->getAttribute('data-author');
-
- // Bridge specific properties
- $item['id'] = $post->getAttribute('id');
-
- $this->items[] = $item;
-
- }
-
- }
-
- private function extractThreadPostsV2($html, $url) {
-
- $lang = $html->find('html', 0)->lang;
-
- $messageList = $html->find('div[class~="block-body"] article')
- or returnServerError('Error finding message list!');
-
- foreach($messageList as $post) {
-
- if(!isset($post->attr['id'])) { // Skip ads
- continue;
- }
-
- $item = array();
-
- $item['uri'] = $url . '#' . $post->getAttribute('id');
-
- $title = $post->find('div[class~="message-content"] article', 0)->plaintext;
- $end = strpos($title, ' ', min(70, strlen($title)));
- $item['title'] = substr($title, 0, $end);
-
- if ($post->find('time[datetime]', 0)) {
- $item['timestamp'] = $post->find('time[datetime]', 0)->datetime;
- } else {
- $item['timestamp'] = $this->fixDate($post->find('time', 0)->title, $lang);
- }
- $item['author'] = $post->getAttribute('data-author');
- $item['content'] = $post->find('div[class~="message-content"] article', 0);
-
- // Bridge specific properties
- $item['id'] = $post->getAttribute('id');
-
- $this->items[] = $item;
-
- }
-
- }
-
- private function extractPagesV1($html) {
-
- // A navigation bar becomes available if the number of posts grows too
- // high. When this happens we need to load further pages (from last backwards)
- if(($pageNav = $html->find('div.PageNav', 0))) {
-
- $lastpage = $pageNav->{'data-last'};
- $baseurl = $pageNav->{'data-baseurl'};
- $sentinel = $pageNav->{'data-sentinel'};
-
- $hosturl = parse_url($this->threadurl, PHP_URL_SCHEME)
- . '://'
- . parse_url($this->threadurl, PHP_URL_HOST)
- . '/';
-
- $page = $lastpage;
-
- // Load at least the last page
- do {
-
- $pageurl = str_replace($sentinel, $lastpage, $baseurl);
-
- // We can optimize performance by caching all but the last page
- if($page != $lastpage) {
- $html = getSimpleHTMLDOMCached($pageurl)
- or returnServerError('Error loading contents from ' . $pageurl . '!');
- } else {
- $html = getSimpleHTMLDOM($pageurl)
- or returnServerError('Error loading contents from ' . $pageurl . '!');
- }
-
- $html = defaultLinkTo($html, $hosturl);
-
- $this->extractThreadPostsV1($html, $pageurl);
-
- $page--;
-
- } while (count($this->items) < $this->getInput('limit') && $page != 1);
-
- }
-
- }
-
- private function extractPagesV2($html) {
-
- // A navigation bar becomes available if the number of posts grows too
- // high. When this happens we need to load further pages (from last backwards)
- if(($pageNav = $html->find('div.pageNav', 0))) {
-
- foreach($pageNav->find('li') as $nav) {
- $lastpage = $nav->plaintext;
- }
-
- // Manually extract baseurl and inject sentinel
- $baseurl = $pageNav->find('li > a', -1)->href;
- $baseurl = str_replace('page-' . $lastpage, 'page-{{sentinel}}', $baseurl);
-
- $sentinel = '{{sentinel}}';
-
- $hosturl = parse_url($this->threadurl, PHP_URL_SCHEME)
- . '://'
- . parse_url($this->threadurl, PHP_URL_HOST);
-
- $page = $lastpage;
-
- // Load at least the last page
- do {
-
- $pageurl = str_replace($sentinel, $lastpage, $baseurl);
-
- // We can optimize performance by caching all but the last page
- if($page != $lastpage) {
- $html = getSimpleHTMLDOMCached($pageurl)
- or returnServerError('Error loading contents from ' . $pageurl . '!');
- } else {
- $html = getSimpleHTMLDOM($pageurl)
- or returnServerError('Error loading contents from ' . $pageurl . '!');
- }
-
- $html = defaultLinkTo($html, $hosturl);
-
- $this->extractThreadPostsV2($html, $pageurl);
-
- $page--;
-
- } while (count($this->items) < $this->getInput('limit') && $page != 1);
-
- }
-
- }
-
- /**
- * Fixes dates depending on the choosen language:
- *
- * de : dd.mm.yy
- * en : dd.mm.yy
- * it : dd/mm/yy
- *
- * Basically strtotime doesn't convert dates correctly due to formats
- * being hard to interpret. So we use the DateTime object.
- *
- * We don't know the timezone, so just assume +00:00 (or whatever
- * DateTime chooses)
- */
- private function fixDate($date, $lang = 'en-US') {
-
- $mnamesen = array(
- 'January',
- 'Feburary',
- 'March',
- 'April',
- 'May',
- 'June',
- 'July',
- 'August',
- 'September',
- 'October',
- 'November',
- 'December'
- );
-
- switch($lang) {
- case 'en-US': // example: Jun 9, 2018 at 11:46 PM
-
- $df = date_create_from_format('M d, Y \a\t H:i A', $date);
- break;
-
- case 'de-DE': // example: 19 Juli 2018 um 19:27 Uhr
-
- $mnamesde = array(
- 'Januar',
- 'Februar',
- 'März',
- 'April',
- 'Mai',
- 'Juni',
- 'Juli',
- 'August',
- 'September',
- 'Oktober',
- 'November',
- 'Dezember'
- );
-
- $mnamesdeshort = array(
- 'Jan.',
- 'Feb.',
- 'Mär.',
- 'Apr.',
- 'Mai',
- 'Juni',
- 'Juli',
- 'Aug.',
- 'Sep.',
- 'Okt.',
- 'Nov.',
- 'Dez.'
- );
-
- $date = str_ireplace($mnamesde, $mnamesen, $date);
- $date = str_ireplace($mnamesdeshort, $mnamesen, $date);
-
- $df = date_create_from_format('d M Y \u\m H:i \U\h\r', $date);
- break;
-
- }
-
- // Debug::log(date_format($df, 'U'));
-
- return date_format($df, 'U');
-
- }
+ }
+
+ // Remove script tags
+ foreach ($content->find('script') as $script) {
+ $script->outertext = '';
+ }
+
+ $item['content'] = $content->innertext;
+
+ // Remove quotes (for the title)
+ foreach ($content->find('.bbCodeQuote') as $quote) {
+ $quote->innertext = '';
+ }
+
+ $title = trim($content->plaintext);
+
+ if (strlen($title) > 70) {
+ $item['title'] = substr($title, 0, strpos($title, ' ', 70)) . '...';
+ } else {
+ $item['title'] = $title;
+ }
+
+ /**
+ * Timestamps are presented in two forms:
+ *
+ * 1) short version (for older posts?)
+ * <span
+ * class="DateTime"
+ * title="22 Oct. 2018 at 23:47"
+ * >22 Oct. 2018</span>
+ *
+ * This form has to be interpreted depending on the current language.
+ *
+ * 2) long version (for newer posts?)
+ * <abbr
+ * class="DateTime"
+ * data-time="1541008785"
+ * data-diff="310694"
+ * data-datestring="31 Oct. 2018"
+ * data-timestring="18:59"
+ * title="31 Oct. 2018 at 18:59"
+ * >Wednesday at 18:59</abbr>
+ *
+ * This form has the timestamp embedded (data-time)
+ */
+ if ($timestamp = $post->find('abbr.DateTime', 0)) { // long version (preffered)
+ $item['timestamp'] = $timestamp->{'data-time'};
+ } elseif ($timestamp = $post->find('span.DateTime', 0)) { // short version
+ $item['timestamp'] = $this->fixDate($timestamp->title, $lang);
+ }
+
+ $item['author'] = $post->getAttribute('data-author');
+
+ // Bridge specific properties
+ $item['id'] = $post->getAttribute('id');
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function extractThreadPostsV2($html, $url)
+ {
+ $lang = $html->find('html', 0)->lang;
+
+ $messageList = $html->find('div[class~="block-body"] article')
+ or returnServerError('Error finding message list!');
+
+ foreach ($messageList as $post) {
+ if (!isset($post->attr['id'])) { // Skip ads
+ continue;
+ }
+
+ $item = [];
+
+ $item['uri'] = $url . '#' . $post->getAttribute('id');
+
+ $title = $post->find('div[class~="message-content"] article', 0)->plaintext;
+ $end = strpos($title, ' ', min(70, strlen($title)));
+ $item['title'] = substr($title, 0, $end);
+
+ if ($post->find('time[datetime]', 0)) {
+ $item['timestamp'] = $post->find('time[datetime]', 0)->datetime;
+ } else {
+ $item['timestamp'] = $this->fixDate($post->find('time', 0)->title, $lang);
+ }
+ $item['author'] = $post->getAttribute('data-author');
+ $item['content'] = $post->find('div[class~="message-content"] article', 0);
+
+ // Bridge specific properties
+ $item['id'] = $post->getAttribute('id');
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function extractPagesV1($html)
+ {
+ // A navigation bar becomes available if the number of posts grows too
+ // high. When this happens we need to load further pages (from last backwards)
+ if (($pageNav = $html->find('div.PageNav', 0))) {
+ $lastpage = $pageNav->{'data-last'};
+ $baseurl = $pageNav->{'data-baseurl'};
+ $sentinel = $pageNav->{'data-sentinel'};
+
+ $hosturl = parse_url($this->threadurl, PHP_URL_SCHEME)
+ . '://'
+ . parse_url($this->threadurl, PHP_URL_HOST)
+ . '/';
+
+ $page = $lastpage;
+
+ // Load at least the last page
+ do {
+ $pageurl = str_replace($sentinel, $lastpage, $baseurl);
+
+ // We can optimize performance by caching all but the last page
+ if ($page != $lastpage) {
+ $html = getSimpleHTMLDOMCached($pageurl)
+ or returnServerError('Error loading contents from ' . $pageurl . '!');
+ } else {
+ $html = getSimpleHTMLDOM($pageurl)
+ or returnServerError('Error loading contents from ' . $pageurl . '!');
+ }
+
+ $html = defaultLinkTo($html, $hosturl);
+
+ $this->extractThreadPostsV1($html, $pageurl);
+
+ $page--;
+ } while (count($this->items) < $this->getInput('limit') && $page != 1);
+ }
+ }
+
+ private function extractPagesV2($html)
+ {
+ // A navigation bar becomes available if the number of posts grows too
+ // high. When this happens we need to load further pages (from last backwards)
+ if (($pageNav = $html->find('div.pageNav', 0))) {
+ foreach ($pageNav->find('li') as $nav) {
+ $lastpage = $nav->plaintext;
+ }
+
+ // Manually extract baseurl and inject sentinel
+ $baseurl = $pageNav->find('li > a', -1)->href;
+ $baseurl = str_replace('page-' . $lastpage, 'page-{{sentinel}}', $baseurl);
+
+ $sentinel = '{{sentinel}}';
+
+ $hosturl = parse_url($this->threadurl, PHP_URL_SCHEME)
+ . '://'
+ . parse_url($this->threadurl, PHP_URL_HOST);
+
+ $page = $lastpage;
+
+ // Load at least the last page
+ do {
+ $pageurl = str_replace($sentinel, $lastpage, $baseurl);
+
+ // We can optimize performance by caching all but the last page
+ if ($page != $lastpage) {
+ $html = getSimpleHTMLDOMCached($pageurl)
+ or returnServerError('Error loading contents from ' . $pageurl . '!');
+ } else {
+ $html = getSimpleHTMLDOM($pageurl)
+ or returnServerError('Error loading contents from ' . $pageurl . '!');
+ }
+
+ $html = defaultLinkTo($html, $hosturl);
+
+ $this->extractThreadPostsV2($html, $pageurl);
+
+ $page--;
+ } while (count($this->items) < $this->getInput('limit') && $page != 1);
+ }
+ }
+
+ /**
+ * Fixes dates depending on the choosen language:
+ *
+ * de : dd.mm.yy
+ * en : dd.mm.yy
+ * it : dd/mm/yy
+ *
+ * Basically strtotime doesn't convert dates correctly due to formats
+ * being hard to interpret. So we use the DateTime object.
+ *
+ * We don't know the timezone, so just assume +00:00 (or whatever
+ * DateTime chooses)
+ */
+ private function fixDate($date, $lang = 'en-US')
+ {
+ $mnamesen = [
+ 'January',
+ 'Feburary',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+ ];
+
+ switch ($lang) {
+ case 'en-US': // example: Jun 9, 2018 at 11:46 PM
+ $df = date_create_from_format('M d, Y \a\t H:i A', $date);
+ break;
+
+ case 'de-DE': // example: 19 Juli 2018 um 19:27 Uhr
+ $mnamesde = [
+ 'Januar',
+ 'Februar',
+ 'März',
+ 'April',
+ 'Mai',
+ 'Juni',
+ 'Juli',
+ 'August',
+ 'September',
+ 'Oktober',
+ 'November',
+ 'Dezember'
+ ];
+
+ $mnamesdeshort = [
+ 'Jan.',
+ 'Feb.',
+ 'Mär.',
+ 'Apr.',
+ 'Mai',
+ 'Juni',
+ 'Juli',
+ 'Aug.',
+ 'Sep.',
+ 'Okt.',
+ 'Nov.',
+ 'Dez.'
+ ];
+
+ $date = str_ireplace($mnamesde, $mnamesen, $date);
+ $date = str_ireplace($mnamesdeshort, $mnamesen, $date);
+
+ $df = date_create_from_format('d M Y \u\m H:i \U\h\r', $date);
+ break;
+ }
+
+ // Debug::log(date_format($df, 'U'));
+
+ return date_format($df, 'U');
+ }
}
diff --git a/bridges/YGGTorrentBridge.php b/bridges/YGGTorrentBridge.php
index 30b5ca7a..f0c31f11 100644
--- a/bridges/YGGTorrentBridge.php
+++ b/bridges/YGGTorrentBridge.php
@@ -3,148 +3,156 @@
/* This is a mashup of FlickrExploreBridge by sebsauvage and FlickrTagBridge
* by erwang.providing the functionality of both in one.
*/
-class YGGTorrentBridge extends BridgeAbstract {
+class YGGTorrentBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'teromene';
+ const NAME = 'Yggtorrent Bridge';
+ const URI = 'https://www5.yggtorrent.fi';
+ const DESCRIPTION = 'Returns torrent search from Yggtorrent';
- const MAINTAINER = 'teromene';
- const NAME = 'Yggtorrent Bridge';
- const URI = 'https://www5.yggtorrent.fi';
- const DESCRIPTION = 'Returns torrent search from Yggtorrent';
+ const PARAMETERS = [
+ [
+ 'cat' => [
+ 'name' => 'category',
+ 'type' => 'list',
+ 'values' => [
+ 'Toutes les catégories' => 'all.all',
+ 'Film/Vidéo - Toutes les sous-catégories' => '2145.all',
+ 'Film/Vidéo - Animation' => '2145.2178',
+ 'Film/Vidéo - Animation Série' => '2145.2179',
+ 'Film/Vidéo - Concert' => '2145.2180',
+ 'Film/Vidéo - Documentaire' => '2145.2181',
+ 'Film/Vidéo - Émission TV' => '2145.2182',
+ 'Film/Vidéo - Film' => '2145.2183',
+ 'Film/Vidéo - Série TV' => '2145.2184',
+ 'Film/Vidéo - Spectacle' => '2145.2185',
+ 'Film/Vidéo - Sport' => '2145.2186',
+ 'Film/Vidéo - Vidéo-clips' => '2145.2186',
+ 'Audio - Toutes les sous-catégories' => '2139.all',
+ 'Audio - Karaoké' => '2139.2147',
+ 'Audio - Musique' => '2139.2148',
+ 'Audio - Podcast Radio' => '2139.2150',
+ 'Audio - Samples' => '2139.2149',
+ 'Jeu vidéo - Toutes les sous-catégories' => '2142.all',
+ 'Jeu vidéo - Autre' => '2142.2167',
+ 'Jeu vidéo - Linux' => '2142.2159',
+ 'Jeu vidéo - MacOS' => '2142.2160',
+ 'Jeu vidéo - Microsoft' => '2142.2162',
+ 'Jeu vidéo - Nintendo' => '2142.2163',
+ 'Jeu vidéo - Smartphone' => '2142.2165',
+ 'Jeu vidéo - Sony' => '2142.2164',
+ 'Jeu vidéo - Tablette' => '2142.2166',
+ 'Jeu vidéo - Windows' => '2142.2161',
+ 'eBook - Toutes les sous-catégories' => '2140.all',
+ 'eBook - Audio' => '2140.2151',
+ 'eBook - Bds' => '2140.2152',
+ 'eBook - Comics' => '2140.2153',
+ 'eBook - Livres' => '2140.2154',
+ 'eBook - Mangas' => '2140.2155',
+ 'eBook - Presse' => '2140.2156',
+ 'Emulation - Toutes les sous-catégories' => '2141.all',
+ 'Emulation - Emulateurs' => '2141.2157',
+ 'Emulation - Roms' => '2141.2158',
+ 'GPS - Toutes les sous-catégories' => '2141.all',
+ 'GPS - Applications' => '2141.2168',
+ 'GPS - Cartes' => '2141.2169',
+ 'GPS - Divers' => '2141.2170'
+ ]
+ ],
+ 'nom' => [
+ 'name' => 'Nom',
+ 'description' => 'Nom du torrent',
+ 'type' => 'text',
+ 'exampleValue' => 'matrix'
+ ],
+ 'description' => [
+ 'name' => 'Description',
+ 'description' => 'Description du torrent',
+ 'type' => 'text'
+ ],
+ 'fichier' => [
+ 'name' => 'Fichier',
+ 'description' => 'Fichier du torrent',
+ 'type' => 'text'
+ ],
+ 'uploader' => [
+ 'name' => 'Uploader',
+ 'description' => 'Uploader du torrent',
+ 'type' => 'text'
+ ],
- const PARAMETERS = array(
- array(
- 'cat' => array(
- 'name' => 'category',
- 'type' => 'list',
- 'values' => array(
- 'Toutes les catégories' => 'all.all',
- 'Film/Vidéo - Toutes les sous-catégories' => '2145.all',
- 'Film/Vidéo - Animation' => '2145.2178',
- 'Film/Vidéo - Animation Série' => '2145.2179',
- 'Film/Vidéo - Concert' => '2145.2180',
- 'Film/Vidéo - Documentaire' => '2145.2181',
- 'Film/Vidéo - Émission TV' => '2145.2182',
- 'Film/Vidéo - Film' => '2145.2183',
- 'Film/Vidéo - Série TV' => '2145.2184',
- 'Film/Vidéo - Spectacle' => '2145.2185',
- 'Film/Vidéo - Sport' => '2145.2186',
- 'Film/Vidéo - Vidéo-clips' => '2145.2186',
- 'Audio - Toutes les sous-catégories' => '2139.all',
- 'Audio - Karaoké' => '2139.2147',
- 'Audio - Musique' => '2139.2148',
- 'Audio - Podcast Radio' => '2139.2150',
- 'Audio - Samples' => '2139.2149',
- 'Jeu vidéo - Toutes les sous-catégories' => '2142.all',
- 'Jeu vidéo - Autre' => '2142.2167',
- 'Jeu vidéo - Linux' => '2142.2159',
- 'Jeu vidéo - MacOS' => '2142.2160',
- 'Jeu vidéo - Microsoft' => '2142.2162',
- 'Jeu vidéo - Nintendo' => '2142.2163',
- 'Jeu vidéo - Smartphone' => '2142.2165',
- 'Jeu vidéo - Sony' => '2142.2164',
- 'Jeu vidéo - Tablette' => '2142.2166',
- 'Jeu vidéo - Windows' => '2142.2161',
- 'eBook - Toutes les sous-catégories' => '2140.all',
- 'eBook - Audio' => '2140.2151',
- 'eBook - Bds' => '2140.2152',
- 'eBook - Comics' => '2140.2153',
- 'eBook - Livres' => '2140.2154',
- 'eBook - Mangas' => '2140.2155',
- 'eBook - Presse' => '2140.2156',
- 'Emulation - Toutes les sous-catégories' => '2141.all',
- 'Emulation - Emulateurs' => '2141.2157',
- 'Emulation - Roms' => '2141.2158',
- 'GPS - Toutes les sous-catégories' => '2141.all',
- 'GPS - Applications' => '2141.2168',
- 'GPS - Cartes' => '2141.2169',
- 'GPS - Divers' => '2141.2170'
- )
- ),
- 'nom' => array(
- 'name' => 'Nom',
- 'description' => 'Nom du torrent',
- 'type' => 'text',
- 'exampleValue' => 'matrix'
- ),
- 'description' => array(
- 'name' => 'Description',
- 'description' => 'Description du torrent',
- 'type' => 'text'
- ),
- 'fichier' => array(
- 'name' => 'Fichier',
- 'description' => 'Fichier du torrent',
- 'type' => 'text'
- ),
- 'uploader' => array(
- 'name' => 'Uploader',
- 'description' => 'Uploader du torrent',
- 'type' => 'text'
- ),
+ ]
+ ];
- )
- );
+ public function collectData()
+ {
+ $catInfo = explode('.', $this->getInput('cat'));
+ $category = $catInfo[0];
+ $subcategory = $catInfo[1];
- public function collectData() {
- $catInfo = explode('.', $this->getInput('cat'));
- $category = $catInfo[0];
- $subcategory = $catInfo[1];
+ $html = getSimpleHTMLDOM(self::URI . '/engine/search?name='
+ . $this->getInput('nom')
+ . '&description='
+ . $this->getInput('description')
+ . '&file='
+ . $this->getInput('fichier')
+ . '&uploader='
+ . $this->getInput('uploader')
+ . '&category='
+ . $category
+ . '&sub_category='
+ . $subcategory
+ . '&do=search&order=desc&sort=publish_date');
- $html = getSimpleHTMLDOM(self::URI . '/engine/search?name='
- . $this->getInput('nom')
- . '&description='
- . $this->getInput('description')
- . '&file='
- . $this->getInput('fichier')
- . '&uploader='
- . $this->getInput('uploader')
- . '&category='
- . $category
- . '&sub_category='
- . $subcategory
- . '&do=search&order=desc&sort=publish_date');
+ $count = 0;
+ $results = $html->find('.results', 0);
+ if (!$results) {
+ return;
+ }
- $count = 0;
- $results = $html->find('.results', 0);
- if(!$results) return;
+ foreach ($results->find('tr') as $row) {
+ $count++;
+ if ($count == 1) {
+ continue; // Skip table header
+ }
+ if ($count == 22) {
+ break; // Stop processing after 21 items (20 + 1 table header)
+ }
+ $item = [];
+ $item['timestamp'] = $row->find('.hidden', 1)->plaintext;
+ $item['title'] = $row->find('a#torrent_name', 0)->plaintext;
+ $item['uri'] = $this->processLink($row->find('a#torrent_name', 0)->href);
+ $item['seeders'] = $row->find('td', 7)->plaintext;
+ $item['leechers'] = $row->find('td', 8)->plaintext;
+ $item['size'] = $row->find('td', 5)->plaintext;
+ $item = array_merge($item, $this->collectTorrentData($item['uri']));
- foreach($results->find('tr') as $row) {
- $count++;
- if($count == 1) continue; // Skip table header
- if($count == 22) break; // Stop processing after 21 items (20 + 1 table header)
- $item = array();
- $item['timestamp'] = $row->find('.hidden', 1)->plaintext;
- $item['title'] = $row->find('a#torrent_name', 0)->plaintext;
- $item['uri'] = $this->processLink($row->find('a#torrent_name', 0)->href);
- $item['seeders'] = $row->find('td', 7)->plaintext;
- $item['leechers'] = $row->find('td', 8)->plaintext;
- $item['size'] = $row->find('td', 5)->plaintext;
- $item = array_merge($item, $this->collectTorrentData($item['uri']));
+ $this->items[] = $item;
+ }
+ }
- $this->items[] = $item;
- }
+ /**
+ * Convert special characters like é to %C3%A9 in the url
+ */
+ private function processLink($url)
+ {
+ $url = explode('/', $url);
+ foreach ($url as $index => $value) {
+ // Skip https://{self::URI}/
+ if ($index < 3) {
+ continue;
+ }
+ // Decode first so that characters like + are not encoded
+ $url[$index] = urlencode(urldecode($value));
+ }
+ return implode('/', $url);
+ }
- }
-
- /**
- * Convert special characters like é to %C3%A9 in the url
- */
- private function processLink($url) {
- $url = explode('/', $url);
- foreach($url as $index => $value) {
- // Skip https://{self::URI}/
- if ($index < 3) {
- continue;
- }
- // Decode first so that characters like + are not encoded
- $url[$index] = urlencode(urldecode($value));
- }
- return implode('/', $url);
- }
-
- private function collectTorrentData($url) {
- $page = defaultLinkTo(getSimpleHTMLDOMCached($url), self::URI);
- $author = $page->find('.informations tr', 5)->find('td', 1)->plaintext;
- $content = $page->find('.default', 1);
- return array('author' => $author, 'content' => $content);
- }
+ private function collectTorrentData($url)
+ {
+ $page = defaultLinkTo(getSimpleHTMLDOMCached($url), self::URI);
+ $author = $page->find('.informations tr', 5)->find('td', 1)->plaintext;
+ $content = $page->find('.default', 1);
+ return ['author' => $author, 'content' => $content];
+ }
}
diff --git a/bridges/YandereBridge.php b/bridges/YandereBridge.php
index 9a93ef6c..0dc11022 100644
--- a/bridges/YandereBridge.php
+++ b/bridges/YandereBridge.php
@@ -1,10 +1,9 @@
<?php
-class YandereBridge extends MoebooruBridge {
-
- const MAINTAINER = 'mitsukarenai';
- const NAME = 'Yande.re';
- const URI = 'https://yande.re/';
- const DESCRIPTION = 'Returns images from given page and tags';
-
+class YandereBridge extends MoebooruBridge
+{
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Yande.re';
+ const URI = 'https://yande.re/';
+ const DESCRIPTION = 'Returns images from given page and tags';
}
diff --git a/bridges/YeggiBridge.php b/bridges/YeggiBridge.php
index e08a8426..07f1dd4d 100644
--- a/bridges/YeggiBridge.php
+++ b/bridges/YeggiBridge.php
@@ -1,98 +1,100 @@
<?php
-class YeggiBridge extends BridgeAbstract {
- const NAME = 'Yeggi Search';
- const URI = 'https://www.yeggi.com';
- const DESCRIPTION = 'Returns 3D Models from Thingiverse, MyMiniFactory, Cults3D, and more';
- const MAINTAINER = 'AntoineTurmel';
- const PARAMETERS = array(
- array(
- 'query' => array(
- 'name' => 'Search query',
- 'type' => 'text',
- 'required' => true,
- 'title' => 'Insert your search term here',
- 'exampleValue' => 'vase'
- ),
- 'sortby' => array(
- 'name' => 'Sort by',
- 'type' => 'list',
- 'required' => false,
- 'values' => array(
- 'Best match' => '0',
- 'Popular' => '1',
- 'Latest' => '2',
- ),
- 'defaultValue' => 'newest'
- ),
- 'show' => array(
- 'name' => 'Show',
- 'type' => 'list',
- 'required' => false,
- 'values' => array(
- 'All' => '0',
- 'Free' => '1',
- 'For sale' => '2',
- ),
- 'defaultValue' => 'all'
- ),
- 'showimage' => array(
- 'name' => 'Show image in content',
- 'type' => 'checkbox',
- 'required' => false,
- 'title' => 'Activate to show the image in the content',
- 'defaultValue' => 'checked'
- )
- )
- );
+class YeggiBridge extends BridgeAbstract
+{
+ const NAME = 'Yeggi Search';
+ const URI = 'https://www.yeggi.com';
+ const DESCRIPTION = 'Returns 3D Models from Thingiverse, MyMiniFactory, Cults3D, and more';
+ const MAINTAINER = 'AntoineTurmel';
+ const PARAMETERS = [
+ [
+ 'query' => [
+ 'name' => 'Search query',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert your search term here',
+ 'exampleValue' => 'vase'
+ ],
+ 'sortby' => [
+ 'name' => 'Sort by',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => [
+ 'Best match' => '0',
+ 'Popular' => '1',
+ 'Latest' => '2',
+ ],
+ 'defaultValue' => 'newest'
+ ],
+ 'show' => [
+ 'name' => 'Show',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => [
+ 'All' => '0',
+ 'Free' => '1',
+ 'For sale' => '2',
+ ],
+ 'defaultValue' => 'all'
+ ],
+ 'showimage' => [
+ 'name' => 'Show image in content',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'title' => 'Activate to show the image in the content',
+ 'defaultValue' => 'checked'
+ ]
+ ]
+ ];
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI());
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
- $results = $html->find('div.item_1_A');
+ $results = $html->find('div.item_1_A');
- foreach($results as $result) {
+ foreach ($results as $result) {
+ $item = [];
+ $title = $result->find('.item_3_B_2', 0)->plaintext;
+ $explodeTitle = explode('&nbsp; ', $title);
+ if (count($explodeTitle) == 2) {
+ $item['title'] = $explodeTitle[1];
+ } else {
+ $item['title'] = $explodeTitle[0];
+ }
+ $item['uri'] = self::URI . $result->find('a', 0)->href;
+ $item['author'] = 'Yeggi';
- $item = array();
- $title = $result->find('.item_3_B_2', 0)->plaintext;
- $explodeTitle = explode('&nbsp; ', $title);
- if(count($explodeTitle) == 2) {
- $item['title'] = $explodeTitle[1];
- } else {
- $item['title'] = $explodeTitle[0];
- }
- $item['uri'] = self::URI . $result->find('a', 0)->href;
- $item['author'] = 'Yeggi';
+ $text = $result->find('i');
+ $item['content'] = $text[0]->plaintext . ' on ' . $text[1]->plaintext;
+ $item['uid'] = hash('md5', $item['title']);
- $text = $result->find('i');
- $item['content'] = $text[0]->plaintext . ' on ' . $text[1]->plaintext;
- $item['uid'] = hash('md5', $item['title']);
+ foreach ($result->find('.item_3_B_2 > a[href^=/q/]') as $tag) {
+ $item['tags'][] = $tag->plaintext;
+ }
- foreach($result->find('.item_3_B_2 > a[href^=/q/]') as $tag) {
- $item['tags'][] = $tag->plaintext;
- }
+ $image = $result->find('img', 0)->src;
- $image = $result->find('img', 0)->src;
+ if ($this->getInput('showimage')) {
+ $item['content'] .= '<br><img src="' . $image . '">';
+ }
- if($this->getInput('showimage')) {
- $item['content'] .= '<br><img src="' . $image . '">';
- }
+ $item['enclosures'] = [$image];
- $item['enclosures'] = array($image);
+ $this->items[] = $item;
+ }
+ }
- $this->items[] = $item;
- }
- }
+ public function getURI()
+ {
+ if (!is_null($this->getInput('query'))) {
+ $uri = self::URI . '/q/' . urlencode($this->getInput('query')) . '/';
+ $uri .= '?o_f=' . $this->getInput('show');
+ $uri .= '&o_s=' . $this->getInput('sortby');
- public function getURI(){
- if(!is_null($this->getInput('query'))) {
- $uri = self::URI . '/q/' . urlencode($this->getInput('query')) . '/';
- $uri .= '?o_f=' . $this->getInput('show');
- $uri .= '&o_s=' . $this->getInput('sortby');
+ return $uri;
+ }
- return $uri;
- }
-
- return parent::getURI();
- }
+ return parent::getURI();
+ }
}
diff --git a/bridges/YouTubeCommunityTabBridge.php b/bridges/YouTubeCommunityTabBridge.php
index 842e7489..c44e9557 100644
--- a/bridges/YouTubeCommunityTabBridge.php
+++ b/bridges/YouTubeCommunityTabBridge.php
@@ -1,271 +1,280 @@
<?php
-class YouTubeCommunityTabBridge extends BridgeAbstract {
- const NAME = 'YouTube Community Tab Bridge';
- const URI = 'https://www.youtube.com';
- const DESCRIPTION = 'Returns posts from a channel\'s community tab';
- const MAINTAINER = 'VerifiedJoseph';
- const PARAMETERS = array(
- 'By channel ID' => array(
- 'channel' => array(
- 'name' => 'Channel ID',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'UCULkRHBdLC5ZcEQBaL0oYHQ'
- )
- ),
- 'By username' => array(
- 'username' => array(
- 'name' => 'Username',
- 'type' => 'text',
- 'required' => true,
- 'exampleValue' => 'YouTubeUK'
- ),
- )
- );
-
- const CACHE_TIMEOUT = 3600; // 1 hour
-
- private $feedUrl = '';
- private $feedName = '';
- private $itemTitle = '';
-
- private $urlRegex = '/youtube\.com\/(channel|user|c)\/([\w]+)\/community/';
- private $jsonRegex = '/var ytInitialData = (.*);<\/script>/';
-
- public function detectParameters($url) {
- $params = array();
-
- if(preg_match($this->urlRegex, $url, $matches)) {
- if ($matches[1] === 'channel') {
- $params['context'] = 'By channel ID';
- $params['channel'] = $matches[2];
- }
-
- if ($matches[1] === 'user') {
- $params['context'] = 'By username';
- $params['username'] = $matches[2];
- }
-
- return $params;
- }
-
- return null;
- }
-
- public function collectData() {
-
- if (is_null($this->getInput('username')) === false) {
- try {
- $this->feedUrl = $this->buildCommunityUri($this->getInput('username'), 'c');
- $html = getSimpleHTMLDOM($this->feedUrl);
-
- } catch (Exception $e) {
- $this->feedUrl = $this->buildCommunityUri($this->getInput('username'), 'user');
- $html = getSimpleHTMLDOM($this->feedUrl);
- }
- } else {
- $this->feedUrl = $this->buildCommunityUri($this->getInput('channel'), 'channel');
- $html = getSimpleHTMLDOM($this->feedUrl);
- }
-
- $json = $this->extractJson($html->find('body', 0)->innertext);
-
- $this->feedName = $json->header->c4TabbedHeaderRenderer->title;
-
- if ($this->hasCommunityTab($json) === false) {
- returnServerError('Channel does not have a community tab');
- }
-
- foreach ($this->getCommunityPosts($json) as $post) {
- $this->itemTitle = '';
-
- if (!isset($post->backstagePostThreadRenderer)) {
- continue;
- }
-
- $details = $post->backstagePostThreadRenderer->post->backstagePostRenderer;
-
- $item = array();
- $item['uri'] = self::URI . '/post/' . $details->postId;
- $item['author'] = $details->authorText->runs[0]->text;
- $item['content'] = '';
-
- if (isset($details->contentText)) {
- $text = $this->getText($details->contentText->runs);
-
- $this->itemTitle = $this->ellipsisTitle($text);
- $item['content'] = $text;
- }
-
- $item['content'] .= $this->getAttachments($details);
- $item['title'] = $this->itemTitle;
-
- $this->items[] = $item;
- }
- }
-
- public function getURI() {
-
- if (!empty($this->feedUri)) {
- return $this->feedUri;
- }
-
- return parent::getURI();
- }
- public function getName() {
-
- if (!empty($this->feedName)) {
- return $this->feedName . ' - YouTube Community Tab';
- }
-
- return parent::getName();
- }
-
- /**
- * Build Community URI
- */
- private function buildCommunityUri($value, $type) {
- return self::URI . '/' . $type . '/' . $value . '/community';
- }
-
- /**
- * Extract JSON from page
- */
- private function extractJson($html) {
-
- if (!preg_match($this->jsonRegex, $html, $parts)) {
- returnServerError('Failed to extract data from page');
- }
-
- $data = json_decode($parts[1]);
-
- if ($data === false) {
- returnServerError('Failed to decode extracted data');
- }
-
- return $data;
- }
-
- /**
- * Check if channel has a community tab
- */
- private function hasCommunityTab($json) {
-
- foreach ($json->contents->twoColumnBrowseResultsRenderer->tabs as $tab) {
- if (isset($tab->tabRenderer)
- && str_ends_with($tab->tabRenderer->endpoint->commandMetadata->webCommandMetadata->url, 'community')) {
-
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Get community tab posts
- */
- private function getCommunityPosts($json) {
-
- foreach ($json->contents->twoColumnBrowseResultsRenderer->tabs as $tab) {
- if (isset($tab->tabRenderer)
- && str_ends_with($tab->tabRenderer->endpoint->commandMetadata->webCommandMetadata->url, 'community')) {
-
- return $tab->tabRenderer->content->sectionListRenderer->contents[0]->itemSectionRenderer->contents;
- }
- }
- }
-
- /**
- * Get text content for a post
- */
- private function getText($runs) {
- $text = '';
-
- foreach ($runs as $part) {
- $text .= $this->formatUrls($part->text);
- }
-
- return nl2br($text);
- }
-
- /**
- * Get attachments for posts
- */
- private function getAttachments($details) {
- $content = '';
-
- if (isset($details->backstageAttachment)) {
- $attachments = $details->backstageAttachment;
-
- // Video
- if (isset($attachments->videoRenderer) && isset($attachments->videoRenderer->videoId)) {
- if (empty($this->itemTitle)) {
- $this->itemTitle = $this->feedName . ' posted a video';
- }
-
- $content = <<<EOD
+class YouTubeCommunityTabBridge extends BridgeAbstract
+{
+ const NAME = 'YouTube Community Tab Bridge';
+ const URI = 'https://www.youtube.com';
+ const DESCRIPTION = 'Returns posts from a channel\'s community tab';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = [
+ 'By channel ID' => [
+ 'channel' => [
+ 'name' => 'Channel ID',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'UCULkRHBdLC5ZcEQBaL0oYHQ'
+ ]
+ ],
+ 'By username' => [
+ 'username' => [
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'YouTubeUK'
+ ],
+ ]
+ ];
+
+ const CACHE_TIMEOUT = 3600; // 1 hour
+
+ private $feedUrl = '';
+ private $feedName = '';
+ private $itemTitle = '';
+
+ private $urlRegex = '/youtube\.com\/(channel|user|c)\/([\w]+)\/community/';
+ private $jsonRegex = '/var ytInitialData = (.*);<\/script>/';
+
+ public function detectParameters($url)
+ {
+ $params = [];
+
+ if (preg_match($this->urlRegex, $url, $matches)) {
+ if ($matches[1] === 'channel') {
+ $params['context'] = 'By channel ID';
+ $params['channel'] = $matches[2];
+ }
+
+ if ($matches[1] === 'user') {
+ $params['context'] = 'By username';
+ $params['username'] = $matches[2];
+ }
+
+ return $params;
+ }
+
+ return null;
+ }
+
+ public function collectData()
+ {
+ if (is_null($this->getInput('username')) === false) {
+ try {
+ $this->feedUrl = $this->buildCommunityUri($this->getInput('username'), 'c');
+ $html = getSimpleHTMLDOM($this->feedUrl);
+ } catch (Exception $e) {
+ $this->feedUrl = $this->buildCommunityUri($this->getInput('username'), 'user');
+ $html = getSimpleHTMLDOM($this->feedUrl);
+ }
+ } else {
+ $this->feedUrl = $this->buildCommunityUri($this->getInput('channel'), 'channel');
+ $html = getSimpleHTMLDOM($this->feedUrl);
+ }
+
+ $json = $this->extractJson($html->find('body', 0)->innertext);
+
+ $this->feedName = $json->header->c4TabbedHeaderRenderer->title;
+
+ if ($this->hasCommunityTab($json) === false) {
+ returnServerError('Channel does not have a community tab');
+ }
+
+ foreach ($this->getCommunityPosts($json) as $post) {
+ $this->itemTitle = '';
+
+ if (!isset($post->backstagePostThreadRenderer)) {
+ continue;
+ }
+
+ $details = $post->backstagePostThreadRenderer->post->backstagePostRenderer;
+
+ $item = [];
+ $item['uri'] = self::URI . '/post/' . $details->postId;
+ $item['author'] = $details->authorText->runs[0]->text;
+ $item['content'] = '';
+
+ if (isset($details->contentText)) {
+ $text = $this->getText($details->contentText->runs);
+
+ $this->itemTitle = $this->ellipsisTitle($text);
+ $item['content'] = $text;
+ }
+
+ $item['content'] .= $this->getAttachments($details);
+ $item['title'] = $this->itemTitle;
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI()
+ {
+ if (!empty($this->feedUri)) {
+ return $this->feedUri;
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if (!empty($this->feedName)) {
+ return $this->feedName . ' - YouTube Community Tab';
+ }
+
+ return parent::getName();
+ }
+
+ /**
+ * Build Community URI
+ */
+ private function buildCommunityUri($value, $type)
+ {
+ return self::URI . '/' . $type . '/' . $value . '/community';
+ }
+
+ /**
+ * Extract JSON from page
+ */
+ private function extractJson($html)
+ {
+ if (!preg_match($this->jsonRegex, $html, $parts)) {
+ returnServerError('Failed to extract data from page');
+ }
+
+ $data = json_decode($parts[1]);
+
+ if ($data === false) {
+ returnServerError('Failed to decode extracted data');
+ }
+
+ return $data;
+ }
+
+ /**
+ * Check if channel has a community tab
+ */
+ private function hasCommunityTab($json)
+ {
+ foreach ($json->contents->twoColumnBrowseResultsRenderer->tabs as $tab) {
+ if (
+ isset($tab->tabRenderer)
+ && str_ends_with($tab->tabRenderer->endpoint->commandMetadata->webCommandMetadata->url, 'community')
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get community tab posts
+ */
+ private function getCommunityPosts($json)
+ {
+ foreach ($json->contents->twoColumnBrowseResultsRenderer->tabs as $tab) {
+ if (
+ isset($tab->tabRenderer)
+ && str_ends_with($tab->tabRenderer->endpoint->commandMetadata->webCommandMetadata->url, 'community')
+ ) {
+ return $tab->tabRenderer->content->sectionListRenderer->contents[0]->itemSectionRenderer->contents;
+ }
+ }
+ }
+
+ /**
+ * Get text content for a post
+ */
+ private function getText($runs)
+ {
+ $text = '';
+
+ foreach ($runs as $part) {
+ $text .= $this->formatUrls($part->text);
+ }
+
+ return nl2br($text);
+ }
+
+ /**
+ * Get attachments for posts
+ */
+ private function getAttachments($details)
+ {
+ $content = '';
+
+ if (isset($details->backstageAttachment)) {
+ $attachments = $details->backstageAttachment;
+
+ // Video
+ if (isset($attachments->videoRenderer) && isset($attachments->videoRenderer->videoId)) {
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = $this->feedName . ' posted a video';
+ }
+
+ $content = <<<EOD
<iframe width="100%" height="410" src="https://www.youtube.com/embed/{$attachments->videoRenderer->videoId}"
frameborder="0" allow="encrypted-media;" allowfullscreen></iframe>
EOD;
- }
+ }
- // Image
- if (isset($attachments->backstageImageRenderer)) {
- if (empty($this->itemTitle)) {
- $this->itemTitle = $this->feedName . ' posted an image';
- }
+ // Image
+ if (isset($attachments->backstageImageRenderer)) {
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = $this->feedName . ' posted an image';
+ }
- $lastThumb = end($attachments->backstageImageRenderer->image->thumbnails);
+ $lastThumb = end($attachments->backstageImageRenderer->image->thumbnails);
- $content = <<<EOD
+ $content = <<<EOD
<p><img src="{$lastThumb->url}"></p>
EOD;
- }
+ }
- // Poll
- if (isset($attachments->pollRenderer)) {
- if (empty($this->itemTitle)) {
- $this->itemTitle = $this->feedName . ' posted a poll';
- }
+ // Poll
+ if (isset($attachments->pollRenderer)) {
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = $this->feedName . ' posted a poll';
+ }
- $pollChoices = '';
+ $pollChoices = '';
- foreach ($attachments->pollRenderer->choices as $choice) {
- $pollChoices .= <<<EOD
+ foreach ($attachments->pollRenderer->choices as $choice) {
+ $pollChoices .= <<<EOD
<li>{$choice->text->runs[0]->text}</li>
EOD;
- }
+ }
- $content = <<<EOD
+ $content = <<<EOD
<hr><p>Poll ({$attachments->pollRenderer->totalVotes->simpleText})<br><ul>{$pollChoices}</ul><p>
EOD;
- }
- }
-
- return $content;
- }
-
- /*
- Ellipsis text for title
- */
- private function ellipsisTitle($text) {
- $length = 100;
-
- if (strlen($text) > $length) {
- $text = explode('<br>', wordwrap($text, $length, '<br>'));
- return $text[0] . '...';
- }
-
- return $text;
- }
-
- private function formatUrls($content) {
- return preg_replace(
- '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims',
- '<a target="_blank" href="$1" target="_blank">$1</a> ',
- $content
- );
- }
+ }
+ }
+
+ return $content;
+ }
+
+ /*
+ Ellipsis text for title
+ */
+ private function ellipsisTitle($text)
+ {
+ $length = 100;
+
+ if (strlen($text) > $length) {
+ $text = explode('<br>', wordwrap($text, $length, '<br>'));
+ return $text[0] . '...';
+ }
+
+ return $text;
+ }
+
+ private function formatUrls($content)
+ {
+ return preg_replace(
+ '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims',
+ '<a target="_blank" href="$1" target="_blank">$1</a> ',
+ $content
+ );
+ }
}
diff --git a/bridges/YoutubeBridge.php b/bridges/YoutubeBridge.php
index 536d6c29..31414472 100644
--- a/bridges/YoutubeBridge.php
+++ b/bridges/YoutubeBridge.php
@@ -1,4 +1,5 @@
<?php
+
/**
* RssBridgeYoutube
* Returns the newest videos
@@ -6,424 +7,442 @@
* change: define('MAX_FILE_SIZE', 600000);
* into: define('MAX_FILE_SIZE', 900000); (or more)
*/
-class YoutubeBridge extends BridgeAbstract {
-
- const NAME = 'YouTube Bridge';
- const URI = 'https://www.youtube.com/';
- const CACHE_TIMEOUT = 10800; // 3h
- const DESCRIPTION = 'Returns the 10 newest videos by username/channel/playlist or search';
- const MAINTAINER = 'em92';
-
- const PARAMETERS = array(
- 'By username' => array(
- 'u' => array(
- 'name' => 'username',
- 'exampleValue' => 'LinusTechTips',
- 'required' => true
- )
- ),
- 'By channel id' => array(
- 'c' => array(
- 'name' => 'channel id',
- 'exampleValue' => 'UCw38-8_Ibv_L6hlKChHO9dQ',
- 'required' => true
- )
- ),
- 'By custom name' => array(
- 'custom' => array(
- 'name' => 'custom name',
- 'exampleValue' => 'LinusTechTips',
- 'required' => true
- )
- ),
- 'By playlist Id' => array(
- 'p' => array(
- 'name' => 'playlist id',
- 'exampleValue' => 'PL8mG-RkN2uTzJc8N0EoyhdC54prvBBLpj',
- 'required' => true
- )
- ),
- 'Search result' => array(
- 's' => array(
- 'name' => 'search keyword',
- 'exampleValue' => 'LinusTechTips',
- 'required' => true
- ),
- 'pa' => array(
- 'name' => 'page',
- 'type' => 'number',
- 'title' => 'This option is not work anymore, as YouTube will always return the same page',
- 'exampleValue' => 1
- )
- ),
- 'global' => array(
- 'duration_min' => array(
- 'name' => 'min. duration (minutes)',
- 'type' => 'number',
- 'title' => 'Minimum duration for the video in minutes',
- 'exampleValue' => 5
- ),
- 'duration_max' => array(
- 'name' => 'max. duration (minutes)',
- 'type' => 'number',
- 'title' => 'Maximum duration for the video in minutes',
- 'exampleValue' => 10
- )
- )
- );
-
- private $feedName = '';
- private $feeduri = '';
- private $channel_name = '';
- // This took from repo BetterVideoRss of VerifiedJoseph.
+class YoutubeBridge extends BridgeAbstract
+{
+ const NAME = 'YouTube Bridge';
+ const URI = 'https://www.youtube.com/';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the 10 newest videos by username/channel/playlist or search';
+ const MAINTAINER = 'em92';
+
+ const PARAMETERS = [
+ 'By username' => [
+ 'u' => [
+ 'name' => 'username',
+ 'exampleValue' => 'LinusTechTips',
+ 'required' => true
+ ]
+ ],
+ 'By channel id' => [
+ 'c' => [
+ 'name' => 'channel id',
+ 'exampleValue' => 'UCw38-8_Ibv_L6hlKChHO9dQ',
+ 'required' => true
+ ]
+ ],
+ 'By custom name' => [
+ 'custom' => [
+ 'name' => 'custom name',
+ 'exampleValue' => 'LinusTechTips',
+ 'required' => true
+ ]
+ ],
+ 'By playlist Id' => [
+ 'p' => [
+ 'name' => 'playlist id',
+ 'exampleValue' => 'PL8mG-RkN2uTzJc8N0EoyhdC54prvBBLpj',
+ 'required' => true
+ ]
+ ],
+ 'Search result' => [
+ 's' => [
+ 'name' => 'search keyword',
+ 'exampleValue' => 'LinusTechTips',
+ 'required' => true
+ ],
+ 'pa' => [
+ 'name' => 'page',
+ 'type' => 'number',
+ 'title' => 'This option is not work anymore, as YouTube will always return the same page',
+ 'exampleValue' => 1
+ ]
+ ],
+ 'global' => [
+ 'duration_min' => [
+ 'name' => 'min. duration (minutes)',
+ 'type' => 'number',
+ 'title' => 'Minimum duration for the video in minutes',
+ 'exampleValue' => 5
+ ],
+ 'duration_max' => [
+ 'name' => 'max. duration (minutes)',
+ 'type' => 'number',
+ 'title' => 'Maximum duration for the video in minutes',
+ 'exampleValue' => 10
+ ]
+ ]
+ ];
+
+ private $feedName = '';
+ private $feeduri = '';
+ private $channel_name = '';
+ // This took from repo BetterVideoRss of VerifiedJoseph.
const URI_REGEX = '/(https?:\/\/(?:www\.)?(?:[a-zA-Z0-9-.]{2,256}\.[a-z]{2,20})(\:[0-9]{2 ,4})?(?:\/[a-zA-Z0-9@:%_\+.,~#"\'!?&\/\/=\-*]+|\/)?)/ims'; //phpcs:ignore
- private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time){
- $html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid", true);
-
- // Skip unavailable videos
- if(strpos($html->innertext, 'IS_UNAVAILABLE_PAGE') !== false) {
- return;
- }
-
- $elAuthor = $html->find('span[itemprop=author] > link[itemprop=name]', 0);
- if (!is_null($elAuthor)) {
- $author = $elAuthor->getAttribute('content');
- }
-
- $elDatePublished = $html->find('meta[itemprop=datePublished]', 0);
- if(!is_null($elDatePublished))
- $time = strtotime($elDatePublished->getAttribute('content'));
-
- $jsonData = $this->getJSONData($html);
- $jsonData = $jsonData->contents->twoColumnWatchNextResults->results->results->contents;
-
- $videoSecondaryInfo = null;
- foreach($jsonData as $item) {
- if (isset($item->videoSecondaryInfoRenderer)) {
- $videoSecondaryInfo = $item->videoSecondaryInfoRenderer;
- break;
- }
- }
- if (!$videoSecondaryInfo) {
- returnServerError('Could not find videoSecondaryInfoRenderer. Error at: ' . $vid);
- }
-
- if(isset($videoSecondaryInfo->description)) {
- foreach($videoSecondaryInfo->description->runs as $description) {
- if(isset($description->navigationEndpoint)) {
- $metadata = $description->navigationEndpoint->commandMetadata->webCommandMetadata;
- $web_type = $metadata->webPageType;
- $url = $metadata->url;
- $text = '';
- switch ($web_type) {
- case 'WEB_PAGE_TYPE_UNKNOWN':
- $url_components = parse_url($url);
- if(isset($url_components['query']) && strpos($url_components['query'], '&q=') !== false) {
- parse_str($url_components['query'], $params);
- $url = urldecode($params['q']);
- }
- $text = $url;
- break;
- case 'WEB_PAGE_TYPE_WATCH':
- case 'WEB_PAGE_TYPE_BROWSE':
- $url = 'https://www.youtube.com' . $url;
- $text = $description->text;
- break;
- }
- $desc .= "<a href=\"$url\" target=\"_blank\">$text</a>";
- } else {
- $desc .= nl2br($description->text);
- }
- }
- }
- }
-
- private function ytBridgeAddItem($vid, $title, $author, $desc, $time, $thumbnail = ''){
- $item = array();
- $item['id'] = $vid;
- $item['title'] = $title;
- $item['author'] = $author;
- $item['timestamp'] = $time;
- $item['uri'] = self::URI . 'watch?v=' . $vid;
- if(!$thumbnail) {
- $thumbnail = '0'; // Fallback to default thumbnail if there aren't any provided.
- }
- $thumbnailUri = str_replace('/www.', '/img.', self::URI) . 'vi/' . $vid . '/' . $thumbnail . '.jpg';
- $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $thumbnailUri . '" /></a><br />' . $desc;
- $this->items[] = $item;
- }
-
- private function ytBridgeParseXmlFeed($xml) {
- foreach($xml->find('entry') as $element) {
- $title = $this->ytBridgeFixTitle($element->find('title', 0)->plaintext);
- $author = $element->find('name', 0)->plaintext;
- $desc = $element->find('media:description', 0)->innertext;
-
- // Make sure the description is easy on the eye :)
- $desc = htmlspecialchars($desc);
- $desc = nl2br($desc);
- $desc = preg_replace(self::URI_REGEX,
- '<a href="$1" target="_blank">$1</a> ',
- $desc);
-
- $vid = str_replace('yt:video:', '', $element->find('id', 0)->plaintext);
- $time = strtotime($element->find('published', 0)->plaintext);
- if(strpos($vid, 'googleads') === false)
- $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
- }
- $this->feedName = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext); // feedName will be used by getName()
- }
-
- private function ytBridgeFixTitle($title) {
- // convert both &#1234; and &quot; to UTF-8
- return html_entity_decode($title, ENT_QUOTES, 'UTF-8');
- }
-
- private function ytGetSimpleHTMLDOM($url, $cached = false){
- $header = array(
- 'Accept-Language: en-US'
- );
- $opts = array();
- $lowercase = true;
- $forceTagsClosed = true;
- $target_charset = DEFAULT_TARGET_CHARSET;
- $stripRN = false;
- $defaultBRText = DEFAULT_BR_TEXT;
- $defaultSpanText = DEFAULT_SPAN_TEXT;
- if ($cached) {
- return getSimpleHTMLDOMCached($url,
- 86400,
- $header,
- $opts,
- $lowercase,
- $forceTagsClosed,
- $target_charset,
- $stripRN,
- $defaultBRText,
- $defaultSpanText);
- }
- return getSimpleHTMLDOM($url,
- $header,
- $opts,
- $lowercase,
- $forceTagsClosed,
- $target_charset,
- $stripRN,
- $defaultBRText,
- $defaultSpanText);
- }
-
- private function getJSONData($html) {
- $scriptRegex = '/var ytInitialData = (.*?);<\/script>/';
- preg_match($scriptRegex, $html, $matches) or returnServerError('Could not find ytInitialData');
- return json_decode($matches[1]);
- }
-
- private function parseJSONListing($jsonData) {
- $duration_min = $this->getInput('duration_min') ?: -1;
- $duration_min = $duration_min * 60;
-
- $duration_max = $this->getInput('duration_max') ?: INF;
- $duration_max = $duration_max * 60;
-
- if($duration_max < $duration_min) {
- returnClientError('Max duration must be greater than min duration!');
- }
-
- // $vid_list = '';
-
- foreach($jsonData as $item) {
- $wrapper = null;
- if(isset($item->gridVideoRenderer)) {
- $wrapper = $item->gridVideoRenderer;
- } elseif(isset($item->videoRenderer)) {
- $wrapper = $item->videoRenderer;
- } elseif(isset($item->playlistVideoRenderer)) {
- $wrapper = $item->playlistVideoRenderer;
- } else
- continue;
-
- $vid = $wrapper->videoId;
- $title = $wrapper->title->runs[0]->text;
- if(isset($wrapper->ownerText)) {
- $this->channel_name = $wrapper->ownerText->runs[0]->text;
- } elseif(isset($wrapper->shortBylineText)) {
- $this->channel_name = $wrapper->shortBylineText->runs[0]->text;
- }
-
- $author = '';
- $desc = '';
- $time = '';
-
- // The duration comes in one of the formats:
- // hh:mm:ss / mm:ss / m:ss
- // 01:03:30 / 15:06 / 1:24
- $durationText = 0;
- if(isset($wrapper->lengthText)) {
- $durationText = $wrapper->lengthText;
- } else {
- foreach($wrapper->thumbnailOverlays as $overlay) {
- if(isset($overlay->thumbnailOverlayTimeStatusRenderer)) {
- $durationText = $overlay->thumbnailOverlayTimeStatusRenderer->text;
- break;
- }
- }
- }
-
- if(isset($durationText->simpleText)) {
- $durationText = trim($durationText->simpleText);
- } else {
- $durationText = 0;
- }
-
- if(preg_match('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', $durationText)) {
- $durationText = preg_replace('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', '$1:$2:$3', $durationText);
- } else {
- $durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText);
- }
- sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds);
- $duration = $hours * 3600 + $minutes * 60 + $seconds;
- if($duration < $duration_min || $duration > $duration_max) {
- continue;
- }
-
- // $vid_list .= $vid . ',';
- $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
- $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
- }
- }
-
- public function collectData(){
-
- $xml = '';
- $html = '';
- $url_feed = '';
- $url_listing = '';
-
- if($this->getInput('u')) { /* User and Channel modes */
- $this->request = $this->getInput('u');
- $url_feed = self::URI . 'feeds/videos.xml?user=' . urlencode($this->request);
- $url_listing = self::URI . 'user/' . urlencode($this->request) . '/videos';
- } elseif($this->getInput('c')) {
- $this->request = $this->getInput('c');
- $url_feed = self::URI . 'feeds/videos.xml?channel_id=' . urlencode($this->request);
- $url_listing = self::URI . 'channel/' . urlencode($this->request) . '/videos';
- } elseif($this->getInput('custom')) {
- $this->request = $this->getInput('custom');
- $url_listing = self::URI . urlencode($this->request) . '/videos';
- }
-
- if(!empty($url_feed) || !empty($url_listing)) {
- $this->feeduri = $url_listing;
- if(!empty($this->getInput('custom'))) {
- $html = $this->ytGetSimpleHTMLDOM($url_listing);
- $jsonData = $this->getJSONData($html);
- $url_feed = $jsonData->metadata->channelMetadataRenderer->rssUrl;
- }
- if(!$this->skipFeeds()) {
- $html = $this->ytGetSimpleHTMLDOM($url_feed);
- $this->ytBridgeParseXmlFeed($html);
- } else {
- if(empty($this->getInput('custom'))) {
- $html = $this->ytGetSimpleHTMLDOM($url_listing);
- $jsonData = $this->getJSONData($html);
- }
- $channel_id = '';
- if(isset($jsonData->contents)) {
- $channel_id = $jsonData->metadata->channelMetadataRenderer->externalId;
- $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[1];
- $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0];
- $jsonData = $jsonData->itemSectionRenderer->contents[0]->gridRenderer->items;
- $this->parseJSONListing($jsonData);
- } else {
- returnServerError('Unable to get data from YouTube. Username/Channel: ' . $this->request);
- }
- }
- $this->feedName = str_replace(' - YouTube', '', $html->find('title', 0)->plaintext);
- } elseif($this->getInput('p')) { /* playlist mode */
- // TODO: this mode makes a lot of excess video query requests.
- // To make less requests, we need to cache following dictionary "videoId -> datePublished, duration"
- // This cache will be used to find out, which videos to fetch
- // to make feed of 15 items or more, if there a lot of videos published on that date.
- $this->request = $this->getInput('p');
- $url_feed = self::URI . 'feeds/videos.xml?playlist_id=' . urlencode($this->request);
- $url_listing = self::URI . 'playlist?list=' . urlencode($this->request);
- $html = $this->ytGetSimpleHTMLDOM($url_listing);
- $jsonData = $this->getJSONData($html);
- // TODO: this method returns only first 100 video items
- // if it has more videos, playlistVideoListRenderer will have continuationItemRenderer as last element
- $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[0];
- $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0]->itemSectionRenderer;
- $jsonData = $jsonData->contents[0]->playlistVideoListRenderer->contents;
- $item_count = count($jsonData);
-
- if ($item_count <= 15 && !$this->skipFeeds() && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) {
- $this->ytBridgeParseXmlFeed($xml);
- } else {
- $this->parseJSONListing($jsonData);
- }
- $this->feedName = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
- usort($this->items, function ($item1, $item2) {
- if(!is_int($item1['timestamp']) && !is_int($item2['timestamp'])) {
- $item1['timestamp'] = strtotime($item1['timestamp']);
- $item2['timestamp'] = strtotime($item2['timestamp']);
- }
- return $item2['timestamp'] - $item1['timestamp'];
- });
- } elseif($this->getInput('s')) { /* search mode */
- $this->request = $this->getInput('s');
- $url_listing = self::URI
- . 'results?search_query='
- . urlencode($this->request)
- . '&sp=CAI%253D';
-
- $html = $this->ytGetSimpleHTMLDOM($url_listing);
-
- $jsonData = $this->getJSONData($html);
- $jsonData = $jsonData->contents->twoColumnSearchResultsRenderer->primaryContents;
- $jsonData = $jsonData->sectionListRenderer->contents;
- foreach($jsonData as $data) { // Search result includes some ads, have to filter them
- if(isset($data->itemSectionRenderer->contents[0]->videoRenderer)) {
- $jsonData = $data->itemSectionRenderer->contents;
- break;
- }
- }
- $this->parseJSONListing($jsonData);
- $this->feeduri = $url_listing;
- $this->feedName = 'Search: ' . $this->request; // feedName will be used by getName()
- } else { /* no valid mode */
- returnClientError("You must either specify either:\n - YouTube
+ private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time)
+ {
+ $html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid", true);
+
+ // Skip unavailable videos
+ if (strpos($html->innertext, 'IS_UNAVAILABLE_PAGE') !== false) {
+ return;
+ }
+
+ $elAuthor = $html->find('span[itemprop=author] > link[itemprop=name]', 0);
+ if (!is_null($elAuthor)) {
+ $author = $elAuthor->getAttribute('content');
+ }
+
+ $elDatePublished = $html->find('meta[itemprop=datePublished]', 0);
+ if (!is_null($elDatePublished)) {
+ $time = strtotime($elDatePublished->getAttribute('content'));
+ }
+
+ $jsonData = $this->getJSONData($html);
+ $jsonData = $jsonData->contents->twoColumnWatchNextResults->results->results->contents;
+
+ $videoSecondaryInfo = null;
+ foreach ($jsonData as $item) {
+ if (isset($item->videoSecondaryInfoRenderer)) {
+ $videoSecondaryInfo = $item->videoSecondaryInfoRenderer;
+ break;
+ }
+ }
+ if (!$videoSecondaryInfo) {
+ returnServerError('Could not find videoSecondaryInfoRenderer. Error at: ' . $vid);
+ }
+
+ if (isset($videoSecondaryInfo->description)) {
+ foreach ($videoSecondaryInfo->description->runs as $description) {
+ if (isset($description->navigationEndpoint)) {
+ $metadata = $description->navigationEndpoint->commandMetadata->webCommandMetadata;
+ $web_type = $metadata->webPageType;
+ $url = $metadata->url;
+ $text = '';
+ switch ($web_type) {
+ case 'WEB_PAGE_TYPE_UNKNOWN':
+ $url_components = parse_url($url);
+ if (isset($url_components['query']) && strpos($url_components['query'], '&q=') !== false) {
+ parse_str($url_components['query'], $params);
+ $url = urldecode($params['q']);
+ }
+ $text = $url;
+ break;
+ case 'WEB_PAGE_TYPE_WATCH':
+ case 'WEB_PAGE_TYPE_BROWSE':
+ $url = 'https://www.youtube.com' . $url;
+ $text = $description->text;
+ break;
+ }
+ $desc .= "<a href=\"$url\" target=\"_blank\">$text</a>";
+ } else {
+ $desc .= nl2br($description->text);
+ }
+ }
+ }
+ }
+
+ private function ytBridgeAddItem($vid, $title, $author, $desc, $time, $thumbnail = '')
+ {
+ $item = [];
+ $item['id'] = $vid;
+ $item['title'] = $title;
+ $item['author'] = $author;
+ $item['timestamp'] = $time;
+ $item['uri'] = self::URI . 'watch?v=' . $vid;
+ if (!$thumbnail) {
+ $thumbnail = '0'; // Fallback to default thumbnail if there aren't any provided.
+ }
+ $thumbnailUri = str_replace('/www.', '/img.', self::URI) . 'vi/' . $vid . '/' . $thumbnail . '.jpg';
+ $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $thumbnailUri . '" /></a><br />' . $desc;
+ $this->items[] = $item;
+ }
+
+ private function ytBridgeParseXmlFeed($xml)
+ {
+ foreach ($xml->find('entry') as $element) {
+ $title = $this->ytBridgeFixTitle($element->find('title', 0)->plaintext);
+ $author = $element->find('name', 0)->plaintext;
+ $desc = $element->find('media:description', 0)->innertext;
+
+ // Make sure the description is easy on the eye :)
+ $desc = htmlspecialchars($desc);
+ $desc = nl2br($desc);
+ $desc = preg_replace(
+ self::URI_REGEX,
+ '<a href="$1" target="_blank">$1</a> ',
+ $desc
+ );
+
+ $vid = str_replace('yt:video:', '', $element->find('id', 0)->plaintext);
+ $time = strtotime($element->find('published', 0)->plaintext);
+ if (strpos($vid, 'googleads') === false) {
+ $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
+ }
+ }
+ $this->feedName = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext); // feedName will be used by getName()
+ }
+
+ private function ytBridgeFixTitle($title)
+ {
+ // convert both &#1234; and &quot; to UTF-8
+ return html_entity_decode($title, ENT_QUOTES, 'UTF-8');
+ }
+
+ private function ytGetSimpleHTMLDOM($url, $cached = false)
+ {
+ $header = [
+ 'Accept-Language: en-US'
+ ];
+ $opts = [];
+ $lowercase = true;
+ $forceTagsClosed = true;
+ $target_charset = DEFAULT_TARGET_CHARSET;
+ $stripRN = false;
+ $defaultBRText = DEFAULT_BR_TEXT;
+ $defaultSpanText = DEFAULT_SPAN_TEXT;
+ if ($cached) {
+ return getSimpleHTMLDOMCached(
+ $url,
+ 86400,
+ $header,
+ $opts,
+ $lowercase,
+ $forceTagsClosed,
+ $target_charset,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText
+ );
+ }
+ return getSimpleHTMLDOM(
+ $url,
+ $header,
+ $opts,
+ $lowercase,
+ $forceTagsClosed,
+ $target_charset,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText
+ );
+ }
+
+ private function getJSONData($html)
+ {
+ $scriptRegex = '/var ytInitialData = (.*?);<\/script>/';
+ preg_match($scriptRegex, $html, $matches) or returnServerError('Could not find ytInitialData');
+ return json_decode($matches[1]);
+ }
+
+ private function parseJSONListing($jsonData)
+ {
+ $duration_min = $this->getInput('duration_min') ?: -1;
+ $duration_min = $duration_min * 60;
+
+ $duration_max = $this->getInput('duration_max') ?: INF;
+ $duration_max = $duration_max * 60;
+
+ if ($duration_max < $duration_min) {
+ returnClientError('Max duration must be greater than min duration!');
+ }
+
+ // $vid_list = '';
+
+ foreach ($jsonData as $item) {
+ $wrapper = null;
+ if (isset($item->gridVideoRenderer)) {
+ $wrapper = $item->gridVideoRenderer;
+ } elseif (isset($item->videoRenderer)) {
+ $wrapper = $item->videoRenderer;
+ } elseif (isset($item->playlistVideoRenderer)) {
+ $wrapper = $item->playlistVideoRenderer;
+ } else {
+ continue;
+ }
+
+ $vid = $wrapper->videoId;
+ $title = $wrapper->title->runs[0]->text;
+ if (isset($wrapper->ownerText)) {
+ $this->channel_name = $wrapper->ownerText->runs[0]->text;
+ } elseif (isset($wrapper->shortBylineText)) {
+ $this->channel_name = $wrapper->shortBylineText->runs[0]->text;
+ }
+
+ $author = '';
+ $desc = '';
+ $time = '';
+
+ // The duration comes in one of the formats:
+ // hh:mm:ss / mm:ss / m:ss
+ // 01:03:30 / 15:06 / 1:24
+ $durationText = 0;
+ if (isset($wrapper->lengthText)) {
+ $durationText = $wrapper->lengthText;
+ } else {
+ foreach ($wrapper->thumbnailOverlays as $overlay) {
+ if (isset($overlay->thumbnailOverlayTimeStatusRenderer)) {
+ $durationText = $overlay->thumbnailOverlayTimeStatusRenderer->text;
+ break;
+ }
+ }
+ }
+
+ if (isset($durationText->simpleText)) {
+ $durationText = trim($durationText->simpleText);
+ } else {
+ $durationText = 0;
+ }
+
+ if (preg_match('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', $durationText)) {
+ $durationText = preg_replace('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', '$1:$2:$3', $durationText);
+ } else {
+ $durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText);
+ }
+ sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds);
+ $duration = $hours * 3600 + $minutes * 60 + $seconds;
+ if ($duration < $duration_min || $duration > $duration_max) {
+ continue;
+ }
+
+ // $vid_list .= $vid . ',';
+ $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
+ $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
+ }
+ }
+
+ public function collectData()
+ {
+ $xml = '';
+ $html = '';
+ $url_feed = '';
+ $url_listing = '';
+
+ if ($this->getInput('u')) { /* User and Channel modes */
+ $this->request = $this->getInput('u');
+ $url_feed = self::URI . 'feeds/videos.xml?user=' . urlencode($this->request);
+ $url_listing = self::URI . 'user/' . urlencode($this->request) . '/videos';
+ } elseif ($this->getInput('c')) {
+ $this->request = $this->getInput('c');
+ $url_feed = self::URI . 'feeds/videos.xml?channel_id=' . urlencode($this->request);
+ $url_listing = self::URI . 'channel/' . urlencode($this->request) . '/videos';
+ } elseif ($this->getInput('custom')) {
+ $this->request = $this->getInput('custom');
+ $url_listing = self::URI . urlencode($this->request) . '/videos';
+ }
+
+ if (!empty($url_feed) || !empty($url_listing)) {
+ $this->feeduri = $url_listing;
+ if (!empty($this->getInput('custom'))) {
+ $html = $this->ytGetSimpleHTMLDOM($url_listing);
+ $jsonData = $this->getJSONData($html);
+ $url_feed = $jsonData->metadata->channelMetadataRenderer->rssUrl;
+ }
+ if (!$this->skipFeeds()) {
+ $html = $this->ytGetSimpleHTMLDOM($url_feed);
+ $this->ytBridgeParseXmlFeed($html);
+ } else {
+ if (empty($this->getInput('custom'))) {
+ $html = $this->ytGetSimpleHTMLDOM($url_listing);
+ $jsonData = $this->getJSONData($html);
+ }
+ $channel_id = '';
+ if (isset($jsonData->contents)) {
+ $channel_id = $jsonData->metadata->channelMetadataRenderer->externalId;
+ $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[1];
+ $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0];
+ $jsonData = $jsonData->itemSectionRenderer->contents[0]->gridRenderer->items;
+ $this->parseJSONListing($jsonData);
+ } else {
+ returnServerError('Unable to get data from YouTube. Username/Channel: ' . $this->request);
+ }
+ }
+ $this->feedName = str_replace(' - YouTube', '', $html->find('title', 0)->plaintext);
+ } elseif ($this->getInput('p')) { /* playlist mode */
+ // TODO: this mode makes a lot of excess video query requests.
+ // To make less requests, we need to cache following dictionary "videoId -> datePublished, duration"
+ // This cache will be used to find out, which videos to fetch
+ // to make feed of 15 items or more, if there a lot of videos published on that date.
+ $this->request = $this->getInput('p');
+ $url_feed = self::URI . 'feeds/videos.xml?playlist_id=' . urlencode($this->request);
+ $url_listing = self::URI . 'playlist?list=' . urlencode($this->request);
+ $html = $this->ytGetSimpleHTMLDOM($url_listing);
+ $jsonData = $this->getJSONData($html);
+ // TODO: this method returns only first 100 video items
+ // if it has more videos, playlistVideoListRenderer will have continuationItemRenderer as last element
+ $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[0];
+ $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0]->itemSectionRenderer;
+ $jsonData = $jsonData->contents[0]->playlistVideoListRenderer->contents;
+ $item_count = count($jsonData);
+
+ if ($item_count <= 15 && !$this->skipFeeds() && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) {
+ $this->ytBridgeParseXmlFeed($xml);
+ } else {
+ $this->parseJSONListing($jsonData);
+ }
+ $this->feedName = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
+ usort($this->items, function ($item1, $item2) {
+ if (!is_int($item1['timestamp']) && !is_int($item2['timestamp'])) {
+ $item1['timestamp'] = strtotime($item1['timestamp']);
+ $item2['timestamp'] = strtotime($item2['timestamp']);
+ }
+ return $item2['timestamp'] - $item1['timestamp'];
+ });
+ } elseif ($this->getInput('s')) { /* search mode */
+ $this->request = $this->getInput('s');
+ $url_listing = self::URI
+ . 'results?search_query='
+ . urlencode($this->request)
+ . '&sp=CAI%253D';
+
+ $html = $this->ytGetSimpleHTMLDOM($url_listing);
+
+ $jsonData = $this->getJSONData($html);
+ $jsonData = $jsonData->contents->twoColumnSearchResultsRenderer->primaryContents;
+ $jsonData = $jsonData->sectionListRenderer->contents;
+ foreach ($jsonData as $data) { // Search result includes some ads, have to filter them
+ if (isset($data->itemSectionRenderer->contents[0]->videoRenderer)) {
+ $jsonData = $data->itemSectionRenderer->contents;
+ break;
+ }
+ }
+ $this->parseJSONListing($jsonData);
+ $this->feeduri = $url_listing;
+ $this->feedName = 'Search: ' . $this->request; // feedName will be used by getName()
+ } else { /* no valid mode */
+ returnClientError("You must either specify either:\n - YouTube
username (?u=...)\n - Channel id (?c=...)\n - Playlist id (?p=...)\n - Search (?s=...)");
- }
- }
-
- private function skipFeeds() {
- return ($this->getInput('duration_min') || $this->getInput('duration_max'));
- }
-
- public function getURI()
- {
- if (!is_null($this->getInput('p'))) {
- return static::URI . 'playlist?list=' . $this->getInput('p');
- } elseif($this->feeduri) {
- return $this->feeduri;
- }
-
- return parent::getURI();
- }
-
- public function getName(){
- // Name depends on queriedContext:
- switch($this->queriedContext) {
- case 'By username':
- case 'By channel id':
- case 'By custom name':
- case 'By playlist Id':
- case 'Search result':
- return htmlspecialchars_decode($this->feedName) . ' - YouTube'; // We already know it's a bridge, right?
- default:
- return parent::getName();
- }
- }
+ }
+ }
+
+ private function skipFeeds()
+ {
+ return ($this->getInput('duration_min') || $this->getInput('duration_max'));
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('p'))) {
+ return static::URI . 'playlist?list=' . $this->getInput('p');
+ } elseif ($this->feeduri) {
+ return $this->feeduri;
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ // Name depends on queriedContext:
+ switch ($this->queriedContext) {
+ case 'By username':
+ case 'By channel id':
+ case 'By custom name':
+ case 'By playlist Id':
+ case 'Search result':
+ return htmlspecialchars_decode($this->feedName) . ' - YouTube'; // We already know it's a bridge, right?
+ default:
+ return parent::getName();
+ }
+ }
}
diff --git a/bridges/ZDNetBridge.php b/bridges/ZDNetBridge.php
index 927e37ae..09bde8e3 100644
--- a/bridges/ZDNetBridge.php
+++ b/bridges/ZDNetBridge.php
@@ -1,204 +1,209 @@
<?php
-class ZDNetBridge extends FeedExpander {
- const MAINTAINER = 'ORelio';
- const NAME = 'ZDNet Bridge';
- const URI = 'https://www.zdnet.com/';
- const DESCRIPTION = 'Technology News, Analysis, Comments and Product Reviews for IT Professionals.';
+class ZDNetBridge extends FeedExpander
+{
+ const MAINTAINER = 'ORelio';
+ const NAME = 'ZDNet Bridge';
+ const URI = 'https://www.zdnet.com/';
+ const DESCRIPTION = 'Technology News, Analysis, Comments and Product Reviews for IT Professionals.';
- //http://www.zdnet.com/zdnet.opml
- const PARAMETERS = array( array(
- 'feed' => array(
- 'name' => 'Feed',
- 'type' => 'list',
- 'values' => array(
- 'Subscribe to ZDNet RSS Feeds' => array(
- 'All Blogs' => 'blog',
- 'Just News' => 'news',
- 'All Reviews' => 'topic/reviews',
- 'Latest Downloads' => 'downloads!recent',
- 'Latest Articles' => '/',
- 'Latest Australia Articles' => 'au',
- 'Latest UK Articles' => 'uk',
- 'Latest US Articles' => 'us',
- 'Latest Asia Articles' => 'as'
- ),
- 'Keep up with ZDNet Blogs RSS:' => array(
- 'Transforming the Datacenter' => 'blog/transforming-datacenter',
- 'SMB India' => 'blog/smb-india',
- 'Indonesia BizTech' => 'blog/indonesia-biztech',
- 'Hong Kong Techie' => 'blog/hong-kong-techie',
- 'Tech Taiwan' => 'blog/tech-taiwan',
- 'Startup India' => 'blog/startup-india',
- 'Starting Up Asia' => 'blog/starting-up-asia',
- 'Next-Gen Partner' => 'blog/partner',
- 'Post-PC Developments' => 'blog/post-pc',
- 'Benelux' => 'blog/benelux',
- 'Heat Sink' => 'blog/heat-sink',
- 'Italy\'s got tech' => 'blog/italy',
- 'African Enterprise' => 'blog/african-enterprise',
- 'New Tech for Old India' => 'blog/new-india',
- 'Estonia Uncovered' => 'blog/estonia',
- 'IT Iberia' => 'blog/iberia',
- 'Brazil Tech' => 'blog/brazil',
- '500 words into the future' => 'blog/500-words-into-the-future',
- 'ÜberTech' => 'blog/ubertech',
- 'All About Microsoft' => 'blog/microsoft',
- 'Back office' => 'blog/back-office',
- 'Barker Bites Back' => 'blog/barker-bites-back',
- 'Between the Lines' => 'blog/btl',
- 'Big on Data' => 'blog/big-data',
- 'bootstrappr' => 'blog/bootstrappr',
- 'By The Way' => 'blog/by-the-way',
- 'Central European Processing' => 'blog/central-europe',
- 'Cloud Builders' => 'blog/cloud-builders',
- 'Communication Breakdown' => 'blog/communication-breakdown',
- 'Collaboration 2.0' => 'blog/collaboration',
- 'Constellation Research' => 'blog/constellation',
- 'Consumerization: BYOD' => 'blog/consumerization',
- 'DIY-IT' => 'blog/diy-it',
- 'Enterprise Web 2.0' => 'blog/hinchcliffe',
- 'Five Nines: The Next Gen Datacenter' => 'blog/datacenter',
- 'Forrester Research' => 'blog/forrester',
- 'Full Duplex' => 'blog/full-duplex',
- 'Gen Why?' => 'blog/gen-why',
- 'Hardware 2.0' => 'blog/hardware',
- 'Identity Matters' => 'blog/identity',
- 'iGeneration' => 'blog/igeneration',
- 'Internet of Everything' => 'blog/cisco',
- 'Beyond IT Failure' => 'blog/projectfailures',
- 'Jamie\'s Mostly Linux Stuff' => 'blog/jamies-mostly-linux-stuff',
- 'Jack\'s Blog' => 'blog/jacks-blog',
- 'Laptops & Desktops' => 'blog/computers',
- 'Linux and Open Source' => 'blog/open-source',
- 'London Calling' => 'blog/london',
- 'Mapping Babel' => 'blog/mapping-babel',
- 'Mixed Signals' => 'blog/mixed-signals',
- 'Mobile India' => 'blog/mobile-india',
- 'Mobile News' => 'blog/mobile-news',
- 'Networking' => 'blog/networking',
- 'Norse Code' => 'blog/norse-code',
- 'Null Pointer' => 'blog/null-pointer',
- 'The Full Tilt' => 'blog/the-full-tilt',
- 'Pinoy Post' => 'blog/pinoy-post',
- 'Practically Tech' => 'blog/practically-tech',
- 'Product Central' => 'blog/product-central',
- 'Pulp Tech' => 'blog/violetblue',
- 'Qubits and Pieces' => 'blog/qubits-and-pieces',
- 'Securify This!' => 'blog/securify-this',
- 'Service Oriented' => 'blog/service-oriented',
- 'Small Talk' => 'blog/small-talk',
- 'Small Business Matters' => 'blog/small-business-matters',
- 'Smartphones and Cell Phones' => 'blog/cell-phones',
- 'Social Business' => 'blog/feeds',
- 'Social CRM: The Conversation' => 'blog/crm',
- 'Software & Services Safari' => 'blog/sommer',
- 'Storage Bits' => 'blog/storage',
- 'Stacking up Open Clouds' => 'blog/apac-redhat',
- 'Techie Isles' => 'blog/techie-isles',
- 'Technolatte' => 'blog/technolatte',
- 'Tech Podium' => 'blog/tech-podium',
- 'Tel Aviv Tech' => 'blog/tel-aviv',
- 'Tech Broiler' => 'blog/perlow',
- 'The SANMAN' => 'blog/the-sanman',
- 'The open source revolution' => 'blog/the-open-source-revolution',
- 'The German View' => 'blog/german',
- 'The Ed Bott Report' => 'blog/bott',
- 'The Mobile Gadgeteer' => 'blog/mobile-gadgeteer',
- 'The Apple Core' => 'blog/apple',
- 'Tom Foremski: IMHO' => 'blog/foremski',
- 'Twisted Wire' => 'blog/twisted-wire',
- 'Vive la tech' => 'blog/france',
- 'Virtually Speaking' => 'blog/virtualization',
- 'View from China' => 'blog/china',
- 'Web design & Free Software' => 'blog/web-design-and-free-software',
- 'ZDNet Government' => 'blog/government',
- 'ZDNet UK Book Reviews' => 'blog/zdnet-uk-book-reviews',
- 'ZDNet UK First Take' => 'blog/zdnet-uk-first-take',
- 'Zero Day' => 'blog/security'
- ),
- 'ZDNet Hot Topics RSS:' => array(
- 'Apple' => 'topic/apple',
- 'Collaboration' => 'topic/collaboration',
- 'Enterprise Software' => 'topic/enterprise-software',
- 'Google' => 'topic/google',
- 'Great debate' => 'topic/great-debate',
- 'Hardware' => 'topic/hardware',
- 'IBM' => 'topic/ibm',
- 'iOS' => 'topic/ios',
- 'iPhone' => 'topic/iphone',
- 'iPad' => 'topic/ipad',
- 'IT Priorities' => 'topic/it-priorities',
- 'Laptops' => 'topic/laptops',
- 'Legal' => 'topic/legal',
- 'Linux' => 'topic/linux',
- 'Microsoft' => 'topic/microsoft',
- 'Mobile OS' => 'topic/mobile-os',
- 'Mobility' => 'topic/mobility',
- 'Networking' => 'topic/networking',
- 'Oracle' => 'topic/oracle',
- 'Processors' => 'topic/processors',
- 'Samsung' => 'topic/samsung',
- 'Security' => 'topic/security',
- 'Small business: going big on mobility' => 'topic/small-business-going-big-on-mobility'
- ),
- 'Product Blogs:' => array(
- 'Digital Cameras & Camcorders' => 'blog/digitalcameras',
- 'Home Theater' => 'blog/home-theater',
- 'Laptops and Desktops' => 'blog/computers',
- 'The Mobile Gadgeteer' => 'blog/mobile-gadgeteer',
- 'Smartphones and Cell Phones' => 'blog/cell-phones',
- 'The ToyBox' => 'blog/gadgetreviews'
- ),
- 'Vertical Blogs:' => array(
- 'ZDNet Education' => 'blog/education',
- 'ZDNet Healthcare' => 'blog/healthcare',
- 'ZDNet Government' => 'blog/government'
- )
- )
- ),
- 'limit' => self::LIMIT,
- ));
+ //http://www.zdnet.com/zdnet.opml
+ const PARAMETERS = [ [
+ 'feed' => [
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'values' => [
+ 'Subscribe to ZDNet RSS Feeds' => [
+ 'All Blogs' => 'blog',
+ 'Just News' => 'news',
+ 'All Reviews' => 'topic/reviews',
+ 'Latest Downloads' => 'downloads!recent',
+ 'Latest Articles' => '/',
+ 'Latest Australia Articles' => 'au',
+ 'Latest UK Articles' => 'uk',
+ 'Latest US Articles' => 'us',
+ 'Latest Asia Articles' => 'as'
+ ],
+ 'Keep up with ZDNet Blogs RSS:' => [
+ 'Transforming the Datacenter' => 'blog/transforming-datacenter',
+ 'SMB India' => 'blog/smb-india',
+ 'Indonesia BizTech' => 'blog/indonesia-biztech',
+ 'Hong Kong Techie' => 'blog/hong-kong-techie',
+ 'Tech Taiwan' => 'blog/tech-taiwan',
+ 'Startup India' => 'blog/startup-india',
+ 'Starting Up Asia' => 'blog/starting-up-asia',
+ 'Next-Gen Partner' => 'blog/partner',
+ 'Post-PC Developments' => 'blog/post-pc',
+ 'Benelux' => 'blog/benelux',
+ 'Heat Sink' => 'blog/heat-sink',
+ 'Italy\'s got tech' => 'blog/italy',
+ 'African Enterprise' => 'blog/african-enterprise',
+ 'New Tech for Old India' => 'blog/new-india',
+ 'Estonia Uncovered' => 'blog/estonia',
+ 'IT Iberia' => 'blog/iberia',
+ 'Brazil Tech' => 'blog/brazil',
+ '500 words into the future' => 'blog/500-words-into-the-future',
+ 'ÜberTech' => 'blog/ubertech',
+ 'All About Microsoft' => 'blog/microsoft',
+ 'Back office' => 'blog/back-office',
+ 'Barker Bites Back' => 'blog/barker-bites-back',
+ 'Between the Lines' => 'blog/btl',
+ 'Big on Data' => 'blog/big-data',
+ 'bootstrappr' => 'blog/bootstrappr',
+ 'By The Way' => 'blog/by-the-way',
+ 'Central European Processing' => 'blog/central-europe',
+ 'Cloud Builders' => 'blog/cloud-builders',
+ 'Communication Breakdown' => 'blog/communication-breakdown',
+ 'Collaboration 2.0' => 'blog/collaboration',
+ 'Constellation Research' => 'blog/constellation',
+ 'Consumerization: BYOD' => 'blog/consumerization',
+ 'DIY-IT' => 'blog/diy-it',
+ 'Enterprise Web 2.0' => 'blog/hinchcliffe',
+ 'Five Nines: The Next Gen Datacenter' => 'blog/datacenter',
+ 'Forrester Research' => 'blog/forrester',
+ 'Full Duplex' => 'blog/full-duplex',
+ 'Gen Why?' => 'blog/gen-why',
+ 'Hardware 2.0' => 'blog/hardware',
+ 'Identity Matters' => 'blog/identity',
+ 'iGeneration' => 'blog/igeneration',
+ 'Internet of Everything' => 'blog/cisco',
+ 'Beyond IT Failure' => 'blog/projectfailures',
+ 'Jamie\'s Mostly Linux Stuff' => 'blog/jamies-mostly-linux-stuff',
+ 'Jack\'s Blog' => 'blog/jacks-blog',
+ 'Laptops & Desktops' => 'blog/computers',
+ 'Linux and Open Source' => 'blog/open-source',
+ 'London Calling' => 'blog/london',
+ 'Mapping Babel' => 'blog/mapping-babel',
+ 'Mixed Signals' => 'blog/mixed-signals',
+ 'Mobile India' => 'blog/mobile-india',
+ 'Mobile News' => 'blog/mobile-news',
+ 'Networking' => 'blog/networking',
+ 'Norse Code' => 'blog/norse-code',
+ 'Null Pointer' => 'blog/null-pointer',
+ 'The Full Tilt' => 'blog/the-full-tilt',
+ 'Pinoy Post' => 'blog/pinoy-post',
+ 'Practically Tech' => 'blog/practically-tech',
+ 'Product Central' => 'blog/product-central',
+ 'Pulp Tech' => 'blog/violetblue',
+ 'Qubits and Pieces' => 'blog/qubits-and-pieces',
+ 'Securify This!' => 'blog/securify-this',
+ 'Service Oriented' => 'blog/service-oriented',
+ 'Small Talk' => 'blog/small-talk',
+ 'Small Business Matters' => 'blog/small-business-matters',
+ 'Smartphones and Cell Phones' => 'blog/cell-phones',
+ 'Social Business' => 'blog/feeds',
+ 'Social CRM: The Conversation' => 'blog/crm',
+ 'Software & Services Safari' => 'blog/sommer',
+ 'Storage Bits' => 'blog/storage',
+ 'Stacking up Open Clouds' => 'blog/apac-redhat',
+ 'Techie Isles' => 'blog/techie-isles',
+ 'Technolatte' => 'blog/technolatte',
+ 'Tech Podium' => 'blog/tech-podium',
+ 'Tel Aviv Tech' => 'blog/tel-aviv',
+ 'Tech Broiler' => 'blog/perlow',
+ 'The SANMAN' => 'blog/the-sanman',
+ 'The open source revolution' => 'blog/the-open-source-revolution',
+ 'The German View' => 'blog/german',
+ 'The Ed Bott Report' => 'blog/bott',
+ 'The Mobile Gadgeteer' => 'blog/mobile-gadgeteer',
+ 'The Apple Core' => 'blog/apple',
+ 'Tom Foremski: IMHO' => 'blog/foremski',
+ 'Twisted Wire' => 'blog/twisted-wire',
+ 'Vive la tech' => 'blog/france',
+ 'Virtually Speaking' => 'blog/virtualization',
+ 'View from China' => 'blog/china',
+ 'Web design & Free Software' => 'blog/web-design-and-free-software',
+ 'ZDNet Government' => 'blog/government',
+ 'ZDNet UK Book Reviews' => 'blog/zdnet-uk-book-reviews',
+ 'ZDNet UK First Take' => 'blog/zdnet-uk-first-take',
+ 'Zero Day' => 'blog/security'
+ ],
+ 'ZDNet Hot Topics RSS:' => [
+ 'Apple' => 'topic/apple',
+ 'Collaboration' => 'topic/collaboration',
+ 'Enterprise Software' => 'topic/enterprise-software',
+ 'Google' => 'topic/google',
+ 'Great debate' => 'topic/great-debate',
+ 'Hardware' => 'topic/hardware',
+ 'IBM' => 'topic/ibm',
+ 'iOS' => 'topic/ios',
+ 'iPhone' => 'topic/iphone',
+ 'iPad' => 'topic/ipad',
+ 'IT Priorities' => 'topic/it-priorities',
+ 'Laptops' => 'topic/laptops',
+ 'Legal' => 'topic/legal',
+ 'Linux' => 'topic/linux',
+ 'Microsoft' => 'topic/microsoft',
+ 'Mobile OS' => 'topic/mobile-os',
+ 'Mobility' => 'topic/mobility',
+ 'Networking' => 'topic/networking',
+ 'Oracle' => 'topic/oracle',
+ 'Processors' => 'topic/processors',
+ 'Samsung' => 'topic/samsung',
+ 'Security' => 'topic/security',
+ 'Small business: going big on mobility' => 'topic/small-business-going-big-on-mobility'
+ ],
+ 'Product Blogs:' => [
+ 'Digital Cameras & Camcorders' => 'blog/digitalcameras',
+ 'Home Theater' => 'blog/home-theater',
+ 'Laptops and Desktops' => 'blog/computers',
+ 'The Mobile Gadgeteer' => 'blog/mobile-gadgeteer',
+ 'Smartphones and Cell Phones' => 'blog/cell-phones',
+ 'The ToyBox' => 'blog/gadgetreviews'
+ ],
+ 'Vertical Blogs:' => [
+ 'ZDNet Education' => 'blog/education',
+ 'ZDNet Healthcare' => 'blog/healthcare',
+ 'ZDNet Government' => 'blog/government'
+ ]
+ ]
+ ],
+ 'limit' => self::LIMIT,
+ ]];
- public function collectData(){
- $baseUri = static::URI;
- $feed = $this->getInput('feed');
- if(strpos($feed, 'downloads!') !== false) {
- $feed = str_replace('downloads!', '', $feed);
- $baseUri = str_replace('www.', 'downloads.', $baseUri);
- }
- $url = $baseUri . trim($feed, '/') . '/rss.xml';
- $limit = $this->getInput('limit') ?? 10;
- $this->collectExpandableDatas($url, $limit);
- }
+ public function collectData()
+ {
+ $baseUri = static::URI;
+ $feed = $this->getInput('feed');
+ if (strpos($feed, 'downloads!') !== false) {
+ $feed = str_replace('downloads!', '', $feed);
+ $baseUri = str_replace('www.', 'downloads.', $baseUri);
+ }
+ $url = $baseUri . trim($feed, '/') . '/rss.xml';
+ $limit = $this->getInput('limit') ?? 10;
+ $this->collectExpandableDatas($url, $limit);
+ }
- protected function parseItem($item){
- $item = parent::parseItem($item);
+ protected function parseItem($item)
+ {
+ $item = parent::parseItem($item);
- $article = getSimpleHTMLDOMCached($item['uri']);
- if(!$article)
- returnServerError('Could not request ZDNet: ' . $url);
+ $article = getSimpleHTMLDOMCached($item['uri']);
+ if (!$article) {
+ returnServerError('Could not request ZDNet: ' . $url);
+ }
- $contents = $article->find('article', 0)->innertext;
- foreach(array(
- '<div class="shareBar"',
- '<div class="shortcodeGalleryWrapper"',
- '<div class="relatedContent',
- '<div class="downloadNow',
- '<div data-shortcode',
- '<div id="sharethrough',
- '<div id="inpage-video',
- '<div class="share-bar-wrapper"',
- ) as $div_start) {
- $contents = stripRecursiveHtmlSection($contents, 'div', $div_start);
- }
- $contents = stripWithDelimiters($contents, '<script', '</script>');
- $contents = stripWithDelimiters($contents, '<meta itemprop="image"', '>');
- $contents = stripWithDelimiters($contents, '<svg class="svg-symbol', '</svg>');
- $contents = trim(stripWithDelimiters($contents, '<section class="sharethrough-top', '</section>'));
- $item['content'] = $contents;
+ $contents = $article->find('article', 0)->innertext;
+ foreach (
+ [
+ '<div class="shareBar"',
+ '<div class="shortcodeGalleryWrapper"',
+ '<div class="relatedContent',
+ '<div class="downloadNow',
+ '<div data-shortcode',
+ '<div id="sharethrough',
+ '<div id="inpage-video',
+ '<div class="share-bar-wrapper"',
+ ] as $div_start
+ ) {
+ $contents = stripRecursiveHtmlSection($contents, 'div', $div_start);
+ }
+ $contents = stripWithDelimiters($contents, '<script', '</script>');
+ $contents = stripWithDelimiters($contents, '<meta itemprop="image"', '>');
+ $contents = stripWithDelimiters($contents, '<svg class="svg-symbol', '</svg>');
+ $contents = trim(stripWithDelimiters($contents, '<section class="sharethrough-top', '</section>'));
+ $item['content'] = $contents;
- return $item;
-
- }
+ return $item;
+ }
}
diff --git a/bridges/ZenodoBridge.php b/bridges/ZenodoBridge.php
index 6d0c134b..1144c90c 100644
--- a/bridges/ZenodoBridge.php
+++ b/bridges/ZenodoBridge.php
@@ -1,54 +1,56 @@
<?php
-class ZenodoBridge extends BridgeAbstract {
- const MAINTAINER = 'theradialactive';
- const NAME = 'Zenodo';
- const URI = 'https://zenodo.org';
- const CACHE_TIMEOUT = 10;
- const DESCRIPTION = 'Returns the newest content of Zenodo';
-
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI());
-
- foreach($html->find('div.record-elem.row') as $element) {
- $item = array();
- $item['uri'] = self::URI . $element->find('h4 > a', 0)->href;
- $item['title'] = trim(htmlspecialchars_decode($element->find('h4 > a', 0)->innertext, ENT_QUOTES));
-
- $authors = $element->find('p', 0);
- if ($authors) {
- $item['author'] = $authors->plaintext;
- }
-
- $summary = $element->find('p.hidden-xs > a', 0);
- if ($summary) {
- $content = $summary->innertext . '<br>';
- } else {
- $content = 'No content';
- }
-
- $type = '<br>Type: ' . $element->find('span.label-default', 0)->innertext;
- $item['categories'] = array($element->find('span.label-default', 0)->innertext);
-
- $raw_date = $element->find('small.text-muted', 0)->innertext;
- $clean_date = str_replace('Uploaded on ', '', $raw_date);
-
- $content = $content . $raw_date;
-
- $item['timestamp'] = $clean_date;
-
- $access = '';
- if ($element->find('span.label-success', 0)) {
- $access = 'Open Access';
- } elseif ($element->find('span.label-warning', 0)) {
- $access = 'Embargoed Access';
- } else {
- $access = $element->find('span.label-error', 0)->innertext;
- }
- $access = '<br>Access: ' . $access;
- $publication = '<br>Publication Date: ' . $element->find('span.label-info', 0)->innertext;
- $item['content'] = $content . $type . $access . $publication;
- $this->items[] = $item;
- }
- }
+class ZenodoBridge extends BridgeAbstract
+{
+ const MAINTAINER = 'theradialactive';
+ const NAME = 'Zenodo';
+ const URI = 'https://zenodo.org';
+ const CACHE_TIMEOUT = 10;
+ const DESCRIPTION = 'Returns the newest content of Zenodo';
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ foreach ($html->find('div.record-elem.row') as $element) {
+ $item = [];
+ $item['uri'] = self::URI . $element->find('h4 > a', 0)->href;
+ $item['title'] = trim(htmlspecialchars_decode($element->find('h4 > a', 0)->innertext, ENT_QUOTES));
+
+ $authors = $element->find('p', 0);
+ if ($authors) {
+ $item['author'] = $authors->plaintext;
+ }
+
+ $summary = $element->find('p.hidden-xs > a', 0);
+ if ($summary) {
+ $content = $summary->innertext . '<br>';
+ } else {
+ $content = 'No content';
+ }
+
+ $type = '<br>Type: ' . $element->find('span.label-default', 0)->innertext;
+ $item['categories'] = [$element->find('span.label-default', 0)->innertext];
+
+ $raw_date = $element->find('small.text-muted', 0)->innertext;
+ $clean_date = str_replace('Uploaded on ', '', $raw_date);
+
+ $content = $content . $raw_date;
+
+ $item['timestamp'] = $clean_date;
+
+ $access = '';
+ if ($element->find('span.label-success', 0)) {
+ $access = 'Open Access';
+ } elseif ($element->find('span.label-warning', 0)) {
+ $access = 'Embargoed Access';
+ } else {
+ $access = $element->find('span.label-error', 0)->innertext;
+ }
+ $access = '<br>Access: ' . $access;
+ $publication = '<br>Publication Date: ' . $element->find('span.label-info', 0)->innertext;
+ $item['content'] = $content . $type . $access . $publication;
+ $this->items[] = $item;
+ }
+ }
}
diff --git a/caches/FileCache.php b/caches/FileCache.php
index 1b8ae6cd..29f4d78b 100644
--- a/caches/FileCache.php
+++ b/caches/FileCache.php
@@ -1,137 +1,150 @@
<?php
+
/**
* Cache with file system
*/
-class FileCache implements CacheInterface {
- protected $path;
- protected $key;
-
- public function __construct() {
- if (!is_writable(PATH_CACHE)) {
- returnServerError(
- 'RSS-Bridge does not have write permissions for '
- . PATH_CACHE . '!'
- );
- }
- }
-
- public function loadData(){
- if(file_exists($this->getCacheFile())) {
- return unserialize(file_get_contents($this->getCacheFile()));
- }
-
- return null;
- }
-
- public function saveData($data){
- // Notice: We use plain serialize() here to reduce memory footprint on
- // large input data.
- $writeStream = file_put_contents($this->getCacheFile(), serialize($data));
-
- if($writeStream === false) {
- throw new \Exception('Cannot write the cache... Do you have the right permissions ?');
- }
-
- return $this;
- }
-
- public function getTime(){
- $cacheFile = $this->getCacheFile();
- clearstatcache(false, $cacheFile);
- if(file_exists($cacheFile)) {
- $time = filemtime($cacheFile);
- return ($time !== false) ? $time : null;
- }
-
- return null;
- }
-
- public function purgeCache($seconds){
- $cachePath = $this->getPath();
- if(file_exists($cachePath)) {
- $cacheIterator = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator($cachePath),
- RecursiveIteratorIterator::CHILD_FIRST
- );
-
- foreach($cacheIterator as $cacheFile) {
- if(in_array($cacheFile->getBasename(), array('.', '..', '.gitkeep')))
- continue;
- elseif($cacheFile->isFile()) {
- if(filemtime($cacheFile->getPathname()) < time() - $seconds)
- unlink($cacheFile->getPathname());
- }
- }
- }
- }
-
- /**
- * Set scope
- * @return self
- */
- public function setScope($scope){
- if(is_null($scope) || !is_string($scope)) {
- throw new \Exception('The given scope is invalid!');
- }
-
- $this->path = PATH_CACHE . trim($scope, " \t\n\r\0\x0B\\\/") . '/';
-
- return $this;
- }
-
- /**
- * Set key
- * @return self
- */
- public function setKey($key){
- if (!empty($key) && is_array($key)) {
- $key = array_map('strtolower', $key);
- }
- $key = json_encode($key);
-
- if (!is_string($key)) {
- throw new \Exception('The given key is invalid!');
- }
-
- $this->key = $key;
- return $this;
- }
-
- /**
- * Return cache path (and create if not exist)
- * @return string Cache path
- */
- private function getPath(){
- if(is_null($this->path)) {
- throw new \Exception('Call "setScope" first!');
- }
-
- if(!is_dir($this->path)) {
- if (mkdir($this->path, 0755, true) !== true) {
- throw new \Exception('Unable to create ' . $this->path);
- }
- }
-
- return $this->path;
- }
-
- /**
- * Get the file name use for cache store
- * @return string Path to the file cache
- */
- private function getCacheFile(){
- return $this->getPath() . $this->getCacheName();
- }
-
- /**
- * Determines file name for store the cache
- * return string
- */
- private function getCacheName(){
- if(is_null($this->key)) {
- throw new \Exception('Call "setKey" first!');
- }
-
- return hash('md5', $this->key) . '.cache';
- }
+class FileCache implements CacheInterface
+{
+ protected $path;
+ protected $key;
+
+ public function __construct()
+ {
+ if (!is_writable(PATH_CACHE)) {
+ returnServerError(
+ 'RSS-Bridge does not have write permissions for '
+ . PATH_CACHE . '!'
+ );
+ }
+ }
+
+ public function loadData()
+ {
+ if (file_exists($this->getCacheFile())) {
+ return unserialize(file_get_contents($this->getCacheFile()));
+ }
+
+ return null;
+ }
+
+ public function saveData($data)
+ {
+ // Notice: We use plain serialize() here to reduce memory footprint on
+ // large input data.
+ $writeStream = file_put_contents($this->getCacheFile(), serialize($data));
+
+ if ($writeStream === false) {
+ throw new \Exception('Cannot write the cache... Do you have the right permissions ?');
+ }
+
+ return $this;
+ }
+
+ public function getTime()
+ {
+ $cacheFile = $this->getCacheFile();
+ clearstatcache(false, $cacheFile);
+ if (file_exists($cacheFile)) {
+ $time = filemtime($cacheFile);
+ return ($time !== false) ? $time : null;
+ }
+
+ return null;
+ }
+
+ public function purgeCache($seconds)
+ {
+ $cachePath = $this->getPath();
+ if (file_exists($cachePath)) {
+ $cacheIterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($cachePath),
+ RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach ($cacheIterator as $cacheFile) {
+ if (in_array($cacheFile->getBasename(), ['.', '..', '.gitkeep'])) {
+ continue;
+ } elseif ($cacheFile->isFile()) {
+ if (filemtime($cacheFile->getPathname()) < time() - $seconds) {
+ unlink($cacheFile->getPathname());
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Set scope
+ * @return self
+ */
+ public function setScope($scope)
+ {
+ if (is_null($scope) || !is_string($scope)) {
+ throw new \Exception('The given scope is invalid!');
+ }
+
+ $this->path = PATH_CACHE . trim($scope, " \t\n\r\0\x0B\\\/") . '/';
+
+ return $this;
+ }
+
+ /**
+ * Set key
+ * @return self
+ */
+ public function setKey($key)
+ {
+ if (!empty($key) && is_array($key)) {
+ $key = array_map('strtolower', $key);
+ }
+ $key = json_encode($key);
+
+ if (!is_string($key)) {
+ throw new \Exception('The given key is invalid!');
+ }
+
+ $this->key = $key;
+ return $this;
+ }
+
+ /**
+ * Return cache path (and create if not exist)
+ * @return string Cache path
+ */
+ private function getPath()
+ {
+ if (is_null($this->path)) {
+ throw new \Exception('Call "setScope" first!');
+ }
+
+ if (!is_dir($this->path)) {
+ if (mkdir($this->path, 0755, true) !== true) {
+ throw new \Exception('Unable to create ' . $this->path);
+ }
+ }
+
+ return $this->path;
+ }
+
+ /**
+ * Get the file name use for cache store
+ * @return string Path to the file cache
+ */
+ private function getCacheFile()
+ {
+ return $this->getPath() . $this->getCacheName();
+ }
+
+ /**
+ * Determines file name for store the cache
+ * return string
+ */
+ private function getCacheName()
+ {
+ if (is_null($this->key)) {
+ throw new \Exception('Call "setKey" first!');
+ }
+
+ return hash('md5', $this->key) . '.cache';
+ }
}
diff --git a/caches/MemcachedCache.php b/caches/MemcachedCache.php
index b431279a..8619c255 100644
--- a/caches/MemcachedCache.php
+++ b/caches/MemcachedCache.php
@@ -1,115 +1,126 @@
<?php
-class MemcachedCache implements CacheInterface {
-
- private $scope;
- private $key;
- private $conn;
- private $expiration = 0;
- private $time = false;
- private $data = null;
-
- public function __construct() {
- if (!extension_loaded('memcached')) {
- returnServerError('"memcached" extension not loaded. Please check "php.ini"');
- }
-
- $host = Configuration::getConfig(get_called_class(), 'host');
- $port = Configuration::getConfig(get_called_class(), 'port');
- if (empty($host) && empty($port)) {
- returnServerError('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG);
- } else if (empty($host)) {
- returnServerError('"host" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
- } else if (empty($port)) {
- returnServerError('"port" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
- } else if (!ctype_digit($port)) {
- returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
- }
-
- $port = intval($port);
-
- if ($port < 1 || $port > 65535) {
- returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
- }
-
- $conn = new Memcached();
- $conn->addServer($host, $port) or returnServerError('Could not connect to memcached server');
- $this->conn = $conn;
- }
-
- public function loadData(){
- if ($this->data) return $this->data;
- $result = $this->conn->get($this->getCacheKey());
- if ($result === false) {
- return null;
- }
-
- $this->time = $result['time'];
- $this->data = $result['data'];
- return $result['data'];
- }
-
- public function saveData($datas){
- $time = time();
- $object_to_save = array(
- 'data' => $datas,
- 'time' => $time,
- );
- $result = $this->conn->set($this->getCacheKey(), $object_to_save, $this->expiration);
-
- if($result === false) {
- returnServerError('Cannot write the cache to memcached server');
- }
-
- $this->time = $time;
-
- return $this;
- }
-
- public function getTime(){
- if ($this->time === false) {
- $this->loadData();
- }
- return $this->time;
- }
-
- public function purgeCache($duration){
- // Note: does not purges cache right now
- // Just sets cache expiration and leave cache purging for memcached itself
- $this->expiration = $duration;
- }
-
- /**
- * Set scope
- * @return self
- */
- public function setScope($scope){
- $this->scope = $scope;
- return $this;
- }
-
- /**
- * Set key
- * @return self
- */
- public function setKey($key){
- if (!empty($key) && is_array($key)) {
- $key = array_map('strtolower', $key);
- }
- $key = json_encode($key);
-
- if (!is_string($key)) {
- throw new \Exception('The given key is invalid!');
- }
-
- $this->key = $key;
- return $this;
- }
-
- private function getCacheKey(){
- if(is_null($this->key)) {
- returnServerError('Call "setKey" first!');
- }
-
- return 'rss_bridge_cache_' . hash('md5', $this->scope . $this->key . 'A');
- }
+
+class MemcachedCache implements CacheInterface
+{
+ private $scope;
+ private $key;
+ private $conn;
+ private $expiration = 0;
+ private $time = false;
+ private $data = null;
+
+ public function __construct()
+ {
+ if (!extension_loaded('memcached')) {
+ returnServerError('"memcached" extension not loaded. Please check "php.ini"');
+ }
+
+ $host = Configuration::getConfig(get_called_class(), 'host');
+ $port = Configuration::getConfig(get_called_class(), 'port');
+ if (empty($host) && empty($port)) {
+ returnServerError('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG);
+ } elseif (empty($host)) {
+ returnServerError('"host" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
+ } elseif (empty($port)) {
+ returnServerError('"port" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
+ } elseif (!ctype_digit($port)) {
+ returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
+ }
+
+ $port = intval($port);
+
+ if ($port < 1 || $port > 65535) {
+ returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
+ }
+
+ $conn = new Memcached();
+ $conn->addServer($host, $port) or returnServerError('Could not connect to memcached server');
+ $this->conn = $conn;
+ }
+
+ public function loadData()
+ {
+ if ($this->data) {
+ return $this->data;
+ }
+ $result = $this->conn->get($this->getCacheKey());
+ if ($result === false) {
+ return null;
+ }
+
+ $this->time = $result['time'];
+ $this->data = $result['data'];
+ return $result['data'];
+ }
+
+ public function saveData($datas)
+ {
+ $time = time();
+ $object_to_save = [
+ 'data' => $datas,
+ 'time' => $time,
+ ];
+ $result = $this->conn->set($this->getCacheKey(), $object_to_save, $this->expiration);
+
+ if ($result === false) {
+ returnServerError('Cannot write the cache to memcached server');
+ }
+
+ $this->time = $time;
+
+ return $this;
+ }
+
+ public function getTime()
+ {
+ if ($this->time === false) {
+ $this->loadData();
+ }
+ return $this->time;
+ }
+
+ public function purgeCache($duration)
+ {
+ // Note: does not purges cache right now
+ // Just sets cache expiration and leave cache purging for memcached itself
+ $this->expiration = $duration;
+ }
+
+ /**
+ * Set scope
+ * @return self
+ */
+ public function setScope($scope)
+ {
+ $this->scope = $scope;
+ return $this;
+ }
+
+ /**
+ * Set key
+ * @return self
+ */
+ public function setKey($key)
+ {
+ if (!empty($key) && is_array($key)) {
+ $key = array_map('strtolower', $key);
+ }
+ $key = json_encode($key);
+
+ if (!is_string($key)) {
+ throw new \Exception('The given key is invalid!');
+ }
+
+ $this->key = $key;
+ return $this;
+ }
+
+ private function getCacheKey()
+ {
+ if (is_null($this->key)) {
+ returnServerError('Call "setKey" first!');
+ }
+
+ return 'rss_bridge_cache_' . hash('md5', $this->scope . $this->key . 'A');
+ }
}
diff --git a/caches/SQLiteCache.php b/caches/SQLiteCache.php
index 5ec69417..e8d020a5 100644
--- a/caches/SQLiteCache.php
+++ b/caches/SQLiteCache.php
@@ -1,128 +1,138 @@
<?php
+
/**
* Cache based on SQLite 3 <https://www.sqlite.org>
*/
-class SQLiteCache implements CacheInterface {
- protected $scope;
- protected $key;
-
- private $db = null;
-
- public function __construct() {
- if (!extension_loaded('sqlite3')) {
- die('"sqlite3" extension not loaded. Please check "php.ini"');
- }
-
- if (!is_writable(PATH_CACHE)) {
- returnServerError(
- 'RSS-Bridge does not have write permissions for '
- . PATH_CACHE . '!'
- );
- }
-
- $file = Configuration::getConfig(get_called_class(), 'file');
- if (empty($file)) {
- die('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG);
- }
- if (dirname($file) == '.') {
- $file = PATH_CACHE . $file;
- } elseif (!is_dir(dirname($file))) {
- die('Invalid configuration for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
- }
-
- if (!is_file($file)) {
- $this->db = new SQLite3($file);
- $this->db->enableExceptions(true);
- $this->db->exec("CREATE TABLE storage ('key' BLOB PRIMARY KEY, 'value' BLOB, 'updated' INTEGER)");
- } else {
- $this->db = new SQLite3($file);
- $this->db->enableExceptions(true);
- }
- $this->db->busyTimeout(5000);
- }
-
- public function loadData(){
- $Qselect = $this->db->prepare('SELECT value FROM storage WHERE key = :key');
- $Qselect->bindValue(':key', $this->getCacheKey());
- $result = $Qselect->execute();
- if ($result instanceof SQLite3Result) {
- $data = $result->fetchArray(SQLITE3_ASSOC);
- if (isset($data['value'])) {
- return unserialize($data['value']);
- }
- }
-
- return null;
- }
-
- public function saveData($data){
- $Qupdate = $this->db->prepare('INSERT OR REPLACE INTO storage (key, value, updated) VALUES (:key, :value, :updated)');
- $Qupdate->bindValue(':key', $this->getCacheKey());
- $Qupdate->bindValue(':value', serialize($data));
- $Qupdate->bindValue(':updated', time());
- $Qupdate->execute();
-
- return $this;
- }
-
- public function getTime(){
- $Qselect = $this->db->prepare('SELECT updated FROM storage WHERE key = :key');
- $Qselect->bindValue(':key', $this->getCacheKey());
- $result = $Qselect->execute();
- if ($result instanceof SQLite3Result) {
- $data = $result->fetchArray(SQLITE3_ASSOC);
- if (isset($data['updated'])) {
- return $data['updated'];
- }
- }
-
- return null;
- }
-
- public function purgeCache($seconds){
- $Qdelete = $this->db->prepare('DELETE FROM storage WHERE updated < :expired');
- $Qdelete->bindValue(':expired', time() - $seconds);
- $Qdelete->execute();
- }
-
- /**
- * Set scope
- * @return self
- */
- public function setScope($scope){
- if(is_null($scope) || !is_string($scope)) {
- throw new \Exception('The given scope is invalid!');
- }
-
- $this->scope = $scope;
- return $this;
- }
-
- /**
- * Set key
- * @return self
- */
- public function setKey($key){
- if (!empty($key) && is_array($key)) {
- $key = array_map('strtolower', $key);
- }
- $key = json_encode($key);
-
- if (!is_string($key)) {
- throw new \Exception('The given key is invalid!');
- }
-
- $this->key = $key;
- return $this;
- }
-
- ////////////////////////////////////////////////////////////////////////////
-
- private function getCacheKey(){
- if(is_null($this->key)) {
- throw new \Exception('Call "setKey" first!');
- }
-
- return hash('sha1', $this->scope . $this->key, true);
- }
+class SQLiteCache implements CacheInterface
+{
+ protected $scope;
+ protected $key;
+
+ private $db = null;
+
+ public function __construct()
+ {
+ if (!extension_loaded('sqlite3')) {
+ die('"sqlite3" extension not loaded. Please check "php.ini"');
+ }
+
+ if (!is_writable(PATH_CACHE)) {
+ returnServerError(
+ 'RSS-Bridge does not have write permissions for '
+ . PATH_CACHE . '!'
+ );
+ }
+
+ $file = Configuration::getConfig(get_called_class(), 'file');
+ if (empty($file)) {
+ die('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG);
+ }
+ if (dirname($file) == '.') {
+ $file = PATH_CACHE . $file;
+ } elseif (!is_dir(dirname($file))) {
+ die('Invalid configuration for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
+ }
+
+ if (!is_file($file)) {
+ $this->db = new SQLite3($file);
+ $this->db->enableExceptions(true);
+ $this->db->exec("CREATE TABLE storage ('key' BLOB PRIMARY KEY, 'value' BLOB, 'updated' INTEGER)");
+ } else {
+ $this->db = new SQLite3($file);
+ $this->db->enableExceptions(true);
+ }
+ $this->db->busyTimeout(5000);
+ }
+
+ public function loadData()
+ {
+ $Qselect = $this->db->prepare('SELECT value FROM storage WHERE key = :key');
+ $Qselect->bindValue(':key', $this->getCacheKey());
+ $result = $Qselect->execute();
+ if ($result instanceof SQLite3Result) {
+ $data = $result->fetchArray(SQLITE3_ASSOC);
+ if (isset($data['value'])) {
+ return unserialize($data['value']);
+ }
+ }
+
+ return null;
+ }
+
+ public function saveData($data)
+ {
+ $Qupdate = $this->db->prepare('INSERT OR REPLACE INTO storage (key, value, updated) VALUES (:key, :value, :updated)');
+ $Qupdate->bindValue(':key', $this->getCacheKey());
+ $Qupdate->bindValue(':value', serialize($data));
+ $Qupdate->bindValue(':updated', time());
+ $Qupdate->execute();
+
+ return $this;
+ }
+
+ public function getTime()
+ {
+ $Qselect = $this->db->prepare('SELECT updated FROM storage WHERE key = :key');
+ $Qselect->bindValue(':key', $this->getCacheKey());
+ $result = $Qselect->execute();
+ if ($result instanceof SQLite3Result) {
+ $data = $result->fetchArray(SQLITE3_ASSOC);
+ if (isset($data['updated'])) {
+ return $data['updated'];
+ }
+ }
+
+ return null;
+ }
+
+ public function purgeCache($seconds)
+ {
+ $Qdelete = $this->db->prepare('DELETE FROM storage WHERE updated < :expired');
+ $Qdelete->bindValue(':expired', time() - $seconds);
+ $Qdelete->execute();
+ }
+
+ /**
+ * Set scope
+ * @return self
+ */
+ public function setScope($scope)
+ {
+ if (is_null($scope) || !is_string($scope)) {
+ throw new \Exception('The given scope is invalid!');
+ }
+
+ $this->scope = $scope;
+ return $this;
+ }
+
+ /**
+ * Set key
+ * @return self
+ */
+ public function setKey($key)
+ {
+ if (!empty($key) && is_array($key)) {
+ $key = array_map('strtolower', $key);
+ }
+ $key = json_encode($key);
+
+ if (!is_string($key)) {
+ throw new \Exception('The given key is invalid!');
+ }
+
+ $this->key = $key;
+ return $this;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+
+ private function getCacheKey()
+ {
+ if (is_null($this->key)) {
+ throw new \Exception('Call "setKey" first!');
+ }
+
+ return hash('sha1', $this->scope . $this->key, true);
+ }
}
diff --git a/contrib/prepare_release/fetch_contributors.php b/contrib/prepare_release/fetch_contributors.php
index 9659b800..76cef24f 100644
--- a/contrib/prepare_release/fetch_contributors.php
+++ b/contrib/prepare_release/fetch_contributors.php
@@ -1,49 +1,49 @@
<?php
+
/* Generate the "Contributors" list for README.md automatically utilizing the GitHub API */
require __DIR__ . '/../../lib/rssbridge.php';
$url = 'https://api.github.com/repos/rss-bridge/rss-bridge/contributors';
-$contributors = array();
+$contributors = [];
$next = true;
-while($next) { /* Collect all contributors */
-
- $headers = [
- 'Accept: application/json',
- 'Content-Type: application/json',
- 'User-Agent: RSS-Bridge'
- ];
- $result = _http_request($url, ['headers' => $headers]);
-
- foreach(json_decode($result['body']) as $contributor)
- $contributors[] = $contributor;
-
- // Extract links to "next", "last", etc...
- $links = explode(',', $result['headers']['link'][0]);
- $next = false;
-
- // Check if there is a link with 'rel="next"'
- foreach($links as $link) {
- list($url, $type) = explode(';', $link, 2);
-
- if(trim($type) === 'rel="next"') {
- $url = trim(preg_replace('/([<>])/', '', $url));
- $next = true;
- break;
- }
- }
-
+while ($next) { /* Collect all contributors */
+ $headers = [
+ 'Accept: application/json',
+ 'Content-Type: application/json',
+ 'User-Agent: RSS-Bridge'
+ ];
+ $result = _http_request($url, ['headers' => $headers]);
+
+ foreach (json_decode($result['body']) as $contributor) {
+ $contributors[] = $contributor;
+ }
+
+ // Extract links to "next", "last", etc...
+ $links = explode(',', $result['headers']['link'][0]);
+ $next = false;
+
+ // Check if there is a link with 'rel="next"'
+ foreach ($links as $link) {
+ list($url, $type) = explode(';', $link, 2);
+
+ if (trim($type) === 'rel="next"') {
+ $url = trim(preg_replace('/([<>])/', '', $url));
+ $next = true;
+ break;
+ }
+ }
}
/* Example JSON data: https://api.github.com/repos/rss-bridge/rss-bridge/contributors */
// We want contributors sorted by name
-usort($contributors, function($a, $b){
- return strcasecmp($a->login, $b->login);
+usort($contributors, function ($a, $b) {
+ return strcasecmp($a->login, $b->login);
});
// Export as Markdown list
-foreach($contributors as $contributor) {
- echo " * [{$contributor->login}]({$contributor->html_url})\n";
+foreach ($contributors as $contributor) {
+ echo " * [{$contributor->login}]({$contributor->html_url})\n";
}
diff --git a/formats/AtomFormat.php b/formats/AtomFormat.php
index 3d9b7c93..5f564266 100644
--- a/formats/AtomFormat.php
+++ b/formats/AtomFormat.php
@@ -1,4 +1,5 @@
<?php
+
/**
* AtomFormat - RFC 4287: The Atom Syndication Format
* https://tools.ietf.org/html/rfc4287
@@ -6,178 +7,185 @@
* Validator:
* https://validator.w3.org/feed/
*/
-class AtomFormat extends FormatAbstract{
- const MIME_TYPE = 'application/atom+xml';
-
- protected const ATOM_NS = 'http://www.w3.org/2005/Atom';
- protected const MRSS_NS = 'http://search.yahoo.com/mrss/';
-
- const LIMIT_TITLE = 140;
-
- public function stringify(){
- $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
- $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
- $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
- $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
-
- $feedUrl = $urlPrefix . $urlHost . $urlRequest;
-
- $extraInfos = $this->getExtraInfos();
- $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
-
- $document = new DomDocument('1.0', $this->getCharset());
- $document->formatOutput = true;
- $feed = $document->createElementNS(self::ATOM_NS, 'feed');
- $document->appendChild($feed);
- $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:media', self::MRSS_NS);
-
- $title = $document->createElement('title');
- $feed->appendChild($title);
- $title->setAttribute('type', 'text');
- $title->appendChild($document->createTextNode($extraInfos['name']));
-
- $id = $document->createElement('id');
- $feed->appendChild($id);
- $id->appendChild($document->createTextNode($feedUrl));
-
- $uriparts = parse_url($uri);
- if(!empty($extraInfos['icon'])) {
- $iconUrl = $extraInfos['icon'];
- } else {
- $iconUrl = $uriparts['scheme'] . '://' . $uriparts['host'] . '/favicon.ico';
- }
- $icon = $document->createElement('icon');
- $feed->appendChild($icon);
- $icon->appendChild($document->createTextNode($iconUrl));
-
- $logo = $document->createElement('logo');
- $feed->appendChild($logo);
- $logo->appendChild($document->createTextNode($iconUrl));
-
- $feedTimestamp = gmdate(DATE_ATOM, $this->lastModified);
- $updated = $document->createElement('updated');
- $feed->appendChild($updated);
- $updated->appendChild($document->createTextNode($feedTimestamp));
-
- // since we can't guarantee that all items have an author,
- // a global feed author is mandatory
- $feedAuthor = 'RSS-Bridge';
- $author = $document->createElement('author');
- $feed->appendChild($author);
- $authorName = $document->createElement('name');
- $author->appendChild($authorName);
- $authorName->appendChild($document->createTextNode($feedAuthor));
-
- $linkAlternate = $document->createElement('link');
- $feed->appendChild($linkAlternate);
- $linkAlternate->setAttribute('rel', 'alternate');
- $linkAlternate->setAttribute('type', 'text/html');
- $linkAlternate->setAttribute('href', $uri);
-
- $linkSelf = $document->createElement('link');
- $feed->appendChild($linkSelf);
- $linkSelf->setAttribute('rel', 'self');
- $linkSelf->setAttribute('type', 'application/atom+xml');
- $linkSelf->setAttribute('href', $feedUrl);
-
- foreach($this->getItems() as $item) {
- $entryTimestamp = $item->getTimestamp();
- $entryTitle = $item->getTitle();
- $entryContent = $item->getContent();
- $entryUri = $item->getURI();
- $entryID = '';
-
- if (!empty($item->getUid()))
- $entryID = 'urn:sha1:' . $item->getUid();
-
- if (empty($entryID)) // Fallback to provided URI
- $entryID = $entryUri;
-
- if (empty($entryID)) // Fallback to title and content
- $entryID = 'urn:sha1:' . hash('sha1', $entryTitle . $entryContent);
-
- if (empty($entryTimestamp))
- $entryTimestamp = $this->lastModified;
-
- if (empty($entryTitle)) {
- $entryTitle = str_replace("\n", ' ', strip_tags($entryContent));
- if (strlen($entryTitle) > self::LIMIT_TITLE) {
- $wrapPos = strpos(wordwrap($entryTitle, self::LIMIT_TITLE), "\n");
- $entryTitle = substr($entryTitle, 0, $wrapPos) . '...';
- }
- }
-
- if (empty($entryContent))
- $entryContent = ' ';
-
- $entry = $document->createElement('entry');
- $feed->appendChild($entry);
-
- $title = $document->createElement('title');
- $entry->appendChild($title);
- $title->setAttribute('type', 'html');
- $title->appendChild($document->createTextNode($entryTitle));
-
- $entryTimestamp = gmdate(DATE_ATOM, $entryTimestamp);
- $published = $document->createElement('published');
- $entry->appendChild($published);
- $published->appendChild($document->createTextNode($entryTimestamp));
-
- $updated = $document->createElement('updated');
- $entry->appendChild($updated);
- $updated->appendChild($document->createTextNode($entryTimestamp));
-
- $id = $document->createElement('id');
- $entry->appendChild($id);
- $id->appendChild($document->createTextNode($entryID));
-
- if (!empty($entryUri)) {
- $entryLinkAlternate = $document->createElement('link');
- $entry->appendChild($entryLinkAlternate);
- $entryLinkAlternate->setAttribute('rel', 'alternate');
- $entryLinkAlternate->setAttribute('type', 'text/html');
- $entryLinkAlternate->setAttribute('href', $entryUri);
- }
-
- if (!empty($item->getAuthor())) {
- $author = $document->createElement('author');
- $entry->appendChild($author);
- $authorName = $document->createElement('name');
- $author->appendChild($authorName);
- $authorName->appendChild($document->createTextNode($item->getAuthor()));
- }
-
- $content = $document->createElement('content');
- $content->setAttribute('type', 'html');
- $content->appendChild($document->createTextNode($this->sanitizeHtml($entryContent)));
- $entry->appendChild($content);
-
- foreach($item->getEnclosures() as $enclosure) {
- $entryEnclosure = $document->createElement('link');
- $entry->appendChild($entryEnclosure);
- $entryEnclosure->setAttribute('rel', 'enclosure');
- $entryEnclosure->setAttribute('type', getMimeType($enclosure));
- $entryEnclosure->setAttribute('href', $enclosure);
- }
-
- foreach($item->getCategories() as $category) {
- $entryCategory = $document->createElement('category');
- $entry->appendChild($entryCategory);
- $entryCategory->setAttribute('term', $category);
- }
-
- if (!empty($item->thumbnail)) {
- $thumbnail = $document->createElementNS(self::MRSS_NS, 'thumbnail');
- $entry->appendChild($thumbnail);
- $thumbnail->setAttribute('url', $item->thumbnail);
- }
- }
-
- $toReturn = $document->saveXML();
-
- // Remove invalid characters
- ini_set('mbstring.substitute_character', 'none');
- $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
- return $toReturn;
- }
+class AtomFormat extends FormatAbstract
+{
+ const MIME_TYPE = 'application/atom+xml';
+
+ protected const ATOM_NS = 'http://www.w3.org/2005/Atom';
+ protected const MRSS_NS = 'http://search.yahoo.com/mrss/';
+
+ const LIMIT_TITLE = 140;
+
+ public function stringify()
+ {
+ $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
+ $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
+ $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
+ $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
+
+ $feedUrl = $urlPrefix . $urlHost . $urlRequest;
+
+ $extraInfos = $this->getExtraInfos();
+ $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
+
+ $document = new DomDocument('1.0', $this->getCharset());
+ $document->formatOutput = true;
+ $feed = $document->createElementNS(self::ATOM_NS, 'feed');
+ $document->appendChild($feed);
+ $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:media', self::MRSS_NS);
+
+ $title = $document->createElement('title');
+ $feed->appendChild($title);
+ $title->setAttribute('type', 'text');
+ $title->appendChild($document->createTextNode($extraInfos['name']));
+
+ $id = $document->createElement('id');
+ $feed->appendChild($id);
+ $id->appendChild($document->createTextNode($feedUrl));
+
+ $uriparts = parse_url($uri);
+ if (!empty($extraInfos['icon'])) {
+ $iconUrl = $extraInfos['icon'];
+ } else {
+ $iconUrl = $uriparts['scheme'] . '://' . $uriparts['host'] . '/favicon.ico';
+ }
+ $icon = $document->createElement('icon');
+ $feed->appendChild($icon);
+ $icon->appendChild($document->createTextNode($iconUrl));
+
+ $logo = $document->createElement('logo');
+ $feed->appendChild($logo);
+ $logo->appendChild($document->createTextNode($iconUrl));
+
+ $feedTimestamp = gmdate(DATE_ATOM, $this->lastModified);
+ $updated = $document->createElement('updated');
+ $feed->appendChild($updated);
+ $updated->appendChild($document->createTextNode($feedTimestamp));
+
+ // since we can't guarantee that all items have an author,
+ // a global feed author is mandatory
+ $feedAuthor = 'RSS-Bridge';
+ $author = $document->createElement('author');
+ $feed->appendChild($author);
+ $authorName = $document->createElement('name');
+ $author->appendChild($authorName);
+ $authorName->appendChild($document->createTextNode($feedAuthor));
+
+ $linkAlternate = $document->createElement('link');
+ $feed->appendChild($linkAlternate);
+ $linkAlternate->setAttribute('rel', 'alternate');
+ $linkAlternate->setAttribute('type', 'text/html');
+ $linkAlternate->setAttribute('href', $uri);
+
+ $linkSelf = $document->createElement('link');
+ $feed->appendChild($linkSelf);
+ $linkSelf->setAttribute('rel', 'self');
+ $linkSelf->setAttribute('type', 'application/atom+xml');
+ $linkSelf->setAttribute('href', $feedUrl);
+
+ foreach ($this->getItems() as $item) {
+ $entryTimestamp = $item->getTimestamp();
+ $entryTitle = $item->getTitle();
+ $entryContent = $item->getContent();
+ $entryUri = $item->getURI();
+ $entryID = '';
+
+ if (!empty($item->getUid())) {
+ $entryID = 'urn:sha1:' . $item->getUid();
+ }
+
+ if (empty($entryID)) { // Fallback to provided URI
+ $entryID = $entryUri;
+ }
+
+ if (empty($entryID)) { // Fallback to title and content
+ $entryID = 'urn:sha1:' . hash('sha1', $entryTitle . $entryContent);
+ }
+
+ if (empty($entryTimestamp)) {
+ $entryTimestamp = $this->lastModified;
+ }
+
+ if (empty($entryTitle)) {
+ $entryTitle = str_replace("\n", ' ', strip_tags($entryContent));
+ if (strlen($entryTitle) > self::LIMIT_TITLE) {
+ $wrapPos = strpos(wordwrap($entryTitle, self::LIMIT_TITLE), "\n");
+ $entryTitle = substr($entryTitle, 0, $wrapPos) . '...';
+ }
+ }
+
+ if (empty($entryContent)) {
+ $entryContent = ' ';
+ }
+
+ $entry = $document->createElement('entry');
+ $feed->appendChild($entry);
+
+ $title = $document->createElement('title');
+ $entry->appendChild($title);
+ $title->setAttribute('type', 'html');
+ $title->appendChild($document->createTextNode($entryTitle));
+
+ $entryTimestamp = gmdate(DATE_ATOM, $entryTimestamp);
+ $published = $document->createElement('published');
+ $entry->appendChild($published);
+ $published->appendChild($document->createTextNode($entryTimestamp));
+
+ $updated = $document->createElement('updated');
+ $entry->appendChild($updated);
+ $updated->appendChild($document->createTextNode($entryTimestamp));
+
+ $id = $document->createElement('id');
+ $entry->appendChild($id);
+ $id->appendChild($document->createTextNode($entryID));
+
+ if (!empty($entryUri)) {
+ $entryLinkAlternate = $document->createElement('link');
+ $entry->appendChild($entryLinkAlternate);
+ $entryLinkAlternate->setAttribute('rel', 'alternate');
+ $entryLinkAlternate->setAttribute('type', 'text/html');
+ $entryLinkAlternate->setAttribute('href', $entryUri);
+ }
+
+ if (!empty($item->getAuthor())) {
+ $author = $document->createElement('author');
+ $entry->appendChild($author);
+ $authorName = $document->createElement('name');
+ $author->appendChild($authorName);
+ $authorName->appendChild($document->createTextNode($item->getAuthor()));
+ }
+
+ $content = $document->createElement('content');
+ $content->setAttribute('type', 'html');
+ $content->appendChild($document->createTextNode($this->sanitizeHtml($entryContent)));
+ $entry->appendChild($content);
+
+ foreach ($item->getEnclosures() as $enclosure) {
+ $entryEnclosure = $document->createElement('link');
+ $entry->appendChild($entryEnclosure);
+ $entryEnclosure->setAttribute('rel', 'enclosure');
+ $entryEnclosure->setAttribute('type', getMimeType($enclosure));
+ $entryEnclosure->setAttribute('href', $enclosure);
+ }
+
+ foreach ($item->getCategories() as $category) {
+ $entryCategory = $document->createElement('category');
+ $entry->appendChild($entryCategory);
+ $entryCategory->setAttribute('term', $category);
+ }
+
+ if (!empty($item->thumbnail)) {
+ $thumbnail = $document->createElementNS(self::MRSS_NS, 'thumbnail');
+ $entry->appendChild($thumbnail);
+ $thumbnail->setAttribute('url', $item->thumbnail);
+ }
+ }
+
+ $toReturn = $document->saveXML();
+
+ // Remove invalid characters
+ ini_set('mbstring.substitute_character', 'none');
+ $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
+ return $toReturn;
+ }
}
diff --git a/formats/HtmlFormat.php b/formats/HtmlFormat.php
index 12b5fc3a..d60c4d81 100644
--- a/formats/HtmlFormat.php
+++ b/formats/HtmlFormat.php
@@ -1,96 +1,97 @@
<?php
-class HtmlFormat extends FormatAbstract {
- const MIME_TYPE = 'text/html';
-
- public function stringify(){
- $extraInfos = $this->getExtraInfos();
- $title = htmlspecialchars($extraInfos['name']);
- $uri = htmlspecialchars($extraInfos['uri']);
- $donationUri = htmlspecialchars($extraInfos['donationUri']);
- $donationsAllowed = Configuration::getConfig('admin', 'donations');
-
- // Dynamically build buttons for all formats (except HTML)
- $formatFac = new FormatFactory();
-
- $buttons = '';
- $links = '';
-
- foreach($formatFac->getFormatNames() as $format) {
- if(strcasecmp($format, 'HTML') === 0) {
- continue;
- }
-
- $query = str_ireplace('format=Html', 'format=' . $format, htmlentities($_SERVER['QUERY_STRING']));
- $buttons .= $this->buildButton($format, $query) . PHP_EOL;
-
- $mime = $formatFac->create($format)->getMimeType();
- $links .= $this->buildLink($format, $query, $mime) . PHP_EOL;
- }
-
- if($donationUri !== '' && $donationsAllowed) {
- $buttons .= '<a href="'
- . $donationUri
- . '" target="_blank"><button class="highlight">Donate to maintainer</button></a>'
- . PHP_EOL;
- $links .= '<link href="'
- . $donationUri
- . ' target="_blank"" title="Donate to Maintainer" rel="alternate">'
- . PHP_EOL;
- }
-
- $entries = '';
- foreach($this->getItems() as $item) {
- $entryAuthor = $item->getAuthor() ? '<br /><p class="author">by: ' . $item->getAuthor() . '</p>' : '';
- $entryTitle = $this->sanitizeHtml(strip_tags($item->getTitle()));
- $entryUri = $item->getURI() ?: $uri;
-
- $entryDate = '';
- if($item->getTimestamp()) {
-
- $entryDate = sprintf(
- '<time datetime="%s">%s</time>',
- date('Y-m-d H:i:s', $item->getTimestamp()),
- date('Y-m-d H:i:s', $item->getTimestamp())
- );
- }
-
- $entryContent = '';
- if($item->getContent()) {
- $entryContent = '<div class="content">'
- . $this->sanitizeHtml($item->getContent())
- . '</div>';
- }
-
- $entryEnclosures = '';
- if(!empty($item->getEnclosures())) {
- $entryEnclosures = '<div class="attachments"><p>Attachments:</p>';
-
- foreach($item->getEnclosures() as $enclosure) {
- $template = '<li class="enclosure"><a href="%s" rel="noopener noreferrer nofollow">%s</a></li>';
- $url = $this->sanitizeHtml($enclosure);
- $anchorText = substr($url, strrpos($url, '/') + 1);
-
- $entryEnclosures .= sprintf($template, $url, $anchorText);
- }
-
- $entryEnclosures .= '</div>';
- }
-
- $entryCategories = '';
- if(!empty($item->getCategories())) {
- $entryCategories = '<div class="categories"><p>Categories:</p>';
-
- foreach($item->getCategories() as $category) {
-
- $entryCategories .= '<li class="category">'
- . $this->sanitizeHtml($category)
- . '</li>';
- }
-
- $entryCategories .= '</div>';
- }
-
- $entries .= <<<EOD
+
+class HtmlFormat extends FormatAbstract
+{
+ const MIME_TYPE = 'text/html';
+
+ public function stringify()
+ {
+ $extraInfos = $this->getExtraInfos();
+ $title = htmlspecialchars($extraInfos['name']);
+ $uri = htmlspecialchars($extraInfos['uri']);
+ $donationUri = htmlspecialchars($extraInfos['donationUri']);
+ $donationsAllowed = Configuration::getConfig('admin', 'donations');
+
+ // Dynamically build buttons for all formats (except HTML)
+ $formatFac = new FormatFactory();
+
+ $buttons = '';
+ $links = '';
+
+ foreach ($formatFac->getFormatNames() as $format) {
+ if (strcasecmp($format, 'HTML') === 0) {
+ continue;
+ }
+
+ $query = str_ireplace('format=Html', 'format=' . $format, htmlentities($_SERVER['QUERY_STRING']));
+ $buttons .= $this->buildButton($format, $query) . PHP_EOL;
+
+ $mime = $formatFac->create($format)->getMimeType();
+ $links .= $this->buildLink($format, $query, $mime) . PHP_EOL;
+ }
+
+ if ($donationUri !== '' && $donationsAllowed) {
+ $buttons .= '<a href="'
+ . $donationUri
+ . '" target="_blank"><button class="highlight">Donate to maintainer</button></a>'
+ . PHP_EOL;
+ $links .= '<link href="'
+ . $donationUri
+ . ' target="_blank"" title="Donate to Maintainer" rel="alternate">'
+ . PHP_EOL;
+ }
+
+ $entries = '';
+ foreach ($this->getItems() as $item) {
+ $entryAuthor = $item->getAuthor() ? '<br /><p class="author">by: ' . $item->getAuthor() . '</p>' : '';
+ $entryTitle = $this->sanitizeHtml(strip_tags($item->getTitle()));
+ $entryUri = $item->getURI() ?: $uri;
+
+ $entryDate = '';
+ if ($item->getTimestamp()) {
+ $entryDate = sprintf(
+ '<time datetime="%s">%s</time>',
+ date('Y-m-d H:i:s', $item->getTimestamp()),
+ date('Y-m-d H:i:s', $item->getTimestamp())
+ );
+ }
+
+ $entryContent = '';
+ if ($item->getContent()) {
+ $entryContent = '<div class="content">'
+ . $this->sanitizeHtml($item->getContent())
+ . '</div>';
+ }
+
+ $entryEnclosures = '';
+ if (!empty($item->getEnclosures())) {
+ $entryEnclosures = '<div class="attachments"><p>Attachments:</p>';
+
+ foreach ($item->getEnclosures() as $enclosure) {
+ $template = '<li class="enclosure"><a href="%s" rel="noopener noreferrer nofollow">%s</a></li>';
+ $url = $this->sanitizeHtml($enclosure);
+ $anchorText = substr($url, strrpos($url, '/') + 1);
+
+ $entryEnclosures .= sprintf($template, $url, $anchorText);
+ }
+
+ $entryEnclosures .= '</div>';
+ }
+
+ $entryCategories = '';
+ if (!empty($item->getCategories())) {
+ $entryCategories = '<div class="categories"><p>Categories:</p>';
+
+ foreach ($item->getCategories() as $category) {
+ $entryCategories .= '<li class="category">'
+ . $this->sanitizeHtml($category)
+ . '</li>';
+ }
+
+ $entryCategories .= '</div>';
+ }
+
+ $entries .= <<<EOD
<section class="feeditem">
<h2><a class="itemtitle" href="{$entryUri}">{$entryTitle}</a></h2>
@@ -102,12 +103,12 @@ class HtmlFormat extends FormatAbstract {
</section>
EOD;
- }
+ }
- $charset = $this->getCharset();
+ $charset = $this->getCharset();
- /* Data are prepared, now let's begin the "MAGIE !!!" */
- $toReturn = <<<EOD
+ /* Data are prepared, now let's begin the "MAGIE !!!" */
+ $toReturn = <<<EOD
<!DOCTYPE html>
<html>
<head>
@@ -130,22 +131,24 @@ EOD;
</html>
EOD;
- // Remove invalid characters
- ini_set('mbstring.substitute_character', 'none');
- $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
- return $toReturn;
- }
+ // Remove invalid characters
+ ini_set('mbstring.substitute_character', 'none');
+ $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
+ return $toReturn;
+ }
- private function buildButton($format, $query) {
- return <<<EOD
+ private function buildButton($format, $query)
+ {
+ return <<<EOD
<a href="./?{$query}"><button class="rss-feed">{$format}</button></a>
EOD;
- }
+ }
- private function buildLink($format, $query, $mime) {
- return <<<EOD
+ private function buildLink($format, $query, $mime)
+ {
+ return <<<EOD
<link href="./?{$query}" title="{$format}" rel="alternate" type="{$mime}">
EOD;
- }
+ }
}
diff --git a/formats/JsonFormat.php b/formats/JsonFormat.php
index 1efc87fe..3b2a29ab 100644
--- a/formats/JsonFormat.php
+++ b/formats/JsonFormat.php
@@ -1,4 +1,5 @@
<?php
+
/**
* JsonFormat - JSON Feed Version 1
* https://jsonfeed.org/version/1
@@ -7,122 +8,126 @@
* https://validator.jsonfeed.org
* https://github.com/vigetlabs/json-feed-validator
*/
-class JsonFormat extends FormatAbstract {
- const MIME_TYPE = 'application/json';
-
- const VENDOR_EXCLUDES = array(
- 'author',
- 'title',
- 'uri',
- 'timestamp',
- 'content',
- 'enclosures',
- 'categories',
- 'uid',
- );
-
- public function stringify(){
- $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
- $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
- $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
- $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
-
- $extraInfos = $this->getExtraInfos();
-
- $data = array(
- 'version' => 'https://jsonfeed.org/version/1',
- 'title' => (!empty($extraInfos['name'])) ? $extraInfos['name'] : $urlHost,
- 'home_page_url' => (!empty($extraInfos['uri'])) ? $extraInfos['uri'] : REPOSITORY,
- 'feed_url' => $urlPrefix . $urlHost . $urlRequest
- );
-
- if (!empty($extraInfos['icon'])) {
- $data['icon'] = $extraInfos['icon'];
- $data['favicon'] = $extraInfos['icon'];
- }
-
- $items = array();
- foreach ($this->getItems() as $item) {
- $entry = array();
-
- $entryAuthor = $item->getAuthor();
- $entryTitle = $item->getTitle();
- $entryUri = $item->getURI();
- $entryTimestamp = $item->getTimestamp();
- $entryContent = $item->getContent() ? $this->sanitizeHtml($item->getContent()) : '';
- $entryEnclosures = $item->getEnclosures();
- $entryCategories = $item->getCategories();
-
- $vendorFields = $item->toArray();
- foreach (self::VENDOR_EXCLUDES as $key) {
- unset($vendorFields[$key]);
- }
-
- $entry['id'] = $item->getUid();
-
- if (empty($entry['id'])) {
- $entry['id'] = $entryUri;
- }
-
- if (!empty($entryTitle)) {
- $entry['title'] = $entryTitle;
- }
- if (!empty($entryAuthor)) {
- $entry['author'] = array(
- 'name' => $entryAuthor
- );
- }
- if (!empty($entryTimestamp)) {
- $entry['date_modified'] = gmdate(DATE_ATOM, $entryTimestamp);
- }
- if (!empty($entryUri)) {
- $entry['url'] = $entryUri;
- }
- if (!empty($entryContent)) {
- if ($this->isHTML($entryContent)) {
- $entry['content_html'] = $entryContent;
- } else {
- $entry['content_text'] = $entryContent;
- }
- }
- if (!empty($entryEnclosures)) {
- $entry['attachments'] = array();
- foreach ($entryEnclosures as $enclosure) {
- $entry['attachments'][] = array(
- 'url' => $enclosure,
- 'mime_type' => getMimeType($enclosure)
- );
- }
- }
- if (!empty($entryCategories)) {
- $entry['tags'] = array();
- foreach ($entryCategories as $category) {
- $entry['tags'][] = $category;
- }
- }
- if (!empty($vendorFields)) {
- $entry['_rssbridge'] = $vendorFields;
- }
-
- if (empty($entry['id']))
- $entry['id'] = hash('sha1', $entryTitle . $entryContent);
-
- $items[] = $entry;
- }
- $data['items'] = $items;
-
- /**
- * The intention here is to discard non-utf8 byte sequences.
- * But the JSON_PARTIAL_OUTPUT_ON_ERROR also discards lots of other errors.
- * So consider this a hack.
- * Switch to JSON_INVALID_UTF8_IGNORE when PHP 7.2 is the latest platform requirement.
- */
- $json = json_encode($data, JSON_PRETTY_PRINT | JSON_PARTIAL_OUTPUT_ON_ERROR);
-
- return $json;
- }
-
- private function isHTML($text) {
- return (strlen(strip_tags($text)) != strlen($text));
- }
+class JsonFormat extends FormatAbstract
+{
+ const MIME_TYPE = 'application/json';
+
+ const VENDOR_EXCLUDES = [
+ 'author',
+ 'title',
+ 'uri',
+ 'timestamp',
+ 'content',
+ 'enclosures',
+ 'categories',
+ 'uid',
+ ];
+
+ public function stringify()
+ {
+ $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
+ $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
+ $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
+ $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
+
+ $extraInfos = $this->getExtraInfos();
+
+ $data = [
+ 'version' => 'https://jsonfeed.org/version/1',
+ 'title' => (!empty($extraInfos['name'])) ? $extraInfos['name'] : $urlHost,
+ 'home_page_url' => (!empty($extraInfos['uri'])) ? $extraInfos['uri'] : REPOSITORY,
+ 'feed_url' => $urlPrefix . $urlHost . $urlRequest
+ ];
+
+ if (!empty($extraInfos['icon'])) {
+ $data['icon'] = $extraInfos['icon'];
+ $data['favicon'] = $extraInfos['icon'];
+ }
+
+ $items = [];
+ foreach ($this->getItems() as $item) {
+ $entry = [];
+
+ $entryAuthor = $item->getAuthor();
+ $entryTitle = $item->getTitle();
+ $entryUri = $item->getURI();
+ $entryTimestamp = $item->getTimestamp();
+ $entryContent = $item->getContent() ? $this->sanitizeHtml($item->getContent()) : '';
+ $entryEnclosures = $item->getEnclosures();
+ $entryCategories = $item->getCategories();
+
+ $vendorFields = $item->toArray();
+ foreach (self::VENDOR_EXCLUDES as $key) {
+ unset($vendorFields[$key]);
+ }
+
+ $entry['id'] = $item->getUid();
+
+ if (empty($entry['id'])) {
+ $entry['id'] = $entryUri;
+ }
+
+ if (!empty($entryTitle)) {
+ $entry['title'] = $entryTitle;
+ }
+ if (!empty($entryAuthor)) {
+ $entry['author'] = [
+ 'name' => $entryAuthor
+ ];
+ }
+ if (!empty($entryTimestamp)) {
+ $entry['date_modified'] = gmdate(DATE_ATOM, $entryTimestamp);
+ }
+ if (!empty($entryUri)) {
+ $entry['url'] = $entryUri;
+ }
+ if (!empty($entryContent)) {
+ if ($this->isHTML($entryContent)) {
+ $entry['content_html'] = $entryContent;
+ } else {
+ $entry['content_text'] = $entryContent;
+ }
+ }
+ if (!empty($entryEnclosures)) {
+ $entry['attachments'] = [];
+ foreach ($entryEnclosures as $enclosure) {
+ $entry['attachments'][] = [
+ 'url' => $enclosure,
+ 'mime_type' => getMimeType($enclosure)
+ ];
+ }
+ }
+ if (!empty($entryCategories)) {
+ $entry['tags'] = [];
+ foreach ($entryCategories as $category) {
+ $entry['tags'][] = $category;
+ }
+ }
+ if (!empty($vendorFields)) {
+ $entry['_rssbridge'] = $vendorFields;
+ }
+
+ if (empty($entry['id'])) {
+ $entry['id'] = hash('sha1', $entryTitle . $entryContent);
+ }
+
+ $items[] = $entry;
+ }
+ $data['items'] = $items;
+
+ /**
+ * The intention here is to discard non-utf8 byte sequences.
+ * But the JSON_PARTIAL_OUTPUT_ON_ERROR also discards lots of other errors.
+ * So consider this a hack.
+ * Switch to JSON_INVALID_UTF8_IGNORE when PHP 7.2 is the latest platform requirement.
+ */
+ $json = json_encode($data, JSON_PRETTY_PRINT | JSON_PARTIAL_OUTPUT_ON_ERROR);
+
+ return $json;
+ }
+
+ private function isHTML($text)
+ {
+ return (strlen(strip_tags($text)) != strlen($text));
+ }
}
diff --git a/formats/MrssFormat.php b/formats/MrssFormat.php
index 386b7d37..45c2181f 100644
--- a/formats/MrssFormat.php
+++ b/formats/MrssFormat.php
@@ -1,4 +1,5 @@
<?php
+
/**
* MrssFormat - RSS 2.0 + Media RSS
* http://www.rssboard.org/rss-specification
@@ -24,146 +25,149 @@
* - Since the Media RSS extension has its own namespace, the output is a valid
* RSS 2.0 feed that works with feed readers that don't support the extension.
*/
-class MrssFormat extends FormatAbstract {
- const MIME_TYPE = 'application/rss+xml';
-
- protected const ATOM_NS = 'http://www.w3.org/2005/Atom';
- protected const MRSS_NS = 'http://search.yahoo.com/mrss/';
-
- const ALLOWED_IMAGE_EXT = array(
- '.gif', '.jpg', '.png'
- );
-
- public function stringify(){
- $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
- $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
- $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
- $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
-
- $feedUrl = $urlPrefix . $urlHost . $urlRequest;
-
- $extraInfos = $this->getExtraInfos();
- $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
-
- $document = new DomDocument('1.0', $this->getCharset());
- $document->formatOutput = true;
- $feed = $document->createElement('rss');
- $document->appendChild($feed);
- $feed->setAttribute('version', '2.0');
- $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:atom', self::ATOM_NS);
- $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:media', self::MRSS_NS);
-
- $channel = $document->createElement('channel');
- $feed->appendChild($channel);
-
- $title = $extraInfos['name'];
- $channelTitle = $document->createElement('title');
- $channel->appendChild($channelTitle);
- $channelTitle->appendChild($document->createTextNode($title));
-
- $link = $document->createElement('link');
- $channel->appendChild($link);
- $link->appendChild($document->createTextNode($uri));
-
- $description = $document->createElement('description');
- $channel->appendChild($description);
- $description->appendChild($document->createTextNode($extraInfos['name']));
-
- $icon = $extraInfos['icon'];
- if (!empty($icon) && in_array(substr($icon, -4), self::ALLOWED_IMAGE_EXT)) {
- $feedImage = $document->createElement('image');
- $channel->appendChild($feedImage);
- $iconUrl = $document->createElement('url');
- $iconUrl->appendChild($document->createTextNode($icon));
- $feedImage->appendChild($iconUrl);
- $iconTitle = $document->createElement('title');
- $iconTitle->appendChild($document->createTextNode($title));
- $feedImage->appendChild($iconTitle);
- $iconLink = $document->createElement('link');
- $iconLink->appendChild($document->createTextNode($uri));
- $feedImage->appendChild($iconLink);
- }
-
- $linkAlternate = $document->createElementNS(self::ATOM_NS, 'link');
- $channel->appendChild($linkAlternate);
- $linkAlternate->setAttribute('rel', 'alternate');
- $linkAlternate->setAttribute('type', 'text/html');
- $linkAlternate->setAttribute('href', $uri);
-
- $linkSelf = $document->createElementNS(self::ATOM_NS, 'link');
- $channel->appendChild($linkSelf);
- $linkSelf->setAttribute('rel', 'self');
- $linkSelf->setAttribute('type', 'application/atom+xml');
- $linkSelf->setAttribute('href', $feedUrl);
-
- foreach($this->getItems() as $item) {
- $itemTimestamp = $item->getTimestamp();
- $itemTitle = $item->getTitle();
- $itemUri = $item->getURI();
- $itemContent = $item->getContent() ? $this->sanitizeHtml($item->getContent()) : '';
- $entryID = $item->getUid();
- $isPermaLink = 'false';
-
- if (empty($entryID) && !empty($itemUri)) { // Fallback to provided URI
- $entryID = $itemUri;
- $isPermaLink = 'true';
- }
-
- if (empty($entryID)) // Fallback to title and content
- $entryID = hash('sha1', $itemTitle . $itemContent);
-
- $entry = $document->createElement('item');
- $channel->appendChild($entry);
-
- if (!empty($itemTitle)) {
- $entryTitle = $document->createElement('title');
- $entry->appendChild($entryTitle);
- $entryTitle->appendChild($document->createTextNode($itemTitle));
- }
-
- if (!empty($itemUri)) {
- $entryLink = $document->createElement('link');
- $entry->appendChild($entryLink);
- $entryLink->appendChild($document->createTextNode($itemUri));
- }
-
- $entryGuid = $document->createElement('guid');
- $entryGuid->setAttribute('isPermaLink', $isPermaLink);
- $entry->appendChild($entryGuid);
- $entryGuid->appendChild($document->createTextNode($entryID));
-
- if (!empty($itemTimestamp)) {
- $entryPublished = $document->createElement('pubDate');
- $entry->appendChild($entryPublished);
- $entryPublished->appendChild($document->createTextNode(gmdate(DATE_RFC2822, $itemTimestamp)));
- }
-
- if (!empty($itemContent)) {
- $entryDescription = $document->createElement('description');
- $entry->appendChild($entryDescription);
- $entryDescription->appendChild($document->createTextNode($itemContent));
- }
-
- foreach($item->getEnclosures() as $enclosure) {
- $entryEnclosure = $document->createElementNS(self::MRSS_NS, 'content');
- $entry->appendChild($entryEnclosure);
- $entryEnclosure->setAttribute('url', $enclosure);
- $entryEnclosure->setAttribute('type', getMimeType($enclosure));
- }
-
- $entryCategories = '';
- foreach($item->getCategories() as $category) {
- $entryCategory = $document->createElement('category');
- $entry->appendChild($entryCategory);
- $entryCategory->appendChild($document->createTextNode($category));
- }
- }
-
- $toReturn = $document->saveXML();
-
- // Remove invalid non-UTF8 characters
- ini_set('mbstring.substitute_character', 'none');
- $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
- return $toReturn;
- }
+class MrssFormat extends FormatAbstract
+{
+ const MIME_TYPE = 'application/rss+xml';
+
+ protected const ATOM_NS = 'http://www.w3.org/2005/Atom';
+ protected const MRSS_NS = 'http://search.yahoo.com/mrss/';
+
+ const ALLOWED_IMAGE_EXT = [
+ '.gif', '.jpg', '.png'
+ ];
+
+ public function stringify()
+ {
+ $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
+ $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
+ $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
+ $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
+
+ $feedUrl = $urlPrefix . $urlHost . $urlRequest;
+
+ $extraInfos = $this->getExtraInfos();
+ $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
+
+ $document = new DomDocument('1.0', $this->getCharset());
+ $document->formatOutput = true;
+ $feed = $document->createElement('rss');
+ $document->appendChild($feed);
+ $feed->setAttribute('version', '2.0');
+ $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:atom', self::ATOM_NS);
+ $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:media', self::MRSS_NS);
+
+ $channel = $document->createElement('channel');
+ $feed->appendChild($channel);
+
+ $title = $extraInfos['name'];
+ $channelTitle = $document->createElement('title');
+ $channel->appendChild($channelTitle);
+ $channelTitle->appendChild($document->createTextNode($title));
+
+ $link = $document->createElement('link');
+ $channel->appendChild($link);
+ $link->appendChild($document->createTextNode($uri));
+
+ $description = $document->createElement('description');
+ $channel->appendChild($description);
+ $description->appendChild($document->createTextNode($extraInfos['name']));
+
+ $icon = $extraInfos['icon'];
+ if (!empty($icon) && in_array(substr($icon, -4), self::ALLOWED_IMAGE_EXT)) {
+ $feedImage = $document->createElement('image');
+ $channel->appendChild($feedImage);
+ $iconUrl = $document->createElement('url');
+ $iconUrl->appendChild($document->createTextNode($icon));
+ $feedImage->appendChild($iconUrl);
+ $iconTitle = $document->createElement('title');
+ $iconTitle->appendChild($document->createTextNode($title));
+ $feedImage->appendChild($iconTitle);
+ $iconLink = $document->createElement('link');
+ $iconLink->appendChild($document->createTextNode($uri));
+ $feedImage->appendChild($iconLink);
+ }
+
+ $linkAlternate = $document->createElementNS(self::ATOM_NS, 'link');
+ $channel->appendChild($linkAlternate);
+ $linkAlternate->setAttribute('rel', 'alternate');
+ $linkAlternate->setAttribute('type', 'text/html');
+ $linkAlternate->setAttribute('href', $uri);
+
+ $linkSelf = $document->createElementNS(self::ATOM_NS, 'link');
+ $channel->appendChild($linkSelf);
+ $linkSelf->setAttribute('rel', 'self');
+ $linkSelf->setAttribute('type', 'application/atom+xml');
+ $linkSelf->setAttribute('href', $feedUrl);
+
+ foreach ($this->getItems() as $item) {
+ $itemTimestamp = $item->getTimestamp();
+ $itemTitle = $item->getTitle();
+ $itemUri = $item->getURI();
+ $itemContent = $item->getContent() ? $this->sanitizeHtml($item->getContent()) : '';
+ $entryID = $item->getUid();
+ $isPermaLink = 'false';
+
+ if (empty($entryID) && !empty($itemUri)) { // Fallback to provided URI
+ $entryID = $itemUri;
+ $isPermaLink = 'true';
+ }
+
+ if (empty($entryID)) { // Fallback to title and content
+ $entryID = hash('sha1', $itemTitle . $itemContent);
+ }
+
+ $entry = $document->createElement('item');
+ $channel->appendChild($entry);
+
+ if (!empty($itemTitle)) {
+ $entryTitle = $document->createElement('title');
+ $entry->appendChild($entryTitle);
+ $entryTitle->appendChild($document->createTextNode($itemTitle));
+ }
+
+ if (!empty($itemUri)) {
+ $entryLink = $document->createElement('link');
+ $entry->appendChild($entryLink);
+ $entryLink->appendChild($document->createTextNode($itemUri));
+ }
+
+ $entryGuid = $document->createElement('guid');
+ $entryGuid->setAttribute('isPermaLink', $isPermaLink);
+ $entry->appendChild($entryGuid);
+ $entryGuid->appendChild($document->createTextNode($entryID));
+
+ if (!empty($itemTimestamp)) {
+ $entryPublished = $document->createElement('pubDate');
+ $entry->appendChild($entryPublished);
+ $entryPublished->appendChild($document->createTextNode(gmdate(DATE_RFC2822, $itemTimestamp)));
+ }
+
+ if (!empty($itemContent)) {
+ $entryDescription = $document->createElement('description');
+ $entry->appendChild($entryDescription);
+ $entryDescription->appendChild($document->createTextNode($itemContent));
+ }
+
+ foreach ($item->getEnclosures() as $enclosure) {
+ $entryEnclosure = $document->createElementNS(self::MRSS_NS, 'content');
+ $entry->appendChild($entryEnclosure);
+ $entryEnclosure->setAttribute('url', $enclosure);
+ $entryEnclosure->setAttribute('type', getMimeType($enclosure));
+ }
+
+ $entryCategories = '';
+ foreach ($item->getCategories() as $category) {
+ $entryCategory = $document->createElement('category');
+ $entry->appendChild($entryCategory);
+ $entryCategory->appendChild($document->createTextNode($category));
+ }
+ }
+
+ $toReturn = $document->saveXML();
+
+ // Remove invalid non-UTF8 characters
+ ini_set('mbstring.substitute_character', 'none');
+ $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
+ return $toReturn;
+ }
}
diff --git a/formats/PlaintextFormat.php b/formats/PlaintextFormat.php
index a1ef9e7f..a1e125c7 100644
--- a/formats/PlaintextFormat.php
+++ b/formats/PlaintextFormat.php
@@ -1,24 +1,27 @@
<?php
+
/**
* Plaintext
* Returns $this->items as raw php data.
*/
-class PlaintextFormat extends FormatAbstract {
- const MIME_TYPE = 'text/plain';
+class PlaintextFormat extends FormatAbstract
+{
+ const MIME_TYPE = 'text/plain';
- public function stringify(){
- $items = $this->getItems();
- $data = array();
+ public function stringify()
+ {
+ $items = $this->getItems();
+ $data = [];
- foreach($items as $item) {
- $data[] = $item->toArray();
- }
+ foreach ($items as $item) {
+ $data[] = $item->toArray();
+ }
- $toReturn = print_r($data, true);
+ $toReturn = print_r($data, true);
- // Remove invalid non-UTF8 characters
- ini_set('mbstring.substitute_character', 'none');
- $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
- return $toReturn;
- }
+ // Remove invalid non-UTF8 characters
+ ini_set('mbstring.substitute_character', 'none');
+ $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
+ return $toReturn;
+ }
}
diff --git a/index.php b/index.php
index f118d500..8ca1990c 100644
--- a/index.php
+++ b/index.php
@@ -7,32 +7,32 @@ Move the CLI arguments to the $_GET array, in order to be able to use
rss-bridge from the command line
*/
if (isset($argv)) {
- parse_str(implode('&', array_slice($argv, 1)), $cliArgs);
- $params = array_merge($_GET, $cliArgs);
+ parse_str(implode('&', array_slice($argv, 1)), $cliArgs);
+ $params = array_merge($_GET, $cliArgs);
} else {
- $params = $_GET;
+ $params = $_GET;
}
try {
- $actionFac = new ActionFactory();
+ $actionFac = new ActionFactory();
- if (array_key_exists('action', $params)) {
- $action = $actionFac->create($params['action']);
- $action->userData = $params;
- $action->execute();
- } else {
- $showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
- echo BridgeList::create($showInactive);
- }
+ if (array_key_exists('action', $params)) {
+ $action = $actionFac->create($params['action']);
+ $action->userData = $params;
+ $action->execute();
+ } else {
+ $showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
+ echo BridgeList::create($showInactive);
+ }
} catch (\Throwable $e) {
- error_log($e);
+ error_log($e);
- $code = $e->getCode();
- if ($code !== -1) {
- header('Content-Type: text/plain', true, $code);
- }
+ $code = $e->getCode();
+ if ($code !== -1) {
+ header('Content-Type: text/plain', true, $code);
+ }
- $message = sprintf("Uncaught Exception %s: '%s'\n", get_class($e), $e->getMessage());
+ $message = sprintf("Uncaught Exception %s: '%s'\n", get_class($e), $e->getMessage());
- print $message;
+ print $message;
}
diff --git a/lib/ActionFactory.php b/lib/ActionFactory.php
index bd1297b4..5a413767 100644
--- a/lib/ActionFactory.php
+++ b/lib/ActionFactory.php
@@ -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,31 +7,31 @@
* 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 ActionFactory
{
- private $folder;
+ private $folder;
- public function __construct(string $folder = PATH_LIB_ACTIONS)
- {
- $this->folder = $folder;
- }
+ public function __construct(string $folder = PATH_LIB_ACTIONS)
+ {
+ $this->folder = $folder;
+ }
- /**
- * @param string $name The name of the action e.g. "Display", "List", or "Connectivity"
- */
- public function create(string $name): ActionInterface
- {
- $name = ucfirst(strtolower($name)) . 'Action';
- $filePath = $this->folder . $name . '.php';
- if(!file_exists($filePath)) {
- throw new \Exception('Invalid action');
- }
- $className = '\\' . $name;
- return new $className();
- }
+ /**
+ * @param string $name The name of the action e.g. "Display", "List", or "Connectivity"
+ */
+ public function create(string $name): ActionInterface
+ {
+ $name = ucfirst(strtolower($name)) . 'Action';
+ $filePath = $this->folder . $name . '.php';
+ if (!file_exists($filePath)) {
+ throw new \Exception('Invalid action');
+ }
+ $className = '\\' . $name;
+ return new $className();
+ }
}
diff --git a/lib/ActionInterface.php b/lib/ActionInterface.php
index c8684d52..78284ab4 100644
--- a/lib/ActionInterface.php
+++ b/lib/ActionInterface.php
@@ -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,21 +7,22 @@
* 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
*/
/**
* Interface for action objects.
*/
-interface ActionInterface {
- /**
- * Execute the action.
- *
- * Note: This function directly outputs data to the user.
- *
- * @return void
- */
- public function execute();
+interface ActionInterface
+{
+ /**
+ * Execute the action.
+ *
+ * Note: This function directly outputs data to the user.
+ *
+ * @return void
+ */
+ public function execute();
}
diff --git a/lib/Authentication.php b/lib/Authentication.php
index ac8ea96a..1ae26edf 100644
--- a/lib/Authentication.php
+++ b/lib/Authentication.php
@@ -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
*/
/**
@@ -30,56 +31,57 @@
* @todo Add functions to detect if a user is authenticated or not. This can be
* utilized for limiting access to authorized users only.
*/
-class Authentication {
- /**
- * Throw an exception when trying to create a new instance of this class.
- * Use {@see Authentication::showPromptIfNeeded()} instead!
- *
- * @throws \LogicException if called.
- */
- public function __construct(){
- throw new \LogicException('Use ' . __CLASS__ . '::showPromptIfNeeded()!');
- }
-
- /**
- * Requests the user for login credentials if necessary.
- *
- * Responds to an authentication request or returns the `WWW-Authenticate`
- * header if authentication is enabled in the configuration of RSS-Bridge
- * (`[authentication] enable = true`).
- *
- * @return void
- */
- public static function showPromptIfNeeded() {
-
- if(Configuration::getConfig('authentication', 'enable') === true) {
- if(!Authentication::verifyPrompt()) {
- header('WWW-Authenticate: Basic realm="RSS-Bridge"', true, 401);
- die('Please authenticate in order to access this instance !');
- }
-
- }
-
- }
-
- /**
- * Verifies if an authentication request was received and compares the
- * provided username and password to the configuration of RSS-Bridge
- * (`[authentication] username` and `[authentication] password`).
- *
- * @return bool True if authentication succeeded.
- */
- public static function verifyPrompt() {
+class Authentication
+{
+ /**
+ * Throw an exception when trying to create a new instance of this class.
+ * Use {@see Authentication::showPromptIfNeeded()} instead!
+ *
+ * @throws \LogicException if called.
+ */
+ public function __construct()
+ {
+ throw new \LogicException('Use ' . __CLASS__ . '::showPromptIfNeeded()!');
+ }
- if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
- if(Configuration::getConfig('authentication', 'username') === $_SERVER['PHP_AUTH_USER']
- && Configuration::getConfig('authentication', 'password') === $_SERVER['PHP_AUTH_PW']) {
- return true;
- } else {
- error_log('[RSS-Bridge] Failed authentication attempt from ' . $_SERVER['REMOTE_ADDR']);
- }
- }
- return false;
+ /**
+ * Requests the user for login credentials if necessary.
+ *
+ * Responds to an authentication request or returns the `WWW-Authenticate`
+ * header if authentication is enabled in the configuration of RSS-Bridge
+ * (`[authentication] enable = true`).
+ *
+ * @return void
+ */
+ public static function showPromptIfNeeded()
+ {
+ if (Configuration::getConfig('authentication', 'enable') === true) {
+ if (!Authentication::verifyPrompt()) {
+ header('WWW-Authenticate: Basic realm="RSS-Bridge"', true, 401);
+ die('Please authenticate in order to access this instance !');
+ }
+ }
+ }
- }
+ /**
+ * Verifies if an authentication request was received and compares the
+ * provided username and password to the configuration of RSS-Bridge
+ * (`[authentication] username` and `[authentication] password`).
+ *
+ * @return bool True if authentication succeeded.
+ */
+ public static function verifyPrompt()
+ {
+ if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
+ if (
+ Configuration::getConfig('authentication', 'username') === $_SERVER['PHP_AUTH_USER']
+ && Configuration::getConfig('authentication', 'password') === $_SERVER['PHP_AUTH_PW']
+ ) {
+ return true;
+ } else {
+ error_log('[RSS-Bridge] Failed authentication attempt from ' . $_SERVER['REMOTE_ADDR']);
+ }
+ }
+ return false;
+ }
}
diff --git a/lib/BridgeAbstract.php b/lib/BridgeAbstract.php
index 38e3da03..c479f53e 100644
--- a/lib/BridgeAbstract.php
+++ b/lib/BridgeAbstract.php
@@ -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
*/
/**
@@ -24,393 +25,410 @@
* @todo Add specification for PARAMETERS ()
* @todo Add specification for $items
*/
-abstract class BridgeAbstract implements BridgeInterface {
-
- /**
- * Name of the bridge
- *
- * Use {@see BridgeAbstract::getName()} to read this parameter
- */
- const NAME = 'Unnamed bridge';
-
- /**
- * URI to the site the bridge is intended to be used for.
- *
- * Use {@see BridgeAbstract::getURI()} to read this parameter
- */
- const URI = '';
-
- /**
- * Donation URI to the site the bridge is intended to be used for.
- *
- * Use {@see BridgeAbstract::getDonationURI()} to read this parameter
- */
- const DONATION_URI = '';
-
- /**
- * A brief description of what the bridge can do
- *
- * Use {@see BridgeAbstract::getDescription()} to read this parameter
- */
- const DESCRIPTION = 'No description provided';
-
- /**
- * The name of the maintainer. Multiple maintainers can be separated by comma
- *
- * Use {@see BridgeAbstract::getMaintainer()} to read this parameter
- */
- const MAINTAINER = 'No maintainer';
-
- /**
- * The default cache timeout for the bridge
- *
- * Use {@see BridgeAbstract::getCacheTimeout()} to read this parameter
- */
- const CACHE_TIMEOUT = 3600;
-
- /**
- * Configuration for the bridge
- *
- * Use {@see BridgeAbstract::getConfiguration()} to read this parameter
- */
- const CONFIGURATION = array();
-
- /**
- * Parameters for the bridge
- *
- * Use {@see BridgeAbstract::getParameters()} to read this parameter
- */
- const PARAMETERS = array();
-
- /**
- * Test cases for detectParameters for the bridge
- */
- const TEST_DETECT_PARAMETERS = array();
-
- /**
- * This is a convenient const for the limit option in bridge contexts.
- * Can be inlined and modified if necessary.
- */
- protected const LIMIT = [
- 'name' => 'Limit',
- 'type' => 'number',
- 'title' => 'Maximum number of items to return',
- ];
-
- /**
- * Holds the list of items collected by the bridge
- *
- * Items must be collected by {@see BridgeInterface::collectData()}
- *
- * Use {@see BridgeAbstract::getItems()} to access items.
- *
- * @var array
- */
- protected $items = array();
-
- /**
- * Holds the list of input parameters used by the bridge
- *
- * Do not access this parameter directly!
- * Use {@see BridgeAbstract::setInputs()} and {@see BridgeAbstract::getInput()} instead!
- *
- * @var array
- */
- protected $inputs = array();
-
- /**
- * Holds the name of the queried context
- *
- * @var string
- */
- protected $queriedContext = '';
-
- /** {@inheritdoc} */
- public function getItems(){
- return $this->items;
- }
-
- /**
- * Sets the input values for a given context.
- *
- * @param array $inputs Associative array of inputs
- * @param string $queriedContext The context name
- * @return void
- */
- protected function setInputs(array $inputs, $queriedContext){
- // Import and assign all inputs to their context
- foreach($inputs as $name => $value) {
- foreach(static::PARAMETERS as $context => $set) {
- if(array_key_exists($name, static::PARAMETERS[$context])) {
- $this->inputs[$context][$name]['value'] = $value;
- }
- }
- }
-
- // Apply default values to missing data
- $contexts = array($queriedContext);
- if(array_key_exists('global', static::PARAMETERS)) {
- $contexts[] = 'global';
- }
-
- foreach($contexts as $context) {
- foreach(static::PARAMETERS[$context] as $name => $properties) {
- if(isset($this->inputs[$context][$name]['value'])) {
- continue;
- }
-
- $type = isset($properties['type']) ? $properties['type'] : 'text';
-
- switch($type) {
- case 'checkbox':
- if(!isset($properties['defaultValue'])) {
- $this->inputs[$context][$name]['value'] = false;
- } else {
- $this->inputs[$context][$name]['value'] = $properties['defaultValue'];
- }
- break;
- case 'list':
- if(!isset($properties['defaultValue'])) {
- $firstItem = reset($properties['values']);
- if(is_array($firstItem)) {
- $firstItem = reset($firstItem);
- }
- $this->inputs[$context][$name]['value'] = $firstItem;
- } else {
- $this->inputs[$context][$name]['value'] = $properties['defaultValue'];
- }
- break;
- default:
- if(isset($properties['defaultValue'])) {
- $this->inputs[$context][$name]['value'] = $properties['defaultValue'];
- }
- break;
- }
- }
- }
-
- // Copy global parameter values to the guessed context
- if(array_key_exists('global', static::PARAMETERS)) {
- foreach(static::PARAMETERS['global'] as $name => $properties) {
- if(isset($inputs[$name])) {
- $value = $inputs[$name];
- } elseif(isset($properties['defaultValue'])) {
- $value = $properties['defaultValue'];
- } else {
- continue;
- }
- $this->inputs[$queriedContext][$name]['value'] = $value;
- }
- }
-
- // Only keep guessed context parameters values
- if(isset($this->inputs[$queriedContext])) {
- $this->inputs = array($queriedContext => $this->inputs[$queriedContext]);
- } else {
- $this->inputs = array();
- }
- }
-
- /**
- * Set inputs for the bridge
- *
- * Returns errors and aborts execution if the provided input parameters are
- * invalid.
- *
- * @param array List of input parameters. Each element in this list must
- * relate to an item in {@see BridgeAbstract::PARAMETERS}
- * @return void
- */
- public function setDatas(array $inputs){
-
- if(isset($inputs['context'])) { // Context hinting (optional)
- $this->queriedContext = $inputs['context'];
- unset($inputs['context']);
- }
-
- if(empty(static::PARAMETERS)) {
-
- if(!empty($inputs)) {
- returnClientError('Invalid parameters value(s)');
- }
-
- return;
-
- }
-
- $validator = new ParameterValidator();
-
- if(!$validator->validateData($inputs, static::PARAMETERS)) {
- $parameters = array_map(
- function($i){ return $i['name']; }, // Just display parameter names
- $validator->getInvalidParameters()
- );
-
- returnClientError(
- 'Invalid parameters value(s): '
- . implode(', ', $parameters)
- );
- }
-
- // Guess the context from input data
- if(empty($this->queriedContext)) {
- $this->queriedContext = $validator->getQueriedContext($inputs, static::PARAMETERS);
- }
-
- if(is_null($this->queriedContext)) {
- returnClientError('Required parameter(s) missing');
- } elseif($this->queriedContext === false) {
- returnClientError('Mixed context parameters');
- }
-
- $this->setInputs($inputs, $this->queriedContext);
-
- }
-
- /**
- * Loads configuration for the bridge
- *
- * Returns errors and aborts execution if the provided configuration is
- * invalid.
- *
- * @return void
- */
- public function loadConfiguration() {
- foreach(static::CONFIGURATION as $optionName => $optionValue) {
-
- $configurationOption = Configuration::getConfig(get_class($this), $optionName);
-
- if($configurationOption !== null) {
- $this->configuration[$optionName] = $configurationOption;
- continue;
- }
-
- if(isset($optionValue['required']) && $optionValue['required'] === true) {
- returnServerError(
- 'Missing configuration option: '
- . $optionName
- );
- } elseif(isset($optionValue['defaultValue'])) {
- $this->configuration[$optionName] = $optionValue['defaultValue'];
- }
-
- }
- }
-
- /**
- * Returns the value for the provided input
- *
- * @param string $input The input name
- * @return mixed|null The input value or null if the input is not defined
- */
- protected function getInput($input){
- if(!isset($this->inputs[$this->queriedContext][$input]['value'])) {
- return null;
- }
- return $this->inputs[$this->queriedContext][$input]['value'];
- }
-
- /**
- * Returns the value for the selected configuration
- *
- * @param string $input The option name
- * @return mixed|null The option value or null if the input is not defined
- */
- public function getOption($name){
- if(!isset($this->configuration[$name])) {
- return null;
- }
- return $this->configuration[$name];
- }
-
- /** {@inheritdoc} */
- public function getDescription(){
- return static::DESCRIPTION;
- }
-
- /** {@inheritdoc} */
- public function getMaintainer(){
- return static::MAINTAINER;
- }
-
- /** {@inheritdoc} */
- public function getName(){
- return static::NAME;
- }
-
- /** {@inheritdoc} */
- public function getIcon(){
- return static::URI . '/favicon.ico';
- }
-
- /** {@inheritdoc} */
- public function getConfiguration(){
- return static::CONFIGURATION;
- }
-
- /** {@inheritdoc} */
- public function getParameters(){
- return static::PARAMETERS;
- }
-
- /** {@inheritdoc} */
- public function getURI(){
- return static::URI;
- }
-
- /** {@inheritdoc} */
- public function getDonationURI(){
- return static::DONATION_URI;
- }
-
- /** {@inheritdoc} */
- public function getCacheTimeout(){
- return static::CACHE_TIMEOUT;
- }
-
- /** {@inheritdoc} */
- public function detectParameters($url){
- $regex = '/^(https?:\/\/)?(www\.)?(.+?)(\/)?$/';
- if(empty(static::PARAMETERS)
- && preg_match($regex, $url, $urlMatches) > 0
- && preg_match($regex, static::URI, $bridgeUriMatches) > 0
- && $urlMatches[3] === $bridgeUriMatches[3]) {
- return array();
- } else {
- return null;
- }
- }
-
- /**
- * Loads a cached value for the specified key
- *
- * @param string $key Key name
- * @param int $duration Cache duration (optional, default: 24 hours)
- * @return mixed Cached value or null if the key doesn't exist or has expired
- */
- protected function loadCacheValue($key, $duration = 86400){
- $cacheFac = new CacheFactory();
-
- $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $cache->setScope(get_called_class());
- $cache->setKey($key);
- if($cache->getTime() < time() - $duration)
- return null;
- return $cache->loadData();
- }
-
- /**
- * Stores a value to cache with the specified key
- *
- * @param string $key Key name
- * @param mixed $value Value to cache
- */
- protected function saveCacheValue($key, $value){
- $cacheFac = new CacheFactory();
-
- $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $cache->setScope(get_called_class());
- $cache->setKey($key);
- $cache->saveData($value);
- }
+abstract class BridgeAbstract implements BridgeInterface
+{
+ /**
+ * Name of the bridge
+ *
+ * Use {@see BridgeAbstract::getName()} to read this parameter
+ */
+ const NAME = 'Unnamed bridge';
+
+ /**
+ * URI to the site the bridge is intended to be used for.
+ *
+ * Use {@see BridgeAbstract::getURI()} to read this parameter
+ */
+ const URI = '';
+
+ /**
+ * Donation URI to the site the bridge is intended to be used for.
+ *
+ * Use {@see BridgeAbstract::getDonationURI()} to read this parameter
+ */
+ const DONATION_URI = '';
+
+ /**
+ * A brief description of what the bridge can do
+ *
+ * Use {@see BridgeAbstract::getDescription()} to read this parameter
+ */
+ const DESCRIPTION = 'No description provided';
+
+ /**
+ * The name of the maintainer. Multiple maintainers can be separated by comma
+ *
+ * Use {@see BridgeAbstract::getMaintainer()} to read this parameter
+ */
+ const MAINTAINER = 'No maintainer';
+
+ /**
+ * The default cache timeout for the bridge
+ *
+ * Use {@see BridgeAbstract::getCacheTimeout()} to read this parameter
+ */
+ const CACHE_TIMEOUT = 3600;
+
+ /**
+ * Configuration for the bridge
+ *
+ * Use {@see BridgeAbstract::getConfiguration()} to read this parameter
+ */
+ const CONFIGURATION = [];
+
+ /**
+ * Parameters for the bridge
+ *
+ * Use {@see BridgeAbstract::getParameters()} to read this parameter
+ */
+ const PARAMETERS = [];
+
+ /**
+ * Test cases for detectParameters for the bridge
+ */
+ const TEST_DETECT_PARAMETERS = [];
+
+ /**
+ * This is a convenient const for the limit option in bridge contexts.
+ * Can be inlined and modified if necessary.
+ */
+ protected const LIMIT = [
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'title' => 'Maximum number of items to return',
+ ];
+
+ /**
+ * Holds the list of items collected by the bridge
+ *
+ * Items must be collected by {@see BridgeInterface::collectData()}
+ *
+ * Use {@see BridgeAbstract::getItems()} to access items.
+ *
+ * @var array
+ */
+ protected $items = [];
+
+ /**
+ * Holds the list of input parameters used by the bridge
+ *
+ * Do not access this parameter directly!
+ * Use {@see BridgeAbstract::setInputs()} and {@see BridgeAbstract::getInput()} instead!
+ *
+ * @var array
+ */
+ protected $inputs = [];
+
+ /**
+ * Holds the name of the queried context
+ *
+ * @var string
+ */
+ protected $queriedContext = '';
+
+ /** {@inheritdoc} */
+ public function getItems()
+ {
+ return $this->items;
+ }
+
+ /**
+ * Sets the input values for a given context.
+ *
+ * @param array $inputs Associative array of inputs
+ * @param string $queriedContext The context name
+ * @return void
+ */
+ protected function setInputs(array $inputs, $queriedContext)
+ {
+ // Import and assign all inputs to their context
+ foreach ($inputs as $name => $value) {
+ foreach (static::PARAMETERS as $context => $set) {
+ if (array_key_exists($name, static::PARAMETERS[$context])) {
+ $this->inputs[$context][$name]['value'] = $value;
+ }
+ }
+ }
+
+ // Apply default values to missing data
+ $contexts = [$queriedContext];
+ if (array_key_exists('global', static::PARAMETERS)) {
+ $contexts[] = 'global';
+ }
+
+ foreach ($contexts as $context) {
+ foreach (static::PARAMETERS[$context] as $name => $properties) {
+ if (isset($this->inputs[$context][$name]['value'])) {
+ continue;
+ }
+
+ $type = isset($properties['type']) ? $properties['type'] : 'text';
+
+ switch ($type) {
+ case 'checkbox':
+ if (!isset($properties['defaultValue'])) {
+ $this->inputs[$context][$name]['value'] = false;
+ } else {
+ $this->inputs[$context][$name]['value'] = $properties['defaultValue'];
+ }
+ break;
+ case 'list':
+ if (!isset($properties['defaultValue'])) {
+ $firstItem = reset($properties['values']);
+ if (is_array($firstItem)) {
+ $firstItem = reset($firstItem);
+ }
+ $this->inputs[$context][$name]['value'] = $firstItem;
+ } else {
+ $this->inputs[$context][$name]['value'] = $properties['defaultValue'];
+ }
+ break;
+ default:
+ if (isset($properties['defaultValue'])) {
+ $this->inputs[$context][$name]['value'] = $properties['defaultValue'];
+ }
+ break;
+ }
+ }
+ }
+
+ // Copy global parameter values to the guessed context
+ if (array_key_exists('global', static::PARAMETERS)) {
+ foreach (static::PARAMETERS['global'] as $name => $properties) {
+ if (isset($inputs[$name])) {
+ $value = $inputs[$name];
+ } elseif (isset($properties['defaultValue'])) {
+ $value = $properties['defaultValue'];
+ } else {
+ continue;
+ }
+ $this->inputs[$queriedContext][$name]['value'] = $value;
+ }
+ }
+
+ // Only keep guessed context parameters values
+ if (isset($this->inputs[$queriedContext])) {
+ $this->inputs = [$queriedContext => $this->inputs[$queriedContext]];
+ } else {
+ $this->inputs = [];
+ }
+ }
+
+ /**
+ * Set inputs for the bridge
+ *
+ * Returns errors and aborts execution if the provided input parameters are
+ * invalid.
+ *
+ * @param array List of input parameters. Each element in this list must
+ * relate to an item in {@see BridgeAbstract::PARAMETERS}
+ * @return void
+ */
+ public function setDatas(array $inputs)
+ {
+ if (isset($inputs['context'])) { // Context hinting (optional)
+ $this->queriedContext = $inputs['context'];
+ unset($inputs['context']);
+ }
+
+ if (empty(static::PARAMETERS)) {
+ if (!empty($inputs)) {
+ returnClientError('Invalid parameters value(s)');
+ }
+
+ return;
+ }
+
+ $validator = new ParameterValidator();
+
+ if (!$validator->validateData($inputs, static::PARAMETERS)) {
+ $parameters = array_map(
+ function ($i) {
+ return $i['name'];
+ }, // Just display parameter names
+ $validator->getInvalidParameters()
+ );
+
+ returnClientError(
+ 'Invalid parameters value(s): '
+ . implode(', ', $parameters)
+ );
+ }
+
+ // Guess the context from input data
+ if (empty($this->queriedContext)) {
+ $this->queriedContext = $validator->getQueriedContext($inputs, static::PARAMETERS);
+ }
+
+ if (is_null($this->queriedContext)) {
+ returnClientError('Required parameter(s) missing');
+ } elseif ($this->queriedContext === false) {
+ returnClientError('Mixed context parameters');
+ }
+
+ $this->setInputs($inputs, $this->queriedContext);
+ }
+
+ /**
+ * Loads configuration for the bridge
+ *
+ * Returns errors and aborts execution if the provided configuration is
+ * invalid.
+ *
+ * @return void
+ */
+ public function loadConfiguration()
+ {
+ foreach (static::CONFIGURATION as $optionName => $optionValue) {
+ $configurationOption = Configuration::getConfig(get_class($this), $optionName);
+
+ if ($configurationOption !== null) {
+ $this->configuration[$optionName] = $configurationOption;
+ continue;
+ }
+
+ if (isset($optionValue['required']) && $optionValue['required'] === true) {
+ returnServerError(
+ 'Missing configuration option: '
+ . $optionName
+ );
+ } elseif (isset($optionValue['defaultValue'])) {
+ $this->configuration[$optionName] = $optionValue['defaultValue'];
+ }
+ }
+ }
+
+ /**
+ * Returns the value for the provided input
+ *
+ * @param string $input The input name
+ * @return mixed|null The input value or null if the input is not defined
+ */
+ protected function getInput($input)
+ {
+ if (!isset($this->inputs[$this->queriedContext][$input]['value'])) {
+ return null;
+ }
+ return $this->inputs[$this->queriedContext][$input]['value'];
+ }
+
+ /**
+ * Returns the value for the selected configuration
+ *
+ * @param string $input The option name
+ * @return mixed|null The option value or null if the input is not defined
+ */
+ public function getOption($name)
+ {
+ if (!isset($this->configuration[$name])) {
+ return null;
+ }
+ return $this->configuration[$name];
+ }
+
+ /** {@inheritdoc} */
+ public function getDescription()
+ {
+ return static::DESCRIPTION;
+ }
+
+ /** {@inheritdoc} */
+ public function getMaintainer()
+ {
+ return static::MAINTAINER;
+ }
+
+ /** {@inheritdoc} */
+ public function getName()
+ {
+ return static::NAME;
+ }
+
+ /** {@inheritdoc} */
+ public function getIcon()
+ {
+ return static::URI . '/favicon.ico';
+ }
+
+ /** {@inheritdoc} */
+ public function getConfiguration()
+ {
+ return static::CONFIGURATION;
+ }
+
+ /** {@inheritdoc} */
+ public function getParameters()
+ {
+ return static::PARAMETERS;
+ }
+
+ /** {@inheritdoc} */
+ public function getURI()
+ {
+ return static::URI;
+ }
+
+ /** {@inheritdoc} */
+ public function getDonationURI()
+ {
+ return static::DONATION_URI;
+ }
+
+ /** {@inheritdoc} */
+ public function getCacheTimeout()
+ {
+ return static::CACHE_TIMEOUT;
+ }
+
+ /** {@inheritdoc} */
+ public function detectParameters($url)
+ {
+ $regex = '/^(https?:\/\/)?(www\.)?(.+?)(\/)?$/';
+ if (
+ empty(static::PARAMETERS)
+ && preg_match($regex, $url, $urlMatches) > 0
+ && preg_match($regex, static::URI, $bridgeUriMatches) > 0
+ && $urlMatches[3] === $bridgeUriMatches[3]
+ ) {
+ return [];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Loads a cached value for the specified key
+ *
+ * @param string $key Key name
+ * @param int $duration Cache duration (optional, default: 24 hours)
+ * @return mixed Cached value or null if the key doesn't exist or has expired
+ */
+ protected function loadCacheValue($key, $duration = 86400)
+ {
+ $cacheFac = new CacheFactory();
+
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope(get_called_class());
+ $cache->setKey($key);
+ if ($cache->getTime() < time() - $duration) {
+ return null;
+ }
+ return $cache->loadData();
+ }
+
+ /**
+ * Stores a value to cache with the specified key
+ *
+ * @param string $key Key name
+ * @param mixed $value Value to cache
+ */
+ protected function saveCacheValue($key, $value)
+ {
+ $cacheFac = new CacheFactory();
+
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope(get_called_class());
+ $cache->setKey($key);
+ $cache->saveData($value);
+ }
}
diff --git a/lib/BridgeCard.php b/lib/BridgeCard.php
index 22520170..78132776 100644
--- a/lib/BridgeCard.php
+++ b/lib/BridgeCard.php
@@ -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
*/
/**
@@ -19,310 +20,326 @@
*
* @todo Return error if a caller creates an object of this class.
*/
-final class BridgeCard {
- /**
- * Get the form header for a bridge card
- *
- * @param string $bridgeName The bridge name
- * @param bool $isHttps If disabled, adds a warning to the form
- * @return string The form header
- */
- private static function getFormHeader($bridgeName, $isHttps = false, $parameterName = '') {
- $form = <<<EOD
+final class BridgeCard
+{
+ /**
+ * Get the form header for a bridge card
+ *
+ * @param string $bridgeName The bridge name
+ * @param bool $isHttps If disabled, adds a warning to the form
+ * @return string The form header
+ */
+ private static function getFormHeader($bridgeName, $isHttps = false, $parameterName = '')
+ {
+ $form = <<<EOD
<form method="GET" action="?">
<input type="hidden" name="action" value="display" />
<input type="hidden" name="bridge" value="{$bridgeName}" />
EOD;
- if(!empty($parameterName)) {
- $form .= <<<EOD
+ if (!empty($parameterName)) {
+ $form .= <<<EOD
<input type="hidden" name="context" value="{$parameterName}" />
EOD;
- }
+ }
- if(!$isHttps) {
- $form .= '<div class="secure-warning">Warning :
+ if (!$isHttps) {
+ $form .= '<div class="secure-warning">Warning :
This bridge is not fetching its content through a secure connection</div>';
- }
-
- return $form;
- }
-
- /**
- * Get the form body for a bridge
- *
- * @param string $bridgeName The bridge name
- * @param array $formats A list of supported formats
- * @param bool $isActive Indicates if a bridge is enabled or not
- * @param bool $isHttps Indicates if a bridge uses HTTPS or not
- * @param string $parameterName Sets the bridge context for the current form
- * @param array $parameters The bridge parameters
- * @return string The form body
- */
- private static function getForm($bridgeName,
- $formats,
- $isActive = false,
- $isHttps = false,
- $parameterName = '',
- $parameters = array()) {
- $form = self::getFormHeader($bridgeName, $isHttps, $parameterName);
-
- if(count($parameters) > 0) {
-
- $form .= '<div class="parameters">';
-
- foreach($parameters as $id => $inputEntry) {
- if(!isset($inputEntry['exampleValue']))
- $inputEntry['exampleValue'] = '';
-
- if(!isset($inputEntry['defaultValue']))
- $inputEntry['defaultValue'] = '';
-
- $idArg = 'arg-'
- . urlencode($bridgeName)
- . '-'
- . urlencode($parameterName)
- . '-'
- . urlencode($id);
-
- $form .= '<label for="'
- . $idArg
- . '">'
- . filter_var($inputEntry['name'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
- . '</label>'
- . PHP_EOL;
-
- if(!isset($inputEntry['type']) || $inputEntry['type'] === 'text') {
- $form .= self::getTextInput($inputEntry, $idArg, $id);
- } elseif($inputEntry['type'] === 'number') {
- $form .= self::getNumberInput($inputEntry, $idArg, $id);
- } else if($inputEntry['type'] === 'list') {
- $form .= self::getListInput($inputEntry, $idArg, $id);
- } elseif($inputEntry['type'] === 'checkbox') {
- $form .= self::getCheckboxInput($inputEntry, $idArg, $id);
- }
-
- if(isset($inputEntry['title'])) {
- $title_filtered = filter_var($inputEntry['title'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
- $form .= '<i class="info" title="' . $title_filtered . '">i</i>';
- } else {
- $form .= '<i class="no-info"></i>';
- }
- }
-
- $form .= '</div>';
-
- }
-
- if($isActive) {
- $form .= '<button type="submit" name="format" formtarget="_blank" value="Html">Generate feed</button>';
- } else {
- $form .= '<span style="font-weight: bold;">Inactive</span>';
- }
-
- return $form . '</form>' . PHP_EOL;
- }
-
- /**
- * Get input field attributes
- *
- * @param array $entry The current entry
- * @return string The input field attributes
- */
- private static function getInputAttributes($entry) {
- $retVal = '';
-
- if(isset($entry['required']) && $entry['required'] === true)
- $retVal .= ' required';
-
- if(isset($entry['pattern']))
- $retVal .= ' pattern="' . $entry['pattern'] . '"';
-
- return $retVal;
- }
-
- /**
- * Get text input
- *
- * @param array $entry The current entry
- * @param string $id The field ID
- * @param string $name The field name
- * @return string The text input field
- */
- private static function getTextInput($entry, $id, $name) {
- return '<input '
- . self::getInputAttributes($entry)
- . ' id="'
- . $id
- . '" type="text" value="'
- . filter_var($entry['defaultValue'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
- . '" placeholder="'
- . filter_var($entry['exampleValue'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
- . '" name="'
- . $name
- . '" />'
- . PHP_EOL;
- }
-
- /**
- * Get number input
- *
- * @param array $entry The current entry
- * @param string $id The field ID
- * @param string $name The field name
- * @return string The number input field
- */
- private static function getNumberInput($entry, $id, $name) {
- return '<input '
- . self::getInputAttributes($entry)
- . ' id="'
- . $id
- . '" type="number" value="'
- . filter_var($entry['defaultValue'], FILTER_SANITIZE_NUMBER_INT)
- . '" placeholder="'
- . filter_var($entry['exampleValue'], FILTER_SANITIZE_NUMBER_INT)
- . '" name="'
- . $name
- . '" />'
- . PHP_EOL;
- }
-
- /**
- * Get list input
- *
- * @param array $entry The current entry
- * @param string $id The field ID
- * @param string $name The field name
- * @return string The list input field
- */
- private static function getListInput($entry, $id, $name) {
- if(isset($entry['required']) && $entry['required'] === true) {
- Debug::log('The "required" attribute is not supported for lists.');
- unset($entry['required']);
- }
-
- $list = '<select '
- . self::getInputAttributes($entry)
- . ' id="'
- . $id
- . '" name="'
- . $name
- . '" >';
-
- foreach($entry['values'] as $name => $value) {
- if(is_array($value)) {
- $list .= '<optgroup label="' . htmlentities($name) . '">';
- foreach($value as $subname => $subvalue) {
- if($entry['defaultValue'] === $subname
- || $entry['defaultValue'] === $subvalue) {
- $list .= '<option value="'
- . $subvalue
- . '" selected>'
- . $subname
- . '</option>';
- } else {
- $list .= '<option value="'
- . $subvalue
- . '">'
- . $subname
- . '</option>';
- }
- }
- $list .= '</optgroup>';
- } else {
- if($entry['defaultValue'] === $name
- || $entry['defaultValue'] === $value) {
- $list .= '<option value="'
- . $value
- . '" selected>'
- . $name
- . '</option>';
- } else {
- $list .= '<option value="'
- . $value
- . '">'
- . $name
- . '</option>';
- }
- }
- }
-
- $list .= '</select>';
-
- return $list;
- }
-
- /**
- * Get checkbox input
- *
- * @param array $entry The current entry
- * @param string $id The field ID
- * @param string $name The field name
- * @return string The checkbox input field
- */
- private static function getCheckboxInput($entry, $id, $name) {
- if(isset($entry['required']) && $entry['required'] === true) {
- Debug::log('The "required" attribute is not supported for checkboxes.');
- unset($entry['required']);
- }
-
- return '<input '
- . self::getInputAttributes($entry)
- . ' id="'
- . $id
- . '" type="checkbox" name="'
- . $name
- . '" '
- . ($entry['defaultValue'] === 'checked' ? 'checked' : '')
- . ' />'
- . PHP_EOL;
- }
-
- /**
- * Gets a single bridge card
- *
- * @param string $bridgeName The bridge name
- * @param array $formats A list of formats
- * @param bool $isActive Indicates if the bridge is active or not
- * @return string The bridge card
- */
- public static function displayBridgeCard($bridgeName, $formats, $isActive = true){
-
- $bridgeFac = new \BridgeFactory();
-
- $bridge = $bridgeFac->create($bridgeName);
-
- if($bridge == false)
- return '';
-
- $isHttps = strpos($bridge->getURI(), 'https') === 0;
-
- $uri = $bridge->getURI();
- $name = $bridge->getName();
- $icon = $bridge->getIcon();
- $description = $bridge->getDescription();
- $parameters = $bridge->getParameters();
- $donationUri = $bridge->getDonationURI();
- $maintainer = $bridge->getMaintainer();
-
- $donationsAllowed = Configuration::getConfig('admin', 'donations');
-
- if(defined('PROXY_URL') && PROXY_BYBRIDGE) {
- $parameters['global']['_noproxy'] = array(
- 'name' => 'Disable proxy (' . ((defined('PROXY_NAME') && PROXY_NAME) ? PROXY_NAME : PROXY_URL) . ')',
- 'type' => 'checkbox'
- );
- }
-
- if(CUSTOM_CACHE_TIMEOUT) {
- $parameters['global']['_cache_timeout'] = array(
- 'name' => 'Cache timeout in seconds',
- 'type' => 'number',
- 'defaultValue' => $bridge->getCacheTimeout()
- );
- }
-
- $card = <<<CARD
+ }
+
+ return $form;
+ }
+
+ /**
+ * Get the form body for a bridge
+ *
+ * @param string $bridgeName The bridge name
+ * @param array $formats A list of supported formats
+ * @param bool $isActive Indicates if a bridge is enabled or not
+ * @param bool $isHttps Indicates if a bridge uses HTTPS or not
+ * @param string $parameterName Sets the bridge context for the current form
+ * @param array $parameters The bridge parameters
+ * @return string The form body
+ */
+ private static function getForm(
+ $bridgeName,
+ $formats,
+ $isActive = false,
+ $isHttps = false,
+ $parameterName = '',
+ $parameters = []
+ ) {
+ $form = self::getFormHeader($bridgeName, $isHttps, $parameterName);
+
+ if (count($parameters) > 0) {
+ $form .= '<div class="parameters">';
+
+ foreach ($parameters as $id => $inputEntry) {
+ if (!isset($inputEntry['exampleValue'])) {
+ $inputEntry['exampleValue'] = '';
+ }
+
+ if (!isset($inputEntry['defaultValue'])) {
+ $inputEntry['defaultValue'] = '';
+ }
+
+ $idArg = 'arg-'
+ . urlencode($bridgeName)
+ . '-'
+ . urlencode($parameterName)
+ . '-'
+ . urlencode($id);
+
+ $form .= '<label for="'
+ . $idArg
+ . '">'
+ . filter_var($inputEntry['name'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
+ . '</label>'
+ . PHP_EOL;
+
+ if (!isset($inputEntry['type']) || $inputEntry['type'] === 'text') {
+ $form .= self::getTextInput($inputEntry, $idArg, $id);
+ } elseif ($inputEntry['type'] === 'number') {
+ $form .= self::getNumberInput($inputEntry, $idArg, $id);
+ } elseif ($inputEntry['type'] === 'list') {
+ $form .= self::getListInput($inputEntry, $idArg, $id);
+ } elseif ($inputEntry['type'] === 'checkbox') {
+ $form .= self::getCheckboxInput($inputEntry, $idArg, $id);
+ }
+
+ if (isset($inputEntry['title'])) {
+ $title_filtered = filter_var($inputEntry['title'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
+ $form .= '<i class="info" title="' . $title_filtered . '">i</i>';
+ } else {
+ $form .= '<i class="no-info"></i>';
+ }
+ }
+
+ $form .= '</div>';
+ }
+
+ if ($isActive) {
+ $form .= '<button type="submit" name="format" formtarget="_blank" value="Html">Generate feed</button>';
+ } else {
+ $form .= '<span style="font-weight: bold;">Inactive</span>';
+ }
+
+ return $form . '</form>' . PHP_EOL;
+ }
+
+ /**
+ * Get input field attributes
+ *
+ * @param array $entry The current entry
+ * @return string The input field attributes
+ */
+ private static function getInputAttributes($entry)
+ {
+ $retVal = '';
+
+ if (isset($entry['required']) && $entry['required'] === true) {
+ $retVal .= ' required';
+ }
+
+ if (isset($entry['pattern'])) {
+ $retVal .= ' pattern="' . $entry['pattern'] . '"';
+ }
+
+ return $retVal;
+ }
+
+ /**
+ * Get text input
+ *
+ * @param array $entry The current entry
+ * @param string $id The field ID
+ * @param string $name The field name
+ * @return string The text input field
+ */
+ private static function getTextInput($entry, $id, $name)
+ {
+ return '<input '
+ . self::getInputAttributes($entry)
+ . ' id="'
+ . $id
+ . '" type="text" value="'
+ . filter_var($entry['defaultValue'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
+ . '" placeholder="'
+ . filter_var($entry['exampleValue'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
+ . '" name="'
+ . $name
+ . '" />'
+ . PHP_EOL;
+ }
+
+ /**
+ * Get number input
+ *
+ * @param array $entry The current entry
+ * @param string $id The field ID
+ * @param string $name The field name
+ * @return string The number input field
+ */
+ private static function getNumberInput($entry, $id, $name)
+ {
+ return '<input '
+ . self::getInputAttributes($entry)
+ . ' id="'
+ . $id
+ . '" type="number" value="'
+ . filter_var($entry['defaultValue'], FILTER_SANITIZE_NUMBER_INT)
+ . '" placeholder="'
+ . filter_var($entry['exampleValue'], FILTER_SANITIZE_NUMBER_INT)
+ . '" name="'
+ . $name
+ . '" />'
+ . PHP_EOL;
+ }
+
+ /**
+ * Get list input
+ *
+ * @param array $entry The current entry
+ * @param string $id The field ID
+ * @param string $name The field name
+ * @return string The list input field
+ */
+ private static function getListInput($entry, $id, $name)
+ {
+ if (isset($entry['required']) && $entry['required'] === true) {
+ Debug::log('The "required" attribute is not supported for lists.');
+ unset($entry['required']);
+ }
+
+ $list = '<select '
+ . self::getInputAttributes($entry)
+ . ' id="'
+ . $id
+ . '" name="'
+ . $name
+ . '" >';
+
+ foreach ($entry['values'] as $name => $value) {
+ if (is_array($value)) {
+ $list .= '<optgroup label="' . htmlentities($name) . '">';
+ foreach ($value as $subname => $subvalue) {
+ if (
+ $entry['defaultValue'] === $subname
+ || $entry['defaultValue'] === $subvalue
+ ) {
+ $list .= '<option value="'
+ . $subvalue
+ . '" selected>'
+ . $subname
+ . '</option>';
+ } else {
+ $list .= '<option value="'
+ . $subvalue
+ . '">'
+ . $subname
+ . '</option>';
+ }
+ }
+ $list .= '</optgroup>';
+ } else {
+ if (
+ $entry['defaultValue'] === $name
+ || $entry['defaultValue'] === $value
+ ) {
+ $list .= '<option value="'
+ . $value
+ . '" selected>'
+ . $name
+ . '</option>';
+ } else {
+ $list .= '<option value="'
+ . $value
+ . '">'
+ . $name
+ . '</option>';
+ }
+ }
+ }
+
+ $list .= '</select>';
+
+ return $list;
+ }
+
+ /**
+ * Get checkbox input
+ *
+ * @param array $entry The current entry
+ * @param string $id The field ID
+ * @param string $name The field name
+ * @return string The checkbox input field
+ */
+ private static function getCheckboxInput($entry, $id, $name)
+ {
+ if (isset($entry['required']) && $entry['required'] === true) {
+ Debug::log('The "required" attribute is not supported for checkboxes.');
+ unset($entry['required']);
+ }
+
+ return '<input '
+ . self::getInputAttributes($entry)
+ . ' id="'
+ . $id
+ . '" type="checkbox" name="'
+ . $name
+ . '" '
+ . ($entry['defaultValue'] === 'checked' ? 'checked' : '')
+ . ' />'
+ . PHP_EOL;
+ }
+
+ /**
+ * Gets a single bridge card
+ *
+ * @param string $bridgeName The bridge name
+ * @param array $formats A list of formats
+ * @param bool $isActive Indicates if the bridge is active or not
+ * @return string The bridge card
+ */
+ public static function displayBridgeCard($bridgeName, $formats, $isActive = true)
+ {
+ $bridgeFac = new \BridgeFactory();
+
+ $bridge = $bridgeFac->create($bridgeName);
+
+ if ($bridge == false) {
+ return '';
+ }
+
+ $isHttps = strpos($bridge->getURI(), 'https') === 0;
+
+ $uri = $bridge->getURI();
+ $name = $bridge->getName();
+ $icon = $bridge->getIcon();
+ $description = $bridge->getDescription();
+ $parameters = $bridge->getParameters();
+ $donationUri = $bridge->getDonationURI();
+ $maintainer = $bridge->getMaintainer();
+
+ $donationsAllowed = Configuration::getConfig('admin', 'donations');
+
+ if (defined('PROXY_URL') && PROXY_BYBRIDGE) {
+ $parameters['global']['_noproxy'] = [
+ 'name' => 'Disable proxy (' . ((defined('PROXY_NAME') && PROXY_NAME) ? PROXY_NAME : PROXY_URL) . ')',
+ 'type' => 'checkbox'
+ ];
+ }
+
+ if (CUSTOM_CACHE_TIMEOUT) {
+ $parameters['global']['_cache_timeout'] = [
+ 'name' => 'Cache timeout in seconds',
+ 'type' => 'number',
+ 'defaultValue' => $bridge->getCacheTimeout()
+ ];
+ }
+
+ $card = <<<CARD
<section id="bridge-{$bridgeName}" data-ref="{$name}">
<h2><a href="{$uri}">{$name}</a></h2>
<p class="description">{$description}</p>
@@ -330,38 +347,39 @@ This bridge is not fetching its content through a secure connection</div>';
<label class="showmore" for="showmore-{$bridgeName}">Show more</label>
CARD;
- // If we don't have any parameter for the bridge, we print a generic form to load it.
- if (count($parameters) === 0) {
- $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps);
-
- // Display form with cache timeout and/or noproxy options (if enabled) when bridge has no parameters
- } else if (count($parameters) === 1 && array_key_exists('global', $parameters)) {
- $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps, '', $parameters['global']);
- } else {
-
- foreach($parameters as $parameterName => $parameter) {
- if(!is_numeric($parameterName) && $parameterName === 'global')
- continue;
-
- if(array_key_exists('global', $parameters))
- $parameter = array_merge($parameter, $parameters['global']);
-
- if(!is_numeric($parameterName))
- $card .= '<h5>' . $parameterName . '</h5>' . PHP_EOL;
-
- $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps, $parameterName, $parameter);
- }
-
- }
-
- $card .= '<label class="showless" for="showmore-' . $bridgeName . '">Show less</label>';
- if($donationUri !== '' && $donationsAllowed) {
- $card .= '<p class="maintainer">' . $maintainer . ' ~ <a href="' . $donationUri . '">Donate</a></p>';
- } else {
- $card .= '<p class="maintainer">' . $maintainer . '</p>';
- }
- $card .= '</section>';
-
- return $card;
- }
+ // If we don't have any parameter for the bridge, we print a generic form to load it.
+ if (count($parameters) === 0) {
+ $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps);
+
+ // Display form with cache timeout and/or noproxy options (if enabled) when bridge has no parameters
+ } elseif (count($parameters) === 1 && array_key_exists('global', $parameters)) {
+ $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps, '', $parameters['global']);
+ } else {
+ foreach ($parameters as $parameterName => $parameter) {
+ if (!is_numeric($parameterName) && $parameterName === 'global') {
+ continue;
+ }
+
+ if (array_key_exists('global', $parameters)) {
+ $parameter = array_merge($parameter, $parameters['global']);
+ }
+
+ if (!is_numeric($parameterName)) {
+ $card .= '<h5>' . $parameterName . '</h5>' . PHP_EOL;
+ }
+
+ $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps, $parameterName, $parameter);
+ }
+ }
+
+ $card .= '<label class="showless" for="showmore-' . $bridgeName . '">Show less</label>';
+ if ($donationUri !== '' && $donationsAllowed) {
+ $card .= '<p class="maintainer">' . $maintainer . ' ~ <a href="' . $donationUri . '">Donate</a></p>';
+ } else {
+ $card .= '<p class="maintainer">' . $maintainer . '</p>';
+ }
+ $card .= '</section>';
+
+ return $card;
+ }
}
diff --git a/lib/BridgeFactory.php b/lib/BridgeFactory.php
index f435261c..3e355b7a 100644
--- a/lib/BridgeFactory.php
+++ b/lib/BridgeFactory.php
@@ -1,87 +1,87 @@
<?php
-final class BridgeFactory {
+final class BridgeFactory
+{
+ private $folder;
+ private $bridgeNames = [];
+ private $whitelist = [];
- private $folder;
- private $bridgeNames = [];
- private $whitelist = [];
+ public function __construct(string $folder = PATH_LIB_BRIDGES)
+ {
+ $this->folder = $folder;
- public function __construct(string $folder = PATH_LIB_BRIDGES)
- {
- $this->folder = $folder;
+ // create names
+ foreach (scandir($this->folder) as $file) {
+ if (preg_match('/^([^.]+)Bridge\.php$/U', $file, $m)) {
+ $this->bridgeNames[] = $m[1];
+ }
+ }
- // create names
- foreach(scandir($this->folder) as $file) {
- if(preg_match('/^([^.]+)Bridge\.php$/U', $file, $m)) {
- $this->bridgeNames[] = $m[1];
- }
- }
+ // create whitelist
+ if (file_exists(WHITELIST)) {
+ $contents = trim(file_get_contents(WHITELIST));
+ } elseif (file_exists(WHITELIST_DEFAULT)) {
+ $contents = trim(file_get_contents(WHITELIST_DEFAULT));
+ } else {
+ $contents = '';
+ }
+ if ($contents === '*') { // Whitelist all bridges
+ $this->whitelist = $this->getBridgeNames();
+ } else {
+ foreach (explode("\n", $contents) as $bridgeName) {
+ $this->whitelist[] = $this->sanitizeBridgeName($bridgeName);
+ }
+ }
+ }
- // create whitelist
- if (file_exists(WHITELIST)) {
- $contents = trim(file_get_contents(WHITELIST));
- } elseif (file_exists(WHITELIST_DEFAULT)) {
- $contents = trim(file_get_contents(WHITELIST_DEFAULT));
- } else {
- $contents = '';
- }
- if ($contents === '*') { // Whitelist all bridges
- $this->whitelist = $this->getBridgeNames();
- } else {
- foreach (explode("\n", $contents) as $bridgeName) {
- $this->whitelist[] = $this->sanitizeBridgeName($bridgeName);
- }
- }
- }
+ public function create(string $name): BridgeInterface
+ {
+ if (preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name)) {
+ $className = sprintf('%sBridge', $this->sanitizeBridgeName($name));
+ return new $className();
+ }
+ throw new \InvalidArgumentException('Bridge name invalid!');
+ }
- public function create(string $name): BridgeInterface
- {
- if(preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name)) {
- $className = sprintf('%sBridge', $this->sanitizeBridgeName($name));
- return new $className();
- }
- throw new \InvalidArgumentException('Bridge name invalid!');
- }
+ public function getBridgeNames(): array
+ {
+ return $this->bridgeNames;
+ }
- public function getBridgeNames(): array
- {
- return $this->bridgeNames;
- }
+ public function isWhitelisted($name): bool
+ {
+ return in_array($this->sanitizeBridgeName($name), $this->whitelist);
+ }
- public function isWhitelisted($name): bool
- {
- return in_array($this->sanitizeBridgeName($name), $this->whitelist);
- }
+ private function sanitizeBridgeName($name)
+ {
+ if (!is_string($name)) {
+ return null;
+ }
- private function sanitizeBridgeName($name) {
+ // Trim trailing '.php' if exists
+ if (preg_match('/(.+)(?:\.php)/', $name, $matches)) {
+ $name = $matches[1];
+ }
- if(!is_string($name)) {
- return null;
- }
+ // Trim trailing 'Bridge' if exists
+ if (preg_match('/(.+)(?:Bridge)/i', $name, $matches)) {
+ $name = $matches[1];
+ }
- // Trim trailing '.php' if exists
- if (preg_match('/(.+)(?:\.php)/', $name, $matches)) {
- $name = $matches[1];
- }
+ // Improve performance for correctly written bridge names
+ if (in_array($name, $this->getBridgeNames())) {
+ $index = array_search($name, $this->getBridgeNames());
+ return $this->getBridgeNames()[$index];
+ }
- // Trim trailing 'Bridge' if exists
- if (preg_match('/(.+)(?:Bridge)/i', $name, $matches)) {
- $name = $matches[1];
- }
+ // The name is valid if a corresponding bridge file is found on disk
+ if (in_array(strtolower($name), array_map('strtolower', $this->getBridgeNames()))) {
+ $index = array_search(strtolower($name), array_map('strtolower', $this->getBridgeNames()));
+ return $this->getBridgeNames()[$index];
+ }
- // Improve performance for correctly written bridge names
- if (in_array($name, $this->getBridgeNames())) {
- $index = array_search($name, $this->getBridgeNames());
- return $this->getBridgeNames()[$index];
- }
-
- // The name is valid if a corresponding bridge file is found on disk
- if (in_array(strtolower($name), array_map('strtolower', $this->getBridgeNames()))) {
- $index = array_search(strtolower($name), array_map('strtolower', $this->getBridgeNames()));
- return $this->getBridgeNames()[$index];
- }
-
- Debug::log('Invalid bridge name specified: "' . $name . '"!');
- return null;
- }
+ Debug::log('Invalid bridge name specified: "' . $name . '"!');
+ return null;
+ }
}
diff --git a/lib/BridgeInterface.php b/lib/BridgeInterface.php
index 70625125..6cf949c8 100644
--- a/lib/BridgeInterface.php
+++ b/lib/BridgeInterface.php
@@ -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
*/
/**
@@ -52,93 +53,94 @@
* * **Cache timeout**
* The default cache timeout for the bridge.
*/
-interface BridgeInterface {
- /**
- * Collects data from the site
- */
- public function collectData();
+interface BridgeInterface
+{
+ /**
+ * Collects data from the site
+ */
+ public function collectData();
- /**
- * Get the user's supplied configuration for the bridge
- */
- public function getConfiguration();
+ /**
+ * Get the user's supplied configuration for the bridge
+ */
+ public function getConfiguration();
- /**
- * Returns the value for the selected configuration
- *
- * @param string $input The option name
- * @return mixed|null The option value or null if the input is not defined
- */
- public function getOption($name);
+ /**
+ * Returns the value for the selected configuration
+ *
+ * @param string $input The option name
+ * @return mixed|null The option value or null if the input is not defined
+ */
+ public function getOption($name);
- /**
- * Returns the description
- *
- * @return string Description
- */
- public function getDescription();
+ /**
+ * Returns the description
+ *
+ * @return string Description
+ */
+ public function getDescription();
- /**
- * Returns an array of collected items
- *
- * @return array Associative array of items
- */
- public function getItems();
+ /**
+ * Returns an array of collected items
+ *
+ * @return array Associative array of items
+ */
+ public function getItems();
- /**
- * Returns the bridge maintainer
- *
- * @return string Bridge maintainer
- */
- public function getMaintainer();
+ /**
+ * Returns the bridge maintainer
+ *
+ * @return string Bridge maintainer
+ */
+ public function getMaintainer();
- /**
- * Returns the bridge name
- *
- * @return string Bridge name
- */
- public function getName();
+ /**
+ * Returns the bridge name
+ *
+ * @return string Bridge name
+ */
+ public function getName();
- /**
- * Returns the bridge icon
- *
- * @return string Bridge icon
- */
- public function getIcon();
+ /**
+ * Returns the bridge icon
+ *
+ * @return string Bridge icon
+ */
+ public function getIcon();
- /**
- * Returns the bridge parameters
- *
- * @return array Bridge parameters
- */
- public function getParameters();
+ /**
+ * Returns the bridge parameters
+ *
+ * @return array Bridge parameters
+ */
+ public function getParameters();
- /**
- * Returns the bridge URI
- *
- * @return string Bridge URI
- */
- public function getURI();
+ /**
+ * Returns the bridge URI
+ *
+ * @return string Bridge URI
+ */
+ public function getURI();
- /**
- * Returns the bridge Donation URI
- *
- * @return string Bridge Donation URI
- */
- public function getDonationURI();
+ /**
+ * Returns the bridge Donation URI
+ *
+ * @return string Bridge Donation URI
+ */
+ public function getDonationURI();
- /**
- * Returns the cache timeout
- *
- * @return int Cache timeout
- */
- public function getCacheTimeout();
+ /**
+ * Returns the cache timeout
+ *
+ * @return int Cache timeout
+ */
+ public function getCacheTimeout();
- /**
- * Returns parameters from given URL or null if URL is not applicable
- *
- * @param string $url URL to extract parameters from
- * @return array|null List of bridge parameters or null if detection failed.
- */
- public function detectParameters($url);
+ /**
+ * Returns parameters from given URL or null if URL is not applicable
+ *
+ * @param string $url URL to extract parameters from
+ * @return array|null List of bridge parameters or null if detection failed.
+ */
+ public function detectParameters($url);
}
diff --git a/lib/BridgeList.php b/lib/BridgeList.php
index c5082e57..921dfe50 100644
--- a/lib/BridgeList.php
+++ b/lib/BridgeList.php
@@ -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
*/
/**
@@ -19,14 +20,16 @@
*
* @todo Return error if a caller creates an object of this class.
*/
-final class BridgeList {
- /**
- * Get the document head
- *
- * @return string The document head
- */
- private static function getHead() {
- return <<<EOD
+final class BridgeList
+{
+ /**
+ * Get the document head
+ *
+ * @return string The document head
+ */
+ private static function getHead()
+ {
+ return <<<EOD
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -45,91 +48,87 @@ final class BridgeList {
</noscript>
</head>
EOD;
- }
-
- /**
- * Get the document body for all bridge cards
- *
- * @param bool $showInactive Inactive bridges are visible on the home page if
- * enabled.
- * @param int $totalBridges (ref) Returns the total number of bridges.
- * @param int $totalActiveBridges (ref) Returns the number of active bridges.
- * @return string The document body for all bridge cards.
- */
- private static function getBridges($showInactive, &$totalBridges, &$totalActiveBridges) {
-
- $body = '';
- $totalActiveBridges = 0;
- $inactiveBridges = '';
-
- $bridgeFac = new \BridgeFactory();
- $bridgeList = $bridgeFac->getBridgeNames();
-
- $formatFac = new FormatFactory();
- $formats = $formatFac->getFormatNames();
-
- $totalBridges = count($bridgeList);
-
- foreach($bridgeList as $bridgeName) {
-
- if($bridgeFac->isWhitelisted($bridgeName)) {
-
- $body .= BridgeCard::displayBridgeCard($bridgeName, $formats);
- $totalActiveBridges++;
-
- } elseif($showInactive) {
-
- // inactive bridges
- $inactiveBridges .= BridgeCard::displayBridgeCard($bridgeName, $formats, false) . PHP_EOL;
-
- }
-
- }
-
- $body .= $inactiveBridges;
-
- return $body;
- }
-
- /**
- * Get the document header
- *
- * @return string The document header
- */
- private static function getHeader() {
- $warning = '';
-
- if(Debug::isEnabled()) {
- if(!Debug::isSecure()) {
- $warning .= <<<EOD
+ }
+
+ /**
+ * Get the document body for all bridge cards
+ *
+ * @param bool $showInactive Inactive bridges are visible on the home page if
+ * enabled.
+ * @param int $totalBridges (ref) Returns the total number of bridges.
+ * @param int $totalActiveBridges (ref) Returns the number of active bridges.
+ * @return string The document body for all bridge cards.
+ */
+ private static function getBridges($showInactive, &$totalBridges, &$totalActiveBridges)
+ {
+ $body = '';
+ $totalActiveBridges = 0;
+ $inactiveBridges = '';
+
+ $bridgeFac = new \BridgeFactory();
+ $bridgeList = $bridgeFac->getBridgeNames();
+
+ $formatFac = new FormatFactory();
+ $formats = $formatFac->getFormatNames();
+
+ $totalBridges = count($bridgeList);
+
+ foreach ($bridgeList as $bridgeName) {
+ if ($bridgeFac->isWhitelisted($bridgeName)) {
+ $body .= BridgeCard::displayBridgeCard($bridgeName, $formats);
+ $totalActiveBridges++;
+ } elseif ($showInactive) {
+ // inactive bridges
+ $inactiveBridges .= BridgeCard::displayBridgeCard($bridgeName, $formats, false) . PHP_EOL;
+ }
+ }
+
+ $body .= $inactiveBridges;
+
+ return $body;
+ }
+
+ /**
+ * Get the document header
+ *
+ * @return string The document header
+ */
+ private static function getHeader()
+ {
+ $warning = '';
+
+ if (Debug::isEnabled()) {
+ if (!Debug::isSecure()) {
+ $warning .= <<<EOD
<section class="critical-warning">Warning : Debug mode is active from any location,
make sure only you can access RSS-Bridge.</section>
EOD;
- } else {
- $warning .= <<<EOD
+ } else {
+ $warning .= <<<EOD
<section class="warning">Warning : Debug mode is active from your IP address,
your requests will bypass the cache.</section>
EOD;
- }
- }
+ }
+ }
- return <<<EOD
+ return <<<EOD
<header>
<div class="logo"></div>
{$warning}
</header>
EOD;
- }
-
- /**
- * Get the searchbar
- *
- * @return string The searchbar
- */
- private static function getSearchbar() {
- $query = filter_input(INPUT_GET, 'q', FILTER_SANITIZE_SPECIAL_CHARS);
-
- return <<<EOD
+ }
+
+ /**
+ * Get the searchbar
+ *
+ * @return string The searchbar
+ */
+ private static function getSearchbar()
+ {
+ $query = filter_input(INPUT_GET, 'q', FILTER_SANITIZE_SPECIAL_CHARS);
+
+ return <<<EOD
<section class="searchbar">
<h3>Search</h3>
<input type="text" name="searchfield"
@@ -137,46 +136,45 @@ EOD;
onchange="search()" onkeyup="search()" value="{$query}">
</section>
EOD;
- }
-
- /**
- * Get the document footer
- *
- * @param int $totalBridges The total number of bridges, shown in the footer
- * @param int $totalActiveBridges The total number of active bridges, shown
- * in the footer.
- * @param bool $showInactive Sets the 'Show active'/'Show inactive' text in
- * the footer.
- * @return string The document footer
- */
- private static function getFooter($totalBridges, $totalActiveBridges, $showInactive) {
- $version = Configuration::getVersion();
-
- $email = Configuration::getConfig('admin', 'email');
- $admininfo = '';
- if (!empty($email)) {
- $admininfo = <<<EOD
+ }
+
+ /**
+ * Get the document footer
+ *
+ * @param int $totalBridges The total number of bridges, shown in the footer
+ * @param int $totalActiveBridges The total number of active bridges, shown
+ * in the footer.
+ * @param bool $showInactive Sets the 'Show active'/'Show inactive' text in
+ * the footer.
+ * @return string The document footer
+ */
+ private static function getFooter($totalBridges, $totalActiveBridges, $showInactive)
+ {
+ $version = Configuration::getVersion();
+
+ $email = Configuration::getConfig('admin', 'email');
+ $admininfo = '';
+ if (!empty($email)) {
+ $admininfo = <<<EOD
<br />
<span>
You may email the administrator of this RSS-Bridge instance
at <a href="mailto:{$email}">{$email}</a>
</span>
EOD;
- }
+ }
- $inactive = '';
-
- if($totalActiveBridges !== $totalBridges) {
-
- if(!$showInactive) {
- $inactive = '<a href="?show_inactive=1"><button class="small">Show inactive bridges</button></a><br>';
- } else {
- $inactive = '<a href="?show_inactive=0"><button class="small">Hide inactive bridges</button></a><br>';
- }
+ $inactive = '';
- }
+ if ($totalActiveBridges !== $totalBridges) {
+ if (!$showInactive) {
+ $inactive = '<a href="?show_inactive=1"><button class="small">Show inactive bridges</button></a><br>';
+ } else {
+ $inactive = '<a href="?show_inactive=0"><button class="small">Hide inactive bridges</button></a><br>';
+ }
+ }
- return <<<EOD
+ return <<<EOD
<section class="footer">
<a href="https://github.com/rss-bridge/rss-bridge">RSS-Bridge ~ Public Domain</a><br>
<p class="version">{$version}</p>
@@ -185,28 +183,27 @@ EOD;
{$admininfo}
</section>
EOD;
- }
-
- /**
- * Create the entire home page
- *
- * @param bool $showInactive Inactive bridges are displayed on the home page,
- * if enabled.
- * @return string The home page
- */
- public static function create($showInactive = true) {
-
- $totalBridges = 0;
- $totalActiveBridges = 0;
-
- return '<!DOCTYPE html><html lang="en">'
- . BridgeList::getHead()
- . '<body onload="search()">'
- . BridgeList::getHeader()
- . BridgeList::getSearchbar()
- . BridgeList::getBridges($showInactive, $totalBridges, $totalActiveBridges)
- . BridgeList::getFooter($totalBridges, $totalActiveBridges, $showInactive)
- . '</body></html>';
-
- }
+ }
+
+ /**
+ * Create the entire home page
+ *
+ * @param bool $showInactive Inactive bridges are displayed on the home page,
+ * if enabled.
+ * @return string The home page
+ */
+ public static function create($showInactive = true)
+ {
+ $totalBridges = 0;
+ $totalActiveBridges = 0;
+
+ return '<!DOCTYPE html><html lang="en">'
+ . BridgeList::getHead()
+ . '<body onload="search()">'
+ . BridgeList::getHeader()
+ . BridgeList::getSearchbar()
+ . BridgeList::getBridges($showInactive, $totalBridges, $totalActiveBridges)
+ . BridgeList::getFooter($totalBridges, $totalActiveBridges, $showInactive)
+ . '</body></html>';
+ }
}
diff --git a/lib/CacheFactory.php b/lib/CacheFactory.php
index 451f625f..ba1c3cb9 100644
--- a/lib/CacheFactory.php
+++ b/lib/CacheFactory.php
@@ -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,62 +7,62 @@
* 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 CacheFactory
{
- private $folder;
- private $cacheNames;
+ private $folder;
+ private $cacheNames;
- public function __construct(string $folder = PATH_LIB_CACHES)
- {
- $this->folder = $folder;
- // create cache names
- foreach(scandir($this->folder) as $file) {
- if(preg_match('/^([^.]+)Cache\.php$/U', $file, $m)) {
- $this->cacheNames[] = $m[1];
- }
- }
- }
+ public function __construct(string $folder = PATH_LIB_CACHES)
+ {
+ $this->folder = $folder;
+ // create cache names
+ foreach (scandir($this->folder) as $file) {
+ if (preg_match('/^([^.]+)Cache\.php$/U', $file, $m)) {
+ $this->cacheNames[] = $m[1];
+ }
+ }
+ }
- /**
- * @param string $name The name of the cache e.g. "File", "Memcached" or "SQLite"
- */
- public function create(string $name): CacheInterface
- {
- $name = $this->sanitizeCacheName($name) . 'Cache';
+ /**
+ * @param string $name The name of the cache e.g. "File", "Memcached" or "SQLite"
+ */
+ public function create(string $name): CacheInterface
+ {
+ $name = $this->sanitizeCacheName($name) . 'Cache';
- if(! preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name)) {
- throw new \InvalidArgumentException('Cache name invalid!');
- }
+ if (! preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name)) {
+ throw new \InvalidArgumentException('Cache name invalid!');
+ }
- $filePath = $this->folder . $name . '.php';
- if(!file_exists($filePath)) {
- throw new \Exception('Invalid cache');
- }
- $className = '\\' . $name;
- return new $className();
- }
+ $filePath = $this->folder . $name . '.php';
+ if (!file_exists($filePath)) {
+ throw new \Exception('Invalid cache');
+ }
+ $className = '\\' . $name;
+ return new $className();
+ }
- protected function sanitizeCacheName(string $name)
- {
- // Trim trailing '.php' if exists
- if (preg_match('/(.+)(?:\.php)/', $name, $matches)) {
- $name = $matches[1];
- }
+ protected function sanitizeCacheName(string $name)
+ {
+ // Trim trailing '.php' if exists
+ if (preg_match('/(.+)(?:\.php)/', $name, $matches)) {
+ $name = $matches[1];
+ }
- // Trim trailing 'Cache' if exists
- if (preg_match('/(.+)(?:Cache)$/i', $name, $matches)) {
- $name = $matches[1];
- }
+ // Trim trailing 'Cache' if exists
+ if (preg_match('/(.+)(?:Cache)$/i', $name, $matches)) {
+ $name = $matches[1];
+ }
- if(in_array(strtolower($name), array_map('strtolower', $this->cacheNames))) {
- $index = array_search(strtolower($name), array_map('strtolower', $this->cacheNames));
- return $this->cacheNames[$index];
- }
- return null;
- }
+ if (in_array(strtolower($name), array_map('strtolower', $this->cacheNames))) {
+ $index = array_search(strtolower($name), array_map('strtolower', $this->cacheNames));
+ return $this->cacheNames[$index];
+ }
+ return null;
+ }
}
diff --git a/lib/CacheInterface.php b/lib/CacheInterface.php
index 091c5f02..67cee681 100644
--- a/lib/CacheInterface.php
+++ b/lib/CacheInterface.php
@@ -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,61 +7,62 @@
* 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
*/
/**
* The cache interface
*/
-interface CacheInterface {
- /**
- * Set scope of the current cache
- *
- * If $scope is an empty string, the cache is set to a global context.
- *
- * @param string $scope The scope the data is related to
- */
- public function setScope($scope);
+interface CacheInterface
+{
+ /**
+ * Set scope of the current cache
+ *
+ * If $scope is an empty string, the cache is set to a global context.
+ *
+ * @param string $scope The scope the data is related to
+ */
+ public function setScope($scope);
- /**
- * Set key to assign the current data
- *
- * Since $key can be anything, the cache implementation must ensure to
- * assign the related data reliably; most commonly by serializing and
- * hashing the key in an appropriate way.
- *
- * @param array $key The key the data is related to
- */
- public function setKey($key);
+ /**
+ * Set key to assign the current data
+ *
+ * Since $key can be anything, the cache implementation must ensure to
+ * assign the related data reliably; most commonly by serializing and
+ * hashing the key in an appropriate way.
+ *
+ * @param array $key The key the data is related to
+ */
+ public function setKey($key);
- /**
- * Loads data from cache
- *
- * @return mixed The cached data or null
- */
- public function loadData();
+ /**
+ * Loads data from cache
+ *
+ * @return mixed The cached data or null
+ */
+ public function loadData();
- /**
- * Stores data to the cache
- *
- * @param mixed $data The data to store
- * @return self The cache object
- */
- public function saveData($data);
+ /**
+ * Stores data to the cache
+ *
+ * @param mixed $data The data to store
+ * @return self The cache object
+ */
+ public function saveData($data);
- /**
- * Returns the timestamp for the curent cache data
- *
- * @return int Timestamp or null
- */
- public function getTime();
+ /**
+ * Returns the timestamp for the curent cache data
+ *
+ * @return int Timestamp or null
+ */
+ public function getTime();
- /**
- * Removes any data that is older than the specified age from cache
- *
- * @param int $seconds The cache age in seconds
- */
- public function purgeCache($seconds);
+ /**
+ * Removes any data that is older than the specified age from cache
+ *
+ * @param int $seconds The cache age in seconds
+ */
+ public function purgeCache($seconds);
}
diff --git a/lib/Configuration.php b/lib/Configuration.php
index e97e7d6b..ce01b7df 100644
--- a/lib/Configuration.php
+++ b/lib/Configuration.php
@@ -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
*/
/**
@@ -16,294 +17,317 @@
*
* This class implements a configuration module for RSS-Bridge.
*/
-final class Configuration {
-
- /**
- * Holds the current release version of RSS-Bridge.
- *
- * Do not access this property directly!
- * Use {@see Configuration::getVersion()} instead.
- *
- * @var string
- *
- * @todo Replace this property by a constant.
- */
- public static $VERSION = 'dev.2022-06-14';
-
- /**
- * Holds the configuration data.
- *
- * Do not access this property directly!
- * Use {@see Configuration::getConfig()} instead.
- *
- * @var array|null
- */
- private static $config = null;
-
- /**
- * Throw an exception when trying to create a new instance of this class.
- *
- * @throws \LogicException if called.
- */
- public function __construct(){
- throw new \LogicException('Can\'t create object of this class!');
- }
-
- /**
- * Verifies the current installation of RSS-Bridge and PHP.
- *
- * Returns an error message and aborts execution if the installation does
- * not satisfy the requirements of RSS-Bridge.
- *
- * **Requirements**
- * - PHP 7.1.0 or higher
- * - `openssl` extension
- * - `libxml` extension
- * - `mbstring` extension
- * - `simplexml` extension
- * - `curl` extension
- * - `json` extension
- * - The cache folder specified by {@see PATH_CACHE} requires write permission
- * - The whitelist file specified by {@see WHITELIST} requires write permission
- *
- * @link http://php.net/supported-versions.php PHP Supported Versions
- * @link http://php.net/manual/en/book.openssl.php OpenSSL
- * @link http://php.net/manual/en/book.libxml.php libxml
- * @link http://php.net/manual/en/book.mbstring.php Multibyte String (mbstring)
- * @link http://php.net/manual/en/book.simplexml.php SimpleXML
- * @link http://php.net/manual/en/book.curl.php Client URL Library (curl)
- * @link http://php.net/manual/en/book.json.php JavaScript Object Notation (json)
- *
- * @return void
- */
- public static function verifyInstallation() {
-
- // Check PHP version
- if(version_compare(PHP_VERSION, '7.4.0') === -1) {
- self::reportError('RSS-Bridge requires at least PHP version 7.4.0!');
- }
- // extensions check
- if(!extension_loaded('openssl'))
- self::reportError('"openssl" extension not loaded. Please check "php.ini"');
-
- if(!extension_loaded('libxml'))
- self::reportError('"libxml" extension not loaded. Please check "php.ini"');
-
- if(!extension_loaded('mbstring'))
- self::reportError('"mbstring" extension not loaded. Please check "php.ini"');
-
- if(!extension_loaded('simplexml'))
- self::reportError('"simplexml" extension not loaded. Please check "php.ini"');
-
- // Allow RSS-Bridge to run without curl module in CLI mode without root certificates
- if(!extension_loaded('curl') && !(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo'))))
- self::reportError('"curl" extension not loaded. Please check "php.ini"');
-
- if(!extension_loaded('json'))
- self::reportError('"json" extension not loaded. Please check "php.ini"');
-
- }
-
- /**
- * Loads the configuration from disk and checks if the parameters are valid.
- *
- * Returns an error message and aborts execution if the configuration is invalid.
- *
- * The RSS-Bridge configuration is split into two files:
- * - {@see FILE_CONFIG_DEFAULT} The default configuration file that ships
- * with every release of RSS-Bridge (do not modify this file!).
- * - {@see FILE_CONFIG} The local configuration file that can be modified
- * by server administrators.
- *
- * RSS-Bridge will first load {@see FILE_CONFIG_DEFAULT} into memory and then
- * replace parameters with the contents of {@see FILE_CONFIG}. That way new
- * parameters are automatically initialized with default values and custom
- * configurations can be reduced to the minimum set of parametes necessary
- * (only the ones that changed).
- *
- * The configuration files must be placed in the root folder of RSS-Bridge
- * (next to `index.php`).
- *
- * _Notice_: The configuration is stored in {@see Configuration::$config}.
- *
- * @return void
- */
- public static function loadConfiguration() {
-
- if(!file_exists(FILE_CONFIG_DEFAULT))
- self::reportError('The default configuration file is missing at ' . FILE_CONFIG_DEFAULT);
-
- Configuration::$config = parse_ini_file(FILE_CONFIG_DEFAULT, true, INI_SCANNER_TYPED);
- if(!Configuration::$config)
- self::reportError('Error parsing ' . FILE_CONFIG_DEFAULT);
-
- if(file_exists(FILE_CONFIG)) {
- // Replace default configuration with custom settings
- foreach(parse_ini_file(FILE_CONFIG, true, INI_SCANNER_TYPED) as $header => $section) {
- foreach($section as $key => $value) {
- Configuration::$config[$header][$key] = $value;
- }
- }
- }
-
- foreach (getenv() as $envkey => $value) {
- // Replace all settings with their respective environment variable if available
- $keyArray = explode('_', $envkey);
- if($keyArray[0] === 'RSSBRIDGE') {
- $header = strtolower($keyArray[1]);
- $key = strtolower($keyArray[2]);
- if($value === 'true' || $value === 'false') {
- $value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
- }
- Configuration::$config[$header][$key] = $value;
- }
- }
-
- if(!is_string(self::getConfig('system', 'timezone'))
- || !in_array(self::getConfig('system', 'timezone'), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
- self::reportConfigurationError('system', 'timezone');
-
- date_default_timezone_set(self::getConfig('system', 'timezone'));
-
- if(!is_string(self::getConfig('proxy', 'url')))
- self::reportConfigurationError('proxy', 'url', 'Is not a valid string');
-
- if(!empty(self::getConfig('proxy', 'url'))) {
- /** URL of the proxy server */
- define('PROXY_URL', self::getConfig('proxy', 'url'));
- }
-
- if(!is_bool(self::getConfig('proxy', 'by_bridge')))
- self::reportConfigurationError('proxy', 'by_bridge', 'Is not a valid Boolean');
-
- /** True if proxy usage can be enabled selectively for each bridge */
- define('PROXY_BYBRIDGE', self::getConfig('proxy', 'by_bridge'));
-
- if(!is_string(self::getConfig('proxy', 'name')))
- self::reportConfigurationError('proxy', 'name', 'Is not a valid string');
-
- /** Name of the proxy server */
- define('PROXY_NAME', self::getConfig('proxy', 'name'));
-
- if(!is_string(self::getConfig('cache', 'type')))
- self::reportConfigurationError('cache', 'type', 'Is not a valid string');
-
- if(!is_bool(self::getConfig('cache', 'custom_timeout')))
- self::reportConfigurationError('cache', 'custom_timeout', 'Is not a valid Boolean');
-
- /** True if the cache timeout can be specified by the user */
- define('CUSTOM_CACHE_TIMEOUT', self::getConfig('cache', 'custom_timeout'));
-
- if(!is_bool(self::getConfig('authentication', 'enable')))
- self::reportConfigurationError('authentication', 'enable', 'Is not a valid Boolean');
-
- if(!is_string(self::getConfig('authentication', 'username')))
- self::reportConfigurationError('authentication', 'username', 'Is not a valid string');
-
- if(!is_string(self::getConfig('authentication', 'password')))
- self::reportConfigurationError('authentication', 'password', 'Is not a valid string');
-
- if(!empty(self::getConfig('admin', 'email'))
- && !filter_var(self::getConfig('admin', 'email'), FILTER_VALIDATE_EMAIL))
- self::reportConfigurationError('admin', 'email', 'Is not a valid email address');
-
- if(!is_bool(self::getConfig('admin', 'donations')))
- self::reportConfigurationError('admin', 'donations', 'Is not a valid Boolean');
-
- if(!is_string(self::getConfig('error', 'output')))
- self::reportConfigurationError('error', 'output', 'Is not a valid String');
-
- if(!is_numeric(self::getConfig('error', 'report_limit'))
- || self::getConfig('error', 'report_limit') < 1)
- self::reportConfigurationError('admin', 'report_limit', 'Value is invalid');
-
- }
-
- /**
- * Returns the value of a parameter identified by section and key.
- *
- * @param string $section The section name.
- * @param string $key The property name (key).
- * @return mixed|null The parameter value.
- */
- public static function getConfig($section, $key) {
- if(array_key_exists($section, self::$config) && array_key_exists($key, self::$config[$section])) {
- return self::$config[$section][$key];
- }
-
- return null;
- }
-
- /**
- * Returns the current version string of RSS-Bridge.
- *
- * This function returns the contents of {@see Configuration::$VERSION} for
- * regular installations and the git branch name and commit id for instances
- * running in a git environment.
- *
- * @return string The version string.
- */
- public static function getVersion() {
-
- $headFile = PATH_ROOT . '.git/HEAD';
-
- // '@' is used to mute open_basedir warning
- if(@is_readable($headFile)) {
-
- $revisionHashFile = '.git/' . substr(file_get_contents($headFile), 5, -1);
- $parts = explode('/', $revisionHashFile);
-
- if(isset($parts[3])) {
- $branchName = $parts[3];
- if(file_exists($revisionHashFile)) {
- return 'git.' . $branchName . '.' . substr(file_get_contents($revisionHashFile), 0, 7);
- }
- }
- }
-
- return Configuration::$VERSION;
-
- }
-
- /**
- * Reports an configuration error for the specified section and key to the
- * user and ends execution
- *
- * @param string $section The section name
- * @param string $key The configuration key
- * @param string $message An optional message to the user
- *
- * @return void
- */
- private static function reportConfigurationError($section, $key, $message = '') {
-
- $report = "Parameter [{$section}] => \"{$key}\" is invalid!" . PHP_EOL;
-
- if(file_exists(FILE_CONFIG)) {
- $report .= 'Please check your configuration file at ' . FILE_CONFIG . PHP_EOL;
- } elseif(!file_exists(FILE_CONFIG_DEFAULT)) {
- $report .= 'The default configuration file is missing at ' . FILE_CONFIG_DEFAULT . PHP_EOL;
- } else {
- $report .= 'The default configuration file is broken.' . PHP_EOL
- . 'Restore the original file from ' . REPOSITORY . PHP_EOL;
- }
-
- $report .= $message;
- self::reportError($report);
-
- }
-
- /**
- * Reports an error message to the user and ends execution
- *
- * @param string $message The error message
- *
- * @return void
- */
- private static function reportError($message) {
-
- header('Content-Type: text/plain', true, 500);
- die('Configuration error' . PHP_EOL . $message);
-
- }
+final class Configuration
+{
+ /**
+ * Holds the current release version of RSS-Bridge.
+ *
+ * Do not access this property directly!
+ * Use {@see Configuration::getVersion()} instead.
+ *
+ * @var string
+ *
+ * @todo Replace this property by a constant.
+ */
+ public static $VERSION = 'dev.2022-06-14';
+
+ /**
+ * Holds the configuration data.
+ *
+ * Do not access this property directly!
+ * Use {@see Configuration::getConfig()} instead.
+ *
+ * @var array|null
+ */
+ private static $config = null;
+
+ /**
+ * Throw an exception when trying to create a new instance of this class.
+ *
+ * @throws \LogicException if called.
+ */
+ public function __construct()
+ {
+ throw new \LogicException('Can\'t create object of this class!');
+ }
+
+ /**
+ * Verifies the current installation of RSS-Bridge and PHP.
+ *
+ * Returns an error message and aborts execution if the installation does
+ * not satisfy the requirements of RSS-Bridge.
+ *
+ * **Requirements**
+ * - PHP 7.1.0 or higher
+ * - `openssl` extension
+ * - `libxml` extension
+ * - `mbstring` extension
+ * - `simplexml` extension
+ * - `curl` extension
+ * - `json` extension
+ * - The cache folder specified by {@see PATH_CACHE} requires write permission
+ * - The whitelist file specified by {@see WHITELIST} requires write permission
+ *
+ * @link http://php.net/supported-versions.php PHP Supported Versions
+ * @link http://php.net/manual/en/book.openssl.php OpenSSL
+ * @link http://php.net/manual/en/book.libxml.php libxml
+ * @link http://php.net/manual/en/book.mbstring.php Multibyte String (mbstring)
+ * @link http://php.net/manual/en/book.simplexml.php SimpleXML
+ * @link http://php.net/manual/en/book.curl.php Client URL Library (curl)
+ * @link http://php.net/manual/en/book.json.php JavaScript Object Notation (json)
+ *
+ * @return void
+ */
+ public static function verifyInstallation()
+ {
+ // Check PHP version
+ if (version_compare(PHP_VERSION, '7.4.0') === -1) {
+ self::reportError('RSS-Bridge requires at least PHP version 7.4.0!');
+ }
+ // extensions check
+ if (!extension_loaded('openssl')) {
+ self::reportError('"openssl" extension not loaded. Please check "php.ini"');
+ }
+
+ if (!extension_loaded('libxml')) {
+ self::reportError('"libxml" extension not loaded. Please check "php.ini"');
+ }
+
+ if (!extension_loaded('mbstring')) {
+ self::reportError('"mbstring" extension not loaded. Please check "php.ini"');
+ }
+
+ if (!extension_loaded('simplexml')) {
+ self::reportError('"simplexml" extension not loaded. Please check "php.ini"');
+ }
+
+ // Allow RSS-Bridge to run without curl module in CLI mode without root certificates
+ if (!extension_loaded('curl') && !(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo')))) {
+ self::reportError('"curl" extension not loaded. Please check "php.ini"');
+ }
+
+ if (!extension_loaded('json')) {
+ self::reportError('"json" extension not loaded. Please check "php.ini"');
+ }
+ }
+
+ /**
+ * Loads the configuration from disk and checks if the parameters are valid.
+ *
+ * Returns an error message and aborts execution if the configuration is invalid.
+ *
+ * The RSS-Bridge configuration is split into two files:
+ * - {@see FILE_CONFIG_DEFAULT} The default configuration file that ships
+ * with every release of RSS-Bridge (do not modify this file!).
+ * - {@see FILE_CONFIG} The local configuration file that can be modified
+ * by server administrators.
+ *
+ * RSS-Bridge will first load {@see FILE_CONFIG_DEFAULT} into memory and then
+ * replace parameters with the contents of {@see FILE_CONFIG}. That way new
+ * parameters are automatically initialized with default values and custom
+ * configurations can be reduced to the minimum set of parametes necessary
+ * (only the ones that changed).
+ *
+ * The configuration files must be placed in the root folder of RSS-Bridge
+ * (next to `index.php`).
+ *
+ * _Notice_: The configuration is stored in {@see Configuration::$config}.
+ *
+ * @return void
+ */
+ public static function loadConfiguration()
+ {
+ if (!file_exists(FILE_CONFIG_DEFAULT)) {
+ self::reportError('The default configuration file is missing at ' . FILE_CONFIG_DEFAULT);
+ }
+
+ Configuration::$config = parse_ini_file(FILE_CONFIG_DEFAULT, true, INI_SCANNER_TYPED);
+ if (!Configuration::$config) {
+ self::reportError('Error parsing ' . FILE_CONFIG_DEFAULT);
+ }
+
+ if (file_exists(FILE_CONFIG)) {
+ // Replace default configuration with custom settings
+ foreach (parse_ini_file(FILE_CONFIG, true, INI_SCANNER_TYPED) as $header => $section) {
+ foreach ($section as $key => $value) {
+ Configuration::$config[$header][$key] = $value;
+ }
+ }
+ }
+
+ foreach (getenv() as $envkey => $value) {
+ // Replace all settings with their respective environment variable if available
+ $keyArray = explode('_', $envkey);
+ if ($keyArray[0] === 'RSSBRIDGE') {
+ $header = strtolower($keyArray[1]);
+ $key = strtolower($keyArray[2]);
+ if ($value === 'true' || $value === 'false') {
+ $value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
+ }
+ Configuration::$config[$header][$key] = $value;
+ }
+ }
+
+ if (
+ !is_string(self::getConfig('system', 'timezone'))
+ || !in_array(self::getConfig('system', 'timezone'), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC))
+ ) {
+ self::reportConfigurationError('system', 'timezone');
+ }
+
+ date_default_timezone_set(self::getConfig('system', 'timezone'));
+
+ if (!is_string(self::getConfig('proxy', 'url'))) {
+ self::reportConfigurationError('proxy', 'url', 'Is not a valid string');
+ }
+
+ if (!empty(self::getConfig('proxy', 'url'))) {
+ /** URL of the proxy server */
+ define('PROXY_URL', self::getConfig('proxy', 'url'));
+ }
+
+ if (!is_bool(self::getConfig('proxy', 'by_bridge'))) {
+ self::reportConfigurationError('proxy', 'by_bridge', 'Is not a valid Boolean');
+ }
+
+ /** True if proxy usage can be enabled selectively for each bridge */
+ define('PROXY_BYBRIDGE', self::getConfig('proxy', 'by_bridge'));
+
+ if (!is_string(self::getConfig('proxy', 'name'))) {
+ self::reportConfigurationError('proxy', 'name', 'Is not a valid string');
+ }
+
+ /** Name of the proxy server */
+ define('PROXY_NAME', self::getConfig('proxy', 'name'));
+
+ if (!is_string(self::getConfig('cache', 'type'))) {
+ self::reportConfigurationError('cache', 'type', 'Is not a valid string');
+ }
+
+ if (!is_bool(self::getConfig('cache', 'custom_timeout'))) {
+ self::reportConfigurationError('cache', 'custom_timeout', 'Is not a valid Boolean');
+ }
+
+ /** True if the cache timeout can be specified by the user */
+ define('CUSTOM_CACHE_TIMEOUT', self::getConfig('cache', 'custom_timeout'));
+
+ if (!is_bool(self::getConfig('authentication', 'enable'))) {
+ self::reportConfigurationError('authentication', 'enable', 'Is not a valid Boolean');
+ }
+
+ if (!is_string(self::getConfig('authentication', 'username'))) {
+ self::reportConfigurationError('authentication', 'username', 'Is not a valid string');
+ }
+
+ if (!is_string(self::getConfig('authentication', 'password'))) {
+ self::reportConfigurationError('authentication', 'password', 'Is not a valid string');
+ }
+
+ if (
+ !empty(self::getConfig('admin', 'email'))
+ && !filter_var(self::getConfig('admin', 'email'), FILTER_VALIDATE_EMAIL)
+ ) {
+ self::reportConfigurationError('admin', 'email', 'Is not a valid email address');
+ }
+
+ if (!is_bool(self::getConfig('admin', 'donations'))) {
+ self::reportConfigurationError('admin', 'donations', 'Is not a valid Boolean');
+ }
+
+ if (!is_string(self::getConfig('error', 'output'))) {
+ self::reportConfigurationError('error', 'output', 'Is not a valid String');
+ }
+
+ if (
+ !is_numeric(self::getConfig('error', 'report_limit'))
+ || self::getConfig('error', 'report_limit') < 1
+ ) {
+ self::reportConfigurationError('admin', 'report_limit', 'Value is invalid');
+ }
+ }
+
+ /**
+ * Returns the value of a parameter identified by section and key.
+ *
+ * @param string $section The section name.
+ * @param string $key The property name (key).
+ * @return mixed|null The parameter value.
+ */
+ public static function getConfig($section, $key)
+ {
+ if (array_key_exists($section, self::$config) && array_key_exists($key, self::$config[$section])) {
+ return self::$config[$section][$key];
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the current version string of RSS-Bridge.
+ *
+ * This function returns the contents of {@see Configuration::$VERSION} for
+ * regular installations and the git branch name and commit id for instances
+ * running in a git environment.
+ *
+ * @return string The version string.
+ */
+ public static function getVersion()
+ {
+ $headFile = PATH_ROOT . '.git/HEAD';
+
+ // '@' is used to mute open_basedir warning
+ if (@is_readable($headFile)) {
+ $revisionHashFile = '.git/' . substr(file_get_contents($headFile), 5, -1);
+ $parts = explode('/', $revisionHashFile);
+
+ if (isset($parts[3])) {
+ $branchName = $parts[3];
+ if (file_exists($revisionHashFile)) {
+ return 'git.' . $branchName . '.' . substr(file_get_contents($revisionHashFile), 0, 7);
+ }
+ }
+ }
+
+ return Configuration::$VERSION;
+ }
+
+ /**
+ * Reports an configuration error for the specified section and key to the
+ * user and ends execution
+ *
+ * @param string $section The section name
+ * @param string $key The configuration key
+ * @param string $message An optional message to the user
+ *
+ * @return void
+ */
+ private static function reportConfigurationError($section, $key, $message = '')
+ {
+ $report = "Parameter [{$section}] => \"{$key}\" is invalid!" . PHP_EOL;
+
+ if (file_exists(FILE_CONFIG)) {
+ $report .= 'Please check your configuration file at ' . FILE_CONFIG . PHP_EOL;
+ } elseif (!file_exists(FILE_CONFIG_DEFAULT)) {
+ $report .= 'The default configuration file is missing at ' . FILE_CONFIG_DEFAULT . PHP_EOL;
+ } else {
+ $report .= 'The default configuration file is broken.' . PHP_EOL
+ . 'Restore the original file from ' . REPOSITORY . PHP_EOL;
+ }
+
+ $report .= $message;
+ self::reportError($report);
+ }
+
+ /**
+ * Reports an error message to the user and ends execution
+ *
+ * @param string $message The error message
+ *
+ * @return void
+ */
+ private static function reportError($message)
+ {
+ header('Content-Type: text/plain', true, 500);
+ die('Configuration error' . PHP_EOL . $message);
+ }
}
diff --git a/lib/Debug.php b/lib/Debug.php
index f912fb3b..75bf5f33 100644
--- a/lib/Debug.php
+++ b/lib/Debug.php
@@ -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
*/
/**
@@ -30,92 +31,93 @@
* Warning: In debug mode your server may display sensitive information! For
* security reasons it is recommended to whitelist only specific IP addresses.
*/
-class Debug {
-
- /**
- * Indicates if debug mode is enabled.
- *
- * Do not access this property directly!
- * Use {@see Debug::isEnabled()} instead.
- *
- * @var bool
- */
- private static $enabled = false;
-
- /**
- * Indicates if debug mode is secure.
- *
- * Do not access this property directly!
- * Use {@see Debug::isSecure()} instead.
- *
- * @var bool
- */
- private static $secure = false;
-
- /**
- * Returns true if debug mode is enabled
- *
- * If debug mode is enabled, sets `display_errors = 1` and `error_reporting = E_ALL`
- *
- * @return bool True if enabled.
- */
- public static function isEnabled() {
- static $firstCall = true; // Initialized on first call
-
- if($firstCall && file_exists(PATH_ROOT . 'DEBUG')) {
-
- $debug_whitelist = trim(file_get_contents(PATH_ROOT . 'DEBUG'));
-
- self::$enabled = empty($debug_whitelist) || in_array($_SERVER['REMOTE_ADDR'],
- explode("\n", str_replace("\r", '', $debug_whitelist)
- )
- );
-
- if(self::$enabled) {
- ini_set('display_errors', '1');
- error_reporting(E_ALL);
-
- self::$secure = !empty($debug_whitelist);
- }
-
- $firstCall = false; // Skip check on next call
-
- }
-
- return self::$enabled;
- }
-
- /**
- * Returns true if debug mode is enabled only for specific IP addresses.
- *
- * Notice: The security flag is set by {@see Debug::isEnabled()}. If this
- * function is called before {@see Debug::isEnabled()}, the default value is
- * false!
- *
- * @return bool True if debug mode is secure
- */
- public static function isSecure() {
- return self::$secure;
- }
-
- /**
- * Adds a debug message to error_log if debug mode is enabled
- *
- * @param string $text The message to add to error_log
- */
- public static function log($text) {
- if(!self::isEnabled()) {
- return;
- }
-
- $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
- $calling = end($backtrace);
- $message = $calling['file'] . ':'
- . $calling['line'] . ' class '
- . (isset($calling['class']) ? $calling['class'] : '<no-class>') . '->'
- . $calling['function'] . ' - '
- . $text;
-
- error_log($message);
- }
+class Debug
+{
+ /**
+ * Indicates if debug mode is enabled.
+ *
+ * Do not access this property directly!
+ * Use {@see Debug::isEnabled()} instead.
+ *
+ * @var bool
+ */
+ private static $enabled = false;
+
+ /**
+ * Indicates if debug mode is secure.
+ *
+ * Do not access this property directly!
+ * Use {@see Debug::isSecure()} instead.
+ *
+ * @var bool
+ */
+ private static $secure = false;
+
+ /**
+ * Returns true if debug mode is enabled
+ *
+ * If debug mode is enabled, sets `display_errors = 1` and `error_reporting = E_ALL`
+ *
+ * @return bool True if enabled.
+ */
+ public static function isEnabled()
+ {
+ static $firstCall = true; // Initialized on first call
+
+ if ($firstCall && file_exists(PATH_ROOT . 'DEBUG')) {
+ $debug_whitelist = trim(file_get_contents(PATH_ROOT . 'DEBUG'));
+
+ self::$enabled = empty($debug_whitelist) || in_array(
+ $_SERVER['REMOTE_ADDR'],
+ explode("\n", str_replace("\r", '', $debug_whitelist))
+ );
+
+ if (self::$enabled) {
+ ini_set('display_errors', '1');
+ error_reporting(E_ALL);
+
+ self::$secure = !empty($debug_whitelist);
+ }
+
+ $firstCall = false; // Skip check on next call
+ }
+
+ return self::$enabled;
+ }
+
+ /**
+ * Returns true if debug mode is enabled only for specific IP addresses.
+ *
+ * Notice: The security flag is set by {@see Debug::isEnabled()}. If this
+ * function is called before {@see Debug::isEnabled()}, the default value is
+ * false!
+ *
+ * @return bool True if debug mode is secure
+ */
+ public static function isSecure()
+ {
+ return self::$secure;
+ }
+
+ /**
+ * Adds a debug message to error_log if debug mode is enabled
+ *
+ * @param string $text The message to add to error_log
+ */
+ public static function log($text)
+ {
+ if (!self::isEnabled()) {
+ return;
+ }
+
+ $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
+ $calling = end($backtrace);
+ $message = $calling['file'] . ':'
+ . $calling['line'] . ' class '
+ . (isset($calling['class']) ? $calling['class'] : '<no-class>') . '->'
+ . $calling['function'] . ' - '
+ . $text;
+
+ error_log($message);
+ }
}
diff --git a/lib/Exceptions.php b/lib/Exceptions.php
index a9d2365b..8cd42de5 100644
--- a/lib/Exceptions.php
+++ b/lib/Exceptions.php
@@ -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,18 +7,19 @@
* 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
*/
/**
* Builds a GitHub search query to find open bugs for the current bridge
*/
-function buildGitHubSearchQuery($bridgeName){
- return REPOSITORY
- . 'issues?q='
- . urlencode('is:issue is:open ' . $bridgeName);
+function buildGitHubSearchQuery($bridgeName)
+{
+ return REPOSITORY
+ . 'issues?q='
+ . urlencode('is:issue is:open ' . $bridgeName);
}
/**
@@ -33,86 +35,87 @@ function buildGitHubSearchQuery($bridgeName){
*
* @todo This function belongs inside a class
*/
-function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null){
- if(!isset($title) || !isset($body) || empty($title) || empty($body)) {
- return null;
- }
-
- // Add title and body
- $uri = REPOSITORY
- . 'issues/new?title='
- . urlencode($title)
- . '&body='
- . urlencode($body);
-
- // Add labels
- if(!is_null($labels) && is_array($labels) && count($labels) > 0) {
- if(count($lables) === 1) {
- $uri .= '&labels=' . urlencode($labels[0]);
- } else {
- foreach($labels as $label) {
- $uri .= '&labels[]=' . urlencode($label);
- }
- }
- } elseif(!is_null($labels) && is_string($labels)) {
- $uri .= '&labels=' . urlencode($labels);
- }
-
- // Add maintainer
- if(!empty($maintainer)) {
- $uri .= '&assignee=' . urlencode($maintainer);
- }
-
- return $uri;
+function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null)
+{
+ if (!isset($title) || !isset($body) || empty($title) || empty($body)) {
+ return null;
+ }
+
+ // Add title and body
+ $uri = REPOSITORY
+ . 'issues/new?title='
+ . urlencode($title)
+ . '&body='
+ . urlencode($body);
+
+ // Add labels
+ if (!is_null($labels) && is_array($labels) && count($labels) > 0) {
+ if (count($lables) === 1) {
+ $uri .= '&labels=' . urlencode($labels[0]);
+ } else {
+ foreach ($labels as $label) {
+ $uri .= '&labels[]=' . urlencode($label);
+ }
+ }
+ } elseif (!is_null($labels) && is_string($labels)) {
+ $uri .= '&labels=' . urlencode($labels);
+ }
+
+ // Add maintainer
+ if (!empty($maintainer)) {
+ $uri .= '&assignee=' . urlencode($maintainer);
+ }
+
+ return $uri;
}
function buildBridgeException(\Throwable $e, BridgeInterface $bridge): string
{
- $title = $bridge->getName() . ' failed with error ' . $e->getCode();
-
- // Build a GitHub compatible message
- $body = 'Error message: `'
- . $e->getMessage()
- . "`\nQuery string: `"
- . (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '')
- . "`\nVersion: `"
- . Configuration::getVersion()
- . '`';
-
- $body_html = nl2br($body);
- $link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer());
- $searchQuery = buildGitHubSearchQuery($bridge::NAME);
-
- $header = buildHeader($e, $bridge);
- $message = <<<EOD
+ $title = $bridge->getName() . ' failed with error ' . $e->getCode();
+
+ // Build a GitHub compatible message
+ $body = 'Error message: `'
+ . $e->getMessage()
+ . "`\nQuery string: `"
+ . (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '')
+ . "`\nVersion: `"
+ . Configuration::getVersion()
+ . '`';
+
+ $body_html = nl2br($body);
+ $link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer());
+ $searchQuery = buildGitHubSearchQuery($bridge::NAME);
+
+ $header = buildHeader($e, $bridge);
+ $message = <<<EOD
<strong>{$bridge->getName()}</strong> was unable to receive or process the
remote website's content!<br>
{$body_html}
EOD;
- $section = buildSection($e, $bridge, $message, $link, $searchQuery);
+ $section = buildSection($e, $bridge, $message, $link, $searchQuery);
- return $section;
+ return $section;
}
function buildTransformException(\Throwable $e, BridgeInterface $bridge): string
{
- $title = $bridge->getName() . ' failed with error ' . $e->getCode();
-
- // Build a GitHub compatible message
- $body = 'Error message: `'
- . $e->getMessage()
- . "`\nQuery string: `"
- . (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '')
- . '`';
-
- $link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer());
- $searchQuery = buildGitHubSearchQuery($bridge::NAME);
- $header = buildHeader($e, $bridge);
- $message = "RSS-Bridge was unable to transform the contents returned by
+ $title = $bridge->getName() . ' failed with error ' . $e->getCode();
+
+ // Build a GitHub compatible message
+ $body = 'Error message: `'
+ . $e->getMessage()
+ . "`\nQuery string: `"
+ . (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '')
+ . '`';
+
+ $link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer());
+ $searchQuery = buildGitHubSearchQuery($bridge::NAME);
+ $header = buildHeader($e, $bridge);
+ $message = "RSS-Bridge was unable to transform the contents returned by
<strong>{$bridge->getName()}</strong>!";
- $section = buildSection($e, $bridge, $message, $link, $searchQuery);
+ $section = buildSection($e, $bridge, $message, $link, $searchQuery);
- return buildPage($title, $header, $section);
+ return buildPage($title, $header, $section);
}
/**
@@ -124,8 +127,9 @@ function buildTransformException(\Throwable $e, BridgeInterface $bridge): string
*
* @todo This function belongs inside a class
*/
-function buildHeader($e, $bridge){
- return <<<EOD
+function buildHeader($e, $bridge)
+{
+ return <<<EOD
<header>
<h1>Error {$e->getCode()}</h1>
<h2>{$e->getMessage()}</h2>
@@ -146,8 +150,9 @@ EOD;
*
* @todo This function belongs inside a class
*/
-function buildSection($e, $bridge, $message, $link, $searchQuery){
- return <<<EOD
+function buildSection($e, $bridge, $message, $link, $searchQuery)
+{
+ return <<<EOD
<section>
<p class="exception-message">{$message}</p>
<div class="advice">
@@ -178,8 +183,9 @@ EOD;
*
* @todo This function belongs inside a class
*/
-function buildPage($title, $header, $section){
- return <<<EOD
+function buildPage($title, $header, $section)
+{
+ return <<<EOD
<!DOCTYPE html>
<html lang="en">
<head>
diff --git a/lib/FactoryAbstract.php b/lib/FactoryAbstract.php
index c91ae2e0..53ffb839 100644
--- a/lib/FactoryAbstract.php
+++ b/lib/FactoryAbstract.php
@@ -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,65 +7,67 @@
* 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
*/
/**
* Abstract class for factories.
*/
-abstract class FactoryAbstract {
-
- /**
- * Holds the working directory
- *
- * @var string
- */
- private $workingDir = null;
+abstract class FactoryAbstract
+{
+ /**
+ * Holds the working directory
+ *
+ * @var string
+ */
+ private $workingDir = null;
- /**
- * Set the working directory.
- *
- * @param string $dir The working directory.
- * @return void
- */
- public function setWorkingDir($dir) {
- $this->workingDir = null;
+ /**
+ * Set the working directory.
+ *
+ * @param string $dir The working directory.
+ * @return void
+ */
+ public function setWorkingDir($dir)
+ {
+ $this->workingDir = null;
- if(!is_string($dir)) {
- throw new \InvalidArgumentException('Working directory must be a string!');
- }
+ if (!is_string($dir)) {
+ throw new \InvalidArgumentException('Working directory must be a string!');
+ }
- if(!file_exists($dir)) {
- throw new \Exception('Working directory does not exist!');
- }
+ if (!file_exists($dir)) {
+ throw new \Exception('Working directory does not exist!');
+ }
- if(!is_dir($dir)) {
- throw new \InvalidArgumentException($dir . ' is not a directory!');
- }
+ if (!is_dir($dir)) {
+ throw new \InvalidArgumentException($dir . ' is not a directory!');
+ }
- $this->workingDir = realpath($dir) . '/';
- }
+ $this->workingDir = realpath($dir) . '/';
+ }
- /**
- * Get the working directory
- *
- * @return string The working directory.
- */
- public function getWorkingDir() {
- if(is_null($this->workingDir)) {
- throw new \LogicException('Working directory is not set!');
- }
+ /**
+ * Get the working directory
+ *
+ * @return string The working directory.
+ */
+ public function getWorkingDir()
+ {
+ if (is_null($this->workingDir)) {
+ throw new \LogicException('Working directory is not set!');
+ }
- return $this->workingDir;
- }
+ return $this->workingDir;
+ }
- /**
- * Creates a new instance for the object specified by name.
- *
- * @param string $name The name of the object to create.
- * @return object The object instance
- */
- abstract public function create($name);
+ /**
+ * Creates a new instance for the object specified by name.
+ *
+ * @param string $name The name of the object to create.
+ * @return object The object instance
+ */
+ abstract public function create($name);
}
diff --git a/lib/FeedExpander.php b/lib/FeedExpander.php
index b84c608a..b79bf3a8 100644
--- a/lib/FeedExpander.php
+++ b/lib/FeedExpander.php
@@ -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
*/
/**
@@ -32,406 +33,452 @@
* @todo The parsing functions should all be private. This class is complicated
* enough without having to consider children overriding functions.
*/
-abstract class FeedExpander extends BridgeAbstract {
-
- /** Indicates an RSS 1.0 feed */
- const FEED_TYPE_RSS_1_0 = 'RSS_1_0';
-
- /** Indicates an RSS 2.0 feed */
- const FEED_TYPE_RSS_2_0 = 'RSS_2_0';
-
- /** Indicates an Atom 1.0 feed */
- const FEED_TYPE_ATOM_1_0 = 'ATOM_1_0';
-
- /**
- * Holds the title of the current feed
- *
- * @var string
- */
- private $title;
-
- /**
- * Holds the URI of the feed
- *
- * @var string
- */
- private $uri;
-
- /**
- * Holds the icon of the feed
- *
- */
- private $icon;
-
- /**
- * Holds the feed type during internal operations.
- *
- * @var string
- */
- private $feedType;
-
- /**
- * Collects data from an existing feed.
- *
- * Children should call this function in {@see BridgeInterface::collectData()}
- * to extract a feed.
- *
- * @param string $url URL to the feed.
- * @param int $maxItems Maximum number of items to collect from the feed
- * (`-1`: no limit).
- * @return self
- */
- public function collectExpandableDatas($url, $maxItems = -1){
- if(empty($url)) {
- returnServerError('There is no $url for this RSS expander');
- }
-
- Debug::log('Loading from ' . $url);
-
- /* Notice we do not use cache here on purpose:
- * we want a fresh view of the RSS stream each time
- */
-
- $mimeTypes = [
- MrssFormat::MIME_TYPE,
- AtomFormat::MIME_TYPE,
- '*/*',
- ];
- $httpHeaders = ['Accept: ' . implode(', ', $mimeTypes)];
- $content = getContents($url, $httpHeaders)
- or returnServerError('Could not request ' . $url);
- $rssContent = simplexml_load_string(trim($content));
-
- if ($rssContent === false) {
- throw new \Exception('Unable to parse string as xml');
- }
-
- Debug::log('Detecting feed format/version');
- switch(true) {
- case isset($rssContent->item[0]):
- Debug::log('Detected RSS 1.0 format');
- $this->feedType = self::FEED_TYPE_RSS_1_0;
- $this->collectRss1($rssContent, $maxItems);
- break;
- case isset($rssContent->channel[0]):
- Debug::log('Detected RSS 0.9x or 2.0 format');
- $this->feedType = self::FEED_TYPE_RSS_2_0;
- $this->collectRss2($rssContent, $maxItems);
- break;
- case isset($rssContent->entry[0]):
- Debug::log('Detected ATOM format');
- $this->feedType = self::FEED_TYPE_ATOM_1_0;
- $this->collectAtom1($rssContent, $maxItems);
- break;
- default:
- Debug::log('Unknown feed format/version');
- returnServerError('The feed format is unknown!');
- break;
- }
-
- return $this;
- }
-
- /**
- * Collect data from a RSS 1.0 compatible feed
- *
- * @link http://web.resource.org/rss/1.0/spec RDF Site Summary (RSS) 1.0
- *
- * @param string $rssContent The RSS content
- * @param int $maxItems Maximum number of items to collect from the feed
- * (`-1`: no limit).
- * @return void
- *
- * @todo Instead of passing $maxItems to all functions, just add all items
- * and remove excessive items later.
- */
- protected function collectRss1($rssContent, $maxItems){
- $this->loadRss2Data($rssContent->channel[0]);
- foreach($rssContent->item as $item) {
- Debug::log('parsing item ' . var_export($item, true));
- $tmp_item = $this->parseItem($item);
- if (!empty($tmp_item)) {
- $this->items[] = $tmp_item;
- }
- if($maxItems !== -1 && count($this->items) >= $maxItems) break;
- }
- }
-
- /**
- * Collect data from a RSS 2.0 compatible feed
- *
- * @link http://www.rssboard.org/rss-specification RSS 2.0 Specification
- *
- * @param object $rssContent The RSS content
- * @param int $maxItems Maximum number of items to collect from the feed
- * (`-1`: no limit).
- * @return void
- *
- * @todo Instead of passing $maxItems to all functions, just add all items
- * and remove excessive items later.
- */
- protected function collectRss2($rssContent, $maxItems){
- $rssContent = $rssContent->channel[0];
- Debug::log('RSS content is ===========\n'
- . var_export($rssContent, true)
- . '===========');
-
- $this->loadRss2Data($rssContent);
- foreach($rssContent->item as $item) {
- Debug::log('parsing item ' . var_export($item, true));
- $tmp_item = $this->parseItem($item);
- if (!empty($tmp_item)) {
- $this->items[] = $tmp_item;
- }
- if($maxItems !== -1 && count($this->items) >= $maxItems) break;
- }
- }
-
- /**
- * Collect data from a Atom 1.0 compatible feed
- *
- * @link https://tools.ietf.org/html/rfc4287 The Atom Syndication Format
- *
- * @param object $content The Atom content
- * @param int $maxItems Maximum number of items to collect from the feed
- * (`-1`: no limit).
- * @return void
- *
- * @todo Instead of passing $maxItems to all functions, just add all items
- * and remove excessive items later.
- */
- protected function collectAtom1($content, $maxItems){
- $this->loadAtomData($content);
- foreach($content->entry as $item) {
- Debug::log('parsing item ' . var_export($item, true));
- $tmp_item = $this->parseItem($item);
- if (!empty($tmp_item)) {
- $this->items[] = $tmp_item;
- }
- if($maxItems !== -1 && count($this->items) >= $maxItems) break;
- }
- }
-
- /**
- * Load RSS 2.0 feed data into RSS-Bridge
- *
- * @param object $rssContent The RSS content
- * @return void
- *
- * @todo set title, link, description, language, and so on
- */
- protected function loadRss2Data($rssContent){
- $this->title = trim((string)$rssContent->title);
- $this->uri = trim((string)$rssContent->link);
-
- if (!empty($rssContent->image)) {
- $this->icon = trim((string)$rssContent->image->url);
- }
- }
-
- /**
- * Load Atom feed data into RSS-Bridge
- *
- * @param object $content The Atom content
- * @return void
- */
- protected function loadAtomData($content){
- $this->title = (string)$content->title;
-
- // Find best link (only one, or first of 'alternate')
- if(!isset($content->link)) {
- $this->uri = '';
- } elseif (count($content->link) === 1) {
- $this->uri = (string)$content->link[0]['href'];
- } else {
- $this->uri = '';
- foreach($content->link as $link) {
- if(strtolower($link['rel']) === 'alternate') {
- $this->uri = (string)$link['href'];
- break;
- }
- }
- }
-
- if(!empty($content->icon)) {
- $this->icon = (string)$content->icon;
- } elseif(!empty($content->logo)) {
- $this->icon = (string)$content->logo;
- }
- }
-
- /**
- * Parse the contents of a single Atom feed item into a RSS-Bridge item for
- * further transformation.
- *
- * @param object $feedItem A single feed item
- * @return object The RSS-Bridge item
- *
- * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
- * of its own?
- */
- protected function parseATOMItem($feedItem){
- // Some ATOM entries also contain RSS 2.0 fields
- $item = $this->parseRss2Item($feedItem);
-
- if(isset($feedItem->id)) $item['uri'] = (string)$feedItem->id;
- if(isset($feedItem->title)) $item['title'] = (string)$feedItem->title;
- if(isset($feedItem->updated)) $item['timestamp'] = strtotime((string)$feedItem->updated);
- if(isset($feedItem->author)) $item['author'] = (string)$feedItem->author->name;
- if(isset($feedItem->content)) $item['content'] = (string)$feedItem->content;
-
- //When "link" field is present, URL is more reliable than "id" field
- if (count($feedItem->link) === 1) {
- $item['uri'] = (string)$feedItem->link[0]['href'];
- } else {
- foreach($feedItem->link as $link) {
- if(strtolower($link['rel']) === 'alternate') {
- $item['uri'] = (string)$link['href'];
- }
- if(strtolower($link['rel']) === 'enclosure') {
- $item['enclosures'][] = (string)$link['href'];
- }
- }
- }
-
- return $item;
- }
-
- /**
- * Parse the contents of a single RSS 0.91 feed item into a RSS-Bridge item
- * for further transformation.
- *
- * @param object $feedItem A single feed item
- * @return object The RSS-Bridge item
- *
- * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
- * of its own?
- */
- protected function parseRss091Item($feedItem){
- $item = array();
- if(isset($feedItem->link)) $item['uri'] = (string)$feedItem->link;
- if(isset($feedItem->title)) $item['title'] = (string)$feedItem->title;
- // rss 0.91 doesn't support timestamps
- // rss 0.91 doesn't support authors
- // rss 0.91 doesn't support enclosures
- if(isset($feedItem->description)) $item['content'] = (string)$feedItem->description;
- return $item;
- }
-
- /**
- * Parse the contents of a single RSS 1.0 feed item into a RSS-Bridge item
- * for further transformation.
- *
- * @param object $feedItem A single feed item
- * @return object The RSS-Bridge item
- *
- * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
- * of its own?
- */
- protected function parseRss1Item($feedItem){
- // 1.0 adds optional elements around the 0.91 standard
- $item = $this->parseRss091Item($feedItem);
-
- $namespaces = $feedItem->getNamespaces(true);
- if(isset($namespaces['dc'])) {
- $dc = $feedItem->children($namespaces['dc']);
- if(isset($dc->date)) $item['timestamp'] = strtotime((string)$dc->date);
- if(isset($dc->creator)) $item['author'] = (string)$dc->creator;
- }
-
- return $item;
- }
-
- /**
- * Parse the contents of a single RSS 2.0 feed item into a RSS-Bridge item
- * for further transformation.
- *
- * @param object $feedItem A single feed item
- * @return object The RSS-Bridge item
- *
- * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
- * of its own?
- */
- protected function parseRss2Item($feedItem){
- // Primary data is compatible to 0.91 with some additional data
- $item = $this->parseRss091Item($feedItem);
-
- $namespaces = $feedItem->getNamespaces(true);
- if(isset($namespaces['dc'])) $dc = $feedItem->children($namespaces['dc']);
- if(isset($namespaces['media'])) $media = $feedItem->children($namespaces['media']);
-
- if(isset($feedItem->guid)) {
- foreach($feedItem->guid->attributes() as $attribute => $value) {
- if($attribute === 'isPermaLink'
- && ($value === 'true' || (
- filter_var($feedItem->guid, FILTER_VALIDATE_URL)
- && (empty($item['uri']) || !filter_var($item['uri'], FILTER_VALIDATE_URL))
- )
- )
- ) {
- $item['uri'] = (string)$feedItem->guid;
- break;
- }
- }
- }
-
- if(isset($feedItem->pubDate)) {
- $item['timestamp'] = strtotime((string)$feedItem->pubDate);
- } elseif(isset($dc->date)) {
- $item['timestamp'] = strtotime((string)$dc->date);
- }
-
- if(isset($feedItem->author)) {
- $item['author'] = (string)$feedItem->author;
- } elseif (isset($feedItem->creator)) {
- $item['author'] = (string)$feedItem->creator;
- } elseif(isset($dc->creator)) {
- $item['author'] = (string)$dc->creator;
- } elseif(isset($media->credit)) {
- $item['author'] = (string)$media->credit;
- }
-
- if(isset($feedItem->enclosure) && !empty($feedItem->enclosure['url'])) {
- $item['enclosures'] = array((string)$feedItem->enclosure['url']);
- }
-
- return $item;
- }
-
- /**
- * Parse the contents of a single feed item, depending on the current feed
- * type, into a RSS-Bridge item.
- *
- * @param object $item The current feed item
- * @return object A RSS-Bridge item, with (hopefully) the whole content
- */
- protected function parseItem($item){
- switch($this->feedType) {
- case self::FEED_TYPE_RSS_1_0:
- return $this->parseRss1Item($item);
- break;
- case self::FEED_TYPE_RSS_2_0:
- return $this->parseRss2Item($item);
- break;
- case self::FEED_TYPE_ATOM_1_0:
- return $this->parseATOMItem($item);
- break;
- default: returnClientError('Unknown version ' . $this->getInput('version') . '!');
- }
- }
-
- /** {@inheritdoc} */
- public function getURI(){
- return !empty($this->uri) ? $this->uri : parent::getURI();
- }
-
- /** {@inheritdoc} */
- public function getName(){
- return !empty($this->title) ? $this->title : parent::getName();
- }
-
- /** {@inheritdoc} */
- public function getIcon(){
- return !empty($this->icon) ? $this->icon : parent::getIcon();
- }
+abstract class FeedExpander extends BridgeAbstract
+{
+ /** Indicates an RSS 1.0 feed */
+ const FEED_TYPE_RSS_1_0 = 'RSS_1_0';
+
+ /** Indicates an RSS 2.0 feed */
+ const FEED_TYPE_RSS_2_0 = 'RSS_2_0';
+
+ /** Indicates an Atom 1.0 feed */
+ const FEED_TYPE_ATOM_1_0 = 'ATOM_1_0';
+
+ /**
+ * Holds the title of the current feed
+ *
+ * @var string
+ */
+ private $title;
+
+ /**
+ * Holds the URI of the feed
+ *
+ * @var string
+ */
+ private $uri;
+
+ /**
+ * Holds the icon of the feed
+ *
+ */
+ private $icon;
+
+ /**
+ * Holds the feed type during internal operations.
+ *
+ * @var string
+ */
+ private $feedType;
+
+ /**
+ * Collects data from an existing feed.
+ *
+ * Children should call this function in {@see BridgeInterface::collectData()}
+ * to extract a feed.
+ *
+ * @param string $url URL to the feed.
+ * @param int $maxItems Maximum number of items to collect from the feed
+ * (`-1`: no limit).
+ * @return self
+ */
+ public function collectExpandableDatas($url, $maxItems = -1)
+ {
+ if (empty($url)) {
+ returnServerError('There is no $url for this RSS expander');
+ }
+
+ Debug::log('Loading from ' . $url);
+
+ /* Notice we do not use cache here on purpose:
+ * we want a fresh view of the RSS stream each time
+ */
+
+ $mimeTypes = [
+ MrssFormat::MIME_TYPE,
+ AtomFormat::MIME_TYPE,
+ '*/*',
+ ];
+ $httpHeaders = ['Accept: ' . implode(', ', $mimeTypes)];
+ $content = getContents($url, $httpHeaders)
+ or returnServerError('Could not request ' . $url);
+ $rssContent = simplexml_load_string(trim($content));
+
+ if ($rssContent === false) {
+ throw new \Exception('Unable to parse string as xml');
+ }
+
+ Debug::log('Detecting feed format/version');
+ switch (true) {
+ case isset($rssContent->item[0]):
+ Debug::log('Detected RSS 1.0 format');
+ $this->feedType = self::FEED_TYPE_RSS_1_0;
+ $this->collectRss1($rssContent, $maxItems);
+ break;
+ case isset($rssContent->channel[0]):
+ Debug::log('Detected RSS 0.9x or 2.0 format');
+ $this->feedType = self::FEED_TYPE_RSS_2_0;
+ $this->collectRss2($rssContent, $maxItems);
+ break;
+ case isset($rssContent->entry[0]):
+ Debug::log('Detected ATOM format');
+ $this->feedType = self::FEED_TYPE_ATOM_1_0;
+ $this->collectAtom1($rssContent, $maxItems);
+ break;
+ default:
+ Debug::log('Unknown feed format/version');
+ returnServerError('The feed format is unknown!');
+ break;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Collect data from a RSS 1.0 compatible feed
+ *
+ * @link http://web.resource.org/rss/1.0/spec RDF Site Summary (RSS) 1.0
+ *
+ * @param string $rssContent The RSS content
+ * @param int $maxItems Maximum number of items to collect from the feed
+ * (`-1`: no limit).
+ * @return void
+ *
+ * @todo Instead of passing $maxItems to all functions, just add all items
+ * and remove excessive items later.
+ */
+ protected function collectRss1($rssContent, $maxItems)
+ {
+ $this->loadRss2Data($rssContent->channel[0]);
+ foreach ($rssContent->item as $item) {
+ Debug::log('parsing item ' . var_export($item, true));
+ $tmp_item = $this->parseItem($item);
+ if (!empty($tmp_item)) {
+ $this->items[] = $tmp_item;
+ }
+ if ($maxItems !== -1 && count($this->items) >= $maxItems) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Collect data from a RSS 2.0 compatible feed
+ *
+ * @link http://www.rssboard.org/rss-specification RSS 2.0 Specification
+ *
+ * @param object $rssContent The RSS content
+ * @param int $maxItems Maximum number of items to collect from the feed
+ * (`-1`: no limit).
+ * @return void
+ *
+ * @todo Instead of passing $maxItems to all functions, just add all items
+ * and remove excessive items later.
+ */
+ protected function collectRss2($rssContent, $maxItems)
+ {
+ $rssContent = $rssContent->channel[0];
+ Debug::log('RSS content is ===========\n'
+ . var_export($rssContent, true)
+ . '===========');
+
+ $this->loadRss2Data($rssContent);
+ foreach ($rssContent->item as $item) {
+ Debug::log('parsing item ' . var_export($item, true));
+ $tmp_item = $this->parseItem($item);
+ if (!empty($tmp_item)) {
+ $this->items[] = $tmp_item;
+ }
+ if ($maxItems !== -1 && count($this->items) >= $maxItems) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Collect data from a Atom 1.0 compatible feed
+ *
+ * @link https://tools.ietf.org/html/rfc4287 The Atom Syndication Format
+ *
+ * @param object $content The Atom content
+ * @param int $maxItems Maximum number of items to collect from the feed
+ * (`-1`: no limit).
+ * @return void
+ *
+ * @todo Instead of passing $maxItems to all functions, just add all items
+ * and remove excessive items later.
+ */
+ protected function collectAtom1($content, $maxItems)
+ {
+ $this->loadAtomData($content);
+ foreach ($content->entry as $item) {
+ Debug::log('parsing item ' . var_export($item, true));
+ $tmp_item = $this->parseItem($item);
+ if (!empty($tmp_item)) {
+ $this->items[] = $tmp_item;
+ }
+ if ($maxItems !== -1 && count($this->items) >= $maxItems) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Load RSS 2.0 feed data into RSS-Bridge
+ *
+ * @param object $rssContent The RSS content
+ * @return void
+ *
+ * @todo set title, link, description, language, and so on
+ */
+ protected function loadRss2Data($rssContent)
+ {
+ $this->title = trim((string)$rssContent->title);
+ $this->uri = trim((string)$rssContent->link);
+
+ if (!empty($rssContent->image)) {
+ $this->icon = trim((string)$rssContent->image->url);
+ }
+ }
+
+ /**
+ * Load Atom feed data into RSS-Bridge
+ *
+ * @param object $content The Atom content
+ * @return void
+ */
+ protected function loadAtomData($content)
+ {
+ $this->title = (string)$content->title;
+
+ // Find best link (only one, or first of 'alternate')
+ if (!isset($content->link)) {
+ $this->uri = '';
+ } elseif (count($content->link) === 1) {
+ $this->uri = (string)$content->link[0]['href'];
+ } else {
+ $this->uri = '';
+ foreach ($content->link as $link) {
+ if (strtolower($link['rel']) === 'alternate') {
+ $this->uri = (string)$link['href'];
+ break;
+ }
+ }
+ }
+
+ if (!empty($content->icon)) {
+ $this->icon = (string)$content->icon;
+ } elseif (!empty($content->logo)) {
+ $this->icon = (string)$content->logo;
+ }
+ }
+
+ /**
+ * Parse the contents of a single Atom feed item into a RSS-Bridge item for
+ * further transformation.
+ *
+ * @param object $feedItem A single feed item
+ * @return object The RSS-Bridge item
+ *
+ * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
+ * of its own?
+ */
+ protected function parseATOMItem($feedItem)
+ {
+ // Some ATOM entries also contain RSS 2.0 fields
+ $item = $this->parseRss2Item($feedItem);
+
+ if (isset($feedItem->id)) {
+ $item['uri'] = (string)$feedItem->id;
+ }
+ if (isset($feedItem->title)) {
+ $item['title'] = (string)$feedItem->title;
+ }
+ if (isset($feedItem->updated)) {
+ $item['timestamp'] = strtotime((string)$feedItem->updated);
+ }
+ if (isset($feedItem->author)) {
+ $item['author'] = (string)$feedItem->author->name;
+ }
+ if (isset($feedItem->content)) {
+ $item['content'] = (string)$feedItem->content;
+ }
+
+ //When "link" field is present, URL is more reliable than "id" field
+ if (count($feedItem->link) === 1) {
+ $item['uri'] = (string)$feedItem->link[0]['href'];
+ } else {
+ foreach ($feedItem->link as $link) {
+ if (strtolower($link['rel']) === 'alternate') {
+ $item['uri'] = (string)$link['href'];
+ }
+ if (strtolower($link['rel']) === 'enclosure') {
+ $item['enclosures'][] = (string)$link['href'];
+ }
+ }
+ }
+
+ return $item;
+ }
+
+ /**
+ * Parse the contents of a single RSS 0.91 feed item into a RSS-Bridge item
+ * for further transformation.
+ *
+ * @param object $feedItem A single feed item
+ * @return object The RSS-Bridge item
+ *
+ * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
+ * of its own?
+ */
+ protected function parseRss091Item($feedItem)
+ {
+ $item = [];
+ if (isset($feedItem->link)) {
+ $item['uri'] = (string)$feedItem->link;
+ }
+ if (isset($feedItem->title)) {
+ $item['title'] = (string)$feedItem->title;
+ }
+ // rss 0.91 doesn't support timestamps
+ // rss 0.91 doesn't support authors
+ // rss 0.91 doesn't support enclosures
+ if (isset($feedItem->description)) {
+ $item['content'] = (string)$feedItem->description;
+ }
+ return $item;
+ }
+
+ /**
+ * Parse the contents of a single RSS 1.0 feed item into a RSS-Bridge item
+ * for further transformation.
+ *
+ * @param object $feedItem A single feed item
+ * @return object The RSS-Bridge item
+ *
+ * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
+ * of its own?
+ */
+ protected function parseRss1Item($feedItem)
+ {
+ // 1.0 adds optional elements around the 0.91 standard
+ $item = $this->parseRss091Item($feedItem);
+
+ $namespaces = $feedItem->getNamespaces(true);
+ if (isset($namespaces['dc'])) {
+ $dc = $feedItem->children($namespaces['dc']);
+ if (isset($dc->date)) {
+ $item['timestamp'] = strtotime((string)$dc->date);
+ }
+ if (isset($dc->creator)) {
+ $item['author'] = (string)$dc->creator;
+ }
+ }
+
+ return $item;
+ }
+
+ /**
+ * Parse the contents of a single RSS 2.0 feed item into a RSS-Bridge item
+ * for further transformation.
+ *
+ * @param object $feedItem A single feed item
+ * @return object The RSS-Bridge item
+ *
+ * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
+ * of its own?
+ */
+ protected function parseRss2Item($feedItem)
+ {
+ // Primary data is compatible to 0.91 with some additional data
+ $item = $this->parseRss091Item($feedItem);
+
+ $namespaces = $feedItem->getNamespaces(true);
+ if (isset($namespaces['dc'])) {
+ $dc = $feedItem->children($namespaces['dc']);
+ }
+ if (isset($namespaces['media'])) {
+ $media = $feedItem->children($namespaces['media']);
+ }
+
+ if (isset($feedItem->guid)) {
+ foreach ($feedItem->guid->attributes() as $attribute => $value) {
+ if (
+ $attribute === 'isPermaLink'
+ && ($value === 'true' || (
+ filter_var($feedItem->guid, FILTER_VALIDATE_URL)
+ && (empty($item['uri']) || !filter_var($item['uri'], FILTER_VALIDATE_URL))
+ )
+ )
+ ) {
+ $item['uri'] = (string)$feedItem->guid;
+ break;
+ }
+ }
+ }
+
+ if (isset($feedItem->pubDate)) {
+ $item['timestamp'] = strtotime((string)$feedItem->pubDate);
+ } elseif (isset($dc->date)) {
+ $item['timestamp'] = strtotime((string)$dc->date);
+ }
+
+ if (isset($feedItem->author)) {
+ $item['author'] = (string)$feedItem->author;
+ } elseif (isset($feedItem->creator)) {
+ $item['author'] = (string)$feedItem->creator;
+ } elseif (isset($dc->creator)) {
+ $item['author'] = (string)$dc->creator;
+ } elseif (isset($media->credit)) {
+ $item['author'] = (string)$media->credit;
+ }
+
+ if (isset($feedItem->enclosure) && !empty($feedItem->enclosure['url'])) {
+ $item['enclosures'] = [(string)$feedItem->enclosure['url']];
+ }
+
+ return $item;
+ }
+
+ /**
+ * Parse the contents of a single feed item, depending on the current feed
+ * type, into a RSS-Bridge item.
+ *
+ * @param object $item The current feed item
+ * @return object A RSS-Bridge item, with (hopefully) the whole content
+ */
+ protected function parseItem($item)
+ {
+ switch ($this->feedType) {
+ case self::FEED_TYPE_RSS_1_0:
+ return $this->parseRss1Item($item);
+ break;
+ case self::FEED_TYPE_RSS_2_0:
+ return $this->parseRss2Item($item);
+ break;
+ case self::FEED_TYPE_ATOM_1_0:
+ return $this->parseATOMItem($item);
+ break;
+ default:
+ returnClientError('Unknown version ' . $this->getInput('version') . '!');
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function getURI()
+ {
+ return !empty($this->uri) ? $this->uri : parent::getURI();
+ }
+
+ /** {@inheritdoc} */
+ public function getName()
+ {
+ return !empty($this->title) ? $this->title : parent::getName();
+ }
+
+ /** {@inheritdoc} */
+ public function getIcon()
+ {
+ return !empty($this->icon) ? $this->icon : parent::getIcon();
+ }
}
diff --git a/lib/FeedItem.php b/lib/FeedItem.php
index 8690eb95..2d3872f2 100644
--- a/lib/FeedItem.php
+++ b/lib/FeedItem.php
@@ -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
*/
/**
@@ -33,493 +34,554 @@
* (i.e. `$feedItem = \FeedItem($item);`). Support for legacy items may be removed
* in future versions of RSS-Bridge.
*/
-class FeedItem {
- /** @var string|null URI to the full article */
- protected $uri = null;
-
- /** @var string|null Title of the item */
- protected $title = null;
-
- /** @var int|null Timestamp of when the item was first released */
- protected $timestamp = null;
-
- /** @var string|null Name of the author */
- protected $author = null;
-
- /** @var string|null Body of the feed */
- protected $content = null;
-
- /** @var array List of links to media objects */
- protected $enclosures = array();
-
- /** @var array List of category names or tags */
- protected $categories = array();
-
- /** @var string Unique ID for the current item */
- protected $uid = null;
-
- /** @var array Associative list of additional parameters */
- protected $misc = array(); // Custom parameters
-
- /**
- * Create object from legacy item.
- *
- * The provided array must be an associative array of key-value-pairs, where
- * keys may correspond to any of the properties of this class.
- *
- * Example use:
- *
- * ```PHP
- * <?php
- * $item = array();
- *
- * $item['uri'] = 'https://www.github.com/rss-bridge/rss-bridge/';
- * $item['title'] = 'Title';
- * $item['timestamp'] = strtotime('now');
- * $item['author'] = 'Unknown author';
- * $item['content'] = 'Hello World!';
- * $item['enclosures'] = array('https://github.com/favicon.ico');
- * $item['categories'] = array('php', 'rss-bridge', 'awesome');
- *
- * $feedItem = new \FeedItem($item);
- *
- * ```
- *
- * The result of the code above is the same as the code below:
- *
- * ```PHP
- * <?php
- * $feedItem = \FeedItem();
- *
- * $feedItem->uri = 'https://www.github.com/rss-bridge/rss-bridge/';
- * $feedItem->title = 'Title';
- * $feedItem->timestamp = strtotime('now');
- * $feedItem->autor = 'Unknown author';
- * $feedItem->content = 'Hello World!';
- * $feedItem->enclosures = array('https://github.com/favicon.ico');
- * $feedItem->categories = array('php', 'rss-bridge', 'awesome');
- * ```
- *
- * @param array $item (optional) A legacy item (empty: no legacy support).
- * @return object A new object of this class
- */
- public function __construct($item = array()) {
- if(!is_array($item))
- Debug::log('Item must be an array!');
-
- foreach($item as $key => $value) {
- $this->__set($key, $value);
- }
- }
-
- /**
- * Get current URI.
- *
- * Use {@see FeedItem::setURI()} to set the URI.
- *
- * @return string|null The URI or null if it hasn't been set.
- */
- public function getURI() {
- return $this->uri;
- }
-
- /**
- * Set URI to the full article.
- *
- * Use {@see FeedItem::getURI()} to get the URI.
- *
- * _Note_: Removes whitespace from the beginning and end of the URI.
- *
- * _Remarks_: Uses the attribute "href" or "src" if the provided URI is an
- * object of simple_html_dom_node.
- *
- * @param object|string $uri URI to the full article.
- * @return self
- */
- public function setURI($uri) {
- $this->uri = null; // Clear previous data
-
- if($uri instanceof simple_html_dom_node) {
- if($uri->hasAttribute('href')) { // Anchor
- $uri = $uri->href;
- } elseif($uri->hasAttribute('src')) { // Image
- $uri = $uri->src;
- } else {
- Debug::log('The item provided as URI is unknown!');
- }
- }
-
- if(!is_string($uri)) {
- Debug::log('URI must be a string!');
- } elseif(!filter_var(
- $uri,
- FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) {
- Debug::log('URI must include a scheme, host and path!');
- } else {
- $scheme = parse_url($uri, PHP_URL_SCHEME);
-
- if($scheme !== 'http' && $scheme !== 'https') {
- Debug::log('URI scheme must be "http" or "https"!');
- } else {
- $this->uri = trim($uri);
- }
- }
-
- return $this;
- }
-
- /**
- * Get current title.
- *
- * Use {@see FeedItem::setTitle()} to set the title.
- *
- * @return string|null The current title or null if it hasn't been set.
- */
- public function getTitle() {
- return $this->title;
- }
-
- /**
- * Set title.
- *
- * Use {@see FeedItem::getTitle()} to get the title.
- *
- * _Note_: Removes whitespace from beginning and end of the title.
- *
- * @param string $title The title
- * @return self
- */
- public function setTitle($title) {
- $this->title = null; // Clear previous data
-
- if(!is_string($title)) {
- Debug::log('Title must be a string!');
- } else {
- $this->title = trim($title);
- }
-
- return $this;
- }
-
- /**
- * Get current timestamp.
- *
- * Use {@see FeedItem::setTimestamp()} to set the timestamp.
- *
- * @return int|null The current timestamp or null if it hasn't been set.
- */
- public function getTimestamp() {
- return $this->timestamp;
- }
-
- /**
- * Set timestamp of first release.
- *
- * _Note_: The timestamp should represent the number of seconds since
- * January 1 1970 00:00:00 GMT (Unix time).
- *
- * _Remarks_: If the provided timestamp is a string (not numeric), this
- * function automatically attempts to parse the string using
- * [strtotime](http://php.net/manual/en/function.strtotime.php)
- *
- * @link http://php.net/manual/en/function.strtotime.php strtotime (PHP)
- * @link https://en.wikipedia.org/wiki/Unix_time Unix time (Wikipedia)
- *
- * @param string|int $timestamp A timestamp of when the item was first released
- * @return self
- */
- public function setTimestamp($timestamp) {
- $this->timestamp = null; // Clear previous data
-
- if(!is_numeric($timestamp)
- && !$timestamp = strtotime($timestamp)) {
- Debug::log('Unable to parse timestamp!');
- }
-
- if($timestamp <= 0) {
- Debug::log('Timestamp must be greater than zero!');
- } else {
- $this->timestamp = $timestamp;
- }
-
- return $this;
- }
-
- /**
- * Get the current author name.
- *
- * Use {@see FeedItem::setAuthor()} to set the author.
- *
- * @return string|null The author or null if it hasn't been set.
- */
- public function getAuthor() {
- return $this->author;
- }
-
- /**
- * Set the author name.
- *
- * Use {@see FeedItem::getAuthor()} to get the author.
- *
- * @param string $author The author name.
- * @return self
- */
- public function setAuthor($author) {
- $this->author = null; // Clear previous data
-
- if(!is_string($author)) {
- Debug::log('Author must be a string!');
- } else {
- $this->author = $author;
- }
-
- return $this;
- }
-
- /**
- * Get item content.
- *
- * Use {@see FeedItem::setContent()} to set the item content.
- *
- * @return string|null The item content or null if it hasn't been set.
- */
- public function getContent() {
- return $this->content;
- }
-
- /**
- * Set item content.
- *
- * Note: This function casts objects of type simple_html_dom and
- * simple_html_dom_node to string.
- *
- * Use {@see FeedItem::getContent()} to get the current item content.
- *
- * @param string|object $content The item content as text or simple_html_dom
- * object.
- * @return self
- */
- public function setContent($content) {
- $this->content = null; // Clear previous data
-
- if($content instanceof simple_html_dom
- || $content instanceof simple_html_dom_node) {
- $content = (string)$content;
- }
-
- if(!is_string($content)) {
- Debug::log('Content must be a string!');
- } else {
- $this->content = $content;
- }
-
- return $this;
- }
-
- /**
- * Get item enclosures.
- *
- * Use {@see FeedItem::setEnclosures()} to set feed enclosures.
- *
- * @return array Enclosures as array of enclosure URIs.
- */
- public function getEnclosures() {
- return $this->enclosures;
- }
-
- /**
- * Set item enclosures.
- *
- * Use {@see FeedItem::getEnclosures()} to get the current item enclosures.
- *
- * @param array $enclosures Array of enclosures, where each element links to
- * one enclosure.
- * @return self
- */
- public function setEnclosures($enclosures) {
- $this->enclosures = array(); // Clear previous data
-
- if(!is_array($enclosures)) {
- Debug::log('Enclosures must be an array!');
- } else {
- foreach($enclosures as $enclosure) {
- if(!filter_var(
- $enclosure,
- FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) {
- Debug::log('Each enclosure must contain a scheme, host and path!');
- } elseif(!in_array($enclosure, $this->enclosures)) {
- $this->enclosures[] = $enclosure;
- }
- }
- }
-
- return $this;
- }
-
- /**
- * Get item categories.
- *
- * Use {@see FeedItem::setCategories()} to set item categories.
- *
- * @param array The item categories.
- */
- public function getCategories() {
- return $this->categories;
- }
-
- /**
- * Set item categories.
- *
- * Use {@see FeedItem::getCategories()} to get the current item categories.
- *
- * @param array $categories Array of categories, where each element defines
- * a single category name.
- * @return self
- */
- public function setCategories($categories) {
- $this->categories = array(); // Clear previous data
-
- if(!is_array($categories)) {
- Debug::log('Categories must be an array!');
- } else {
- foreach($categories as $category) {
- if(!is_string($category)) {
- Debug::log('Category must be a string!');
- } else {
- $this->categories[] = $category;
- }
- }
- }
-
- return $this;
- }
-
- /**
- * Get unique id
- *
- * Use {@see FeedItem::setUid()} to set the unique id.
- *
- * @param string The unique id.
- */
- public function getUid() {
- return $this->uid;
- }
-
- /**
- * Set unique id.
- *
- * Use {@see FeedItem::getUid()} to get the unique id.
- *
- * @param string $uid A string that uniquely identifies the current item
- * @return self
- */
- public function setUid($uid) {
- $this->uid = null; // Clear previous data
-
- if(!is_string($uid)) {
- Debug::log('Unique id must be a string!');
- } elseif (preg_match('/^[a-f0-9]{40}$/', $uid)) {
- // keep id if it already is a SHA-1 hash
- $this->uid = $uid;
- } else {
- $this->uid = sha1($uid);
- }
-
- return $this;
- }
-
- /**
- * Add miscellaneous elements to the item.
- *
- * @param string $key Name of the element.
- * @param mixed $value Value of the element.
- * @return self
- */
- public function addMisc($key, $value) {
-
- if(!is_string($key)) {
- Debug::log('Key must be a string!');
- } elseif(in_array($key, get_object_vars($this))) {
- Debug::log('Key must be unique!');
- } else {
- $this->misc[$key] = $value;
- }
-
- return $this;
- }
-
- /**
- * Transform current object to array
- *
- * @return array
- */
- public function toArray() {
- return array_merge(
- array(
- 'uri' => $this->uri,
- 'title' => $this->title,
- 'timestamp' => $this->timestamp,
- 'author' => $this->author,
- 'content' => $this->content,
- 'enclosures' => $this->enclosures,
- 'categories' => $this->categories,
- 'uid' => $this->uid,
- ), $this->misc
- );
- }
-
- /**
- * Set item property
- *
- * Allows simple assignment to parameters. This method is slower, but easier
- * to implement in some cases:
- *
- * ```PHP
- * $item = new \FeedItem();
- * $item->content = 'Hello World!';
- * $item->my_id = 42;
- * ```
- *
- * @param string $name Property name
- * @param mixed $value Property value
- */
- public function __set($name, $value) {
- switch($name) {
- case 'uri': $this->setURI($value); break;
- case 'title': $this->setTitle($value); break;
- case 'timestamp': $this->setTimestamp($value); break;
- case 'author': $this->setAuthor($value); break;
- case 'content': $this->setContent($value); break;
- case 'enclosures': $this->setEnclosures($value); break;
- case 'categories': $this->setCategories($value); break;
- case 'uid': $this->setUid($value); break;
- default: $this->addMisc($name, $value);
- }
- }
-
- /**
- * Get item property
- *
- * Allows simple assignment to parameters. This method is slower, but easier
- * to implement in some cases.
- *
- * @param string $name Property name
- * @return mixed Property value
- */
- public function __get($name) {
- switch($name) {
- case 'uri': return $this->getURI();
- case 'title': return $this->getTitle();
- case 'timestamp': return $this->getTimestamp();
- case 'author': return $this->getAuthor();
- case 'content': return $this->getContent();
- case 'enclosures': return $this->getEnclosures();
- case 'categories': return $this->getCategories();
- case 'uid': return $this->getUid();
- default:
- if(array_key_exists($name, $this->misc))
- return $this->misc[$name];
- return null;
- }
- }
+class FeedItem
+{
+ /** @var string|null URI to the full article */
+ protected $uri = null;
+
+ /** @var string|null Title of the item */
+ protected $title = null;
+
+ /** @var int|null Timestamp of when the item was first released */
+ protected $timestamp = null;
+
+ /** @var string|null Name of the author */
+ protected $author = null;
+
+ /** @var string|null Body of the feed */
+ protected $content = null;
+
+ /** @var array List of links to media objects */
+ protected $enclosures = [];
+
+ /** @var array List of category names or tags */
+ protected $categories = [];
+
+ /** @var string Unique ID for the current item */
+ protected $uid = null;
+
+ /** @var array Associative list of additional parameters */
+ protected $misc = []; // Custom parameters
+
+ /**
+ * Create object from legacy item.
+ *
+ * The provided array must be an associative array of key-value-pairs, where
+ * keys may correspond to any of the properties of this class.
+ *
+ * Example use:
+ *
+ * ```PHP
+ * <?php
+ * $item = array();
+ *
+ * $item['uri'] = 'https://www.github.com/rss-bridge/rss-bridge/';
+ * $item['title'] = 'Title';
+ * $item['timestamp'] = strtotime('now');
+ * $item['author'] = 'Unknown author';
+ * $item['content'] = 'Hello World!';
+ * $item['enclosures'] = array('https://github.com/favicon.ico');
+ * $item['categories'] = array('php', 'rss-bridge', 'awesome');
+ *
+ * $feedItem = new \FeedItem($item);
+ *
+ * ```
+ *
+ * The result of the code above is the same as the code below:
+ *
+ * ```PHP
+ * <?php
+ * $feedItem = \FeedItem();
+ *
+ * $feedItem->uri = 'https://www.github.com/rss-bridge/rss-bridge/';
+ * $feedItem->title = 'Title';
+ * $feedItem->timestamp = strtotime('now');
+ * $feedItem->autor = 'Unknown author';
+ * $feedItem->content = 'Hello World!';
+ * $feedItem->enclosures = array('https://github.com/favicon.ico');
+ * $feedItem->categories = array('php', 'rss-bridge', 'awesome');
+ * ```
+ *
+ * @param array $item (optional) A legacy item (empty: no legacy support).
+ * @return object A new object of this class
+ */
+ public function __construct($item = [])
+ {
+ if (!is_array($item)) {
+ Debug::log('Item must be an array!');
+ }
+
+ foreach ($item as $key => $value) {
+ $this->__set($key, $value);
+ }
+ }
+
+ /**
+ * Get current URI.
+ *
+ * Use {@see FeedItem::setURI()} to set the URI.
+ *
+ * @return string|null The URI or null if it hasn't been set.
+ */
+ public function getURI()
+ {
+ return $this->uri;
+ }
+
+ /**
+ * Set URI to the full article.
+ *
+ * Use {@see FeedItem::getURI()} to get the URI.
+ *
+ * _Note_: Removes whitespace from the beginning and end of the URI.
+ *
+ * _Remarks_: Uses the attribute "href" or "src" if the provided URI is an
+ * object of simple_html_dom_node.
+ *
+ * @param object|string $uri URI to the full article.
+ * @return self
+ */
+ public function setURI($uri)
+ {
+ $this->uri = null; // Clear previous data
+
+ if ($uri instanceof simple_html_dom_node) {
+ if ($uri->hasAttribute('href')) { // Anchor
+ $uri = $uri->href;
+ } elseif ($uri->hasAttribute('src')) { // Image
+ $uri = $uri->src;
+ } else {
+ Debug::log('The item provided as URI is unknown!');
+ }
+ }
+
+ if (!is_string($uri)) {
+ Debug::log('URI must be a string!');
+ } elseif (
+ !filter_var(
+ $uri,
+ FILTER_VALIDATE_URL,
+ FILTER_FLAG_PATH_REQUIRED
+ )
+ ) {
+ Debug::log('URI must include a scheme, host and path!');
+ } else {
+ $scheme = parse_url($uri, PHP_URL_SCHEME);
+
+ if ($scheme !== 'http' && $scheme !== 'https') {
+ Debug::log('URI scheme must be "http" or "https"!');
+ } else {
+ $this->uri = trim($uri);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get current title.
+ *
+ * Use {@see FeedItem::setTitle()} to set the title.
+ *
+ * @return string|null The current title or null if it hasn't been set.
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Set title.
+ *
+ * Use {@see FeedItem::getTitle()} to get the title.
+ *
+ * _Note_: Removes whitespace from beginning and end of the title.
+ *
+ * @param string $title The title
+ * @return self
+ */
+ public function setTitle($title)
+ {
+ $this->title = null; // Clear previous data
+
+ if (!is_string($title)) {
+ Debug::log('Title must be a string!');
+ } else {
+ $this->title = trim($title);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get current timestamp.
+ *
+ * Use {@see FeedItem::setTimestamp()} to set the timestamp.
+ *
+ * @return int|null The current timestamp or null if it hasn't been set.
+ */
+ public function getTimestamp()
+ {
+ return $this->timestamp;
+ }
+
+ /**
+ * Set timestamp of first release.
+ *
+ * _Note_: The timestamp should represent the number of seconds since
+ * January 1 1970 00:00:00 GMT (Unix time).
+ *
+ * _Remarks_: If the provided timestamp is a string (not numeric), this
+ * function automatically attempts to parse the string using
+ * [strtotime](http://php.net/manual/en/function.strtotime.php)
+ *
+ * @link http://php.net/manual/en/function.strtotime.php strtotime (PHP)
+ * @link https://en.wikipedia.org/wiki/Unix_time Unix time (Wikipedia)
+ *
+ * @param string|int $timestamp A timestamp of when the item was first released
+ * @return self
+ */
+ public function setTimestamp($timestamp)
+ {
+ $this->timestamp = null; // Clear previous data
+
+ if (
+ !is_numeric($timestamp)
+ && !$timestamp = strtotime($timestamp)
+ ) {
+ Debug::log('Unable to parse timestamp!');
+ }
+
+ if ($timestamp <= 0) {
+ Debug::log('Timestamp must be greater than zero!');
+ } else {
+ $this->timestamp = $timestamp;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the current author name.
+ *
+ * Use {@see FeedItem::setAuthor()} to set the author.
+ *
+ * @return string|null The author or null if it hasn't been set.
+ */
+ public function getAuthor()
+ {
+ return $this->author;
+ }
+
+ /**
+ * Set the author name.
+ *
+ * Use {@see FeedItem::getAuthor()} to get the author.
+ *
+ * @param string $author The author name.
+ * @return self
+ */
+ public function setAuthor($author)
+ {
+ $this->author = null; // Clear previous data
+
+ if (!is_string($author)) {
+ Debug::log('Author must be a string!');
+ } else {
+ $this->author = $author;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get item content.
+ *
+ * Use {@see FeedItem::setContent()} to set the item content.
+ *
+ * @return string|null The item content or null if it hasn't been set.
+ */
+ public function getContent()
+ {
+ return $this->content;
+ }
+
+ /**
+ * Set item content.
+ *
+ * Note: This function casts objects of type simple_html_dom and
+ * simple_html_dom_node to string.
+ *
+ * Use {@see FeedItem::getContent()} to get the current item content.
+ *
+ * @param string|object $content The item content as text or simple_html_dom
+ * object.
+ * @return self
+ */
+ public function setContent($content)
+ {
+ $this->content = null; // Clear previous data
+
+ if (
+ $content instanceof simple_html_dom
+ || $content instanceof simple_html_dom_node
+ ) {
+ $content = (string)$content;
+ }
+
+ if (!is_string($content)) {
+ Debug::log('Content must be a string!');
+ } else {
+ $this->content = $content;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get item enclosures.
+ *
+ * Use {@see FeedItem::setEnclosures()} to set feed enclosures.
+ *
+ * @return array Enclosures as array of enclosure URIs.
+ */
+ public function getEnclosures()
+ {
+ return $this->enclosures;
+ }
+
+ /**
+ * Set item enclosures.
+ *
+ * Use {@see FeedItem::getEnclosures()} to get the current item enclosures.
+ *
+ * @param array $enclosures Array of enclosures, where each element links to
+ * one enclosure.
+ * @return self
+ */
+ public function setEnclosures($enclosures)
+ {
+ $this->enclosures = []; // Clear previous data
+
+ if (!is_array($enclosures)) {
+ Debug::log('Enclosures must be an array!');
+ } else {
+ foreach ($enclosures as $enclosure) {
+ if (
+ !filter_var(
+ $enclosure,
+ FILTER_VALIDATE_URL,
+ FILTER_FLAG_PATH_REQUIRED
+ )
+ ) {
+ Debug::log('Each enclosure must contain a scheme, host and path!');
+ } elseif (!in_array($enclosure, $this->enclosures)) {
+ $this->enclosures[] = $enclosure;
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get item categories.
+ *
+ * Use {@see FeedItem::setCategories()} to set item categories.
+ *
+ * @param array The item categories.
+ */
+ public function getCategories()
+ {
+ return $this->categories;
+ }
+
+ /**
+ * Set item categories.
+ *
+ * Use {@see FeedItem::getCategories()} to get the current item categories.
+ *
+ * @param array $categories Array of categories, where each element defines
+ * a single category name.
+ * @return self
+ */
+ public function setCategories($categories)
+ {
+ $this->categories = []; // Clear previous data
+
+ if (!is_array($categories)) {
+ Debug::log('Categories must be an array!');
+ } else {
+ foreach ($categories as $category) {
+ if (!is_string($category)) {
+ Debug::log('Category must be a string!');
+ } else {
+ $this->categories[] = $category;
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get unique id
+ *
+ * Use {@see FeedItem::setUid()} to set the unique id.
+ *
+ * @param string The unique id.
+ */
+ public function getUid()
+ {
+ return $this->uid;
+ }
+
+ /**
+ * Set unique id.
+ *
+ * Use {@see FeedItem::getUid()} to get the unique id.
+ *
+ * @param string $uid A string that uniquely identifies the current item
+ * @return self
+ */
+ public function setUid($uid)
+ {
+ $this->uid = null; // Clear previous data
+
+ if (!is_string($uid)) {
+ Debug::log('Unique id must be a string!');
+ } elseif (preg_match('/^[a-f0-9]{40}$/', $uid)) {
+ // keep id if it already is a SHA-1 hash
+ $this->uid = $uid;
+ } else {
+ $this->uid = sha1($uid);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add miscellaneous elements to the item.
+ *
+ * @param string $key Name of the element.
+ * @param mixed $value Value of the element.
+ * @return self
+ */
+ public function addMisc($key, $value)
+ {
+ if (!is_string($key)) {
+ Debug::log('Key must be a string!');
+ } elseif (in_array($key, get_object_vars($this))) {
+ Debug::log('Key must be unique!');
+ } else {
+ $this->misc[$key] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Transform current object to array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ return array_merge(
+ [
+ 'uri' => $this->uri,
+ 'title' => $this->title,
+ 'timestamp' => $this->timestamp,
+ 'author' => $this->author,
+ 'content' => $this->content,
+ 'enclosures' => $this->enclosures,
+ 'categories' => $this->categories,
+ 'uid' => $this->uid,
+ ],
+ $this->misc
+ );
+ }
+
+ /**
+ * Set item property
+ *
+ * Allows simple assignment to parameters. This method is slower, but easier
+ * to implement in some cases:
+ *
+ * ```PHP
+ * $item = new \FeedItem();
+ * $item->content = 'Hello World!';
+ * $item->my_id = 42;
+ * ```
+ *
+ * @param string $name Property name
+ * @param mixed $value Property value
+ */
+ public function __set($name, $value)
+ {
+ switch ($name) {
+ case 'uri':
+ $this->setURI($value);
+ break;
+ case 'title':
+ $this->setTitle($value);
+ break;
+ case 'timestamp':
+ $this->setTimestamp($value);
+ break;
+ case 'author':
+ $this->setAuthor($value);
+ break;
+ case 'content':
+ $this->setContent($value);
+ break;
+ case 'enclosures':
+ $this->setEnclosures($value);
+ break;
+ case 'categories':
+ $this->setCategories($value);
+ break;
+ case 'uid':
+ $this->setUid($value);
+ break;
+ default:
+ $this->addMisc($name, $value);
+ }
+ }
+
+ /**
+ * Get item property
+ *
+ * Allows simple assignment to parameters. This method is slower, but easier
+ * to implement in some cases.
+ *
+ * @param string $name Property name
+ * @return mixed Property value
+ */
+ public function __get($name)
+ {
+ switch ($name) {
+ case 'uri':
+ return $this->getURI();
+ case 'title':
+ return $this->getTitle();
+ case 'timestamp':
+ return $this->getTimestamp();
+ case 'author':
+ return $this->getAuthor();
+ case 'content':
+ return $this->getContent();
+ case 'enclosures':
+ return $this->getEnclosures();
+ case 'categories':
+ return $this->getCategories();
+ case 'uid':
+ return $this->getUid();
+ default:
+ if (array_key_exists($name, $this->misc)) {
+ return $this->misc[$name];
+ }
+ return null;
+ }
+ }
}
diff --git a/lib/FormatAbstract.php b/lib/FormatAbstract.php
index 768b0157..7a4c6c92 100644
--- a/lib/FormatAbstract.php
+++ b/lib/FormatAbstract.php
@@ -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 https://unlicense.org/ UNLICENSE
- * @link https://github.com/rss-bridge/rss-bridge
+ * @package Core
+ * @license https://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
*/
/**
@@ -16,126 +17,135 @@
*
* This class implements {@see FormatInterface}
*/
-abstract class FormatAbstract implements FormatInterface {
-
- /** The default charset (UTF-8) */
- const DEFAULT_CHARSET = 'UTF-8';
-
- /** MIME type of format output */
- const MIME_TYPE = 'text/plain';
-
- /** @var string $charset The charset */
- protected $charset;
-
- /** @var array $items The items */
- protected $items;
-
- /**
- * @var int $lastModified A timestamp to indicate the last modified time of
- * the output data.
- */
- protected $lastModified;
-
- /** @var array $extraInfos The extra infos */
- protected $extraInfos;
-
- /** {@inheritdoc} */
- public function getMimeType(){
- return static::MIME_TYPE;
- }
-
- /**
- * {@inheritdoc}
- *
- * @param string $charset {@inheritdoc}
- */
- public function setCharset($charset){
- $this->charset = $charset;
-
- return $this;
- }
-
- /** {@inheritdoc} */
- public function getCharset(){
- $charset = $this->charset;
-
- return is_null($charset) ? static::DEFAULT_CHARSET : $charset;
- }
-
- /**
- * Set the last modified time
- *
- * @param int $lastModified The last modified time
- * @return void
- */
- public function setLastModified($lastModified){
- $this->lastModified = $lastModified;
- }
-
- /**
- * {@inheritdoc}
- *
- * @param array $items {@inheritdoc}
- */
- public function setItems(array $items){
- $this->items = $items;
-
- return $this;
- }
-
- /** {@inheritdoc} */
- public function getItems(){
- if(!is_array($this->items))
- throw new \LogicException('Feed the ' . get_class($this) . ' with "setItems" method before !');
-
- return $this->items;
- }
-
- /**
- * {@inheritdoc}
- *
- * @param array $extraInfos {@inheritdoc}
- */
- public function setExtraInfos(array $extraInfos = array()){
- foreach(array('name', 'uri', 'icon', 'donationUri') as $infoName) {
- if(!isset($extraInfos[$infoName])) {
- $extraInfos[$infoName] = '';
- }
- }
-
- $this->extraInfos = $extraInfos;
-
- return $this;
- }
-
- /** {@inheritdoc} */
- public function getExtraInfos(){
- if(is_null($this->extraInfos)) { // No extra info ?
- $this->setExtraInfos(); // Define with default value
- }
-
- return $this->extraInfos;
- }
-
- /**
- * Sanitize HTML while leaving it functional.
- *
- * Keeps HTML as-is (with clickable hyperlinks) while reducing annoying and
- * potentially dangerous things.
- *
- * @param string $html The HTML content
- * @return string The sanitized HTML content
- *
- * @todo This belongs into `html.php`
- * @todo Maybe switch to http://htmlpurifier.org/
- * @todo Maybe switch to http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/index.php
- */
- protected function sanitizeHtml(string $html): string
- {
- $html = str_replace('<script', '<&zwnj;script', $html); // Disable scripts, but leave them visible.
- $html = str_replace('<iframe', '<&zwnj;iframe', $html);
- $html = str_replace('<link', '<&zwnj;link', $html);
- // We leave alone object and embed so that videos can play in RSS readers.
- return $html;
- }
+abstract class FormatAbstract implements FormatInterface
+{
+ /** The default charset (UTF-8) */
+ const DEFAULT_CHARSET = 'UTF-8';
+
+ /** MIME type of format output */
+ const MIME_TYPE = 'text/plain';
+
+ /** @var string $charset The charset */
+ protected $charset;
+
+ /** @var array $items The items */
+ protected $items;
+
+ /**
+ * @var int $lastModified A timestamp to indicate the last modified time of
+ * the output data.
+ */
+ protected $lastModified;
+
+ /** @var array $extraInfos The extra infos */
+ protected $extraInfos;
+
+ /** {@inheritdoc} */
+ public function getMimeType()
+ {
+ return static::MIME_TYPE;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $charset {@inheritdoc}
+ */
+ public function setCharset($charset)
+ {
+ $this->charset = $charset;
+
+ return $this;
+ }
+
+ /** {@inheritdoc} */
+ public function getCharset()
+ {
+ $charset = $this->charset;
+
+ return is_null($charset) ? static::DEFAULT_CHARSET : $charset;
+ }
+
+ /**
+ * Set the last modified time
+ *
+ * @param int $lastModified The last modified time
+ * @return void
+ */
+ public function setLastModified($lastModified)
+ {
+ $this->lastModified = $lastModified;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param array $items {@inheritdoc}
+ */
+ public function setItems(array $items)
+ {
+ $this->items = $items;
+
+ return $this;
+ }
+
+ /** {@inheritdoc} */
+ public function getItems()
+ {
+ if (!is_array($this->items)) {
+ throw new \LogicException('Feed the ' . get_class($this) . ' with "setItems" method before !');
+ }
+
+ return $this->items;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param array $extraInfos {@inheritdoc}
+ */
+ public function setExtraInfos(array $extraInfos = [])
+ {
+ foreach (['name', 'uri', 'icon', 'donationUri'] as $infoName) {
+ if (!isset($extraInfos[$infoName])) {
+ $extraInfos[$infoName] = '';
+ }
+ }
+
+ $this->extraInfos = $extraInfos;
+
+ return $this;
+ }
+
+ /** {@inheritdoc} */
+ public function getExtraInfos()
+ {
+ if (is_null($this->extraInfos)) { // No extra info ?
+ $this->setExtraInfos(); // Define with default value
+ }
+
+ return $this->extraInfos;
+ }
+
+ /**
+ * Sanitize HTML while leaving it functional.
+ *
+ * Keeps HTML as-is (with clickable hyperlinks) while reducing annoying and
+ * potentially dangerous things.
+ *
+ * @param string $html The HTML content
+ * @return string The sanitized HTML content
+ *
+ * @todo This belongs into `html.php`
+ * @todo Maybe switch to http://htmlpurifier.org/
+ * @todo Maybe switch to http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/index.php
+ */
+ protected function sanitizeHtml(string $html): string
+ {
+ $html = str_replace('<script', '<&zwnj;script', $html); // Disable scripts, but leave them visible.
+ $html = str_replace('<iframe', '<&zwnj;iframe', $html);
+ $html = str_replace('<link', '<&zwnj;link', $html);
+ // We leave alone object and embed so that videos can play in RSS readers.
+ return $html;
+ }
}
diff --git a/lib/FormatFactory.php b/lib/FormatFactory.php
index 2044a899..e2ef52fa 100644
--- a/lib/FormatFactory.php
+++ b/lib/FormatFactory.php
@@ -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,65 +7,66 @@
* 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 FormatFactory
{
- private $folder;
- private $formatNames;
+ private $folder;
+ private $formatNames;
- public function __construct(string $folder = PATH_LIB_FORMATS)
- {
- $this->folder = $folder;
+ public function __construct(string $folder = PATH_LIB_FORMATS)
+ {
+ $this->folder = $folder;
- // create format names
- foreach(scandir($this->folder) as $file) {
- if(preg_match('/^([^.]+)Format\.php$/U', $file, $m)) {
- $this->formatNames[] = $m[1];
- }
- }
- }
+ // create format names
+ foreach (scandir($this->folder) as $file) {
+ if (preg_match('/^([^.]+)Format\.php$/U', $file, $m)) {
+ $this->formatNames[] = $m[1];
+ }
+ }
+ }
- /**
- * @throws \InvalidArgumentException
- * @param string $name The name of the format e.g. "Atom", "Mrss" or "Json"
- */
- public function create(string $name): FormatInterface
- {
- if (! preg_match('/^[a-zA-Z0-9-]*$/', $name)) {
- throw new \InvalidArgumentException('Format name invalid!');
- }
- $name = $this->sanitizeFormatName($name);
- if ($name === null) {
- throw new \InvalidArgumentException('Unknown format given!');
- }
- $className = '\\' . $name . 'Format';
- return new $className;
- }
+ /**
+ * @throws \InvalidArgumentException
+ * @param string $name The name of the format e.g. "Atom", "Mrss" or "Json"
+ */
+ public function create(string $name): FormatInterface
+ {
+ if (! preg_match('/^[a-zA-Z0-9-]*$/', $name)) {
+ throw new \InvalidArgumentException('Format name invalid!');
+ }
+ $name = $this->sanitizeFormatName($name);
+ if ($name === null) {
+ throw new \InvalidArgumentException('Unknown format given!');
+ }
+ $className = '\\' . $name . 'Format';
+ return new $className();
+ }
- public function getFormatNames(): array
- {
- return $this->formatNames;
- }
+ public function getFormatNames(): array
+ {
+ return $this->formatNames;
+ }
- protected function sanitizeFormatName(string $name) {
- $name = ucfirst(strtolower($name));
+ protected function sanitizeFormatName(string $name)
+ {
+ $name = ucfirst(strtolower($name));
- // Trim trailing '.php' if exists
- if (preg_match('/(.+)(?:\.php)/', $name, $matches)) {
- $name = $matches[1];
- }
+ // Trim trailing '.php' if exists
+ if (preg_match('/(.+)(?:\.php)/', $name, $matches)) {
+ $name = $matches[1];
+ }
- // Trim trailing 'Format' if exists
- if (preg_match('/(.+)(?:Format)/i', $name, $matches)) {
- $name = $matches[1];
- }
- if (in_array($name, $this->formatNames)) {
- return $name;
- }
- return null;
- }
+ // Trim trailing 'Format' if exists
+ if (preg_match('/(.+)(?:Format)/i', $name, $matches)) {
+ $name = $matches[1];
+ }
+ if (in_array($name, $this->formatNames)) {
+ return $name;
+ }
+ return null;
+ }
}
diff --git a/lib/FormatInterface.php b/lib/FormatInterface.php
index 5fd46ef9..8f98d6e4 100644
--- a/lib/FormatInterface.php
+++ b/lib/FormatInterface.php
@@ -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
*/
/**
@@ -18,66 +19,67 @@
* @todo Explain parameters and return values in more detail
* @todo Return self more often (to allow call chaining)
*/
-interface FormatInterface {
- /**
- * Generate a string representation of the current data
- *
- * @return string The string representation
- */
- public function stringify();
+interface FormatInterface
+{
+ /**
+ * Generate a string representation of the current data
+ *
+ * @return string The string representation
+ */
+ public function stringify();
- /**
- * Set items
- *
- * @param array $bridges The items
- * @return self The format object
- *
- * @todo Rename parameter `$bridges` to `$items`
- */
- public function setItems(array $bridges);
+ /**
+ * Set items
+ *
+ * @param array $bridges The items
+ * @return self The format object
+ *
+ * @todo Rename parameter `$bridges` to `$items`
+ */
+ public function setItems(array $bridges);
- /**
- * Return items
- *
- * @throws \LogicException if the items are not set
- * @return array The items
- */
- public function getItems();
+ /**
+ * Return items
+ *
+ * @throws \LogicException if the items are not set
+ * @return array The items
+ */
+ public function getItems();
- /**
- * Set extra information
- *
- * @param array $infos Extra information
- * @return self The format object
- */
- public function setExtraInfos(array $infos);
+ /**
+ * Set extra information
+ *
+ * @param array $infos Extra information
+ * @return self The format object
+ */
+ public function setExtraInfos(array $infos);
- /**
- * Return extra information
- *
- * @return array Extra information
- */
- public function getExtraInfos();
+ /**
+ * Return extra information
+ *
+ * @return array Extra information
+ */
+ public function getExtraInfos();
- /**
- * Return MIME type
- *
- * @return string The MIME type
- */
- public function getMimeType();
+ /**
+ * Return MIME type
+ *
+ * @return string The MIME type
+ */
+ public function getMimeType();
- /**
- * Set charset
- *
- * @param string $charset The charset
- * @return self The format object
- */
- public function setCharset($charset);
+ /**
+ * Set charset
+ *
+ * @param string $charset The charset
+ * @return self The format object
+ */
+ public function setCharset($charset);
- /**
- * Return current charset
- *
- * @return string The charset
- */
- public function getCharset();
+ /**
+ * Return current charset
+ *
+ * @return string The charset
+ */
+ public function getCharset();
}
diff --git a/lib/ParameterValidator.php b/lib/ParameterValidator.php
index 12e07942..a903ff8d 100644
--- a/lib/ParameterValidator.php
+++ b/lib/ParameterValidator.php
@@ -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,234 +7,259 @@
* 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
*/
/**
* Validator for bridge parameters
*/
-class ParameterValidator {
-
- /**
- * Holds the list of invalid parameters
- *
- * @var array
- */
- private $invalid = array();
-
- /**
- * Add item to list of invalid parameters
- *
- * @param string $name The name of the parameter
- * @param string $reason The reason for that parameter being invalid
- * @return void
- */
- private function addInvalidParameter($name, $reason){
- $this->invalid[] = array(
- 'name' => $name,
- 'reason' => $reason
- );
- }
-
- /**
- * Return list of invalid parameters.
- *
- * Each element is an array of 'name' and 'reason'.
- *
- * @return array List of invalid parameters
- */
- public function getInvalidParameters() {
- return $this->invalid;
- }
-
- /**
- * Validate value for a text input
- *
- * @param string $value The value of a text input
- * @param string|null $pattern (optional) A regex pattern
- * @return string|null The filtered value or null if the value is invalid
- */
- private function validateTextValue($value, $pattern = null){
- if(!is_null($pattern)) {
- $filteredValue = filter_var($value,
- FILTER_VALIDATE_REGEXP,
- array('options' => array(
- 'regexp' => '/^' . $pattern . '$/'
- )
- ));
- } else {
- $filteredValue = filter_var($value);
- }
-
- if($filteredValue === false)
- return null;
-
- return $filteredValue;
- }
-
- /**
- * Validate value for a number input
- *
- * @param int $value The value of a number input
- * @return int|null The filtered value or null if the value is invalid
- */
- private function validateNumberValue($value){
- $filteredValue = filter_var($value, FILTER_VALIDATE_INT);
-
- if($filteredValue === false)
- return null;
-
- return $filteredValue;
- }
-
- /**
- * Validate value for a checkbox
- *
- * @param bool $value The value of a checkbox
- * @return bool The filtered value
- */
- private function validateCheckboxValue($value){
- return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
- }
-
- /**
- * Validate value for a list
- *
- * @param string $value The value of a list
- * @param array $expectedValues A list of expected values
- * @return string|null The filtered value or null if the value is invalid
- */
- private function validateListValue($value, $expectedValues){
- $filteredValue = filter_var($value);
-
- if($filteredValue === false)
- return null;
-
- if(!in_array($filteredValue, $expectedValues)) { // Check sub-values?
- foreach($expectedValues as $subName => $subValue) {
- if(is_array($subValue) && in_array($filteredValue, $subValue))
- return $filteredValue;
- }
- return null;
- }
-
- return $filteredValue;
- }
-
- /**
- * Check if all required parameters are satisfied
- *
- * @param array $data (ref) A list of input values
- * @param array $parameters The bridge parameters
- * @return bool True if all parameters are satisfied
- */
- public function validateData(&$data, $parameters){
-
- if(!is_array($data))
- return false;
-
- foreach($data as $name => $value) {
- // Some RSS readers add a cache-busting parameter (_=<timestamp>) to feed URLs, detect and ignore them.
- if ($name === '_') continue;
-
- $registered = false;
- foreach($parameters as $context => $set) {
- if(array_key_exists($name, $set)) {
- $registered = true;
- if(!isset($set[$name]['type'])) {
- $set[$name]['type'] = 'text';
- }
-
- switch($set[$name]['type']) {
- case 'number':
- $data[$name] = $this->validateNumberValue($value);
- break;
- case 'checkbox':
- $data[$name] = $this->validateCheckboxValue($value);
- break;
- case 'list':
- $data[$name] = $this->validateListValue($value, $set[$name]['values']);
- break;
- default:
- case 'text':
- if(isset($set[$name]['pattern'])) {
- $data[$name] = $this->validateTextValue($value, $set[$name]['pattern']);
- } else {
- $data[$name] = $this->validateTextValue($value);
- }
- break;
- }
-
- if(is_null($data[$name]) && isset($set[$name]['required']) && $set[$name]['required']) {
- $this->addInvalidParameter($name, 'Parameter is invalid!');
- }
- }
- }
-
- if(!$registered) {
- $this->addInvalidParameter($name, 'Parameter is not registered!');
- }
- }
-
- return empty($this->invalid);
- }
-
- /**
- * Get the name of the context matching the provided inputs
- *
- * @param array $data Associative array of user data
- * @param array $parameters Array of bridge parameters
- * @return string|null Returns the context name or null if no match was found
- */
- public function getQueriedContext($data, $parameters){
- $queriedContexts = array();
-
- // Detect matching context
- foreach($parameters as $context => $set) {
- $queriedContexts[$context] = null;
-
- // Ensure all user data exist in the current context
- $notInContext = array_diff_key($data, $set);
- if(array_key_exists('global', $parameters))
- $notInContext = array_diff_key($notInContext, $parameters['global']);
- if(sizeof($notInContext) > 0)
- continue;
-
- // Check if all parameters of the context are satisfied
- foreach($set as $id => $properties) {
- if(isset($data[$id]) && !empty($data[$id])) {
- $queriedContexts[$context] = true;
- } elseif (isset($properties['type'])
- && ($properties['type'] === 'checkbox' || $properties['type'] === 'list')) {
- continue;
- } elseif(isset($properties['required']) && $properties['required'] === true) {
- $queriedContexts[$context] = false;
- break;
- }
- }
- }
-
- // Abort if one of the globally required parameters is not satisfied
- if(array_key_exists('global', $parameters)
- && $queriedContexts['global'] === false) {
- return null;
- }
- unset($queriedContexts['global']);
-
- switch(array_sum($queriedContexts)) {
- case 0: // Found no match, is there a context without parameters?
- if(isset($data['context'])) return $data['context'];
- foreach($queriedContexts as $context => $queried) {
- if(is_null($queried)) {
- return $context;
- }
- }
- return null;
- case 1: // Found unique match
- return array_search(true, $queriedContexts);
- default: return false;
- }
- }
+class ParameterValidator
+{
+ /**
+ * Holds the list of invalid parameters
+ *
+ * @var array
+ */
+ private $invalid = [];
+
+ /**
+ * Add item to list of invalid parameters
+ *
+ * @param string $name The name of the parameter
+ * @param string $reason The reason for that parameter being invalid
+ * @return void
+ */
+ private function addInvalidParameter($name, $reason)
+ {
+ $this->invalid[] = [
+ 'name' => $name,
+ 'reason' => $reason
+ ];
+ }
+
+ /**
+ * Return list of invalid parameters.
+ *
+ * Each element is an array of 'name' and 'reason'.
+ *
+ * @return array List of invalid parameters
+ */
+ public function getInvalidParameters()
+ {
+ return $this->invalid;
+ }
+
+ /**
+ * Validate value for a text input
+ *
+ * @param string $value The value of a text input
+ * @param string|null $pattern (optional) A regex pattern
+ * @return string|null The filtered value or null if the value is invalid
+ */
+ private function validateTextValue($value, $pattern = null)
+ {
+ if (!is_null($pattern)) {
+ $filteredValue = filter_var(
+ $value,
+ FILTER_VALIDATE_REGEXP,
+ ['options' => [
+ 'regexp' => '/^' . $pattern . '$/'
+ ]
+ ]
+ );
+ } else {
+ $filteredValue = filter_var($value);
+ }
+
+ if ($filteredValue === false) {
+ return null;
+ }
+
+ return $filteredValue;
+ }
+
+ /**
+ * Validate value for a number input
+ *
+ * @param int $value The value of a number input
+ * @return int|null The filtered value or null if the value is invalid
+ */
+ private function validateNumberValue($value)
+ {
+ $filteredValue = filter_var($value, FILTER_VALIDATE_INT);
+
+ if ($filteredValue === false) {
+ return null;
+ }
+
+ return $filteredValue;
+ }
+
+ /**
+ * Validate value for a checkbox
+ *
+ * @param bool $value The value of a checkbox
+ * @return bool The filtered value
+ */
+ private function validateCheckboxValue($value)
+ {
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
+ }
+
+ /**
+ * Validate value for a list
+ *
+ * @param string $value The value of a list
+ * @param array $expectedValues A list of expected values
+ * @return string|null The filtered value or null if the value is invalid
+ */
+ private function validateListValue($value, $expectedValues)
+ {
+ $filteredValue = filter_var($value);
+
+ if ($filteredValue === false) {
+ return null;
+ }
+
+ if (!in_array($filteredValue, $expectedValues)) { // Check sub-values?
+ foreach ($expectedValues as $subName => $subValue) {
+ if (is_array($subValue) && in_array($filteredValue, $subValue)) {
+ return $filteredValue;
+ }
+ }
+ return null;
+ }
+
+ return $filteredValue;
+ }
+
+ /**
+ * Check if all required parameters are satisfied
+ *
+ * @param array $data (ref) A list of input values
+ * @param array $parameters The bridge parameters
+ * @return bool True if all parameters are satisfied
+ */
+ public function validateData(&$data, $parameters)
+ {
+ if (!is_array($data)) {
+ return false;
+ }
+
+ foreach ($data as $name => $value) {
+ // Some RSS readers add a cache-busting parameter (_=<timestamp>) to feed URLs, detect and ignore them.
+ if ($name === '_') {
+ continue;
+ }
+
+ $registered = false;
+ foreach ($parameters as $context => $set) {
+ if (array_key_exists($name, $set)) {
+ $registered = true;
+ if (!isset($set[$name]['type'])) {
+ $set[$name]['type'] = 'text';
+ }
+
+ switch ($set[$name]['type']) {
+ case 'number':
+ $data[$name] = $this->validateNumberValue($value);
+ break;
+ case 'checkbox':
+ $data[$name] = $this->validateCheckboxValue($value);
+ break;
+ case 'list':
+ $data[$name] = $this->validateListValue($value, $set[$name]['values']);
+ break;
+ default:
+ case 'text':
+ if (isset($set[$name]['pattern'])) {
+ $data[$name] = $this->validateTextValue($value, $set[$name]['pattern']);
+ } else {
+ $data[$name] = $this->validateTextValue($value);
+ }
+ break;
+ }
+
+ if (is_null($data[$name]) && isset($set[$name]['required']) && $set[$name]['required']) {
+ $this->addInvalidParameter($name, 'Parameter is invalid!');
+ }
+ }
+ }
+
+ if (!$registered) {
+ $this->addInvalidParameter($name, 'Parameter is not registered!');
+ }
+ }
+
+ return empty($this->invalid);
+ }
+
+ /**
+ * Get the name of the context matching the provided inputs
+ *
+ * @param array $data Associative array of user data
+ * @param array $parameters Array of bridge parameters
+ * @return string|null Returns the context name or null if no match was found
+ */
+ public function getQueriedContext($data, $parameters)
+ {
+ $queriedContexts = [];
+
+ // Detect matching context
+ foreach ($parameters as $context => $set) {
+ $queriedContexts[$context] = null;
+
+ // Ensure all user data exist in the current context
+ $notInContext = array_diff_key($data, $set);
+ if (array_key_exists('global', $parameters)) {
+ $notInContext = array_diff_key($notInContext, $parameters['global']);
+ }
+ if (sizeof($notInContext) > 0) {
+ continue;
+ }
+
+ // Check if all parameters of the context are satisfied
+ foreach ($set as $id => $properties) {
+ if (isset($data[$id]) && !empty($data[$id])) {
+ $queriedContexts[$context] = true;
+ } elseif (
+ isset($properties['type'])
+ && ($properties['type'] === 'checkbox' || $properties['type'] === 'list')
+ ) {
+ continue;
+ } elseif (isset($properties['required']) && $properties['required'] === true) {
+ $queriedContexts[$context] = false;
+ break;
+ }
+ }
+ }
+
+ // Abort if one of the globally required parameters is not satisfied
+ if (
+ array_key_exists('global', $parameters)
+ && $queriedContexts['global'] === false
+ ) {
+ return null;
+ }
+ unset($queriedContexts['global']);
+
+ switch (array_sum($queriedContexts)) {
+ case 0: // Found no match, is there a context without parameters?
+ if (isset($data['context'])) {
+ return $data['context'];
+ }
+ foreach ($queriedContexts as $context => $queried) {
+ if (is_null($queried)) {
+ return $context;
+ }
+ }
+ return null;
+ case 1: // Found unique match
+ return array_search(true, $queriedContexts);
+ default:
+ return false;
+ }
+ }
}
diff --git a/lib/XPathAbstract.php b/lib/XPathAbstract.php
index 0ca1587b..686addf4 100644
--- a/lib/XPathAbstract.php
+++ b/lib/XPathAbstract.php
@@ -15,572 +15,598 @@
* This class extends {@see BridgeAbstract}, which means it incorporates and
* extends all of its functionality.
**/
-abstract class XPathAbstract extends BridgeAbstract {
-
- /**
- * Source Web page URL (should provide either HTML or XML content)
- * You can specify any website URL which serves data suited for display in RSS feeds
- * (for example a news blog).
- *
- * Use {@see XPathAbstract::getSourceUrl()} to read this parameter
- */
- const FEED_SOURCE_URL = '';
-
- /**
- * XPath expression for extracting the feed title from the source page.
- * If this is left blank or does not provide any data {@see BridgeAbstract::getName()}
- * is used instead as the feed's title.
- *
- * Use {@see XPathAbstract::getExpressionTitle()} to read this parameter
- */
- const XPATH_EXPRESSION_FEED_TITLE = './/title';
-
- /**
- * XPath expression for extracting the feed favicon URL from the source page.
- * If this is left blank or does not provide any data {@see BridgeAbstract::getIcon()}
- * is used instead as the feed's favicon URL.
- *
- * Use {@see XPathAbstract::getExpressionIcon()} to read this parameter
- */
- const XPATH_EXPRESSION_FEED_ICON = './/link[@rel="icon"]/@href';
-
- /**
- * XPath expression for extracting the feed items from the source page
- * Enter an XPath expression matching a list of dom nodes, each node containing one
- * feed article item in total (usually a surrounding <div> or <span> tag). This will
- * be the context nodes for all of the following expressions. This expression usually
- * starts with a single forward slash.
- *
- * Use {@see XPathAbstract::getExpressionItem()} to read this parameter
- */
- const XPATH_EXPRESSION_ITEM = '';
-
- /**
- * XPath expression for extracting an item title from the item context
- * This expression should match a node contained within each article item node
- * containing the article headline. It should start with a dot followed by two
- * forward slashes, referring to any descendant nodes of the article item node.
- *
- * Use {@see XPathAbstract::getExpressionItemTitle()} to read this parameter
- */
- const XPATH_EXPRESSION_ITEM_TITLE = '';
-
- /**
- * XPath expression for extracting an item's content from the item context
- * This expression should match a node contained within each article item node
- * containing the article content or description. It should start with a dot
- * followed by two forward slashes, referring to any descendant nodes of the
- * article item node.
- *
- * Use {@see XPathAbstract::getExpressionItemContent()} to read this parameter
- */
- const XPATH_EXPRESSION_ITEM_CONTENT = '';
-
- /**
- * XPath expression for extracting an item link from the item context
- * This expression should match a node's attribute containing the article URL
- * (usually the href attribute of an <a> tag). It should start with a dot
- * followed by two forward slashes, referring to any descendant nodes of
- * the article item node. Attributes can be selected by prepending an @ char
- * before the attributes name.
- *
- * Use {@see XPathAbstract::getExpressionItemUri()} to read this parameter
- */
- const XPATH_EXPRESSION_ITEM_URI = '';
-
- /**
- * XPath expression for extracting an item author from the item context
- * This expression should match a node contained within each article item
- * node containing the article author's name. It should start with a dot
- * followed by two forward slashes, referring to any descendant nodes of
- * the article item node.
- *
- * Use {@see XPathAbstract::getExpressionItemAuthor()} to read this parameter
- */
- const XPATH_EXPRESSION_ITEM_AUTHOR = '';
-
- /**
- * XPath expression for extracting an item timestamp from the item context
- * This expression should match a node or node's attribute containing the
- * article timestamp or date (parsable by PHP's strtotime function). It
- * should start with a dot followed by two forward slashes, referring to
- * any descendant nodes of the article item node. Attributes can be
- * selected by prepending an @ char before the attributes name.
- *
- * Use {@see XPathAbstract::getExpressionItemTimestamp()} to read this parameter
- */
- const XPATH_EXPRESSION_ITEM_TIMESTAMP = '';
-
- /**
- * XPath expression for extracting item enclosures (media content like
- * images or movies) from the item context
- * This expression should match a node's attribute containing an article
- * image URL (usually the src attribute of an <img> tag or a style
- * attribute). It should start with a dot followed by two forward slashes,
- * referring to any descendant nodes of the article item node. Attributes
- * can be selected by prepending an @ char before the attributes name.
- *
- * Use {@see XPathAbstract::getExpressionItemEnclosures()} to read this parameter
- */
- const XPATH_EXPRESSION_ITEM_ENCLOSURES = '';
-
- /**
- * XPath expression for extracting an item category from the item context
- * This expression should match a node or node's attribute contained
- * within each article item node containing the article category. This
- * could be inside <div> or <span> tags or sometimes be hidden
- * in a data attribute. It should start with a dot followed by two
- * forward slashes, referring to any descendant nodes of the article
- * item node. Attributes can be selected by prepending an @ char
- * before the attributes name.
- *
- * Use {@see XPathAbstract::getExpressionItemCategories()} to read this parameter
- */
- const XPATH_EXPRESSION_ITEM_CATEGORIES = '';
-
- /**
- * Fix encoding
- * Set this to true for fixing feed encoding by invoking PHP's utf8_decode
- * function on all extracted texts. Try this in case you see "broken" or
- * "weird" characters in your feed where you'd normally expect umlauts
- * or any other non-ascii characters.
- *
- * Use {@see XPathAbstract::getSettingFixEncoding()} to read this parameter
- */
- const SETTING_FIX_ENCODING = false;
-
- /**
- * Internal storage for resulting feed name, automatically detected
- * @var string
- */
- private $feedName;
-
- /**
- * Internal storage for resulting feed name, automatically detected
- * @var string
- */
- private $feedUri;
-
- /**
- * Internal storage for resulting feed favicon, automatically detected
- * @var string
- */
- private $feedIcon;
-
- public function getName(){
- return $this->feedName ?: parent::getName();
- }
-
- public function getURI() {
- return $this->feedUri ?: parent::getURI();
- }
-
- public function getIcon() {
- return $this->feedIcon ?: parent::getIcon();
- }
-
- /**
- * Source Web page URL (should provide either HTML or XML content)
- * @return string
- */
- protected function getSourceUrl(){
- return static::FEED_SOURCE_URL;
- }
-
- /**
- * XPath expression for extracting the feed title from the source page
- * @return string
- */
- protected function getExpressionTitle(){
- return static::XPATH_EXPRESSION_FEED_TITLE;
- }
-
- /**
- * XPath expression for extracting the feed favicon from the source page
- * @return string
- */
- protected function getExpressionIcon(){
- return static::XPATH_EXPRESSION_FEED_ICON;
- }
-
- /**
- * XPath expression for extracting the feed items from the source page
- * @return string
- */
- protected function getExpressionItem(){
- return static::XPATH_EXPRESSION_ITEM;
- }
-
- /**
- * XPath expression for extracting an item title from the item context
- * @return string
- */
- protected function getExpressionItemTitle(){
- return static::XPATH_EXPRESSION_ITEM_TITLE;
- }
-
- /**
- * XPath expression for extracting an item's content from the item context
- * @return string
- */
- protected function getExpressionItemContent(){
- return static::XPATH_EXPRESSION_ITEM_CONTENT;
- }
-
- /**
- * XPath expression for extracting an item link from the item context
- * @return string
- */
- protected function getExpressionItemUri(){
- return static::XPATH_EXPRESSION_ITEM_URI;
- }
-
- /**
- * XPath expression for extracting an item author from the item context
- * @return string
- */
- protected function getExpressionItemAuthor(){
- return static::XPATH_EXPRESSION_ITEM_AUTHOR;
- }
-
- /**
- * XPath expression for extracting an item timestamp from the item context
- * @return string
- */
- protected function getExpressionItemTimestamp(){
- return static::XPATH_EXPRESSION_ITEM_TIMESTAMP;
- }
-
- /**
- * XPath expression for extracting item enclosures (media content like
- * images or movies) from the item context
- * @return string
- */
- protected function getExpressionItemEnclosures(){
- return static::XPATH_EXPRESSION_ITEM_ENCLOSURES;
- }
-
- /**
- * XPath expression for extracting an item category from the item context
- * @return string
- */
- protected function getExpressionItemCategories(){
- return static::XPATH_EXPRESSION_ITEM_CATEGORIES;
- }
-
- /**
- * Fix encoding
- * @return string
- */
- protected function getSettingFixEncoding(){
- return static::SETTING_FIX_ENCODING;
- }
-
- /**
- * Internal helper method for quickly accessing all the user defined constants
- * in derived classes
- *
- * @param $name
- * @return bool|string
- */
- private function getParam($name){
- switch($name) {
-
- case 'url':
- return $this->getSourceUrl();
- case 'feed_title':
- return $this->getExpressionTitle();
- case 'feed_icon':
- return $this->getExpressionIcon();
- case 'item':
- return $this->getExpressionItem();
- case 'title':
- return $this->getExpressionItemTitle();
- case 'content':
- return $this->getExpressionItemContent();
- case 'uri':
- return $this->getExpressionItemUri();
- case 'author':
- return $this->getExpressionItemAuthor();
- case 'timestamp':
- return $this->getExpressionItemTimestamp();
- case 'enclosures':
- return $this->getExpressionItemEnclosures();
- case 'categories':
- return $this->getExpressionItemCategories();
- case 'fix_encoding':
- return $this->getSettingFixEncoding();
- }
- }
-
- /**
- * Should provide the source website HTML content
- * can be easily overwritten for example if special headers or auth infos are required
- * @return string
- */
- protected function provideWebsiteContent() {
- return getContents($this->feedUri);
- }
-
- /**
- * Should provide the feeds title
- *
- * @param DOMXPath $xpath
- * @return string
- */
- protected function provideFeedTitle(DOMXPath $xpath) {
- $title = $xpath->query($this->getParam('feed_title'));
- if(count($title) === 1) {
- return $this->getItemValueOrNodeValue($title);
- }
- }
-
- /**
- * Should provide the URL of the feed's favicon
- *
- * @param DOMXPath $xpath
- * @return string
- */
- protected function provideFeedIcon(DOMXPath $xpath) {
- $icon = $xpath->query($this->getParam('feed_icon'));
- if(count($icon) === 1) {
- return $this->cleanMediaUrl($this->getItemValueOrNodeValue($icon));
- }
- }
-
- /**
- * Should provide the feed's items.
- *
- * @param DOMXPath $xpath
- * @return DOMNodeList
- */
- protected function provideFeedItems(DOMXPath $xpath) {
- return @$xpath->query($this->getParam('item'));
- }
-
- public function collectData() {
-
- $this->feedUri = $this->getParam('url');
-
- $webPageHtml = new DOMDocument();
- libxml_use_internal_errors(true);
- $webPageHtml->loadHTML($this->provideWebsiteContent());
- libxml_clear_errors();
- libxml_use_internal_errors(false);
-
- $xpath = new DOMXPath($webPageHtml);
-
- $this->feedName = $this->provideFeedTitle($xpath);
- $this->feedIcon = $this->provideFeedIcon($xpath);
-
- $entries = $this->provideFeedItems($xpath);
- if($entries === false) {
- return;
- }
-
- foreach ($entries as $entry) {
- $item = new \FeedItem();
- foreach(array('title', 'content', 'uri', 'author', 'timestamp', 'enclosures', 'categories') as $param) {
-
- $expression = $this->getParam($param);
- if('' === $expression) {
- continue;
- }
-
- //can be a string or DOMNodeList, depending on the expression result
- $typedResult = @$xpath->evaluate($expression, $entry);
- if ($typedResult === false || ($typedResult instanceof DOMNodeList && count($typedResult) === 0)
- || (is_string($typedResult) && strlen(trim($typedResult)) === 0)) {
- continue;
- }
-
- $item->__set($param, $this->formatParamValue($param, $this->getItemValueOrNodeValue($typedResult)));
-
- }
-
- $itemId = $this->generateItemId($item);
- if(null !== $itemId) {
- $item->setUid($itemId);
- }
-
- $this->items[] = $item;
- }
-
- }
-
- /**
- * @param $param
- * @param $value
- * @return string|array
- */
- protected function formatParamValue($param, $value)
- {
- $value = $this->fixEncoding($value);
- switch ($param) {
- case 'title':
- return $this->formatItemTitle($value);
- case 'content':
- return $this->formatItemContent($value);
- case 'uri':
- return $this->formatItemUri($value);
- case 'author':
- return $this->formatItemAuthor($value);
- case 'timestamp':
- return $this->formatItemTimestamp($value);
- case 'enclosures':
- return $this->formatItemEnclosures($value);
- case 'categories':
- return $this->formatItemCategories($value);
- }
- return $value;
- }
-
- /**
- * Formats the title of a feed item. Takes extracted raw title and returns it formatted
- * as string.
- * Can be easily overwritten for in case the value needs to be transformed into something
- * else.
- * @param string $value
- * @return string
- */
- protected function formatItemTitle($value) {
- return $value;
- }
-
- /**
- * Formats the timestamp of a feed item. Takes extracted raw timestamp and returns unix
- * timestamp as integer.
- * Can be easily overwritten for example if a special format has to be expected on the
- * source website.
- * @param string $value
- * @return string
- */
- protected function formatItemContent($value) {
- return $value;
- }
-
- /**
- * Formats the URI of a feed item. Takes extracted raw URI and returns it formatted
- * as string.
- * Can be easily overwritten for in case the value needs to be transformed into something
- * else.
- * @param string $value
- * @return string
- */
- protected function formatItemUri($value) {
- if(strlen($value) === 0) {
- return '';
- }
- if(strpos($value, 'http://') === 0 || strpos($value, 'https://') === 0) {
- return $value;
- }
-
- return urljoin($this->feedUri, $value);
- }
-
- /**
- * Formats the author of a feed item. Takes extracted raw author and returns it formatted
- * as string.
- * Can be easily overwritten for in case the value needs to be transformed into something
- * else.
- * @param string $value
- * @return string
- */
- protected function formatItemAuthor($value) {
- return $value;
- }
-
- /**
- * Formats the timestamp of a feed item. Takes extracted raw timestamp and returns unix
- * timestamp as integer.
- * Can be easily overwritten for example if a special format has to be expected on the
- * source website.
- * @param string $value
- * @return false|int
- */
- protected function formatItemTimestamp($value) {
- return strtotime($value);
- }
-
- /**
- * Formats the enclosures of a feed item. Takes extracted raw enclosures and returns them
- * formatted as array.
- * Can be easily overwritten for in case the values need to be transformed into something
- * else.
- * @param string $value
- * @return array
- */
- protected function formatItemEnclosures($value) {
- return array($this->cleanMediaUrl($value));
- }
-
- /**
- * Formats the categories of a feed item. Takes extracted raw categories and returns them
- * formatted as array.
- * Can be easily overwritten for in case the values need to be transformed into something
- * else.
- * @param string $value
- * @return array
- */
- protected function formatItemCategories($value) {
- return array($value);
- }
-
- /**
- * @param $mediaUrl
- * @return string|void
- */
- protected function cleanMediaUrl($mediaUrl)
- {
- $pattern = '~(?:http(?:s)?:)?[\/a-zA-Z0-9\-=_,\.\%]+\.(?:jpg|gif|png|jpeg|ico|mp3|webp){1}~i';
- $result = preg_match($pattern, $mediaUrl, $matches);
- if(1 !== $result) {
- return;
- }
- return urljoin($this->feedUri, $matches[0]);
- }
-
- /**
- * @param $typedResult
- * @return string
- */
- protected function getItemValueOrNodeValue($typedResult)
- {
- if($typedResult instanceof DOMNodeList) {
- $item = $typedResult->item(0);
- if ($item instanceof DOMElement) {
- return trim($item->nodeValue);
- } elseif ($item instanceof DOMAttr) {
- return trim($item->value);
- } elseif ($item instanceof DOMText) {
- return trim($item->wholeText);
- }
- } elseif(is_string($typedResult) && strlen($typedResult) > 0) {
- return trim($typedResult);
- }
- returnServerError('Unknown type of XPath expression result.');
- }
-
- /**
- * Fixes feed encoding by invoking PHP's utf8_decode function on extracted texts.
- * Useful in case of "broken" or "weird" characters in the feed where you'd normally
- * expect umlauts.
- *
- * @param $input
- * @return string
- */
- protected function fixEncoding($input)
- {
- return $this->getParam('fix_encoding') ? utf8_decode($input) : $input;
- }
-
- /**
- * Allows overriding default mechanism determining items Uid's
- *
- * @param FeedItem $item
- * @return string|null
- */
- protected function generateItemId(\FeedItem $item) {
- return null; //auto generation
- }
+abstract class XPathAbstract extends BridgeAbstract
+{
+ /**
+ * Source Web page URL (should provide either HTML or XML content)
+ * You can specify any website URL which serves data suited for display in RSS feeds
+ * (for example a news blog).
+ *
+ * Use {@see XPathAbstract::getSourceUrl()} to read this parameter
+ */
+ const FEED_SOURCE_URL = '';
+
+ /**
+ * XPath expression for extracting the feed title from the source page.
+ * If this is left blank or does not provide any data {@see BridgeAbstract::getName()}
+ * is used instead as the feed's title.
+ *
+ * Use {@see XPathAbstract::getExpressionTitle()} to read this parameter
+ */
+ const XPATH_EXPRESSION_FEED_TITLE = './/title';
+
+ /**
+ * XPath expression for extracting the feed favicon URL from the source page.
+ * If this is left blank or does not provide any data {@see BridgeAbstract::getIcon()}
+ * is used instead as the feed's favicon URL.
+ *
+ * Use {@see XPathAbstract::getExpressionIcon()} to read this parameter
+ */
+ const XPATH_EXPRESSION_FEED_ICON = './/link[@rel="icon"]/@href';
+
+ /**
+ * XPath expression for extracting the feed items from the source page
+ * Enter an XPath expression matching a list of dom nodes, each node containing one
+ * feed article item in total (usually a surrounding <div> or <span> tag). This will
+ * be the context nodes for all of the following expressions. This expression usually
+ * starts with a single forward slash.
+ *
+ * Use {@see XPathAbstract::getExpressionItem()} to read this parameter
+ */
+ const XPATH_EXPRESSION_ITEM = '';
+
+ /**
+ * XPath expression for extracting an item title from the item context
+ * This expression should match a node contained within each article item node
+ * containing the article headline. It should start with a dot followed by two
+ * forward slashes, referring to any descendant nodes of the article item node.
+ *
+ * Use {@see XPathAbstract::getExpressionItemTitle()} to read this parameter
+ */
+ const XPATH_EXPRESSION_ITEM_TITLE = '';
+
+ /**
+ * XPath expression for extracting an item's content from the item context
+ * This expression should match a node contained within each article item node
+ * containing the article content or description. It should start with a dot
+ * followed by two forward slashes, referring to any descendant nodes of the
+ * article item node.
+ *
+ * Use {@see XPathAbstract::getExpressionItemContent()} to read this parameter
+ */
+ const XPATH_EXPRESSION_ITEM_CONTENT = '';
+
+ /**
+ * XPath expression for extracting an item link from the item context
+ * This expression should match a node's attribute containing the article URL
+ * (usually the href attribute of an <a> tag). It should start with a dot
+ * followed by two forward slashes, referring to any descendant nodes of
+ * the article item node. Attributes can be selected by prepending an @ char
+ * before the attributes name.
+ *
+ * Use {@see XPathAbstract::getExpressionItemUri()} to read this parameter
+ */
+ const XPATH_EXPRESSION_ITEM_URI = '';
+
+ /**
+ * XPath expression for extracting an item author from the item context
+ * This expression should match a node contained within each article item
+ * node containing the article author's name. It should start with a dot
+ * followed by two forward slashes, referring to any descendant nodes of
+ * the article item node.
+ *
+ * Use {@see XPathAbstract::getExpressionItemAuthor()} to read this parameter
+ */
+ const XPATH_EXPRESSION_ITEM_AUTHOR = '';
+
+ /**
+ * XPath expression for extracting an item timestamp from the item context
+ * This expression should match a node or node's attribute containing the
+ * article timestamp or date (parsable by PHP's strtotime function). It
+ * should start with a dot followed by two forward slashes, referring to
+ * any descendant nodes of the article item node. Attributes can be
+ * selected by prepending an @ char before the attributes name.
+ *
+ * Use {@see XPathAbstract::getExpressionItemTimestamp()} to read this parameter
+ */
+ const XPATH_EXPRESSION_ITEM_TIMESTAMP = '';
+
+ /**
+ * XPath expression for extracting item enclosures (media content like
+ * images or movies) from the item context
+ * This expression should match a node's attribute containing an article
+ * image URL (usually the src attribute of an <img> tag or a style
+ * attribute). It should start with a dot followed by two forward slashes,
+ * referring to any descendant nodes of the article item node. Attributes
+ * can be selected by prepending an @ char before the attributes name.
+ *
+ * Use {@see XPathAbstract::getExpressionItemEnclosures()} to read this parameter
+ */
+ const XPATH_EXPRESSION_ITEM_ENCLOSURES = '';
+
+ /**
+ * XPath expression for extracting an item category from the item context
+ * This expression should match a node or node's attribute contained
+ * within each article item node containing the article category. This
+ * could be inside <div> or <span> tags or sometimes be hidden
+ * in a data attribute. It should start with a dot followed by two
+ * forward slashes, referring to any descendant nodes of the article
+ * item node. Attributes can be selected by prepending an @ char
+ * before the attributes name.
+ *
+ * Use {@see XPathAbstract::getExpressionItemCategories()} to read this parameter
+ */
+ const XPATH_EXPRESSION_ITEM_CATEGORIES = '';
+
+ /**
+ * Fix encoding
+ * Set this to true for fixing feed encoding by invoking PHP's utf8_decode
+ * function on all extracted texts. Try this in case you see "broken" or
+ * "weird" characters in your feed where you'd normally expect umlauts
+ * or any other non-ascii characters.
+ *
+ * Use {@see XPathAbstract::getSettingFixEncoding()} to read this parameter
+ */
+ const SETTING_FIX_ENCODING = false;
+
+ /**
+ * Internal storage for resulting feed name, automatically detected
+ * @var string
+ */
+ private $feedName;
+
+ /**
+ * Internal storage for resulting feed name, automatically detected
+ * @var string
+ */
+ private $feedUri;
+
+ /**
+ * Internal storage for resulting feed favicon, automatically detected
+ * @var string
+ */
+ private $feedIcon;
+
+ public function getName()
+ {
+ return $this->feedName ?: parent::getName();
+ }
+
+ public function getURI()
+ {
+ return $this->feedUri ?: parent::getURI();
+ }
+
+ public function getIcon()
+ {
+ return $this->feedIcon ?: parent::getIcon();
+ }
+
+ /**
+ * Source Web page URL (should provide either HTML or XML content)
+ * @return string
+ */
+ protected function getSourceUrl()
+ {
+ return static::FEED_SOURCE_URL;
+ }
+
+ /**
+ * XPath expression for extracting the feed title from the source page
+ * @return string
+ */
+ protected function getExpressionTitle()
+ {
+ return static::XPATH_EXPRESSION_FEED_TITLE;
+ }
+
+ /**
+ * XPath expression for extracting the feed favicon from the source page
+ * @return string
+ */
+ protected function getExpressionIcon()
+ {
+ return static::XPATH_EXPRESSION_FEED_ICON;
+ }
+
+ /**
+ * XPath expression for extracting the feed items from the source page
+ * @return string
+ */
+ protected function getExpressionItem()
+ {
+ return static::XPATH_EXPRESSION_ITEM;
+ }
+
+ /**
+ * XPath expression for extracting an item title from the item context
+ * @return string
+ */
+ protected function getExpressionItemTitle()
+ {
+ return static::XPATH_EXPRESSION_ITEM_TITLE;
+ }
+
+ /**
+ * XPath expression for extracting an item's content from the item context
+ * @return string
+ */
+ protected function getExpressionItemContent()
+ {
+ return static::XPATH_EXPRESSION_ITEM_CONTENT;
+ }
+
+ /**
+ * XPath expression for extracting an item link from the item context
+ * @return string
+ */
+ protected function getExpressionItemUri()
+ {
+ return static::XPATH_EXPRESSION_ITEM_URI;
+ }
+
+ /**
+ * XPath expression for extracting an item author from the item context
+ * @return string
+ */
+ protected function getExpressionItemAuthor()
+ {
+ return static::XPATH_EXPRESSION_ITEM_AUTHOR;
+ }
+
+ /**
+ * XPath expression for extracting an item timestamp from the item context
+ * @return string
+ */
+ protected function getExpressionItemTimestamp()
+ {
+ return static::XPATH_EXPRESSION_ITEM_TIMESTAMP;
+ }
+
+ /**
+ * XPath expression for extracting item enclosures (media content like
+ * images or movies) from the item context
+ * @return string
+ */
+ protected function getExpressionItemEnclosures()
+ {
+ return static::XPATH_EXPRESSION_ITEM_ENCLOSURES;
+ }
+
+ /**
+ * XPath expression for extracting an item category from the item context
+ * @return string
+ */
+ protected function getExpressionItemCategories()
+ {
+ return static::XPATH_EXPRESSION_ITEM_CATEGORIES;
+ }
+
+ /**
+ * Fix encoding
+ * @return string
+ */
+ protected function getSettingFixEncoding()
+ {
+ return static::SETTING_FIX_ENCODING;
+ }
+
+ /**
+ * Internal helper method for quickly accessing all the user defined constants
+ * in derived classes
+ *
+ * @param $name
+ * @return bool|string
+ */
+ private function getParam($name)
+ {
+ switch ($name) {
+ case 'url':
+ return $this->getSourceUrl();
+ case 'feed_title':
+ return $this->getExpressionTitle();
+ case 'feed_icon':
+ return $this->getExpressionIcon();
+ case 'item':
+ return $this->getExpressionItem();
+ case 'title':
+ return $this->getExpressionItemTitle();
+ case 'content':
+ return $this->getExpressionItemContent();
+ case 'uri':
+ return $this->getExpressionItemUri();
+ case 'author':
+ return $this->getExpressionItemAuthor();
+ case 'timestamp':
+ return $this->getExpressionItemTimestamp();
+ case 'enclosures':
+ return $this->getExpressionItemEnclosures();
+ case 'categories':
+ return $this->getExpressionItemCategories();
+ case 'fix_encoding':
+ return $this->getSettingFixEncoding();
+ }
+ }
+
+ /**
+ * Should provide the source website HTML content
+ * can be easily overwritten for example if special headers or auth infos are required
+ * @return string
+ */
+ protected function provideWebsiteContent()
+ {
+ return getContents($this->feedUri);
+ }
+
+ /**
+ * Should provide the feeds title
+ *
+ * @param DOMXPath $xpath
+ * @return string
+ */
+ protected function provideFeedTitle(DOMXPath $xpath)
+ {
+ $title = $xpath->query($this->getParam('feed_title'));
+ if (count($title) === 1) {
+ return $this->getItemValueOrNodeValue($title);
+ }
+ }
+
+ /**
+ * Should provide the URL of the feed's favicon
+ *
+ * @param DOMXPath $xpath
+ * @return string
+ */
+ protected function provideFeedIcon(DOMXPath $xpath)
+ {
+ $icon = $xpath->query($this->getParam('feed_icon'));
+ if (count($icon) === 1) {
+ return $this->cleanMediaUrl($this->getItemValueOrNodeValue($icon));
+ }
+ }
+
+ /**
+ * Should provide the feed's items.
+ *
+ * @param DOMXPath $xpath
+ * @return DOMNodeList
+ */
+ protected function provideFeedItems(DOMXPath $xpath)
+ {
+ return @$xpath->query($this->getParam('item'));
+ }
+
+ public function collectData()
+ {
+ $this->feedUri = $this->getParam('url');
+
+ $webPageHtml = new DOMDocument();
+ libxml_use_internal_errors(true);
+ $webPageHtml->loadHTML($this->provideWebsiteContent());
+ libxml_clear_errors();
+ libxml_use_internal_errors(false);
+
+ $xpath = new DOMXPath($webPageHtml);
+
+ $this->feedName = $this->provideFeedTitle($xpath);
+ $this->feedIcon = $this->provideFeedIcon($xpath);
+
+ $entries = $this->provideFeedItems($xpath);
+ if ($entries === false) {
+ return;
+ }
+
+ foreach ($entries as $entry) {
+ $item = new \FeedItem();
+ foreach (['title', 'content', 'uri', 'author', 'timestamp', 'enclosures', 'categories'] as $param) {
+ $expression = $this->getParam($param);
+ if ('' === $expression) {
+ continue;
+ }
+
+ //can be a string or DOMNodeList, depending on the expression result
+ $typedResult = @$xpath->evaluate($expression, $entry);
+ if (
+ $typedResult === false || ($typedResult instanceof DOMNodeList && count($typedResult) === 0)
+ || (is_string($typedResult) && strlen(trim($typedResult)) === 0)
+ ) {
+ continue;
+ }
+
+ $item->__set($param, $this->formatParamValue($param, $this->getItemValueOrNodeValue($typedResult)));
+ }
+
+ $itemId = $this->generateItemId($item);
+ if (null !== $itemId) {
+ $item->setUid($itemId);
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * @param $param
+ * @param $value
+ * @return string|array
+ */
+ protected function formatParamValue($param, $value)
+ {
+ $value = $this->fixEncoding($value);
+ switch ($param) {
+ case 'title':
+ return $this->formatItemTitle($value);
+ case 'content':
+ return $this->formatItemContent($value);
+ case 'uri':
+ return $this->formatItemUri($value);
+ case 'author':
+ return $this->formatItemAuthor($value);
+ case 'timestamp':
+ return $this->formatItemTimestamp($value);
+ case 'enclosures':
+ return $this->formatItemEnclosures($value);
+ case 'categories':
+ return $this->formatItemCategories($value);
+ }
+ return $value;
+ }
+
+ /**
+ * Formats the title of a feed item. Takes extracted raw title and returns it formatted
+ * as string.
+ * Can be easily overwritten for in case the value needs to be transformed into something
+ * else.
+ * @param string $value
+ * @return string
+ */
+ protected function formatItemTitle($value)
+ {
+ return $value;
+ }
+
+ /**
+ * Formats the timestamp of a feed item. Takes extracted raw timestamp and returns unix
+ * timestamp as integer.
+ * Can be easily overwritten for example if a special format has to be expected on the
+ * source website.
+ * @param string $value
+ * @return string
+ */
+ protected function formatItemContent($value)
+ {
+ return $value;
+ }
+
+ /**
+ * Formats the URI of a feed item. Takes extracted raw URI and returns it formatted
+ * as string.
+ * Can be easily overwritten for in case the value needs to be transformed into something
+ * else.
+ * @param string $value
+ * @return string
+ */
+ protected function formatItemUri($value)
+ {
+ if (strlen($value) === 0) {
+ return '';
+ }
+ if (strpos($value, 'http://') === 0 || strpos($value, 'https://') === 0) {
+ return $value;
+ }
+
+ return urljoin($this->feedUri, $value);
+ }
+
+ /**
+ * Formats the author of a feed item. Takes extracted raw author and returns it formatted
+ * as string.
+ * Can be easily overwritten for in case the value needs to be transformed into something
+ * else.
+ * @param string $value
+ * @return string
+ */
+ protected function formatItemAuthor($value)
+ {
+ return $value;
+ }
+
+ /**
+ * Formats the timestamp of a feed item. Takes extracted raw timestamp and returns unix
+ * timestamp as integer.
+ * Can be easily overwritten for example if a special format has to be expected on the
+ * source website.
+ * @param string $value
+ * @return false|int
+ */
+ protected function formatItemTimestamp($value)
+ {
+ return strtotime($value);
+ }
+
+ /**
+ * Formats the enclosures of a feed item. Takes extracted raw enclosures and returns them
+ * formatted as array.
+ * Can be easily overwritten for in case the values need to be transformed into something
+ * else.
+ * @param string $value
+ * @return array
+ */
+ protected function formatItemEnclosures($value)
+ {
+ return [$this->cleanMediaUrl($value)];
+ }
+
+ /**
+ * Formats the categories of a feed item. Takes extracted raw categories and returns them
+ * formatted as array.
+ * Can be easily overwritten for in case the values need to be transformed into something
+ * else.
+ * @param string $value
+ * @return array
+ */
+ protected function formatItemCategories($value)
+ {
+ return [$value];
+ }
+
+ /**
+ * @param $mediaUrl
+ * @return string|void
+ */
+ protected function cleanMediaUrl($mediaUrl)
+ {
+ $pattern = '~(?:http(?:s)?:)?[\/a-zA-Z0-9\-=_,\.\%]+\.(?:jpg|gif|png|jpeg|ico|mp3|webp){1}~i';
+ $result = preg_match($pattern, $mediaUrl, $matches);
+ if (1 !== $result) {
+ return;
+ }
+ return urljoin($this->feedUri, $matches[0]);
+ }
+
+ /**
+ * @param $typedResult
+ * @return string
+ */
+ protected function getItemValueOrNodeValue($typedResult)
+ {
+ if ($typedResult instanceof DOMNodeList) {
+ $item = $typedResult->item(0);
+ if ($item instanceof DOMElement) {
+ return trim($item->nodeValue);
+ } elseif ($item instanceof DOMAttr) {
+ return trim($item->value);
+ } elseif ($item instanceof DOMText) {
+ return trim($item->wholeText);
+ }
+ } elseif (is_string($typedResult) && strlen($typedResult) > 0) {
+ return trim($typedResult);
+ }
+ returnServerError('Unknown type of XPath expression result.');
+ }
+
+ /**
+ * Fixes feed encoding by invoking PHP's utf8_decode function on extracted texts.
+ * Useful in case of "broken" or "weird" characters in the feed where you'd normally
+ * expect umlauts.
+ *
+ * @param $input
+ * @return string
+ */
+ protected function fixEncoding($input)
+ {
+ return $this->getParam('fix_encoding') ? utf8_decode($input) : $input;
+ }
+
+ /**
+ * Allows overriding default mechanism determining items Uid's
+ *
+ * @param FeedItem $item
+ * @return string|null
+ */
+ protected function generateItemId(\FeedItem $item)
+ {
+ return null; //auto generation
+ }
}
diff --git a/lib/contents.php b/lib/contents.php
index cc80248b..a01d81e1 100644
--- a/lib/contents.php
+++ b/lib/contents.php
@@ -1,48 +1,50 @@
<?php
-final class HttpException extends \Exception {}
+final class HttpException extends \Exception
+{
+}
// todo: move this somewhere useful, possibly into a function
const RSSBRIDGE_HTTP_STATUS_CODES = [
- '100' => 'Continue',
- '101' => 'Switching Protocols',
- '200' => 'OK',
- '201' => 'Created',
- '202' => 'Accepted',
- '203' => 'Non-Authoritative Information',
- '204' => 'No Content',
- '205' => 'Reset Content',
- '206' => 'Partial Content',
- '300' => 'Multiple Choices',
- '302' => 'Found',
- '303' => 'See Other',
- '304' => 'Not Modified',
- '305' => 'Use Proxy',
- '400' => 'Bad Request',
- '401' => 'Unauthorized',
- '402' => 'Payment Required',
- '403' => 'Forbidden',
- '404' => 'Not Found',
- '405' => 'Method Not Allowed',
- '406' => 'Not Acceptable',
- '407' => 'Proxy Authentication Required',
- '408' => 'Request Timeout',
- '409' => 'Conflict',
- '410' => 'Gone',
- '411' => 'Length Required',
- '412' => 'Precondition Failed',
- '413' => 'Request Entity Too Large',
- '414' => 'Request-URI Too Long',
- '415' => 'Unsupported Media Type',
- '416' => 'Requested Range Not Satisfiable',
- '417' => 'Expectation Failed',
- '429' => 'Too Many Requests',
- '500' => 'Internal Server Error',
- '501' => 'Not Implemented',
- '502' => 'Bad Gateway',
- '503' => 'Service Unavailable',
- '504' => 'Gateway Timeout',
- '505' => 'HTTP Version Not Supported'
+ '100' => 'Continue',
+ '101' => 'Switching Protocols',
+ '200' => 'OK',
+ '201' => 'Created',
+ '202' => 'Accepted',
+ '203' => 'Non-Authoritative Information',
+ '204' => 'No Content',
+ '205' => 'Reset Content',
+ '206' => 'Partial Content',
+ '300' => 'Multiple Choices',
+ '302' => 'Found',
+ '303' => 'See Other',
+ '304' => 'Not Modified',
+ '305' => 'Use Proxy',
+ '400' => 'Bad Request',
+ '401' => 'Unauthorized',
+ '402' => 'Payment Required',
+ '403' => 'Forbidden',
+ '404' => 'Not Found',
+ '405' => 'Method Not Allowed',
+ '406' => 'Not Acceptable',
+ '407' => 'Proxy Authentication Required',
+ '408' => 'Request Timeout',
+ '409' => 'Conflict',
+ '410' => 'Gone',
+ '411' => 'Length Required',
+ '412' => 'Precondition Failed',
+ '413' => 'Request Entity Too Large',
+ '414' => 'Request-URI Too Long',
+ '415' => 'Unsupported Media Type',
+ '416' => 'Requested Range Not Satisfiable',
+ '417' => 'Expectation Failed',
+ '429' => 'Too Many Requests',
+ '500' => 'Internal Server Error',
+ '501' => 'Not Implemented',
+ '502' => 'Bad Gateway',
+ '503' => 'Service Unavailable',
+ '504' => 'Gateway Timeout',
+ '505' => 'HTTP Version Not Supported'
];
/**
@@ -61,70 +63,70 @@ const RSSBRIDGE_HTTP_STATUS_CODES = [
* @return string|array
*/
function getContents(
- string $url,
- array $httpHeaders = [],
- array $curlOptions = [],
- bool $returnFull = false
+ string $url,
+ array $httpHeaders = [],
+ array $curlOptions = [],
+ bool $returnFull = false
) {
- $cacheFactory = new CacheFactory();
+ $cacheFactory = new CacheFactory();
- $cache = $cacheFactory->create(Configuration::getConfig('cache', 'type'));
- $cache->setScope('server');
- $cache->purgeCache(86400); // 24 hours (forced)
- $cache->setKey([$url]);
+ $cache = $cacheFactory->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope('server');
+ $cache->purgeCache(86400); // 24 hours (forced)
+ $cache->setKey([$url]);
- $config = [
- 'headers' => $httpHeaders,
- 'curl_options' => $curlOptions,
- ];
- if (defined('PROXY_URL') && !defined('NOPROXY')) {
- $config['proxy'] = PROXY_URL;
- }
- if(!Debug::isEnabled() && $cache->getTime()) {
- $config['if_not_modified_since'] = $cache->getTime();
- }
+ $config = [
+ 'headers' => $httpHeaders,
+ 'curl_options' => $curlOptions,
+ ];
+ if (defined('PROXY_URL') && !defined('NOPROXY')) {
+ $config['proxy'] = PROXY_URL;
+ }
+ if (!Debug::isEnabled() && $cache->getTime()) {
+ $config['if_not_modified_since'] = $cache->getTime();
+ }
- $result = _http_request($url, $config);
- $response = [
- 'code' => $result['code'],
- 'status_lines' => $result['status_lines'],
- 'header' => $result['headers'],
- 'content' => $result['body'],
- ];
+ $result = _http_request($url, $config);
+ $response = [
+ 'code' => $result['code'],
+ 'status_lines' => $result['status_lines'],
+ 'header' => $result['headers'],
+ 'content' => $result['body'],
+ ];
- switch($result['code']) {
- case 200:
- case 201:
- case 202:
- if(isset($result['headers']['cache-control'])) {
- $cachecontrol = $result['headers']['cache-control'];
- $lastValue = array_pop($cachecontrol);
- $directives = explode(',', $lastValue);
- $directives = array_map('trim', $directives);
- if(in_array('no-cache', $directives) || in_array('no-store', $directives)) {
- // Don't cache as instructed by the server
- break;
- }
- }
- $cache->saveData($result['body']);
- break;
- case 304: // Not Modified
- $response['content'] = $cache->loadData();
- break;
- default:
- throw new HttpException(
- sprintf(
- '%s %s',
- $result['code'],
- RSSBRIDGE_HTTP_STATUS_CODES[$result['code']] ?? ''
- ),
- $result['code']
- );
- }
- if ($returnFull === true) {
- return $response;
- }
- return $response['content'];
+ switch ($result['code']) {
+ case 200:
+ case 201:
+ case 202:
+ if (isset($result['headers']['cache-control'])) {
+ $cachecontrol = $result['headers']['cache-control'];
+ $lastValue = array_pop($cachecontrol);
+ $directives = explode(',', $lastValue);
+ $directives = array_map('trim', $directives);
+ if (in_array('no-cache', $directives) || in_array('no-store', $directives)) {
+ // Don't cache as instructed by the server
+ break;
+ }
+ }
+ $cache->saveData($result['body']);
+ break;
+ case 304: // Not Modified
+ $response['content'] = $cache->loadData();
+ break;
+ default:
+ throw new HttpException(
+ sprintf(
+ '%s %s',
+ $result['code'],
+ RSSBRIDGE_HTTP_STATUS_CODES[$result['code']] ?? ''
+ ),
+ $result['code']
+ );
+ }
+ if ($returnFull === true) {
+ return $response;
+ }
+ return $response['content'];
}
/**
@@ -136,85 +138,85 @@ function getContents(
*/
function _http_request(string $url, array $config = []): array
{
- $defaults = [
- 'useragent' => Configuration::getConfig('http', 'useragent'),
- 'timeout' => Configuration::getConfig('http', 'timeout'),
- 'headers' => [],
- 'proxy' => null,
- 'curl_options' => [],
- 'if_not_modified_since' => null,
- 'retries' => 3,
- ];
- $config = array_merge($defaults, $config);
+ $defaults = [
+ 'useragent' => Configuration::getConfig('http', 'useragent'),
+ 'timeout' => Configuration::getConfig('http', 'timeout'),
+ 'headers' => [],
+ 'proxy' => null,
+ 'curl_options' => [],
+ 'if_not_modified_since' => null,
+ 'retries' => 3,
+ ];
+ $config = array_merge($defaults, $config);
- $ch = curl_init($url);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
- curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
- curl_setopt($ch, CURLOPT_HEADER, false);
- curl_setopt($ch, CURLOPT_HTTPHEADER, $config['headers']);
- curl_setopt($ch, CURLOPT_USERAGENT, $config['useragent']);
- curl_setopt($ch, CURLOPT_TIMEOUT, $config['timeout']);
- curl_setopt($ch, CURLOPT_ENCODING, '');
- curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
- if($config['proxy']) {
- curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
- }
- if (curl_setopt_array($ch, $config['curl_options']) === false) {
- throw new \Exception('Tried to set an illegal curl option');
- }
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
+ curl_setopt($ch, CURLOPT_HEADER, false);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $config['headers']);
+ curl_setopt($ch, CURLOPT_USERAGENT, $config['useragent']);
+ curl_setopt($ch, CURLOPT_TIMEOUT, $config['timeout']);
+ curl_setopt($ch, CURLOPT_ENCODING, '');
+ curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
+ if ($config['proxy']) {
+ curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
+ }
+ if (curl_setopt_array($ch, $config['curl_options']) === false) {
+ throw new \Exception('Tried to set an illegal curl option');
+ }
- if ($config['if_not_modified_since']) {
- curl_setopt($ch, CURLOPT_TIMEVALUE, $config['if_not_modified_since']);
- curl_setopt($ch, CURLOPT_TIMECONDITION, CURL_TIMECOND_IFMODSINCE);
- }
+ if ($config['if_not_modified_since']) {
+ curl_setopt($ch, CURLOPT_TIMEVALUE, $config['if_not_modified_since']);
+ curl_setopt($ch, CURLOPT_TIMECONDITION, CURL_TIMECOND_IFMODSINCE);
+ }
- $responseStatusLines = [];
- $responseHeaders = [];
- curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $rawHeader) use (&$responseHeaders, &$responseStatusLines) {
- $len = strlen($rawHeader);
- if ($rawHeader === "\r\n") {
- return $len;
- }
- if (preg_match('#^HTTP/(2|1.1|1.0)#', $rawHeader)) {
- $responseStatusLines[] = $rawHeader;
- return $len;
- }
- $header = explode(':', $rawHeader);
- if (count($header) === 1) {
- return $len;
- }
- $name = mb_strtolower(trim($header[0]));
- $value = trim(implode(':', array_slice($header, 1)));
- if (!isset($responseHeaders[$name])) {
- $responseHeaders[$name] = [];
- }
- $responseHeaders[$name][] = $value;
- return $len;
- });
+ $responseStatusLines = [];
+ $responseHeaders = [];
+ curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $rawHeader) use (&$responseHeaders, &$responseStatusLines) {
+ $len = strlen($rawHeader);
+ if ($rawHeader === "\r\n") {
+ return $len;
+ }
+ if (preg_match('#^HTTP/(2|1.1|1.0)#', $rawHeader)) {
+ $responseStatusLines[] = $rawHeader;
+ return $len;
+ }
+ $header = explode(':', $rawHeader);
+ if (count($header) === 1) {
+ return $len;
+ }
+ $name = mb_strtolower(trim($header[0]));
+ $value = trim(implode(':', array_slice($header, 1)));
+ if (!isset($responseHeaders[$name])) {
+ $responseHeaders[$name] = [];
+ }
+ $responseHeaders[$name][] = $value;
+ return $len;
+ });
- $attempts = 0;
- while(true) {
- $attempts++;
- $data = curl_exec($ch);
- if ($data !== false) {
- // The network call was successful, so break out of the loop
- break;
- }
- if ($attempts > $config['retries']) {
- // Finally give up
- throw new HttpException(sprintf('%s (%s)', curl_error($ch), curl_errno($ch)));
- }
- }
+ $attempts = 0;
+ while (true) {
+ $attempts++;
+ $data = curl_exec($ch);
+ if ($data !== false) {
+ // The network call was successful, so break out of the loop
+ break;
+ }
+ if ($attempts > $config['retries']) {
+ // Finally give up
+ throw new HttpException(sprintf('%s (%s)', curl_error($ch), curl_errno($ch)));
+ }
+ }
- $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- curl_close($ch);
- return [
- 'code' => $statusCode,
- 'status_lines' => $responseStatusLines,
- 'headers' => $responseHeaders,
- 'body' => $data,
- ];
+ $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+ return [
+ 'code' => $statusCode,
+ 'status_lines' => $responseStatusLines,
+ 'headers' => $responseHeaders,
+ 'body' => $data,
+ ];
}
/**
@@ -243,28 +245,31 @@ function _http_request(string $url, array $config = []): array
* tags when returning plaintext.
* @return false|simple_html_dom Contents as simplehtmldom object.
*/
-function getSimpleHTMLDOM($url,
- $header = array(),
- $opts = array(),
- $lowercase = true,
- $forceTagsClosed = true,
- $target_charset = DEFAULT_TARGET_CHARSET,
- $stripRN = true,
- $defaultBRText = DEFAULT_BR_TEXT,
- $defaultSpanText = DEFAULT_SPAN_TEXT){
-
- $content = getContents(
- $url,
- $header ?? [],
- $opts ?? []
- );
- return str_get_html($content,
- $lowercase,
- $forceTagsClosed,
- $target_charset,
- $stripRN,
- $defaultBRText,
- $defaultSpanText);
+function getSimpleHTMLDOM(
+ $url,
+ $header = [],
+ $opts = [],
+ $lowercase = true,
+ $forceTagsClosed = true,
+ $target_charset = DEFAULT_TARGET_CHARSET,
+ $stripRN = true,
+ $defaultBRText = DEFAULT_BR_TEXT,
+ $defaultSpanText = DEFAULT_SPAN_TEXT
+) {
+ $content = getContents(
+ $url,
+ $header ?? [],
+ $opts ?? []
+ );
+ return str_get_html(
+ $content,
+ $lowercase,
+ $forceTagsClosed,
+ $target_charset,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText
+ );
}
/**
@@ -297,53 +302,58 @@ function getSimpleHTMLDOM($url,
* tags when returning plaintext.
* @return false|simple_html_dom Contents as simplehtmldom object.
*/
-function getSimpleHTMLDOMCached($url,
- $duration = 86400,
- $header = array(),
- $opts = array(),
- $lowercase = true,
- $forceTagsClosed = true,
- $target_charset = DEFAULT_TARGET_CHARSET,
- $stripRN = true,
- $defaultBRText = DEFAULT_BR_TEXT,
- $defaultSpanText = DEFAULT_SPAN_TEXT){
-
- Debug::log('Caching url ' . $url . ', duration ' . $duration);
+function getSimpleHTMLDOMCached(
+ $url,
+ $duration = 86400,
+ $header = [],
+ $opts = [],
+ $lowercase = true,
+ $forceTagsClosed = true,
+ $target_charset = DEFAULT_TARGET_CHARSET,
+ $stripRN = true,
+ $defaultBRText = DEFAULT_BR_TEXT,
+ $defaultSpanText = DEFAULT_SPAN_TEXT
+) {
+ Debug::log('Caching url ' . $url . ', duration ' . $duration);
- // Initialize cache
- $cacheFac = new CacheFactory();
+ // Initialize cache
+ $cacheFac = new CacheFactory();
- $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $cache->setScope('pages');
- $cache->purgeCache(86400); // 24 hours (forced)
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope('pages');
+ $cache->purgeCache(86400); // 24 hours (forced)
- $params = array($url);
- $cache->setKey($params);
+ $params = [$url];
+ $cache->setKey($params);
- // Determine if cached file is within duration
- $time = $cache->getTime();
- if($time !== false
- && (time() - $duration < $time)
- && !Debug::isEnabled()) { // Contents within duration
- $content = $cache->loadData();
- } else { // Content not within duration
- $content = getContents(
- $url,
- $header ?? [],
- $opts ?? []
- );
- if($content !== false) {
- $cache->saveData($content);
- }
- }
+ // Determine if cached file is within duration
+ $time = $cache->getTime();
+ if (
+ $time !== false
+ && (time() - $duration < $time)
+ && !Debug::isEnabled()
+ ) { // Contents within duration
+ $content = $cache->loadData();
+ } else { // Content not within duration
+ $content = getContents(
+ $url,
+ $header ?? [],
+ $opts ?? []
+ );
+ if ($content !== false) {
+ $cache->saveData($content);
+ }
+ }
- return str_get_html($content,
- $lowercase,
- $forceTagsClosed,
- $target_charset,
- $stripRN,
- $defaultBRText,
- $defaultSpanText);
+ return str_get_html(
+ $content,
+ $lowercase,
+ $forceTagsClosed,
+ $target_charset,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText
+ );
}
/**
@@ -360,49 +370,53 @@ function getSimpleHTMLDOMCached($url,
* @param string $url The URL or path to the file.
* @return string The MIME type of the file.
*/
-function getMimeType($url) {
- static $mime = null;
+function getMimeType($url)
+{
+ static $mime = null;
- if (is_null($mime)) {
- // Default values, overriden by /etc/mime.types when present
- $mime = array(
- 'jpg' => 'image/jpeg',
- 'gif' => 'image/gif',
- 'png' => 'image/png',
- 'image' => 'image/*',
- 'mp3' => 'audio/mpeg',
- );
- // '@' is used to mute open_basedir warning, see issue #818
- if (@is_readable('/etc/mime.types')) {
- $file = fopen('/etc/mime.types', 'r');
- while(($line = fgets($file)) !== false) {
- $line = trim(preg_replace('/#.*/', '', $line));
- if(!$line)
- continue;
- $parts = preg_split('/\s+/', $line);
- if(count($parts) == 1)
- continue;
- $type = array_shift($parts);
- foreach($parts as $part)
- $mime[$part] = $type;
- }
- fclose($file);
- }
- }
+ if (is_null($mime)) {
+ // Default values, overriden by /etc/mime.types when present
+ $mime = [
+ 'jpg' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'png' => 'image/png',
+ 'image' => 'image/*',
+ 'mp3' => 'audio/mpeg',
+ ];
+ // '@' is used to mute open_basedir warning, see issue #818
+ if (@is_readable('/etc/mime.types')) {
+ $file = fopen('/etc/mime.types', 'r');
+ while (($line = fgets($file)) !== false) {
+ $line = trim(preg_replace('/#.*/', '', $line));
+ if (!$line) {
+ continue;
+ }
+ $parts = preg_split('/\s+/', $line);
+ if (count($parts) == 1) {
+ continue;
+ }
+ $type = array_shift($parts);
+ foreach ($parts as $part) {
+ $mime[$part] = $type;
+ }
+ }
+ fclose($file);
+ }
+ }
- if (strpos($url, '?') !== false) {
- $url_temp = substr($url, 0, strpos($url, '?'));
- if (strpos($url, '#') !== false) {
- $anchor = substr($url, strpos($url, '#'));
- $url_temp .= $anchor;
- }
- $url = $url_temp;
- }
+ if (strpos($url, '?') !== false) {
+ $url_temp = substr($url, 0, strpos($url, '?'));
+ if (strpos($url, '#') !== false) {
+ $anchor = substr($url, strpos($url, '#'));
+ $url_temp .= $anchor;
+ }
+ $url = $url_temp;
+ }
- $ext = strtolower(pathinfo($url, PATHINFO_EXTENSION));
- if (!empty($mime[$ext])) {
- return $mime[$ext];
- }
+ $ext = strtolower(pathinfo($url, PATHINFO_EXTENSION));
+ if (!empty($mime[$ext])) {
+ return $mime[$ext];
+ }
- return 'application/octet-stream';
+ return 'application/octet-stream';
}
diff --git a/lib/error.php b/lib/error.php
index c2f26247..f9950cea 100644
--- a/lib/error.php
+++ b/lib/error.php
@@ -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
*/
/**
@@ -20,8 +21,9 @@
* @link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes List of HTTP
* status codes
*/
-function returnError($message, $code){
- throw new \Exception($message, $code);
+function returnError($message, $code)
+{
+ throw new \Exception($message, $code);
}
/**
@@ -29,8 +31,9 @@ function returnError($message, $code){
*
* @param string $message The error message
*/
-function returnClientError($message){
- returnError($message, 400);
+function returnClientError($message)
+{
+ returnError($message, 400);
}
/**
@@ -38,8 +41,9 @@ function returnClientError($message){
*
* @param string $message The error message
*/
-function returnServerError($message){
- returnError($message, 500);
+function returnServerError($message)
+{
+ returnError($message, 500);
}
/**
@@ -50,27 +54,28 @@ function returnServerError($message){
*
* @return int The total number the same error has appeared
*/
-function logBridgeError($bridgeName, $code) {
- $cacheFac = new CacheFactory();
+function logBridgeError($bridgeName, $code)
+{
+ $cacheFac = new CacheFactory();
- $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
- $cache->setScope('error_reporting');
- $cache->setkey($bridgeName . '_' . $code);
- $cache->purgeCache(86400); // 24 hours
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope('error_reporting');
+ $cache->setkey($bridgeName . '_' . $code);
+ $cache->purgeCache(86400); // 24 hours
- if($report = $cache->loadData()) {
- $report = json_decode($report, true);
- $report['time'] = time();
- $report['count']++;
- } else {
- $report = array(
- 'error' => $code,
- 'time' => time(),
- 'count' => 1,
- );
- }
+ if ($report = $cache->loadData()) {
+ $report = json_decode($report, true);
+ $report['time'] = time();
+ $report['count']++;
+ } else {
+ $report = [
+ 'error' => $code,
+ 'time' => time(),
+ 'count' => 1,
+ ];
+ }
- $cache->saveData(json_encode($report));
+ $cache->saveData(json_encode($report));
- return $report['count'];
+ return $report['count'];
}
diff --git a/lib/html.php b/lib/html.php
index 69bd1424..e82d5e0e 100644
--- a/lib/html.php
+++ b/lib/html.php
@@ -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
*/
/**
@@ -25,27 +26,29 @@
* @todo Check if this implementation is still necessary, because simplehtmldom
* already removes some of the tags (search for `remove_noise` in simple_html_dom.php).
*/
-function sanitize($html,
- $tags_to_remove = array('script', 'iframe', 'input', 'form'),
- $attributes_to_keep = array('title', 'href', 'src'),
- $text_to_keep = array()){
-
- $htmlContent = str_get_html($html);
-
- foreach($htmlContent->find('*') as $element) {
- if(in_array($element->tag, $text_to_keep)) {
- $element->outertext = $element->plaintext;
- } elseif(in_array($element->tag, $tags_to_remove)) {
- $element->outertext = '';
- } else {
- foreach($element->getAllAttributes() as $attributeName => $attribute) {
- if(!in_array($attributeName, $attributes_to_keep))
- $element->removeAttribute($attributeName);
- }
- }
- }
-
- return $htmlContent;
+function sanitize(
+ $html,
+ $tags_to_remove = ['script', 'iframe', 'input', 'form'],
+ $attributes_to_keep = ['title', 'href', 'src'],
+ $text_to_keep = []
+) {
+ $htmlContent = str_get_html($html);
+
+ foreach ($htmlContent->find('*') as $element) {
+ if (in_array($element->tag, $text_to_keep)) {
+ $element->outertext = $element->plaintext;
+ } elseif (in_array($element->tag, $tags_to_remove)) {
+ $element->outertext = '';
+ } else {
+ foreach ($element->getAllAttributes() as $attributeName => $attribute) {
+ if (!in_array($attributeName, $attributes_to_keep)) {
+ $element->removeAttribute($attributeName);
+ }
+ }
+ }
+ }
+
+ return $htmlContent;
}
/**
@@ -74,23 +77,18 @@ function sanitize($html,
* @param string $htmlContent The HTML content
* @return string The HTML content with all ocurrences replaced
*/
-function backgroundToImg($htmlContent) {
-
- $regex = '/background-image[ ]{0,}:[ ]{0,}url\([\'"]{0,}(.*?)[\'"]{0,}\)/';
- $htmlContent = str_get_html($htmlContent);
-
- foreach($htmlContent->find('*') as $element) {
-
- if(preg_match($regex, $element->style, $matches) > 0) {
-
- $element->outertext = '<img style="display:block;" src="' . $matches[1] . '" />';
-
- }
-
- }
-
- return $htmlContent;
-
+function backgroundToImg($htmlContent)
+{
+ $regex = '/background-image[ ]{0,}:[ ]{0,}url\([\'"]{0,}(.*?)[\'"]{0,}\)/';
+ $htmlContent = str_get_html($htmlContent);
+
+ foreach ($htmlContent->find('*') as $element) {
+ if (preg_match($regex, $element->style, $matches) > 0) {
+ $element->outertext = '<img style="display:block;" src="' . $matches[1] . '" />';
+ }
+ }
+
+ return $htmlContent;
}
/**
@@ -104,26 +102,27 @@ function backgroundToImg($htmlContent) {
* @param string $server Fully qualified URL to the page containing relative links
* @return object Content with fixed URLs.
*/
-function defaultLinkTo($content, $server){
- $string_convert = false;
- if (is_string($content)) {
- $string_convert = true;
- $content = str_get_html($content);
- }
-
- foreach($content->find('img') as $image) {
- $image->src = urljoin($server, $image->src);
- }
-
- foreach($content->find('a') as $anchor) {
- $anchor->href = urljoin($server, $anchor->href);
- }
-
- if ($string_convert) {
- $content = $content->outertext;
- }
-
- return $content;
+function defaultLinkTo($content, $server)
+{
+ $string_convert = false;
+ if (is_string($content)) {
+ $string_convert = true;
+ $content = str_get_html($content);
+ }
+
+ foreach ($content->find('img') as $image) {
+ $image->src = urljoin($server, $image->src);
+ }
+
+ foreach ($content->find('a') as $anchor) {
+ $anchor->href = urljoin($server, $anchor->href);
+ }
+
+ if ($string_convert) {
+ $content = $content->outertext;
+ }
+
+ return $content;
}
/**
@@ -135,12 +134,13 @@ function defaultLinkTo($content, $server){
* @return string|bool Extracted string, e.g. `John Doe`, or false if the
* delimiters were not found.
*/
-function extractFromDelimiters($string, $start, $end) {
- if (strpos($string, $start) !== false) {
- $section_retrieved = substr($string, strpos($string, $start) + strlen($start));
- $section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
- return $section_retrieved;
- } return false;
+function extractFromDelimiters($string, $start, $end)
+{
+ if (strpos($string, $start) !== false) {
+ $section_retrieved = substr($string, strpos($string, $start) + strlen($start));
+ $section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
+ return $section_retrieved;
+ } return false;
}
/**
@@ -151,13 +151,14 @@ function extractFromDelimiters($string, $start, $end) {
* @param string $end End delimiter, e.g. `</script>`
* @return string Cleaned string, e.g. `foobar`
*/
-function stripWithDelimiters($string, $start, $end) {
- while(strpos($string, $start) !== false) {
- $section_to_remove = substr($string, strpos($string, $start));
- $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
- $string = str_replace($section_to_remove, '', $string);
- }
- return $string;
+function stripWithDelimiters($string, $start, $end)
+{
+ while (strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ }
+ return $string;
}
/**
@@ -170,28 +171,29 @@ function stripWithDelimiters($string, $start, $end) {
*
* @todo This function needs more documentation to make it maintainable.
*/
-function stripRecursiveHTMLSection($string, $tag_name, $tag_start){
- $open_tag = '<' . $tag_name;
- $close_tag = '</' . $tag_name . '>';
- $close_tag_length = strlen($close_tag);
- if(strpos($tag_start, $open_tag) === 0) {
- while(strpos($string, $tag_start) !== false) {
- $max_recursion = 100;
- $section_to_remove = null;
- $section_start = strpos($string, $tag_start);
- $search_offset = $section_start;
- do {
- $max_recursion--;
- $section_end = strpos($string, $close_tag, $search_offset);
- $search_offset = $section_end + $close_tag_length;
- $section_to_remove = substr($string, $section_start, $section_end - $section_start + $close_tag_length);
- $open_tag_count = substr_count($section_to_remove, $open_tag);
- $close_tag_count = substr_count($section_to_remove, $close_tag);
- } while ($open_tag_count > $close_tag_count && $max_recursion > 0);
- $string = str_replace($section_to_remove, '', $string);
- }
- }
- return $string;
+function stripRecursiveHTMLSection($string, $tag_name, $tag_start)
+{
+ $open_tag = '<' . $tag_name;
+ $close_tag = '</' . $tag_name . '>';
+ $close_tag_length = strlen($close_tag);
+ if (strpos($tag_start, $open_tag) === 0) {
+ while (strpos($string, $tag_start) !== false) {
+ $max_recursion = 100;
+ $section_to_remove = null;
+ $section_start = strpos($string, $tag_start);
+ $search_offset = $section_start;
+ do {
+ $max_recursion--;
+ $section_end = strpos($string, $close_tag, $search_offset);
+ $search_offset = $section_end + $close_tag_length;
+ $section_to_remove = substr($string, $section_start, $section_end - $section_start + $close_tag_length);
+ $open_tag_count = substr_count($section_to_remove, $open_tag);
+ $close_tag_count = substr_count($section_to_remove, $close_tag);
+ } while ($open_tag_count > $close_tag_count && $max_recursion > 0);
+ $string = str_replace($section_to_remove, '', $string);
+ }
+ }
+ return $string;
}
/**
@@ -202,8 +204,8 @@ function stripRecursiveHTMLSection($string, $tag_name, $tag_start){
* @param string $string Input string in Markdown format
* @return string output string in HTML format
*/
-function markdownToHtml($string) {
-
- $Parsedown = new Parsedown();
- return $Parsedown->text($string);
+function markdownToHtml($string)
+{
+ $Parsedown = new Parsedown();
+ return $Parsedown->text($string);
}
diff --git a/lib/php8backports.php b/lib/php8backports.php
index 3b2bb966..30dfdbd9 100644
--- a/lib/php8backports.php
+++ b/lib/php8backports.php
@@ -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
*/
// based on https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/Str.php
@@ -34,19 +35,22 @@
// THE SOFTWARE.
if (!function_exists('str_starts_with')) {
- function str_starts_with($haystack, $needle) {
- return (string)$needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0;
- }
+ function str_starts_with($haystack, $needle)
+ {
+ return (string)$needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0;
+ }
}
if (!function_exists('str_ends_with')) {
- function str_ends_with($haystack, $needle) {
- return $needle !== '' && substr($haystack, -strlen($needle)) === (string)$needle;
- }
+ function str_ends_with($haystack, $needle)
+ {
+ return $needle !== '' && substr($haystack, -strlen($needle)) === (string)$needle;
+ }
}
if (!function_exists('str_contains')) {
- function str_contains($haystack, $needle) {
- return $needle !== '' && mb_strpos($haystack, $needle) !== false;
- }
+ function str_contains($haystack, $needle)
+ {
+ return $needle !== '' && mb_strpos($haystack, $needle) !== false;
+ }
}
diff --git a/lib/rssbridge.php b/lib/rssbridge.php
index cd156fe8..560c0fe4 100644
--- a/lib/rssbridge.php
+++ b/lib/rssbridge.php
@@ -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
*/
/** Path to the root folder of RSS-Bridge (where index.php is located) */
@@ -64,19 +65,19 @@ require_once PATH_LIB_VENDOR . 'php-urljoin/src/urljoin.php';
require_once PATH_LIB_VENDOR . 'simplehtmldom/simple_html_dom.php';
spl_autoload_register(function ($className) {
- $folders = [
- __DIR__ . '/../actions/',
- __DIR__ . '/../bridges/',
- __DIR__ . '/../caches/',
- __DIR__ . '/../formats/',
- __DIR__ . '/../lib/',
- ];
- foreach ($folders as $folder) {
- $file = $folder . $className . '.php';
- if (is_file($file)) {
- require $file;
- }
- }
+ $folders = [
+ __DIR__ . '/../actions/',
+ __DIR__ . '/../bridges/',
+ __DIR__ . '/../caches/',
+ __DIR__ . '/../formats/',
+ __DIR__ . '/../lib/',
+ ];
+ foreach ($folders as $folder) {
+ $file = $folder . $className . '.php';
+ if (is_file($file)) {
+ require $file;
+ }
+ }
});
Configuration::verifyInstallation();
diff --git a/phpcs.xml b/phpcs.xml
index 72e4e046..3f4450d2 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -5,10 +5,28 @@
<exclude-pattern>./vendor</exclude-pattern>
<exclude-pattern>./config.default.ini.php</exclude-pattern>
<exclude-pattern>./config.ini.php</exclude-pattern>
+
+ <rule ref="PSR12">
+ <exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace"/>
+ <exclude name="PSR1.Files.SideEffects.FoundWithSymbols"/>
+ <exclude name="PSR12.Properties.ConstantVisibility.NotFound"/>
+ </rule>
+
+ <rule ref="Generic.Arrays.DisallowLongArraySyntax" />
+
+ <rule ref="Squiz.WhiteSpace.FunctionOpeningBraceSpace" />
+
+ <rule ref="Generic.Files.LineLength">
+ <properties>
+ <property name="lineLimit" value="140"/>
+ <property name="absoluteLineLimit" value="140"/>
+ <property name="ignoreComments" value="true"/>
+ </properties>
+ </rule>
+
<!-- Duplicate class names are not allowed -->
<rule ref="Generic.Classes.DuplicateClassName"/>
- <!-- Statements must not be empty -->
- <rule ref="Generic.CodeAnalysis.EmptyStatement"/>
+
<!-- Unconditional if-statements are not allowed -->
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
<!-- Do not use final statements inside final classes -->
@@ -24,15 +42,7 @@
<property name="ignoreNewlines" value="true"/>
</properties>
</rule>
- <!-- One line should not have more than 80 characters -->
- <!-- One line must never exceed 120 characters -->
- <rule ref="Generic.Files.LineLength">
- <properties>
- <property name="lineLimit" value="80"/>
- <property name="absoluteLineLimit" value="120"/>
- <property name="ignoreComments" value="true"/>
- </properties>
- </rule>
+
<!-- When calling a function: -->
<!-- Do not add a space before the opening parenthesis -->
<!-- Do not add a space after the opening parenthesis -->
@@ -46,37 +56,7 @@
<rule ref="Generic.PHP.LowerCaseConstant"/>
<!-- Use a single string instead of concating -->
<rule ref="Generic.Strings.UnnecessaryStringConcat"/>
- <!-- Use tabs for indentation -->
- <rule ref="Generic.WhiteSpace.DisallowSpaceIndent"/>
- <!-- Parameters with default values must appear last in functions -->
- <rule ref="PEAR.Functions.ValidDefaultValue"/>
- <!-- Use PascalCase for class names -->
- <rule ref="PEAR.NamingConventions.ValidClassName"/>
- <!-- abstract and final declarations MUST precede the visibility declaration -->
- <!-- static declaration MUST come after the visibility declaration -->
- <rule ref="PSR2.Methods.MethodDeclaration" />
- <!-- Use 'elseif' instead of 'else if' -->
- <rule ref="PSR2.ControlStructures.ElseIfDeclaration"/>
- <!-- Do not add spaces after opening or before closing bracket -->
- <rule ref="PSR2.ControlStructures.ControlStructureSpacing"/>
- <!-- Add a new line at the end of a file -->
- <rule ref="PSR2.Files.EndFileNewline"/>
- <!-- Add space after closing parenthesis -->
- <!-- Add body into new line -->
- <!-- Close body in new line -->
- <rule ref="Squiz.ControlStructures.ControlSignature">
- <!-- No space after keyword (before opening parenthesis) -->
- <exclude name="Squiz.ControlStructures.ControlSignature.SpaceAfterKeyword"/>
- </rule>
- <!-- When declaring a function: -->
- <!-- Do not add a space before a comma -->
- <!-- Add a space after a comma -->
- <!-- Add a space before and after an equal sign -->
- <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing">
- <properties>
- <property name="equalsSpacing" value="1"/>
- </properties>
- </rule>
+
<!-- Do not add spaces when casting -->
<rule ref="Squiz.WhiteSpace.CastSpacing"/>
<!-- Operators must have a space around them -->
@@ -93,13 +73,7 @@
<property name="ignoreBlankLines" value="false"/>
</properties>
</rule>
- <rule ref="Squiz.WhiteSpace.FunctionSpacing">
- <properties>
- <property name="spacing" value="1" />
- <property name="spacingBeforeFirst" value="0" />
- <property name="spacingAfterLast" value="0" />
- </properties>
- </rule>
+
<!-- Whenever possible use single quote strings -->
<rule ref="Squiz.Strings.DoubleQuoteUsage">
<exclude name="Squiz.Strings.DoubleQuoteUsage.ContainsVar" />
diff --git a/tests/Actions/ActionImplementationTest.php b/tests/Actions/ActionImplementationTest.php
index 0caf6d80..3f063682 100644
--- a/tests/Actions/ActionImplementationTest.php
+++ b/tests/Actions/ActionImplementationTest.php
@@ -5,54 +5,60 @@ namespace RssBridge\Tests\Actions;
use ActionInterface;
use PHPUnit\Framework\TestCase;
-class ActionImplementationTest extends TestCase {
- private $class;
- private $obj;
-
- /**
- * @dataProvider dataActionsProvider
- */
- public function testClassName($path) {
- $this->setAction($path);
- $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
- $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
- $this->assertStringEndsWith('Action', $this->class, 'class name must end with "Action"');
- }
-
- /**
- * @dataProvider dataActionsProvider
- */
- public function testClassType($path) {
- $this->setAction($path);
- $this->assertInstanceOf(ActionInterface::class, $this->obj);
- }
-
- /**
- * @dataProvider dataActionsProvider
- */
- public function testVisibleMethods($path) {
- $allowedMethods = get_class_methods(ActionInterface::class);
- sort($allowedMethods);
-
- $this->setAction($path);
-
- $methods = get_class_methods($this->obj);
- sort($methods);
-
- $this->assertEquals($allowedMethods, $methods);
- }
-
- public function dataActionsProvider() {
- $actions = array();
- foreach (glob(PATH_LIB_ACTIONS . '*.php') as $path) {
- $actions[basename($path, '.php')] = array($path);
- }
- return $actions;
- }
-
- private function setAction($path) {
- $this->class = '\\' . basename($path, '.php');
- $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
- $this->obj = new $this->class();
- }
+class ActionImplementationTest extends TestCase
+{
+ private $class;
+ private $obj;
+
+ /**
+ * @dataProvider dataActionsProvider
+ */
+ public function testClassName($path)
+ {
+ $this->setAction($path);
+ $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
+ $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
+ $this->assertStringEndsWith('Action', $this->class, 'class name must end with "Action"');
+ }
+
+ /**
+ * @dataProvider dataActionsProvider
+ */
+ public function testClassType($path)
+ {
+ $this->setAction($path);
+ $this->assertInstanceOf(ActionInterface::class, $this->obj);
+ }
+
+ /**
+ * @dataProvider dataActionsProvider
+ */
+ public function testVisibleMethods($path)
+ {
+ $allowedMethods = get_class_methods(ActionInterface::class);
+ sort($allowedMethods);
+
+ $this->setAction($path);
+
+ $methods = get_class_methods($this->obj);
+ sort($methods);
+
+ $this->assertEquals($allowedMethods, $methods);
+ }
+
+ public function dataActionsProvider()
+ {
+ $actions = [];
+ foreach (glob(PATH_LIB_ACTIONS . '*.php') as $path) {
+ $actions[basename($path, '.php')] = [$path];
+ }
+ return $actions;
+ }
+
+ private function setAction($path)
+ {
+ $this->class = '\\' . basename($path, '.php');
+ $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
+ $this->obj = new $this->class();
+ }
}
diff --git a/tests/Actions/ListActionTest.php b/tests/Actions/ListActionTest.php
index 1ecf50ed..2eb2049d 100644
--- a/tests/Actions/ListActionTest.php
+++ b/tests/Actions/ListActionTest.php
@@ -6,85 +6,88 @@ use ActionFactory;
use BridgeFactory;
use PHPUnit\Framework\TestCase;
-class ListActionTest extends TestCase {
-
- private $data;
-
- /**
- * @runInSeparateProcess
- * @requires function xdebug_get_headers
- */
- public function testHeaders() {
- $this->initAction();
-
- $this->assertContains(
- 'Content-Type: application/json',
- xdebug_get_headers()
- );
- }
-
- /**
- * @runInSeparateProcess
- */
- public function testOutput() {
- $this->initAction();
-
- $items = json_decode($this->data, true);
-
- $this->assertNotNull($items, 'invalid JSON output: ' . json_last_error_msg());
-
- $this->assertArrayHasKey('total', $items, 'Missing "total" parameter');
- $this->assertIsInt($items['total'], 'Invalid type');
-
- $this->assertArrayHasKey('bridges', $items, 'Missing "bridges" array');
-
- $this->assertEquals(
- $items['total'],
- count($items['bridges']),
- 'Item count doesn\'t match'
- );
-
- $bridgeFac = new BridgeFactory();
-
- $this->assertEquals(
- count($bridgeFac->getBridgeNames()),
- count($items['bridges']),
- 'Number of bridges doesn\'t match'
- );
-
- $expectedKeys = array(
- 'status',
- 'uri',
- 'name',
- 'icon',
- 'parameters',
- 'maintainer',
- 'description'
- );
-
- $allowedStatus = array(
- 'active',
- 'inactive'
- );
-
- foreach($items['bridges'] as $bridge) {
- foreach($expectedKeys as $key) {
- $this->assertArrayHasKey($key, $bridge, 'Missing key "' . $key . '"');
- }
-
- $this->assertContains($bridge['status'], $allowedStatus, 'Invalid status value');
- }
- }
-
- private function initAction() {
- $actionFac = new ActionFactory();
-
- $action = $actionFac->create('list');
-
- ob_start();
- $action->execute();
- $this->data = ob_get_contents();
- ob_clean();
- ob_end_flush();
- }
+class ListActionTest extends TestCase
+{
+ private $data;
+
+ /**
+ * @runInSeparateProcess
+ * @requires function xdebug_get_headers
+ */
+ public function testHeaders()
+ {
+ $this->initAction();
+
+ $this->assertContains(
+ 'Content-Type: application/json',
+ xdebug_get_headers()
+ );
+ }
+
+ /**
+ * @runInSeparateProcess
+ */
+ public function testOutput()
+ {
+ $this->initAction();
+
+ $items = json_decode($this->data, true);
+
+ $this->assertNotNull($items, 'invalid JSON output: ' . json_last_error_msg());
+
+ $this->assertArrayHasKey('total', $items, 'Missing "total" parameter');
+ $this->assertIsInt($items['total'], 'Invalid type');
+
+ $this->assertArrayHasKey('bridges', $items, 'Missing "bridges" array');
+
+ $this->assertEquals(
+ $items['total'],
+ count($items['bridges']),
+ 'Item count doesn\'t match'
+ );
+
+ $bridgeFac = new BridgeFactory();
+
+ $this->assertEquals(
+ count($bridgeFac->getBridgeNames()),
+ count($items['bridges']),
+ 'Number of bridges doesn\'t match'
+ );
+
+ $expectedKeys = [
+ 'status',
+ 'uri',
+ 'name',
+ 'icon',
+ 'parameters',
+ 'maintainer',
+ 'description'
+ ];
+
+ $allowedStatus = [
+ 'active',
+ 'inactive'
+ ];
+
+ foreach ($items['bridges'] as $bridge) {
+ foreach ($expectedKeys as $key) {
+ $this->assertArrayHasKey($key, $bridge, 'Missing key "' . $key . '"');
+ }
+
+ $this->assertContains($bridge['status'], $allowedStatus, 'Invalid status value');
+ }
+ }
+
+ private function initAction()
+ {
+ $actionFac = new ActionFactory();
+
+ $action = $actionFac->create('list');
+
+ ob_start();
+ $action->execute();
+ $this->data = ob_get_contents();
+ ob_clean();
+ ob_end_flush();
+ }
}
diff --git a/tests/Bridges/BridgeImplementationTest.php b/tests/Bridges/BridgeImplementationTest.php
index e0e095a6..60f94d4a 100644
--- a/tests/Bridges/BridgeImplementationTest.php
+++ b/tests/Bridges/BridgeImplementationTest.php
@@ -7,223 +7,236 @@ use BridgeInterface;
use FeedExpander;
use PHPUnit\Framework\TestCase;
-class BridgeImplementationTest extends TestCase {
- private $class;
- private $obj;
-
- /**
- * @dataProvider dataBridgesProvider
- */
- public function testClassName($path) {
- $this->setBridge($path);
- $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
- $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
- $this->assertStringEndsWith('Bridge', $this->class, 'class name must end with "Bridge"');
- }
-
- /**
- * @dataProvider dataBridgesProvider
- */
- public function testClassType($path) {
- $this->setBridge($path);
- $this->assertInstanceOf(BridgeInterface::class, $this->obj);
- }
-
- /**
- * @dataProvider dataBridgesProvider
- */
- public function testConstants($path) {
- $this->setBridge($path);
-
- $this->assertIsString($this->obj::NAME, 'class::NAME');
- $this->assertNotEmpty($this->obj::NAME, 'class::NAME');
- $this->assertIsString($this->obj::URI, 'class::URI');
- $this->assertNotEmpty($this->obj::URI, 'class::URI');
- $this->assertIsString($this->obj::DESCRIPTION, 'class::DESCRIPTION');
- $this->assertNotEmpty($this->obj::DESCRIPTION, 'class::DESCRIPTION');
- $this->assertIsString($this->obj::MAINTAINER, 'class::MAINTAINER');
- $this->assertNotEmpty($this->obj::MAINTAINER, 'class::MAINTAINER');
-
- $this->assertIsArray($this->obj::PARAMETERS, 'class::PARAMETERS');
- $this->assertIsInt($this->obj::CACHE_TIMEOUT, 'class::CACHE_TIMEOUT');
- $this->assertGreaterThanOrEqual(0, $this->obj::CACHE_TIMEOUT, 'class::CACHE_TIMEOUT');
- }
-
- /**
- * @dataProvider dataBridgesProvider
- */
- public function testParameters($path) {
- $this->setBridge($path);
-
- $multiMinimum = 2;
- if (isset($this->obj::PARAMETERS['global'])) {
- ++$multiMinimum;
- }
- $multiContexts = (count($this->obj::PARAMETERS) >= $multiMinimum);
- $paramsSeen = array();
-
- $allowedTypes = array(
- 'text',
- 'number',
- 'list',
- 'checkbox'
- );
-
- foreach($this->obj::PARAMETERS as $context => $params) {
- if ($multiContexts) {
- $this->assertIsString($context, 'invalid context name');
-
- $this->assertNotEmpty($context, 'The context name cannot be empty');
- }
-
- if (empty($params)) {
- continue;
- }
-
- foreach ($paramsSeen as $seen) {
- $this->assertNotEquals($seen, $params, 'same set of parameters not allowed');
- }
- $paramsSeen[] = $params;
-
- foreach ($params as $field => $options) {
- $this->assertIsString($field, $field . ': invalid id');
- $this->assertNotEmpty($field, $field . ':empty id');
-
- $this->assertIsString($options['name'], $field . ': invalid name');
- $this->assertNotEmpty($options['name'], $field . ': empty name');
-
- if (isset($options['type'])) {
- $this->assertIsString($options['type'], $field . ': invalid type');
- $this->assertContains($options['type'], $allowedTypes, $field . ': unknown type');
-
- if ($options['type'] == 'list') {
- $this->assertArrayHasKey('values', $options, $field . ': missing list values');
- $this->assertIsArray($options['values'], $field . ': invalid list values');
- $this->assertNotEmpty($options['values'], $field . ': empty list values');
-
- foreach ($options['values'] as $valueName => $value) {
- $this->assertIsString($valueName, $field . ': invalid value name');
- }
- }
- }
-
- if (isset($options['required'])) {
- $this->assertIsBool($options['required'], $field . ': invalid required');
-
- if($options['required'] === true && isset($options['type'])) {
- switch($options['type']) {
- case 'list':
- case 'checkbox':
- $this->assertArrayNotHasKey(
- 'required',
- $options,
- $field . ': "required" attribute not supported for ' . $options['type']
- );
- break;
- }
- }
- }
-
- if (isset($options['title'])) {
- $this->assertIsString($options['title'], $field . ': invalid title');
- $this->assertNotEmpty($options['title'], $field . ': empty title');
- }
-
- if (isset($options['pattern'])) {
- $this->assertIsString($options['pattern'], $field . ': invalid pattern');
- $this->assertNotEquals('', $options['pattern'], $field . ': empty pattern');
- }
-
- if (isset($options['exampleValue'])) {
- if (is_string($options['exampleValue']))
- $this->assertNotEquals('', $options['exampleValue'], $field . ': empty exampleValue');
- }
-
- if (isset($options['defaultValue'])) {
- if (is_string($options['defaultValue']))
- $this->assertNotEquals('', $options['defaultValue'], $field . ': empty defaultValue');
- }
- }
- }
-
- foreach($this->obj::TEST_DETECT_PARAMETERS as $url => $params) {
- $this->assertEquals($this->obj->detectParameters($url), $params);
- }
-
- $this->assertTrue(true);
- }
-
- /**
- * @dataProvider dataBridgesProvider
- */
- public function testVisibleMethods($path) {
- $allowedBridgeAbstract = get_class_methods(BridgeAbstract::class);
- sort($allowedBridgeAbstract);
- $allowedFeedExpander = get_class_methods(FeedExpander::class);
- sort($allowedFeedExpander);
-
- $this->setBridge($path);
-
- $methods = get_class_methods($this->obj);
- sort($methods);
- if ($this->obj instanceof FeedExpander) {
- $this->assertEquals($allowedFeedExpander, $methods);
- } else {
- $this->assertEquals($allowedBridgeAbstract, $methods);
- }
- }
-
- /**
- * @dataProvider dataBridgesProvider
- */
- public function testMethodValues($path) {
- $this->setBridge($path);
-
- $value = $this->obj->getDescription();
- $this->assertIsString($value, '$class->getDescription()');
- $this->assertNotEmpty($value, '$class->getDescription()');
-
- $value = $this->obj->getMaintainer();
- $this->assertIsString($value, '$class->getMaintainer()');
- $this->assertNotEmpty($value, '$class->getMaintainer()');
-
- $value = $this->obj->getName();
- $this->assertIsString($value, '$class->getName()');
- $this->assertNotEmpty($value, '$class->getName()');
-
- $value = $this->obj->getURI();
- $this->assertIsString($value, '$class->getURI()');
- $this->assertNotEmpty($value, '$class->getURI()');
-
- $value = $this->obj->getIcon();
- $this->assertIsString($value, '$class->getIcon()');
- }
-
- /**
- * @dataProvider dataBridgesProvider
- */
- public function testUri($path) {
- $this->setBridge($path);
-
- $this->checkUrl($this->obj::URI);
- $this->checkUrl($this->obj->getURI());
- }
-
- public function dataBridgesProvider() {
- $bridges = array();
- foreach (glob(PATH_LIB_BRIDGES . '*Bridge.php') as $path) {
- $bridges[basename($path, '.php')] = array($path);
- }
- return $bridges;
- }
-
- private function setBridge($path) {
- $this->class = '\\' . basename($path, '.php');
- $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
- $this->obj = new $this->class();
- }
-
- private function checkUrl($url) {
- $this->assertNotFalse(filter_var($url, FILTER_VALIDATE_URL), 'no valid URL: ' . $url);
- }
+class BridgeImplementationTest extends TestCase
+{
+ private $class;
+ private $obj;
+
+ /**
+ * @dataProvider dataBridgesProvider
+ */
+ public function testClassName($path)
+ {
+ $this->setBridge($path);
+ $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
+ $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
+ $this->assertStringEndsWith('Bridge', $this->class, 'class name must end with "Bridge"');
+ }
+
+ /**
+ * @dataProvider dataBridgesProvider
+ */
+ public function testClassType($path)
+ {
+ $this->setBridge($path);
+ $this->assertInstanceOf(BridgeInterface::class, $this->obj);
+ }
+
+ /**
+ * @dataProvider dataBridgesProvider
+ */
+ public function testConstants($path)
+ {
+ $this->setBridge($path);
+
+ $this->assertIsString($this->obj::NAME, 'class::NAME');
+ $this->assertNotEmpty($this->obj::NAME, 'class::NAME');
+ $this->assertIsString($this->obj::URI, 'class::URI');
+ $this->assertNotEmpty($this->obj::URI, 'class::URI');
+ $this->assertIsString($this->obj::DESCRIPTION, 'class::DESCRIPTION');
+ $this->assertNotEmpty($this->obj::DESCRIPTION, 'class::DESCRIPTION');
+ $this->assertIsString($this->obj::MAINTAINER, 'class::MAINTAINER');
+ $this->assertNotEmpty($this->obj::MAINTAINER, 'class::MAINTAINER');
+
+ $this->assertIsArray($this->obj::PARAMETERS, 'class::PARAMETERS');
+ $this->assertIsInt($this->obj::CACHE_TIMEOUT, 'class::CACHE_TIMEOUT');
+ $this->assertGreaterThanOrEqual(0, $this->obj::CACHE_TIMEOUT, 'class::CACHE_TIMEOUT');
+ }
+
+ /**
+ * @dataProvider dataBridgesProvider
+ */
+ public function testParameters($path)
+ {
+ $this->setBridge($path);
+
+ $multiMinimum = 2;
+ if (isset($this->obj::PARAMETERS['global'])) {
+ ++$multiMinimum;
+ }
+ $multiContexts = (count($this->obj::PARAMETERS) >= $multiMinimum);
+ $paramsSeen = [];
+
+ $allowedTypes = [
+ 'text',
+ 'number',
+ 'list',
+ 'checkbox'
+ ];
+
+ foreach ($this->obj::PARAMETERS as $context => $params) {
+ if ($multiContexts) {
+ $this->assertIsString($context, 'invalid context name');
+
+ $this->assertNotEmpty($context, 'The context name cannot be empty');
+ }
+
+ if (empty($params)) {
+ continue;
+ }
+
+ foreach ($paramsSeen as $seen) {
+ $this->assertNotEquals($seen, $params, 'same set of parameters not allowed');
+ }
+ $paramsSeen[] = $params;
+
+ foreach ($params as $field => $options) {
+ $this->assertIsString($field, $field . ': invalid id');
+ $this->assertNotEmpty($field, $field . ':empty id');
+
+ $this->assertIsString($options['name'], $field . ': invalid name');
+ $this->assertNotEmpty($options['name'], $field . ': empty name');
+
+ if (isset($options['type'])) {
+ $this->assertIsString($options['type'], $field . ': invalid type');
+ $this->assertContains($options['type'], $allowedTypes, $field . ': unknown type');
+
+ if ($options['type'] == 'list') {
+ $this->assertArrayHasKey('values', $options, $field . ': missing list values');
+ $this->assertIsArray($options['values'], $field . ': invalid list values');
+ $this->assertNotEmpty($options['values'], $field . ': empty list values');
+
+ foreach ($options['values'] as $valueName => $value) {
+ $this->assertIsString($valueName, $field . ': invalid value name');
+ }
+ }
+ }
+
+ if (isset($options['required'])) {
+ $this->assertIsBool($options['required'], $field . ': invalid required');
+
+ if ($options['required'] === true && isset($options['type'])) {
+ switch ($options['type']) {
+ case 'list':
+ case 'checkbox':
+ $this->assertArrayNotHasKey(
+ 'required',
+ $options,
+ $field . ': "required" attribute not supported for ' . $options['type']
+ );
+ break;
+ }
+ }
+ }
+
+ if (isset($options['title'])) {
+ $this->assertIsString($options['title'], $field . ': invalid title');
+ $this->assertNotEmpty($options['title'], $field . ': empty title');
+ }
+
+ if (isset($options['pattern'])) {
+ $this->assertIsString($options['pattern'], $field . ': invalid pattern');
+ $this->assertNotEquals('', $options['pattern'], $field . ': empty pattern');
+ }
+
+ if (isset($options['exampleValue'])) {
+ if (is_string($options['exampleValue'])) {
+ $this->assertNotEquals('', $options['exampleValue'], $field . ': empty exampleValue');
+ }
+ }
+
+ if (isset($options['defaultValue'])) {
+ if (is_string($options['defaultValue'])) {
+ $this->assertNotEquals('', $options['defaultValue'], $field . ': empty defaultValue');
+ }
+ }
+ }
+ }
+
+ foreach ($this->obj::TEST_DETECT_PARAMETERS as $url => $params) {
+ $this->assertEquals($this->obj->detectParameters($url), $params);
+ }
+
+ $this->assertTrue(true);
+ }
+
+ /**
+ * @dataProvider dataBridgesProvider
+ */
+ public function testVisibleMethods($path)
+ {
+ $allowedBridgeAbstract = get_class_methods(BridgeAbstract::class);
+ sort($allowedBridgeAbstract);
+ $allowedFeedExpander = get_class_methods(FeedExpander::class);
+ sort($allowedFeedExpander);
+
+ $this->setBridge($path);
+
+ $methods = get_class_methods($this->obj);
+ sort($methods);
+ if ($this->obj instanceof FeedExpander) {
+ $this->assertEquals($allowedFeedExpander, $methods);
+ } else {
+ $this->assertEquals($allowedBridgeAbstract, $methods);
+ }
+ }
+
+ /**
+ * @dataProvider dataBridgesProvider
+ */
+ public function testMethodValues($path)
+ {
+ $this->setBridge($path);
+
+ $value = $this->obj->getDescription();
+ $this->assertIsString($value, '$class->getDescription()');
+ $this->assertNotEmpty($value, '$class->getDescription()');
+
+ $value = $this->obj->getMaintainer();
+ $this->assertIsString($value, '$class->getMaintainer()');
+ $this->assertNotEmpty($value, '$class->getMaintainer()');
+
+ $value = $this->obj->getName();
+ $this->assertIsString($value, '$class->getName()');
+ $this->assertNotEmpty($value, '$class->getName()');
+
+ $value = $this->obj->getURI();
+ $this->assertIsString($value, '$class->getURI()');
+ $this->assertNotEmpty($value, '$class->getURI()');
+
+ $value = $this->obj->getIcon();
+ $this->assertIsString($value, '$class->getIcon()');
+ }
+
+ /**
+ * @dataProvider dataBridgesProvider
+ */
+ public function testUri($path)
+ {
+ $this->setBridge($path);
+
+ $this->checkUrl($this->obj::URI);
+ $this->checkUrl($this->obj->getURI());
+ }
+
+ public function dataBridgesProvider()
+ {
+ $bridges = [];
+ foreach (glob(PATH_LIB_BRIDGES . '*Bridge.php') as $path) {
+ $bridges[basename($path, '.php')] = [$path];
+ }
+ return $bridges;
+ }
+
+ private function setBridge($path)
+ {
+ $this->class = '\\' . basename($path, '.php');
+ $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
+ $this->obj = new $this->class();
+ }
+
+ private function checkUrl($url)
+ {
+ $this->assertNotFalse(filter_var($url, FILTER_VALIDATE_URL), 'no valid URL: ' . $url);
+ }
}
diff --git a/tests/Caches/CacheImplementationTest.php b/tests/Caches/CacheImplementationTest.php
index 12018685..a3ad5f79 100644
--- a/tests/Caches/CacheImplementationTest.php
+++ b/tests/Caches/CacheImplementationTest.php
@@ -5,39 +5,44 @@ namespace RssBridge\Tests\Caches;
use CacheInterface;
use PHPUnit\Framework\TestCase;
-class CacheImplementationTest extends TestCase {
- private $class;
-
- /**
- * @dataProvider dataCachesProvider
- */
- public function testClassName($path) {
- $this->setCache($path);
- $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
- $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
- $this->assertStringEndsWith('Cache', $this->class, 'class name must end with "Cache"');
- }
-
- /**
- * @dataProvider dataCachesProvider
- */
- public function testClassType($path) {
- $this->setCache($path);
- $this->assertTrue(is_subclass_of($this->class, CacheInterface::class), 'class must be subclass of CacheInterface');
- }
-
- ////////////////////////////////////////////////////////////////////////////
-
- public function dataCachesProvider() {
- $caches = array();
- foreach (glob(PATH_LIB_CACHES . '*.php') as $path) {
- $caches[basename($path, '.php')] = array($path);
- }
- return $caches;
- }
-
- private function setCache($path) {
- $this->class = '\\' . basename($path, '.php');
- $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
- }
+class CacheImplementationTest extends TestCase
+{
+ private $class;
+
+ /**
+ * @dataProvider dataCachesProvider
+ */
+ public function testClassName($path)
+ {
+ $this->setCache($path);
+ $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
+ $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
+ $this->assertStringEndsWith('Cache', $this->class, 'class name must end with "Cache"');
+ }
+
+ /**
+ * @dataProvider dataCachesProvider
+ */
+ public function testClassType($path)
+ {
+ $this->setCache($path);
+ $this->assertTrue(is_subclass_of($this->class, CacheInterface::class), 'class must be subclass of CacheInterface');
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+
+ public function dataCachesProvider()
+ {
+ $caches = [];
+ foreach (glob(PATH_LIB_CACHES . '*.php') as $path) {
+ $caches[basename($path, '.php')] = [$path];
+ }
+ return $caches;
+ }
+
+ private function setCache($path)
+ {
+ $this->class = '\\' . basename($path, '.php');
+ $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
+ }
}
diff --git a/tests/Formats/AtomFormatTest.php b/tests/Formats/AtomFormatTest.php
index a871ea86..77bb9cbc 100644
--- a/tests/Formats/AtomFormatTest.php
+++ b/tests/Formats/AtomFormatTest.php
@@ -1,4 +1,5 @@
<?php
+
/**
* AtomFormat - RFC 4287: The Atom Syndication Format
* https://tools.ietf.org/html/rfc4287
@@ -10,18 +11,20 @@ require_once __DIR__ . '/BaseFormatTest.php';
use PHPUnit\Framework\TestCase;
-class AtomFormatTest extends BaseFormatTest {
- private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedAtomFormat/';
+class AtomFormatTest extends BaseFormatTest
+{
+ private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedAtomFormat/';
- /**
- * @dataProvider sampleProvider
- * @runInSeparateProcess
- */
- public function testOutput(string $name, string $path) {
- $data = $this->formatData('Atom', $this->loadSample($path));
- $this->assertNotFalse(simplexml_load_string($data));
+ /**
+ * @dataProvider sampleProvider
+ * @runInSeparateProcess
+ */
+ public function testOutput(string $name, string $path)
+ {
+ $data = $this->formatData('Atom', $this->loadSample($path));
+ $this->assertNotFalse(simplexml_load_string($data));
- $expected = self::PATH_EXPECTED . $name . '.xml';
- $this->assertXmlStringEqualsXmlFile($expected, $data);
- }
+ $expected = self::PATH_EXPECTED . $name . '.xml';
+ $this->assertXmlStringEqualsXmlFile($expected, $data);
+ }
}
diff --git a/tests/Formats/BaseFormatTest.php b/tests/Formats/BaseFormatTest.php
index 94da7b04..ace4d3ea 100644
--- a/tests/Formats/BaseFormatTest.php
+++ b/tests/Formats/BaseFormatTest.php
@@ -5,59 +5,65 @@ namespace RssBridge\Tests\Formats;
use PHPUnit\Framework\TestCase;
use FormatFactory;
-abstract class BaseFormatTest extends TestCase {
- protected const PATH_SAMPLES = __DIR__ . '/samples/';
-
- /**
- * @return array<string, array{string, string}>
- */
- public function sampleProvider() {
- $samples = [];
- foreach (glob(self::PATH_SAMPLES . '*.json') as $path) {
- $name = basename($path, '.json');
- $samples[$name] = [
- $name,
- $path,
- ];
- }
- return $samples;
- }
-
- /**
- * Cannot be part of the sample returned by sampleProvider since this modifies $_SERVER
- * and thus needs to be run in a separate process to avoid side effects.
- */
- protected function loadSample(string $path): \stdClass {
- $data = json_decode(file_get_contents($path), true);
- if (isset($data['meta']) && isset($data['items'])) {
- if (!empty($data['server']))
- $this->setServerVars($data['server']);
-
- $items = array();
- foreach($data['items'] as $item) {
- $items[] = new \FeedItem($item);
- }
-
- return (object)array(
- 'meta' => $data['meta'],
- 'items' => $items,
- );
- } else {
- $this->fail('invalid test sample: ' . basename($path, '.json'));
- }
- }
-
- private function setServerVars(array $list): void {
- $_SERVER = array_merge($_SERVER, $list);
- }
-
- protected function formatData(string $formatName, \stdClass $sample): string {
- $formatFac = new FormatFactory();
- $format = $formatFac->create($formatName);
- $format->setItems($sample->items);
- $format->setExtraInfos($sample->meta);
- $format->setLastModified(strtotime('2000-01-01 12:00:00 UTC'));
-
- return $format->stringify();
- }
+abstract class BaseFormatTest extends TestCase
+{
+ protected const PATH_SAMPLES = __DIR__ . '/samples/';
+
+ /**
+ * @return array<string, array{string, string}>
+ */
+ public function sampleProvider()
+ {
+ $samples = [];
+ foreach (glob(self::PATH_SAMPLES . '*.json') as $path) {
+ $name = basename($path, '.json');
+ $samples[$name] = [
+ $name,
+ $path,
+ ];
+ }
+ return $samples;
+ }
+
+ /**
+ * Cannot be part of the sample returned by sampleProvider since this modifies $_SERVER
+ * and thus needs to be run in a separate process to avoid side effects.
+ */
+ protected function loadSample(string $path): \stdClass
+ {
+ $data = json_decode(file_get_contents($path), true);
+ if (isset($data['meta']) && isset($data['items'])) {
+ if (!empty($data['server'])) {
+ $this->setServerVars($data['server']);
+ }
+
+ $items = [];
+ foreach ($data['items'] as $item) {
+ $items[] = new \FeedItem($item);
+ }
+
+ return (object)[
+ 'meta' => $data['meta'],
+ 'items' => $items,
+ ];
+ } else {
+ $this->fail('invalid test sample: ' . basename($path, '.json'));
+ }
+ }
+
+ private function setServerVars(array $list): void
+ {
+ $_SERVER = array_merge($_SERVER, $list);
+ }
+
+ protected function formatData(string $formatName, \stdClass $sample): string
+ {
+ $formatFac = new FormatFactory();
+ $format = $formatFac->create($formatName);
+ $format->setItems($sample->items);
+ $format->setExtraInfos($sample->meta);
+ $format->setLastModified(strtotime('2000-01-01 12:00:00 UTC'));
+
+ return $format->stringify();
+ }
}
diff --git a/tests/Formats/FormatImplementationTest.php b/tests/Formats/FormatImplementationTest.php
index e4501d68..55c6335f 100644
--- a/tests/Formats/FormatImplementationTest.php
+++ b/tests/Formats/FormatImplementationTest.php
@@ -2,39 +2,44 @@
use PHPUnit\Framework\TestCase;
-class FormatImplementationTest extends TestCase {
- private $class;
- private $obj;
+class FormatImplementationTest extends TestCase
+{
+ private $class;
+ private $obj;
- /**
- * @dataProvider dataFormatsProvider
- */
- public function testClassName($path) {
- $this->setFormat($path);
- $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
- $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
- $this->assertStringEndsWith('Format', $this->class, 'class name must end with "Format"');
- }
+ /**
+ * @dataProvider dataFormatsProvider
+ */
+ public function testClassName($path)
+ {
+ $this->setFormat($path);
+ $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
+ $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
+ $this->assertStringEndsWith('Format', $this->class, 'class name must end with "Format"');
+ }
- /**
- * @dataProvider dataFormatsProvider
- */
- public function testClassType($path) {
- $this->setFormat($path);
- $this->assertInstanceOf(FormatInterface::class, $this->obj);
- }
+ /**
+ * @dataProvider dataFormatsProvider
+ */
+ public function testClassType($path)
+ {
+ $this->setFormat($path);
+ $this->assertInstanceOf(FormatInterface::class, $this->obj);
+ }
- public function dataFormatsProvider() {
- $formats = array();
- foreach (glob(PATH_LIB_FORMATS . '*.php') as $path) {
- $formats[basename($path, '.php')] = array($path);
- }
- return $formats;
- }
+ public function dataFormatsProvider()
+ {
+ $formats = [];
+ foreach (glob(PATH_LIB_FORMATS . '*.php') as $path) {
+ $formats[basename($path, '.php')] = [$path];
+ }
+ return $formats;
+ }
- private function setFormat($path) {
- $this->class = basename($path, '.php');
- $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
- $this->obj = new $this->class();
- }
+ private function setFormat($path)
+ {
+ $this->class = basename($path, '.php');
+ $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
+ $this->obj = new $this->class();
+ }
}
diff --git a/tests/Formats/JsonFormatTest.php b/tests/Formats/JsonFormatTest.php
index 3b9f8d47..c21d3f34 100644
--- a/tests/Formats/JsonFormatTest.php
+++ b/tests/Formats/JsonFormatTest.php
@@ -1,4 +1,5 @@
<?php
+
/**
* JsonFormat - JSON Feed Version 1
* https://jsonfeed.org/version/1
@@ -10,18 +11,20 @@ require_once __DIR__ . '/BaseFormatTest.php';
use PHPUnit\Framework\TestCase;
-class JsonFormatTest extends BaseFormatTest {
- private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedJsonFormat/';
+class JsonFormatTest extends BaseFormatTest
+{
+ private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedJsonFormat/';
- /**
- * @dataProvider sampleProvider
- * @runInSeparateProcess
- */
- public function testOutput(string $name, string $path) {
- $data = $this->formatData('Json', $this->loadSample($path));
- $this->assertNotNull(json_decode($data), 'invalid JSON output: ' . json_last_error_msg());
+ /**
+ * @dataProvider sampleProvider
+ * @runInSeparateProcess
+ */
+ public function testOutput(string $name, string $path)
+ {
+ $data = $this->formatData('Json', $this->loadSample($path));
+ $this->assertNotNull(json_decode($data), 'invalid JSON output: ' . json_last_error_msg());
- $expected = self::PATH_EXPECTED . $name . '.json';
- $this->assertJsonStringEqualsJsonFile($expected, $data);
- }
+ $expected = self::PATH_EXPECTED . $name . '.json';
+ $this->assertJsonStringEqualsJsonFile($expected, $data);
+ }
}
diff --git a/tests/Formats/MrssFormatTest.php b/tests/Formats/MrssFormatTest.php
index 6def6afb..af74923e 100644
--- a/tests/Formats/MrssFormatTest.php
+++ b/tests/Formats/MrssFormatTest.php
@@ -1,4 +1,5 @@
<?php
+
/**
* MrssFormat - RSS 2.0 + Media RSS
* http://www.rssboard.org/rss-specification
@@ -11,18 +12,20 @@ require_once __DIR__ . '/BaseFormatTest.php';
use PHPUnit\Framework\TestCase;
-class MrssFormatTest extends BaseFormatTest {
- private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedMrssFormat/';
+class MrssFormatTest extends BaseFormatTest
+{
+ private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedMrssFormat/';
- /**
- * @dataProvider sampleProvider
- * @runInSeparateProcess
- */
- public function testOutput(string $name, string $path) {
- $data = $this->formatData('Mrss', $this->loadSample($path));
- $this->assertNotFalse(simplexml_load_string($data));
+ /**
+ * @dataProvider sampleProvider
+ * @runInSeparateProcess
+ */
+ public function testOutput(string $name, string $path)
+ {
+ $data = $this->formatData('Mrss', $this->loadSample($path));
+ $this->assertNotFalse(simplexml_load_string($data));
- $expected = self::PATH_EXPECTED . $name . '.xml';
- $this->assertXmlStringEqualsXmlFile($expected, $data);
- }
+ $expected = self::PATH_EXPECTED . $name . '.xml';
+ $this->assertXmlStringEqualsXmlFile($expected, $data);
+ }
}