diff --git a/.gitattributes b/.gitattributes index 13ebe2ca..36544021 100644 --- a/.gitattributes +++ b/.gitattributes @@ -22,18 +22,24 @@ *.RTF diff=astextplain # Ignore files in git archive (i.e. GitHub release builds) + ## Docker Dockerfile export-ignore .dockerignore export-ignore + ## Travis .travis.yml export-ignore + ## GitHub .github/ export-ignore + ## Git .gitattributes export-ignore .gitignore export-ignore + ## Scalingo scalingo.json export-ignore + ## RSS-Bridge phpunit.xml export-ignore phpcs.xml export-ignore @@ -42,8 +48,22 @@ tests/ export-ignore cache/.gitkeep export-ignore bridges/DemoBridge.php export-ignore bridges/FeedExpanderExampleBridge.php export-ignore + ## Composer -composer.json export-ignore -composer.lock export-ignore +# +# Keep the following lines commented out. Heroku does +# not function if the composer files are ignored during +# export. For more information see +# https://github.com/rss-bridge/rss-bridge/issues/1165 +# +# composer.json export-ignore +# composer.lock export-ignore + ## Heroku -app.json export-ignore +# +# Keep the following line commented out. Heroku does +# not function if app.json is ignored during export. +# For more information see +# https://github.com/rss-bridge/rss-bridge/issues/1165 +# +# app.json export-ignore diff --git a/Dockerfile b/Dockerfile index 7d0611be..fa9979d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \ && apt-get --yes update && apt-get --yes install libxml2-dev \ && docker-php-ext-install -j$(nproc) simplexml \ && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \ - && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf + && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \ + && sed -ri -e 's/(MinProtocol\s*=\s*)TLSv1\.2/\1None/' /etc/ssl/openssl.cnf \ + && sed -ri -e 's/(CipherString\s*=\s*DEFAULT)@SECLEVEL=2/\1/' /etc/ssl/openssl.cnf COPY --chown=www-data:www-data ./ /app/ \ No newline at end of file diff --git a/README.md b/README.md index 95086c03..a9db8eaf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -rss-bridge +![RSS-Bridge](static/logo_600px.png) === -[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![Debian Release](https://img.shields.io/badge/dynamic/json.svg?label=debian%20release&url=https%3A%2F%2Fsources.debian.org%2Fapi%2Fsrc%2Frss-bridge%2F&query=%24.versions%5B0%5D.version&colorB=blue)](https://tracker.debian.org/pkg/rss-bridge) [![Guix Release](https://img.shields.io/badge/guix%20release-unknown-light--gray.svg)](https://www.gnu.org/software/guix/packages/R/) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge) [![Docker Build Status](https://img.shields.io/docker/build/rssbridge/rss-bridge.svg)](https://hub.docker.com/r/rssbridge/rss-bridge/) +[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg?logo=github)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![Debian Release](https://img.shields.io/badge/dynamic/json.svg?logo=debian&label=debian%20release&url=https%3A%2F%2Fsources.debian.org%2Fapi%2Fsrc%2Frss-bridge%2F&query=%24.versions%5B0%5D.version&colorB=blue)](https://tracker.debian.org/pkg/rss-bridge) [![Guix Release](https://img.shields.io/badge/guix%20release-unknown-blue.svg)](https://www.gnu.org/software/guix/packages/R/) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge) [![Docker Build Status](https://img.shields.io/docker/build/rssbridge/rss-bridge.svg?logo=docker)](https://hub.docker.com/r/rssbridge/rss-bridge/) -RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites which don't have one. It can be used on webservers or as stand alone application in CLI mode. +RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one. It can be used on webservers or as a stand-alone application in CLI mode. **Important**: RSS-Bridge is __not__ a feed reader or feed aggregator, but a tool to generate feeds that are consumed by feed readers and feed aggregators. Find a list of feed aggregators on [Wikipedia](https://en.wikipedia.org/wiki/Comparison_of_feed_aggregators). @@ -15,7 +15,6 @@ Supported sites/pages (examples) * `DuckDuckGo`: Most recent results from [DuckDuckGo.com](https://duckduckgo.com/) * `Facebook` : Returns the latest posts on a page or profile on [Facebook](https://facebook.com/) * `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr -* `GooglePlus` : Most recent posts of user timeline * `GoogleSearch` : Most recent results from Google Search * `Identi.ca` : Identica user timeline (Should be compatible with other Pump.io instances) * `Instagram`: Most recent photos from an Instagram user @@ -77,7 +76,7 @@ RSS-Bridge allows you to take full control over which bridges are displayed to t Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) -**Notice**: By default RSS-Bridge will only show a small subset of bridges. Make sure to read up on [whitelisting](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) to unlock the full potential of RSS-Bridge! +**Notice**: By default, RSS-Bridge will only show a small subset of bridges. Make sure to read up on [whitelisting](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) to unlock the full potential of RSS-Bridge! Deploy === @@ -111,110 +110,128 @@ Use this script to generate the list automatically (using the GitHub API): https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8 --> - * [16mhz](https://github.com/16mhz) - * [adamchainz](https://github.com/adamchainz) - * [Ahiles3005](https://github.com/Ahiles3005) - * [Albirew](https://github.com/Albirew) - * [aledeg](https://github.com/aledeg) - * [alex73](https://github.com/alex73) - * [alexAubin](https://github.com/alexAubin) - * [AmauryCarrade](https://github.com/AmauryCarrade) - * [ArthurHoaro](https://github.com/ArthurHoaro) - * [Astalaseven](https://github.com/Astalaseven) - * [Astyan-42](https://github.com/Astyan-42) - * [az5he6ch](https://github.com/az5he6ch) - * [azdkj532](https://github.com/azdkj532) - * [b1nj](https://github.com/b1nj) - * [benasse](https://github.com/benasse) - * [captn3m0](https://github.com/captn3m0) - * [chemel](https://github.com/chemel) - * [ckiw](https://github.com/ckiw) - * [cnlpete](https://github.com/cnlpete) - * [corenting](https://github.com/corenting) - * [couraudt](https://github.com/couraudt) - * [da2x](https://github.com/da2x) - * [Daiyousei](https://github.com/Daiyousei) - * [disk0x](https://github.com/disk0x) - * [DJCrashdummy](https://github.com/DJCrashdummy) - * [Djuuu](https://github.com/Djuuu) - * [DnAp](https://github.com/DnAp) - * [Draeli](https://github.com/Draeli) - * [Dreckiger-Dan](https://github.com/Dreckiger-Dan) - * [em92](https://github.com/em92) - * [eMerzh](https://github.com/eMerzh) - * [EtienneM](https://github.com/EtienneM) - * [fluffy-critter](https://github.com/fluffy-critter) - * [Frenzie](https://github.com/Frenzie) - * [fulmeek](https://github.com/fulmeek) - * [Ginko-Aloe](https://github.com/Ginko-Aloe) - * [Glandos](https://github.com/Glandos) - * [GregThib](https://github.com/GregThib) - * [griffaurel](https://github.com/griffaurel) - * [Grummfy](https://github.com/Grummfy) - * [hunhejj](https://github.com/hunhejj) - * [j0k3r](https://github.com/j0k3r) - * [JackNUMBER](https://github.com/JackNUMBER) - * [jdigilio](https://github.com/jdigilio) - * [JeremyRand](https://github.com/JeremyRand) - * [Jocker666z](https://github.com/Jocker666z) - * [killruana](https://github.com/killruana) - * [klimplant](https://github.com/klimplant) - * [kranack](https://github.com/kranack) - * [kraoc](https://github.com/kraoc) - * [l1n](https://github.com/l1n) - * [laBecasse](https://github.com/laBecasse) - * [lagaisse](https://github.com/lagaisse) - * [lalannev](https://github.com/lalannev) - * [ldidry](https://github.com/ldidry) - * [Limero](https://github.com/Limero) - * [LogMANOriginal](https://github.com/LogMANOriginal) - * [lorenzos](https://github.com/lorenzos) - * [m0zes](https://github.com/m0zes) - * [matthewseal](https://github.com/matthewseal) - * [mcbyte-it](https://github.com/mcbyte-it) - * [mdemoss](https://github.com/mdemoss) - * [melangue](https://github.com/melangue) - * [metaMMA](https://github.com/metaMMA) - * [mitsukarenai](https://github.com/mitsukarenai) - * [MonsieurPoutounours](https://github.com/MonsieurPoutounours) - * [mr-flibble](https://github.com/mr-flibble) - * [mro](https://github.com/mro) - * [mxmehl](https://github.com/mxmehl) - * [nel50n](https://github.com/nel50n) - * [niawag](https://github.com/niawag) - * [Nono-m0le](https://github.com/Nono-m0le) - * [ObsidianWitch](https://github.com/ObsidianWitch) - * [ORelio](https://github.com/ORelio) - * [PaulVayssiere](https://github.com/PaulVayssiere) - * [pellaeon](https://github.com/pellaeon) - * [Piranhaplant](https://github.com/Piranhaplant) - * [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf) - * [pitchoule](https://github.com/pitchoule) - * [pmaziere](https://github.com/pmaziere) - * [Pofilo](https://github.com/Pofilo) - * [prysme01](https://github.com/prysme01) - * [quentinus95](https://github.com/quentinus95) - * [regisenguehard](https://github.com/regisenguehard) - * [Riduidel](https://github.com/Riduidel) - * [rogerdc](https://github.com/rogerdc) - * [Roliga](https://github.com/Roliga) - * [sebsauvage](https://github.com/sebsauvage) - * [somini](https://github.com/somini) - * [squeek502](https://github.com/squeek502) - * [Strubbl](https://github.com/Strubbl) - * [sublimz](https://github.com/sublimz) - * [sysadminstory](https://github.com/sysadminstory) - * [tameroski](https://github.com/tameroski) - * [teromene](https://github.com/teromene) - * [thefranke](https://github.com/thefranke) - * [TheRadialActive](https://github.com/TheRadialActive) - * [triatic](https://github.com/triatic) - * [VerifiedJoseph](https://github.com/VerifiedJoseph) - * [WalterBarrett](https://github.com/WalterBarrett) - * [wtuuju](https://github.com/wtuuju) - * [xurxof](https://github.com/xurxof) - * [yardenac](https://github.com/yardenac) - * [ZeNairolf](https://github.com/ZeNairolf) +* [16mhz](https://github.com/16mhz) +* [adamchainz](https://github.com/adamchainz) +* [Ahiles3005](https://github.com/Ahiles3005) +* [Albirew](https://github.com/Albirew) +* [aledeg](https://github.com/aledeg) +* [alex73](https://github.com/alex73) +* [alexAubin](https://github.com/alexAubin) +* [AmauryCarrade](https://github.com/AmauryCarrade) +* [AntoineTurmel](https://github.com/AntoineTurmel) +* [ArthurHoaro](https://github.com/ArthurHoaro) +* [Astalaseven](https://github.com/Astalaseven) +* [Astyan-42](https://github.com/Astyan-42) +* [az5he6ch](https://github.com/az5he6ch) +* [azdkj532](https://github.com/azdkj532) +* [b1nj](https://github.com/b1nj) +* [benasse](https://github.com/benasse) +* [captn3m0](https://github.com/captn3m0) +* [chemel](https://github.com/chemel) +* [ckiw](https://github.com/ckiw) +* [cnlpete](https://github.com/cnlpete) +* [corenting](https://github.com/corenting) +* [couraudt](https://github.com/couraudt) +* [cyberjacob](https://github.com/cyberjacob) +* [da2x](https://github.com/da2x) +* [Daiyousei](https://github.com/Daiyousei) +* [dawidsowa](https://github.com/dawidsowa) +* [disk0x](https://github.com/disk0x) +* [DJCrashdummy](https://github.com/DJCrashdummy) +* [Djuuu](https://github.com/Djuuu) +* [DnAp](https://github.com/DnAp) +* [dominik-th](https://github.com/dominik-th) +* [Draeli](https://github.com/Draeli) +* [Dreckiger-Dan](https://github.com/Dreckiger-Dan) +* [em92](https://github.com/em92) +* [eMerzh](https://github.com/eMerzh) +* [EtienneM](https://github.com/EtienneM) +* [floviolleau](https://github.com/floviolleau) +* [fluffy-critter](https://github.com/fluffy-critter) +* [Frenzie](https://github.com/Frenzie) +* [fulmeek](https://github.com/fulmeek) +* [Ginko-Aloe](https://github.com/Ginko-Aloe) +* [Glandos](https://github.com/Glandos) +* [gloony](https://github.com/gloony) +* [GregThib](https://github.com/GregThib) +* [griffaurel](https://github.com/griffaurel) +* [Grummfy](https://github.com/Grummfy) +* [hunhejj](https://github.com/hunhejj) +* [husim0](https://github.com/husim0) +* [IceWreck](https://github.com/IceWreck) +* [j0k3r](https://github.com/j0k3r) +* [JackNUMBER](https://github.com/JackNUMBER) +* [jdigilio](https://github.com/jdigilio) +* [JeremyRand](https://github.com/JeremyRand) +* [Jocker666z](https://github.com/Jocker666z) +* [johnnygroovy](https://github.com/johnnygroovy) +* [killruana](https://github.com/killruana) +* [klimplant](https://github.com/klimplant) +* [kranack](https://github.com/kranack) +* [kraoc](https://github.com/kraoc) +* [l1n](https://github.com/l1n) +* [laBecasse](https://github.com/laBecasse) +* [lagaisse](https://github.com/lagaisse) +* [lalannev](https://github.com/lalannev) +* [ldidry](https://github.com/ldidry) +* [Leomaradan](https://github.com/Leomaradan) +* [Limero](https://github.com/Limero) +* [LogMANOriginal](https://github.com/LogMANOriginal) +* [lorenzos](https://github.com/lorenzos) +* [lukasklinger](https://github.com/lukasklinger) +* [m0zes](https://github.com/m0zes) +* [matthewseal](https://github.com/matthewseal) +* [mcbyte-it](https://github.com/mcbyte-it) +* [mdemoss](https://github.com/mdemoss) +* [melangue](https://github.com/melangue) +* [metaMMA](https://github.com/metaMMA) +* [mitsukarenai](https://github.com/mitsukarenai) +* [MonsieurPoutounours](https://github.com/MonsieurPoutounours) +* [mr-flibble](https://github.com/mr-flibble) +* [mro](https://github.com/mro) +* [mxmehl](https://github.com/mxmehl) +* [nel50n](https://github.com/nel50n) +* [niawag](https://github.com/niawag) +* [Nono-m0le](https://github.com/Nono-m0le) +* [ObsidianWitch](https://github.com/ObsidianWitch) +* [OliverParoczai](https://github.com/OliverParoczai) +* [oratosquilla-oratoria](https://github.com/oratosquilla-oratoria) +* [ORelio](https://github.com/ORelio) +* [PaulVayssiere](https://github.com/PaulVayssiere) +* [pellaeon](https://github.com/pellaeon) +* [Piranhaplant](https://github.com/Piranhaplant) +* [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf) +* [pitchoule](https://github.com/pitchoule) +* [pmaziere](https://github.com/pmaziere) +* [Pofilo](https://github.com/Pofilo) +* [prysme01](https://github.com/prysme01) +* [quentinus95](https://github.com/quentinus95) +* [regisenguehard](https://github.com/regisenguehard) +* [Riduidel](https://github.com/Riduidel) +* [rogerdc](https://github.com/rogerdc) +* [Roliga](https://github.com/Roliga) +* [sebsauvage](https://github.com/sebsauvage) +* [shutosg](https://github.com/shutosg) +* [somini](https://github.com/somini) +* [squeek502](https://github.com/squeek502) +* [stjohnjohnson](https://github.com/stjohnjohnson) +* [Strubbl](https://github.com/Strubbl) +* [sublimz](https://github.com/sublimz) +* [sunchaserinfo](https://github.com/sunchaserinfo) +* [sysadminstory](https://github.com/sysadminstory) +* [tameroski](https://github.com/tameroski) +* [teromene](https://github.com/teromene) +* [thefranke](https://github.com/thefranke) +* [ThePadawan](https://github.com/ThePadawan) +* [TheRadialActive](https://github.com/TheRadialActive) +* [TitiTestScalingo](https://github.com/TitiTestScalingo) +* [triatic](https://github.com/triatic) +* [VerifiedJoseph](https://github.com/VerifiedJoseph) +* [WalterBarrett](https://github.com/WalterBarrett) +* [wtuuju](https://github.com/wtuuju) +* [xurxof](https://github.com/xurxof) +* [yardenac](https://github.com/yardenac) +* [ZeNairolf](https://github.com/ZeNairolf) Licenses === diff --git a/actions/ConnectivityAction.php b/actions/ConnectivityAction.php new file mode 100644 index 00000000..69272dda --- /dev/null +++ b/actions/ConnectivityAction.php @@ -0,0 +1,136 @@ +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": "", + * "successful": true/false + * } + * + * @param string $bridgeName Name of the bridge to generate the report for + * @return void + */ + private function reportBridgeConnectivity($bridgeName) { + + $bridgeFac = new \BridgeFactory(); + $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES); + + if(!$bridgeFac->isWhitelisted($bridgeName)) { + header('Content-Type: text/html'); + returnServerError('Bridge is not whitelisted!'); + } + + header('Content-Type: text/json'); + + $retVal = array( + 'bridge' => $bridgeName, + 'successful' => false, + 'http_code' => 200, + ); + + $bridge = $bridgeFac->create($bridgeName); + + if($bridge === false) { + echo json_encode($retVal); + return; + } + + $curl_opts = array( + CURLOPT_CONNECTTIMEOUT => 5 + ); + + try { + $reply = getContents($bridge::URI, array(), $curl_opts, true); + + if($reply) { + $retVal['successful'] = true; + if (isset($reply['header'])) { + if (strpos($reply['header'], 'HTTP/1.1 301 Moved Permanently') !== false) { + $retVal['http_code'] = 301; + } + } + } + } catch(Exception $e) { + $retVal['successful'] = false; + } + + echo json_encode($retVal); + + } + + private function returnEntryPage() { + echo << + + + + + + + + + +
+
+
+
+ + +
+ + +EOD; + } +} diff --git a/actions/DetectAction.php b/actions/DetectAction.php index 2ad79a27..86605de4 100644 --- a/actions/DetectAction.php +++ b/actions/DetectAction.php @@ -19,13 +19,16 @@ class DetectAction extends ActionAbstract { $format = $this->userData['format'] or returnClientError('You must specify a format!'); - foreach(Bridge::getBridgeNames() as $bridgeName) { + $bridgeFac = new \BridgeFactory(); + $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES); - if(!Bridge::isWhitelisted($bridgeName)) { + foreach($bridgeFac->getBridgeNames() as $bridgeName) { + + if(!$bridgeFac->isWhitelisted($bridgeName)) { continue; } - $bridge = Bridge::create($bridgeName); + $bridge = $bridgeFac->create($bridgeName); if($bridge === false) { continue; diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php index a1b106f5..579630a1 100644 --- a/actions/DisplayAction.php +++ b/actions/DisplayAction.php @@ -12,26 +12,32 @@ */ class DisplayAction extends ActionAbstract { + private function get_return_code($error) { + $returnCode = $error->getCode(); + if ($returnCode === 301 || $returnCode === 302) { + # Don't pass redirect codes to the exterior + $returnCode = 508; + } + return $returnCode; + } + public function execute() { $bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null; $format = $this->userData['format'] or returnClientError('You must specify a format!'); - // DEPRECATED: 'nameFormat' scheme is replaced by 'name' in format parameter values - // this is to keep compatibility until futher complete removal - if(($pos = strpos($format, 'Format')) === (strlen($format) - strlen('Format'))) { - $format = substr($format, 0, $pos); - } + $bridgeFac = new \BridgeFactory(); + $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES); // whitelist control - if(!Bridge::isWhitelisted($bridge)) { + if(!$bridgeFac->isWhitelisted($bridge)) { throw new \Exception('This bridge is not whitelisted', 401); die; } // Data retrieval - $bridge = Bridge::create($bridge); + $bridge = $bridgeFac->create($bridge); $noproxy = array_key_exists('_noproxy', $this->userData) && filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN); @@ -85,7 +91,9 @@ class DisplayAction extends ActionAbstract { ); // Initialize cache - $cache = Cache::create(Configuration::getConfig('cache', 'type')); + $cacheFac = new CacheFactory(); + $cacheFac->setWorkingDir(PATH_LIB_CACHES); + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); $cache->setScope(''); $cache->purgeCache(86400); // 24 hours $cache->setKey($cache_params); @@ -147,63 +155,77 @@ class DisplayAction extends ActionAbstract { } catch(Error $e) { error_log($e); - $item = new \FeedItem(); + 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)); + // Create "new" error message every 24 hours + $this->userData['_error_time'] = urlencode((int)(time() / 86400)); - // Error 0 is a special case (i.e. "trying to get property of non-object") - if($e->getCode() === 0) { - $item->setTitle( - 'Bridge encountered an unexpected situation! (' - . $this->userData['_error_time'] - . ')' - ); - } else { - $item->setTitle( - 'Bridge returned error ' - . $e->getCode() - . '! (' - . $this->userData['_error_time'] - . ')' - ); + // Error 0 is a special case (i.e. "trying to get property of non-object") + if($e->getCode() === 0) { + $item->setTitle( + 'Bridge encountered an unexpected situation! (' + . $this->userData['_error_time'] + . ')' + ); + } else { + $item->setTitle( + 'Bridge returned error ' + . $e->getCode() + . '! (' + . $this->userData['_error_time'] + . ')' + ); + } + + $item->setURI( + (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '') + . '?' + . http_build_query($this->userData) + ); + + $item->setTimestamp(time()); + $item->setContent(buildBridgeException($e, $bridge)); + + $items[] = $item; + } elseif(Configuration::getConfig('error', 'output') === 'http') { + header('Content-Type: text/html', true, $this->get_return_code($e)); + die(buildTransformException($e, $bridge)); + } } - - $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; } catch(Exception $e) { error_log($e); - $item = new \FeedItem(); + 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)); + // Create "new" error message every 24 hours + $this->userData['_error_time'] = urlencode((int)(time() / 86400)); - $item->setURI( - (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '') - . '?' - . http_build_query($this->userData) - ); + $item->setURI( + (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '') + . '?' + . http_build_query($this->userData) + ); - $item->setTitle( - 'Bridge returned error ' - . $e->getCode() - . '! (' - . $this->userData['_error_time'] - . ')' - ); - $item->setTimestamp(time()); - $item->setContent(buildBridgeException($e, $bridge)); + $item->setTitle( + 'Bridge returned error ' + . $e->getCode() + . '! (' + . $this->userData['_error_time'] + . ')' + ); + $item->setTimestamp(time()); + $item->setContent(buildBridgeException($e, $bridge)); - $items[] = $item; + $items[] = $item; + } elseif(Configuration::getConfig('error', 'output') === 'http') { + header('Content-Type: text/html', true, $this->get_return_code($e)); + die(buildTransformException($e, $bridge)); + } + } } // Store data in cache @@ -216,7 +238,9 @@ class DisplayAction extends ActionAbstract { // Data transformation try { - $format = Format::create($format); + $formatFac = new FormatFactory(); + $formatFac->setWorkingDir(PATH_LIB_FORMATS); + $format = $formatFac->create($format); $format->setItems($items); $format->setExtraInfos($infos); $format->setLastModified($cache->getTime()); diff --git a/actions/ListAction.php b/actions/ListAction.php index 03e06119..92aef0e0 100644 --- a/actions/ListAction.php +++ b/actions/ListAction.php @@ -17,9 +17,12 @@ class ListAction extends ActionAbstract { $list->bridges = array(); $list->total = 0; - foreach(Bridge::getBridgeNames() as $bridgeName) { + $bridgeFac = new \BridgeFactory(); + $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES); - $bridge = Bridge::create($bridgeName); + foreach($bridgeFac->getBridgeNames() as $bridgeName) { + + $bridge = $bridgeFac->create($bridgeName); if($bridge === false) { // Broken bridge, show as inactive @@ -31,7 +34,7 @@ class ListAction extends ActionAbstract { } - $status = Bridge::isWhitelisted($bridgeName) ? 'active' : 'inactive'; + $status = $bridgeFac->isWhitelisted($bridgeName) ? 'active' : 'inactive'; $list->bridges[$bridgeName] = array( 'status' => $status, diff --git a/bridges/AmazonPriceTrackerBridge.php b/bridges/AmazonPriceTrackerBridge.php index 6fa11c91..950178a7 100644 --- a/bridges/AmazonPriceTrackerBridge.php +++ b/bridges/AmazonPriceTrackerBridge.php @@ -134,11 +134,11 @@ EOT; // data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0" // data-asin-currency-code="USD" data-substitute-count="-1" ... /> if ($asinData) { - return [ + return array( 'price' => $asinData->getAttribute('data-asin-price'), 'currency' => $asinData->getAttribute('data-asin-currency-code'), 'shipping' => $asinData->getAttribute('data-asin-shipping') - ]; + ); } return false; @@ -150,11 +150,11 @@ EOT; preg_match('/^\s*([A-Z]{3}|£|\$)\s?([\d.,]+)\s*$/', $priceDiv->plaintext, $matches); if (count($matches) === 3) { - return [ + return array( 'price' => $matches[2], 'currency' => $matches[1], 'shipping' => '0' - ]; + ); } return false; diff --git a/bridges/AppleAppStoreBridge.php b/bridges/AppleAppStoreBridge.php new file mode 100644 index 00000000..c1403fe0 --- /dev/null +++ b/bridges/AppleAppStoreBridge.php @@ -0,0 +1,149 @@ + array( + 'name' => 'Application ID', + 'required' => true, + 'exampleValue' => '310633997' + ), + 'p' => array( + 'name' => 'Platform', + 'type' => 'list', + 'values' => array( + 'iPad' => 'ipad', + 'iPhone' => 'iphone', + 'Mac' => 'mac', + + // The following 2 are present in responses + // but not yet tested + 'Web' => 'web', + 'Apple TV' => 'appletv', + ), + 'defaultValue' => 'iphone', + ), + 'country' => array( + 'name' => 'Store Country', + 'type' => 'list', + 'values' => array( + 'US' => 'US', + 'India' => 'IN', + 'Canada' => 'CA' + ), + 'defaultValue' => 'US', + ), + )); + + const PLATFORM_MAPPING = array( + 'iphone' => 'ios', + 'ipad' => 'ios', + ); + + private function makeHtmlUrl($id, $country){ + return 'https://apps.apple.com/' . $country . '/app/id' . $id; + } + + private function makeJsonUrl($id, $platform, $country){ + return "https://amp-api.apps.apple.com/v1/catalog/$country/apps/$id?platform=$platform&extend=versionHistory"; + } + + public function getName(){ + if (isset($this->name)) { + return $this->name . ' - AppStore Updates'; + } + + return parent::getName(); + } + + /** + * In case of some platforms, the data is present in the initial response + */ + private function getDataFromShoebox($id, $platform, $country){ + $uri = $this->makeHtmlUrl($id, $country); + $html = getSimpleHTMLDOMCached($uri, 3600); + $script = $html->find('script[id="shoebox-ember-data-store"]', 0); + + $json = json_decode($script->innertext, true); + return $json['data']; + } + + private function getJWTToken($id, $platform, $country){ + $uri = $this->makeHtmlUrl($id, $country); + + $html = getSimpleHTMLDOMCached($uri, 3600); + + $meta = $html->find('meta[name="web-experience-app/config/environment"]', 0); + + $json = urldecode($meta->content); + + $json = json_decode($json); + + return $json->MEDIA_API->token; + } + + private function getAppData($id, $platform, $country, $token){ + $uri = $this->makeJsonUrl($id, $platform, $country); + + $headers = array( + "Authorization: Bearer $token", + ); + + $json = json_decode(getContents($uri, $headers), true); + + return $json['data'][0]; + } + + /** + * Parses the version history from the data received + * @return array list of versions with details on each element + */ + private function getVersionHistory($data, $platform){ + switch($platform) { + case 'mac': + return $data['relationships']['platforms']['data'][0]['attributes']['versionHistory']; + default: + $os = self::PLATFORM_MAPPING[$platform]; + return $data['attributes']['platformAttributes'][$os]['versionHistory']; + } + } + + public function collectData() { + $id = $this->getInput('id'); + $country = $this->getInput('country'); + $platform = $this->getInput('p'); + + switch ($platform) { + case 'mac': + $data = $this->getDataFromShoebox($id, $platform, $country); + break; + + default: + $token = $this->getJWTToken($id, $platform, $country); + $data = $this->getAppData($id, $platform, $country, $token); + } + + $versionHistory = $this->getVersionHistory($data, $platform); + $name = $this->name = $data['attributes']['name']; + $author = $data['attributes']['artistName']; + + foreach ($versionHistory as $row) { + $item = array(); + + $item['content'] = nl2br($row['releaseNotes']); + $item['title'] = $name . ' - ' . $row['versionDisplay']; + $item['timestamp'] = $row['releaseDate']; + $item['author'] = $author; + + $item['uri'] = $this->makeHtmlUrl($id, $country); + + $this->items[] = $item; + } + } +} diff --git a/bridges/AppleMusicBridge.php b/bridges/AppleMusicBridge.php index 5a4f40a4..30119777 100644 --- a/bridges/AppleMusicBridge.php +++ b/bridges/AppleMusicBridge.php @@ -5,19 +5,19 @@ class AppleMusicBridge extends BridgeAbstract { const URI = 'https://www.apple.com'; const DESCRIPTION = 'Fetches the latest releases from an artist'; const MAINTAINER = 'Limero'; - const PARAMETERS = [[ - 'url' => [ + const PARAMETERS = array(array( + 'url' => array( 'name' => 'Artist URL', 'exampleValue' => 'https://itunes.apple.com/us/artist/dunderpatrullen/329796274', 'required' => true, - ], - 'imgSize' => [ + ), + 'imgSize' => array( 'name' => 'Image size for thumbnails (in px)', 'type' => 'number', 'defaultValue' => 512, 'required' => true, - ] - ]]; + ) + )); const CACHE_TIMEOUT = 21600; // 6 hours public function collectData() { @@ -36,12 +36,12 @@ class AppleMusicBridge extends BridgeAbstract { // Loop through each object foreach ($json->included as $obj) { if ($obj->type === 'lockup/album') { - $this->items[] = [ + $this->items[] = array( 'title' => $obj->attributes->artistName . ' - ' . $obj->attributes->name, 'uri' => $obj->attributes->url, 'timestamp' => $obj->attributes->releaseDate, 'enclosures' => $obj->relationships->artwork->data->id, - ]; + ); } elseif ($obj->type === 'image') { $images[$obj->id] = $obj->attributes->url; } @@ -49,9 +49,9 @@ class AppleMusicBridge extends BridgeAbstract { // Add the images to each item foreach ($this->items as &$item) { - $item['enclosures'] = [ + $item['enclosures'] = array( str_replace('{w}x{h}bb.{f}', $imgSize . 'x0w.jpg', $images[$item['enclosures']]), - ]; + ); } // Sort the order to put the latest albums first diff --git a/bridges/AtmoNouvelleAquitaineBridge.php b/bridges/AtmoNouvelleAquitaineBridge.php new file mode 100644 index 00000000..d395fa78 --- /dev/null +++ b/bridges/AtmoNouvelleAquitaineBridge.php @@ -0,0 +1,4638 @@ + array( + 'name' => 'Choisir une ville', + 'type' => 'list', + 'values' => self::CITIES + ) + )); + const CACHE_TIMEOUT = 7200; + + 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]; + } + + public function collectData() { + $uri = self::URI . $this->getInput('cities'); + + $html = getSimpleHTMLDOM($uri) + or returnServerError('Could not request ' . $uri); + + $this->dom = $html->find('#block-system-main .city-prevision-map', 0); + + $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']); + + $this->items[] = $item; + } + + private function getIndex() { + $index = $this->dom->find('.indice', 0)->innertext; + + if ($index == 'XX') { + return -1; + } + + return $index; + } + + 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']; + } + + return 'Aucune donnée'; + } + + 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; + } + } + + return $indexes; + } + + 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 ($tomorrowTrendIndexNode && $tomorrowTrendIndexNode->hasAttribute('data-index')) { + $tomorrowTrendIndex = $tomorrowTrendIndexNode->getAttribute('data-index'); + } else { + return -1; + } + + return $tomorrowTrendIndex; + } + + private function getTomorrowTrendQualityText($trendIndex, $indexes) { + if ($trendIndex == -1) { + if (array_key_exists('no-available', $indexes)) { + return $indexes['no-available']; + } + + return 'Aucune donnée'; + } + + return $this->getClosest($trendIndex, $indexes); + } + + private function getIndexMessage() { + $index = $this->getIndex(); + $maxIndexText = $this->getMaxIndexText(); + + if ($index == -1) { + return 'Aucune donnée pour l\'indice.'; + } + + return "L'indice d'aujourd'hui est $index$maxIndexText."; + } + + 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.'; + } + + return "La qualité de l'air est $quality."; + } + + private function getTomorrowTrendIndexMessage() { + $trendIndex = $this->getTomorrowTrendIndex(); + $maxIndexText = $this->getMaxIndexText(); + + if ($trendIndex == -1) { + return 'Aucune donnée pour l\'indice prévu demain.'; + } + + return "L'indice prévu pour demain est $trendIndex$maxIndexText."; + } + + 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."; + } + + 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' + ); +} diff --git a/bridges/AutoJMBridge.php b/bridges/AutoJMBridge.php index e8490d95..25fb2cb8 100644 --- a/bridges/AutoJMBridge.php +++ b/bridges/AutoJMBridge.php @@ -15,16 +15,6 @@ class AutoJMBridge extends BridgeAbstract { 'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/', 'exampleValue' => 'achat-voitures-neuves-peugeot-nouvelle-308-5p' ), - 'isDispo' => array( - 'name' => 'Disponibilité', - 'type' => 'list', - 'values' => array( - '-' => '', - 'En stock' => 1, - 'Sur commande' => 0 - ), - 'title' => 'Critère de disponibilité' - ), 'energy' => array( 'name' => 'Carburant', 'type' => 'list', @@ -92,7 +82,6 @@ class AutoJMBridge extends BridgeAbstract { // Build the form $post_data = array( - 'form[isDispo]' => $this->getInput('isDispo'), 'form[energy]' => $this->getInput('energy'), 'form[transmission]' => $this->getInput('transmission'), 'form[priceMin]' => $this->getInput('priceMin'), @@ -121,7 +110,7 @@ class AutoJMBridge extends BridgeAbstract { $html = str_get_html($data->content); // Go through every finisha of the model - $list = $html->find('h2'); + $list = $html->find('h3'); foreach ($list as $finish) { $finish_name = $finish->plaintext; $motorizations = $finish->next_sibling()->find('li'); diff --git a/bridges/BAEBridge.php b/bridges/BAEBridge.php index caa2cf72..6c5d8ba2 100644 --- a/bridges/BAEBridge.php +++ b/bridges/BAEBridge.php @@ -55,9 +55,7 @@ class BAEBridge extends BridgeAbstract { $content .= '
'; $content .= $htmlDetail->find('section', 0)->innertext; - $content = str_replace('src="/', 'src="' . parent::getURI() . '/', $content); - $content = str_replace('href="/', 'href="' . parent::getURI() . '/', $content); - $item['content'] = $content; + $item['content'] = defaultLinkTo($content, parent::getURI()); $image = $htmlDetail->find('#zoom', 0); if ($image) { $item['enclosures'] = array(parent::getURI() . $image->getAttribute('src')); diff --git a/bridges/BandcampBridge.php b/bridges/BandcampBridge.php index 6c75ed5e..fa071465 100644 --- a/bridges/BandcampBridge.php +++ b/bridges/BandcampBridge.php @@ -1,73 +1,262 @@ array( - 'name' => 'tag', - 'type' => 'text', - 'required' => true + const DESCRIPTION = 'New bandcamp releases by tag, band or album'; + const PARAMETERS = array( + 'By tag' => array( + 'tag' => array( + 'name' => 'tag', + 'type' => 'text', + 'required' => true + ) + ), + 'By band' => array( + 'band' => array( + 'name' => 'band', + 'type' => 'text', + 'title' => 'Band name as seen in the band page URL', + 'required' => true + ), + 'type' => array( + 'name' => 'Articles are', + 'type' => 'list', + 'values' => array( + 'Releases' => 'releases', + 'Releases, new one when track list changes' => 'changes', + 'Individual tracks' => 'tracks' + ), + 'defaultValue' => 'changes' + ), + 'limit' => array( + 'name' => 'limit', + 'type' => 'number', + 'title' => 'Number of releases to return', + 'defaultValue' => 5 + ) + ), + 'By album' => array( + 'band' => array( + 'name' => 'band', + 'type' => 'text', + 'title' => 'Band name as seen in the album page URL', + 'required' => true + ), + 'album' => array( + 'name' => 'album', + 'type' => 'text', + 'title' => 'Album name as seen in the album page URL', + 'required' => true + ), + 'type' => array( + 'name' => 'Articles are', + 'type' => 'list', + 'values' => array( + 'Releases' => 'releases', + 'Releases, new one when track list changes' => 'changes', + 'Individual tracks' => 'tracks' + ), + 'defaultValue' => 'tracks' + ) ) - )); + ); const IMGURI = 'https://f4.bcbits.com/'; const IMGSIZE_300PX = 23; const IMGSIZE_700PX = 16; + private $feedName; + public function getIcon() { return 'https://s4.bcbits.com/img/bc_favicon.ico'; } public function collectData(){ - $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) - or returnServerError('Could not complete request to: ' . $url); - - $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 + 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) ); - $item['content'] = "
$full_title"; - $item['enclosures'] = array($img); - $this->items[] = $item; + $opts = array( + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => $data + ); + $content = getContents($url, $header, $opts) + or returnServerError('Could not complete request to: ' . $url); + + $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'] = "
$full_title"; + $item['enclosures'] = array($img); + $this->items[] = $item; + } + break; + case 'By band': + case 'By album': + $html = getSimpleHTMLDOMCached($this->getURI(), 86400); + + $titleElement = $html->find('head meta[name=title]', 0) + or returnServerError('Unable to find title on: ' . $this->getURI()); + $this->feedName = $titleElement->content; + + $regex = '/band_id=(\d+)/'; + if(preg_match($regex, $html, $matches) == false) + returnServerError('Unable to find band ID on: ' . $this->getURI()); + $band_id = $matches[1]; + + $tralbums = array(); + switch($this->queriedContext) { + case 'By band': + $query_data = array( + 'band_id' => $band_id + ); + $band_data = $this->apiGet('mobile/22/band_details', $query_data); + + $num_albums = min(count($band_data->discography), $this->getInput('limit')); + for($i = 0; $i < $num_albums; $i++) { + $album_basic_data = $band_data->discography[$i]; + + // 'a' or 't' for albums and individual tracks respectively + $tralbum_type = substr($album_basic_data->item_type, 0, 1); + + $query_data = array( + 'band_id' => $band_id, + 'tralbum_type' => $tralbum_type, + 'tralbum_id' => $album_basic_data->item_id + ); + $tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data); + } + break; + case 'By album': + $regex = '/album=(\d+)/'; + if(preg_match($regex, $html, $matches) == false) + returnServerError('Unable to find album ID on: ' . $this->getURI()); + $album_id = $matches[1]; + + $query_data = array( + 'band_id' => $band_id, + 'tralbum_type' => 'a', + 'tralbum_id' => $album_id + ); + $tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data); + + break; + } + + foreach ($tralbums as $tralbum_data) { + if ($tralbum_data->type === 'a' && $this->getInput('type') === 'tracks') { + foreach ($tralbum_data->tracks as $track) { + $query_data = array( + 'band_id' => $band_id, + 'tralbum_type' => 't', + 'tralbum_id' => $track->track_id + ); + $track_data = $this->apiGet('mobile/22/tralbum_details', $query_data); + + $this->items[] = $this->buildTralbumItem($track_data); + } + } else { + $this->items[] = $this->buildTralbumItem($tralbum_data); + } + } + break; } } + private function buildTralbumItem($tralbum_data){ + $band_data = $tralbum_data->band; + + // Format title like: ARTIST - ALBUM/TRACK (OPTIONAL RELEASER) + // Format artist/author like: ARTIST (OPTIONAL RELEASER) + // + // If the album/track is released under a label/a band other than the artist + // themselves, append that releaser name to the title and artist/author. + // + // This sadly doesn't always work right for individual tracks as the artist + // of the track is always set to the releaser. + $artist = $tralbum_data->tralbum_artist; + $full_title = $artist . ' - ' . $tralbum_data->title; + $full_artist = $artist; + if (isset($tralbum_data->label)) { + $full_title .= ' (' . $tralbum_data->label . ')'; + $full_artist .= ' (' . $tralbum_data->label . ')'; + } elseif ($band_data->name !== $artist) { + $full_title .= ' (' . $band_data->name . ')'; + $full_artist .= ' (' . $band_data->name . ')'; + } + + $small_img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_300PX); + $img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_700PX); + + $item = array( + 'uri' => $tralbum_data->bandcamp_url, + 'author' => $full_artist, + 'title' => $full_title, + 'enclosures' => array($img), + 'timestamp' => $tralbum_data->release_date + ); + + $item['categories'] = array(); + foreach ($tralbum_data->tags as $tag) { + $item['categories'][] = $tag->norm_name; + } + + // Give articles a unique UID depending on its track list + // Releases should then show up as new articles when tracks are added + if ($this->getInput('type') === 'changes') { + $item['uid'] = "bandcamp/$band_data->band_id/$tralbum_data->id/"; + foreach ($tralbum_data->tracks as $track) { + $item['uid'] .= $track->track_id; + } + } + + $item['content'] = "
$full_title
"; + if ($tralbum_data->type === 'a') { + $item['content'] .= '
    '; + foreach ($tralbum_data->tracks as $track) { + $item['content'] .= "
  1. $track->title
  2. "; + } + $item['content'] .= '
'; + } + if (!empty($tralbum_data->about)) { + $item['content'] .= '

' + . nl2br($tralbum_data->about) + . '

'; + } + + return $item; + } + private function buildRequestJson(){ $requestJson = array( 'tag' => $this->getInput('tag'), @@ -81,11 +270,94 @@ class BandcampBridge extends BridgeAbstract { return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg'; } + private function apiGet($endpoint, $query_data) { + $url = self::URI . 'api/' . $endpoint . '?' . http_build_query($query_data); + $data = json_decode(getContents($url)) + or returnServerError('API request to "' . $url . '" failed.'); + return $data; + } + + public function getURI(){ + switch($this->queriedContext) { + case 'By tag': + if(!is_null($this->getInput('tag'))) { + return self::URI + . 'tag/' + . urlencode($this->getInput('tag')) + . '?sort_field=date'; + } + break; + case 'By band': + if(!is_null($this->getInput('band'))) { + return 'https://' + . $this->getInput('band') + . '.bandcamp.com/music'; + } + break; + case 'By album': + if(!is_null($this->getInput('band')) && !is_null($this->getInput('album'))) { + return 'https://' + . $this->getInput('band') + . '.bandcamp.com/album/' + . $this->getInput('album'); + } + break; + } + + return parent::getURI(); + } + public function getName(){ - if(!is_null($this->getInput('tag'))) { - return $this->getInput('tag') . ' - Bandcamp Tag'; + switch($this->queriedContext) { + case 'By tag': + if(!is_null($this->getInput('tag'))) { + return $this->getInput('tag') . ' - Bandcamp Tag'; + } + break; + case 'By band': + if(isset($this->feedName)) { + return $this->feedName . ' - Bandcamp Band'; + } elseif(!is_null($this->getInput('band'))) { + return $this->getInput('band') . ' - Bandcamp Band'; + } + break; + case 'By album': + if(isset($this->feedName)) { + return $this->feedName . ' - Bandcamp Album'; + } elseif(!is_null($this->getInput('album'))) { + return $this->getInput('album') . ' - Bandcamp Album'; + } + break; } return parent::getName(); } + + public function detectParameters($url) { + $params = array(); + + // By tag + $regex = '/^(https?:\/\/)?bandcamp\.com\/tag\/([^\/.&?\n]+)/'; + if(preg_match($regex, $url, $matches) > 0) { + $params['tag'] = urldecode($matches[2]); + return $params; + } + + // By band + $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/'; + if(preg_match($regex, $url, $matches) > 0) { + $params['band'] = urldecode($matches[2]); + return $params; + } + + // By album + $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/'; + if(preg_match($regex, $url, $matches) > 0) { + $params['band'] = urldecode($matches[2]); + $params['album'] = urldecode($matches[3]); + return $params; + } + + return null; + } } diff --git a/bridges/BastaBridge.php b/bridges/BastaBridge.php index 17d3da76..613005fd 100644 --- a/bridges/BastaBridge.php +++ b/bridges/BastaBridge.php @@ -3,17 +3,11 @@ class BastaBridge extends BridgeAbstract { const MAINTAINER = 'qwertygc'; const NAME = 'Bastamag Bridge'; - const URI = 'http://www.bastamag.net/'; + const URI = 'https://www.bastamag.net/'; const CACHE_TIMEOUT = 7200; // 2h const DESCRIPTION = 'Returns the newest articles.'; public function collectData(){ - // Replaces all relative image URLs by absolute URLs. - // Relative URLs always start with 'local/'! - function replaceImageUrl($content){ - return preg_replace('/src=["\']{1}([^"\']+)/ims', 'src=\'' . self::URI . '$1\'', $content); - } - $html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend') or returnServerError('Could not request Bastamag.'); @@ -25,7 +19,13 @@ class BastaBridge extends BridgeAbstract { $item['title'] = $element->find('title', 0)->innertext; $item['uri'] = $element->find('guid', 0)->plaintext; $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext); - $item['content'] = replaceImageUrl(getSimpleHTMLDOM($item['uri'])->find('div.texte', 0)->innertext); + // Replaces all relative image URLs by absolute URLs. + // Relative URLs always start with 'local/'! + $item['content'] = preg_replace( + '/src=["\']{1}([^"\']+)/ims', + 'src=\'' . self::URI . '$1\'', + getSimpleHTMLDOM($item['uri'])->find('div.texte', 0)->innertext + ); $this->items[] = $item; $limit++; } diff --git a/bridges/BingSearchBridge.php b/bridges/BingSearchBridge.php index eb8a5fc9..357feb6c 100644 --- a/bridges/BingSearchBridge.php +++ b/bridges/BingSearchBridge.php @@ -92,7 +92,7 @@ class BingSearchBridge extends BridgeAbstract or returnServerError('Could not request ' . self::NAME); $sizeKey = $this->getInput('image_size'); - $items = []; + $items = array(); foreach ($html->find('a.iusc') as $element) { $data = json_decode(htmlspecialchars_decode($element->getAttribute('m')), true); diff --git a/bridges/BloombergBridge.php b/bridges/BloombergBridge.php deleted file mode 100644 index 9eb12191..00000000 --- a/bridges/BloombergBridge.php +++ /dev/null @@ -1,69 +0,0 @@ - array(), - 'From Search' => array( - 'q' => array( - 'name' => 'Keyword', - 'required' => true - ) - ) - ); - - public function getName() - { - switch($this->queriedContext) { - case 'Trending Stories': - return self::NAME . ' Trending Stories'; - case 'From Search': - if (!is_null($this->getInput('q'))) { - return self::NAME . ' Search : ' . $this->getInput('q'); - } - break; - } - - return parent::getName(); - } - - public function getIcon() { - return 'https://assets.bwbx.io/s3/javelin/public/hub/images/favicon-black-63fe5249d3.png'; - } - - public function collectData() - { - switch($this->queriedContext) { - case 'Trending Stories': // Get list of top new
s from the front page. - $html = getSimpleHTMLDOMCached($this->getURI(), 300); - $stories = $html->find('ul.top-news-v3__stories article.top-news-v3-story'); - break; - case 'From Search': // Get list of
elements from search. - $html = getSimpleHTMLDOMCached( - $this->getURI() . - 'search?sort=time:desc&page=1&query=' . - urlencode($this->getInput('q')), 300 - ); - $stories = $html->find('div.search-result-items article.search-result-story'); - break; - } - foreach ($stories as $element) { - $item['uri'] = $element->find('h1 a', 0)->href; - if (preg_match('#^https://#i', $item['uri']) !== 1) { - $item['uri'] = $this->getURI() . $item['uri']; - } - $articleHtml = getSimpleHTMLDOMCached($item['uri']); - if (!$articleHtml) { - continue; - } - $item['title'] = $element->find('h1 a', 0)->plaintext; - $item['timestamp'] = strtotime($articleHtml->find('meta[name=iso-8601-publish-date],meta[name=date]', 0)->content); - $item['content'] = $articleHtml->find('meta[name=description]', 0)->content; - $this->items[] = $item; - } - } -} diff --git a/bridges/BrutBridge.php b/bridges/BrutBridge.php index 432cb502..32265b69 100644 --- a/bridges/BrutBridge.php +++ b/bridges/BrutBridge.php @@ -92,6 +92,21 @@ class BrutBridge extends BridgeAbstract { return parent::getURI(); } + public function getName() { + + if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) { + $parameters = $this->getParameters(); + + $editionValues = array_flip($parameters[0]['edition']['values']); + $categoryValues = array_flip($parameters[0]['category']['values']); + + return $categoryValues[$this->getInput('category')] . ' - ' . + $editionValues[$this->getInput('edition')] . ' - Brut.'; + } + + return parent::getName(); + } + private function processDate($description) { if ($this->getInput('edition') === 'uk') { diff --git a/bridges/CNETFranceBridge.php b/bridges/CNETFranceBridge.php new file mode 100644 index 00000000..d005fd1f --- /dev/null +++ b/bridges/CNETFranceBridge.php @@ -0,0 +1,63 @@ + array( + 'title' => array( + 'name' => 'Exclude by title', + 'required' => false, + 'title' => 'Title term, separated by semicolon (;)', + 'defaultValue' => 'bon plan;bons plans;au meilleur prix;des meilleures offres;Amazon Prime Day;RED by SFR ou B&You' + ), + 'url' => array( + 'name' => 'Exclude by url', + 'required' => false, + 'title' => 'URL term, separated by semicolon (;)', + 'defaultValue' => 'bon-plan;bons-plans' + ) + ) + ); + + private $bannedTitle = array(); + private $bannedURL = array(); + + public function collectData() + { + $title = $this->getInput('title'); + $url = $this->getInput('url'); + + if ($title !== null) { + $this->bannedTitle = explode(';', $title); + } + + if ($url !== null) { + $this->bannedURL = explode(';', $url); + } + + $this->collectExpandableDatas('https://www.cnetfrance.fr/feeds/rss/news/'); + } + + protected function parseItem($feedItem) + { + $item = parent::parseItem($feedItem); + + foreach ($this->bannedTitle as $term) { + if (preg_match('/' . $term . '/mi', $item['title']) === 1) { + return null; + } + } + + foreach ($this->bannedURL as $term) { + if (preg_match('/' . $term . '/mi', $item['uri']) === 1) { + return null; + } + } + + return $item; + } +} diff --git a/bridges/CachetBridge.php b/bridges/CachetBridge.php index a60b8f73..75b18017 100644 --- a/bridges/CachetBridge.php +++ b/bridges/CachetBridge.php @@ -22,7 +22,7 @@ class CachetBridge extends BridgeAbstract { ); const CACHE_TIMEOUT = 300; - private $componentCache = []; + private $componentCache = array(); public function getURI() { return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host'); @@ -114,13 +114,13 @@ class CachetBridge extends BridgeAbstract { $uidOrig = $permalink . $incident->created_at; $uid = hash('sha512', $uidOrig); $timestamp = strtotime($incident->created_at); - $categories = []; + $categories = array(); $categories[] = $incident->human_status; if ($componentName !== '') { $categories[] = $componentName; } - $item = []; + $item = array(); $item['uri'] = $permalink; $item['title'] = $title; $item['timestamp'] = $timestamp; diff --git a/bridges/CastorusBridge.php b/bridges/CastorusBridge.php index 3ed1331e..fbd5007c 100644 --- a/bridges/CastorusBridge.php +++ b/bridges/CastorusBridge.php @@ -2,7 +2,7 @@ class CastorusBridge extends BridgeAbstract { const MAINTAINER = 'logmanoriginal'; const NAME = 'Castorus Bridge'; - const URI = 'http://www.castorus.com'; + const URI = 'https://www.castorus.com'; const CACHE_TIMEOUT = 600; // 10min const DESCRIPTION = 'Returns the latest changes'; @@ -83,7 +83,7 @@ class CastorusBridge extends BridgeAbstract { if(!$html) returnServerError('Could not load data from ' . self::URI . '!'); - $activities = $html->find('div#activite/li'); + $activities = $html->find('div#activite > li'); if(!$activities) returnServerError('Failed to find activities!'); diff --git a/bridges/CollegeDeFranceBridge.php b/bridges/CollegeDeFranceBridge.php index 1f816832..9640c867 100644 --- a/bridges/CollegeDeFranceBridge.php +++ b/bridges/CollegeDeFranceBridge.php @@ -3,7 +3,7 @@ class CollegeDeFranceBridge extends BridgeAbstract { const MAINTAINER = 'pit-fgfjiudghdf'; const NAME = 'CollegeDeFrance'; - const URI = 'http://www.college-de-france.fr/'; + const URI = 'https://www.college-de-france.fr/'; const CACHE_TIMEOUT = 10800; // 3h const DESCRIPTION = 'Returns the latest audio and video from CollegeDeFrance'; diff --git a/bridges/ComicsKingdomBridge.php b/bridges/ComicsKingdomBridge.php new file mode 100644 index 00000000..b6228dc6 --- /dev/null +++ b/bridges/ComicsKingdomBridge.php @@ -0,0 +1,65 @@ + array( + 'name' => 'comicname', + 'type' => 'text', + 'required' => true + ) + )); + + public function collectData(){ + $html = getSimpleHTMLDOM($this->getURI(), array(), array(), true, false) + or returnServerError('Could not request Comics Kingdom: ' . $this->getURI()); + + // Get author from first page + $author = $html->find('div.author p', 0)->plaintext + or returnServerError('Comics Kingdom comic does not exist: ' . $this->getURI());; + + // Get current date/link + $link = $html->find('meta[property=og:url]', 0)->content; + for($i = 0; $i < 5; $i++) { + $item = array(); + + $page = getSimpleHTMLDOM($link) + or returnServerError('Could not request Comics Kingdom: ' . $link); + + $imagelink = $page->find('meta[property=og:image]', 0)->content; + $prevSlug = $page->find('slider-arrow[:is-left-arrow=true]', 0); + $link = $this->getURI() . '/' . $prevSlug->getAttribute('date-slug'); + + $date = explode('/', $link); + + $item['id'] = $imagelink; + $item['uri'] = $link; + $item['author'] = $author; + $item['title'] = 'Comics Kingdom ' . $this->getInput('comicname'); + $item['timestamp'] = DateTime::createFromFormat('Y-m-d', $date[count($date) - 1])->getTimestamp(); + $item['content'] = ''; + + $this->items[] = $item; + } + } + + public function getURI(){ + if(!is_null($this->getInput('comicname'))) { + return self::URI . urlencode($this->getInput('comicname')); + } + + return parent::getURI(); + } + + public function getName(){ + if(!is_null($this->getInput('comicname'))) { + return $this->getInput('comicname') . ' - Comics Kingdom'; + } + + return parent::getName(); + } +} diff --git a/bridges/ContainerLinuxReleasesBridge.php b/bridges/ContainerLinuxReleasesBridge.php index d2f63256..d459b0fc 100644 --- a/bridges/ContainerLinuxReleasesBridge.php +++ b/bridges/ContainerLinuxReleasesBridge.php @@ -10,20 +10,20 @@ class ContainerLinuxReleasesBridge extends BridgeAbstract { const BETA = 'beta'; const ALPHA = 'alpha'; - const PARAMETERS = [ - [ - 'channel' => [ + const PARAMETERS = array( + array( + 'channel' => array( 'name' => 'Release Channel', 'type' => 'list', 'defaultValue' => self::STABLE, - 'values' => [ + 'values' => array( 'Stable' => self::STABLE, 'Beta' => self::BETA, 'Alpha' => self::ALPHA, - ], - ] - ] - ]; + ), + ) + ) + ); private function getReleaseFeed($jsonUrl) { $json = getContents($jsonUrl) @@ -39,7 +39,7 @@ class ContainerLinuxReleasesBridge extends BridgeAbstract { $data = $this->getReleaseFeed($this->getJsonUri()); foreach ($data as $releaseVersion => $release) { - $item = []; + $item = array(); $item['uri'] = "https://coreos.com/releases/#$releaseVersion"; $item['title'] = $releaseVersion; diff --git a/bridges/CuriousCatBridge.php b/bridges/CuriousCatBridge.php new file mode 100644 index 00000000..0ebc8bd6 --- /dev/null +++ b/bridges/CuriousCatBridge.php @@ -0,0 +1,109 @@ + array( + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'koethekoethe', + ) + )); + + const CACHE_TIMEOUT = 3600; + + public function collectData() { + + $url = self::URI . '/api/v2/profile?username=' . urlencode($this->getInput('username')); + + $apiJson = getContents($url) + or returnServerError('Could not request: ' . $url); + + $apiData = json_decode($apiJson, true); + + foreach($apiData['posts'] as $post) { + $item = array(); + + $item['author'] = 'Anonymous'; + + if ($post['senderData']['id'] !== false) { + $item['author'] = $post['senderData']['username']; + } + + $item['uri'] = $this->getURI() . '/post/' . $post['id']; + $item['title'] = $this->ellipsisTitle($post['comment']); + + $item['content'] = $this->processContent($post); + $item['timestamp'] = $post['timestamp']; + + $this->items[] = $item; + } + } + + public function getURI() { + + if (!is_null($this->getInput('username'))) { + return self::URI . '/' . $this->getInput('username'); + } + + return parent::getURI(); + } + + public function getName() { + + if (!is_null($this->getInput('username'))) { + return $this->getInput('username') . ' - Curious Cat'; + } + + return parent::getName(); + } + + private function processContent($post) { + + $author = 'Anonymous'; + + if ($post['senderData']['id'] !== false) { + $authorUrl = self::URI . '/' . $post['senderData']['username']; + + $author = <<{$post['senderData']['username']} +EOD; + } + + $question = $this->formatUrls($post['comment']); + $answer = $this->formatUrls($post['reply']); + + $content = <<{$author} asked:

+
{$question}

+

{$post['addresseeData']['username']} answered:

+
{$answer}
+EOD; + + return $content; + } + + private function ellipsisTitle($text) { + $length = 150; + + if (strlen($text) > $length) { + $text = explode('
', wordwrap($text, $length, '
')); + return $text[0] . '...'; + } + + return $text; + } + + private function formatUrls($content) { + + return preg_replace( + '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims', + '$1 ', + $content + ); + + } +} diff --git a/bridges/DailymotionBridge.php b/bridges/DailymotionBridge.php index ff8d4828..dc4f5d3f 100644 --- a/bridges/DailymotionBridge.php +++ b/bridges/DailymotionBridge.php @@ -4,7 +4,7 @@ class DailymotionBridge extends BridgeAbstract { const MAINTAINER = 'mitsukarenai'; const NAME = 'Dailymotion Bridge'; const URI = 'https://www.dailymotion.com/'; - const CACHE_TIMEOUT = 10800; // 3h + const CACHE_TIMEOUT = 3600; // 1h const DESCRIPTION = 'Returns the 5 newest videos by username/playlist or search'; const PARAMETERS = array ( @@ -27,74 +27,99 @@ class DailymotionBridge extends BridgeAbstract { ), 'pa' => array( 'name' => 'Page', - 'type' => 'number' + 'type' => 'number', + 'defaultValue' => 1, ) ) ); - protected function getMetadata($id){ - $metadata = array(); - $html2 = getSimpleHTMLDOM(self::URI . 'video/' . $id); - if(!$html2) { - return $metadata; - } + private $feedName = ''; - $metadata['title'] = $html2->find('meta[property=og:title]', 0)->getAttribute('content'); - $metadata['timestamp'] = strtotime( - $html2->find('meta[property=video:release_date]', 0)->getAttribute('content') - ); - $metadata['thumbnailUri'] = $html2->find('meta[property=og:image]', 0)->getAttribute('content'); - $metadata['uri'] = $html2->find('meta[property=og:url]', 0)->getAttribute('content'); - return $metadata; - } + private $apiUrl = 'https://api.dailymotion.com'; + private $apiFields = 'created_time,description,id,owner.screenname,tags,thumbnail_url,title,url'; public function getIcon() { return 'https://static1-ssl.dmcdn.net/images/neon/favicons/android-icon-36x36.png.vf806ca4ed0deed812'; } - public function collectData(){ - $html = ''; - $limit = 5; - $count = 0; + public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()) - or returnServerError('Could not request Dailymotion.'); + if ($this->queriedContext === 'By username' || $this->queriedContext === 'By playlist id') { - foreach($html->find('div.media a.preview_link') as $element) { - if($count < $limit) { + $apiJson = getContents($this->getApiUrl()) + or returnServerError('Could not request: ' . $this->getApiUrl()); + + $apiData = json_decode($apiJson, true); + + $this->feedName = $this->getPlaylistTitle($this->getInput('p')); + + foreach ($apiData['list'] as $apiItem) { $item = array(); + + $item['uri'] = $apiItem['url']; + $item['uid'] = $apiItem['id']; + $item['title'] = $apiItem['title']; + $item['timestamp'] = $apiItem['created_time']; + $item['author'] = $apiItem['owner.screenname']; + $item['content'] = '

+

' . $apiItem['description'] . '

'; + $item['categories'] = $apiItem['tags']; + $item['enclosures'][] = $apiItem['thumbnail_url']; + + $this->items[] = $item; + } + } + + if ($this->queriedContext === 'From search results') { + + $html = getSimpleHTMLDOM($this->getURI()) + or returnServerError('Could not request Dailymotion.'); + + foreach($html->find('div.media a.preview_link') as $element) { + $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'] = '
' - . $item['title'] - . ''; + . $item['uri'] + . '">
' + . $item['title'] + . ''; $this->items[] = $item; - $count++; + + if (count($this->items) >= 5) { + break; + } } } } - public function getName(){ + public function getName() { switch($this->queriedContext) { case 'By username': $specific = $this->getInput('u'); break; case 'By playlist id': $specific = strtok($this->getInput('p'), '_'); + + if ($this->feedName) { + $specific = $this->feedName; + } + break; case 'From search results': $specific = $this->getInput('s'); @@ -102,26 +127,77 @@ class DailymotionBridge extends BridgeAbstract { default: return parent::getName(); } - return $specific . ' : Dailymotion Bridge'; + return $specific . ' : Dailymotion'; } public function getURI(){ $uri = self::URI; switch($this->queriedContext) { case 'By username': - $uri .= 'user/' . urlencode($this->getInput('u')) . '/1'; + $uri .= 'user/' . urlencode($this->getInput('u')); break; case 'By playlist id': $uri .= 'playlist/' . urlencode(strtok($this->getInput('p'), '_')); break; case 'From search results': $uri .= 'search/' . urlencode($this->getInput('s')); - if($this->getInput('pa')) { - $uri .= '/' . $this->getInput('pa'); + + if(!is_null($this->getInput('pa'))) { + $pa = $this->getInput('pa'); + + if ($this->getInput('pa') < 1) { + $pa = 1; + } + + $uri .= '/' . $pa; } break; default: return parent::getURI(); } return $uri; } + + private function getMetadata($id) { + $metadata = array(); + + $html = getSimpleHTMLDOM(self::URI . 'video/' . $id); + + if(!$html) { + return $metadata; + } + + $metadata['title'] = $html->find('meta[property=og:title]', 0)->getAttribute('content'); + $metadata['timestamp'] = strtotime( + $html->find('meta[property=video:release_date]', 0)->getAttribute('content') + ); + $metadata['thumbnailUri'] = $html->find('meta[property=og:image]', 0)->getAttribute('content'); + $metadata['uri'] = $html->find('meta[property=og:url]', 0)->getAttribute('content'); + return $metadata; + } + + private function getPlaylistTitle($id) { + $title = ''; + + $url = self::URI . 'playlist/' . $id; + + $html = getSimpleHTMLDOM($url) + or returnServerError('Could not request: ' . $url); + + $title = $html->find('meta[property=og:title]', 0)->getAttribute('content'); + return $title; + } + + private function getApiUrl() { + + switch($this->queriedContext) { + case 'By username': + return $this->apiUrl . '/user/' . $this->getInput('u') + . '/videos?fields=' . urlencode($this->apiFields) . '&availability=1&sort=recent&limit=5'; + break; + case 'By playlist id': + return $this->apiUrl . '/playlist/' . $this->getInput('p') + . '/videos?fields=' . urlencode($this->apiFields) . '&limit=5'; + break; + } + } } diff --git a/bridges/DanbooruBridge.php b/bridges/DanbooruBridge.php index 755399f4..ea4b2be8 100644 --- a/bridges/DanbooruBridge.php +++ b/bridges/DanbooruBridge.php @@ -40,7 +40,7 @@ class DanbooruBridge extends BridgeAbstract { defaultLinkTo($element, $this->getURI()); $item = array(); - $item['uri'] = $element->find('a', 0)->href; + $item['uri'] = html_entity_decode($element->find('a', 0)->href); $item['postid'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE)); $item['timestamp'] = time(); $thumbnailUri = $element->find('img', 0)->src; diff --git a/bridges/DarkReadingBridge.php b/bridges/DarkReadingBridge.php new file mode 100644 index 00000000..3baaad75 --- /dev/null +++ b/bridges/DarkReadingBridge.php @@ -0,0 +1,79 @@ + array( + 'name' => 'Feed', + 'type' => 'list', + 'values' => array( + 'All Dark Reading Stories' => '000_AllArticles', + 'Attacks/Breaches' => '644_Attacks/Breaches', + 'Application Security' => '645_Application%20Security', + 'Database Security' => '646_Database%20Security', + 'Cloud' => '647_Cloud', + 'Endpoint' => '648_Endpoint', + 'Authentication' => '649_Authentication', + 'Privacy' => '650_Privacy', + 'Mobile' => '651_Mobile', + 'Perimeter' => '652_Perimeter', + 'Risk' => '653_Risk', + 'Compliance' => '654_Compliance', + 'Operations' => '655_Operations', + 'Careers and People' => '656_Careers%20and%20People', + 'Identity and Access Management' => '657_Identity%20and%20Access%20Management', + 'Analytics' => '658_Analytics', + 'Threat Intelligence' => '659_Threat%20Intelligence', + 'Security Monitoring' => '660_Security%20Monitoring', + 'Vulnerabilities / Threats' => '661_Vulnerabilities%20/%20Threats', + 'Advanced Threats' => '662_Advanced%20Threats', + 'Insider Threats' => '663_Insider%20Threats', + 'Vulnerability Management' => '664_Vulnerability%20Management', + ) + ) + )); + + public function collectData(){ + $feed = $this->getInput('feed'); + $feed_splitted = explode('_', $feed); + $feed_id = $feed_splitted[0]; + $feed_name = $feed_splitted[1]; + if(empty($feed) || !ctype_digit($feed_id) || !preg_match('/[A-Za-z%20\/]/', $feed_name)) { + returnClientError('Invalid feed, please check the "feed" parameter.'); + } + $feed_url = $this->getURI() . 'rss_simple.asp'; + if ($feed_id != '000') { + $feed_url .= '?f_n=' . $feed_id . '&f_ln=' . $feed_name; + } + $this->collectExpandableDatas($feed_url); + } + + protected function parseItem($newsItem){ + $item = parent::parseItem($newsItem); + $article = getSimpleHTMLDOMCached($item['uri']) + or returnServerError('Could not request Dark Reading: ' . $item['uri']); + $item['content'] = $this->extractArticleContent($article); + $item['enclosures'] = array(); //remove author profile picture + return $item; + } + + private function extractArticleContent($article){ + $content = $article->find('div#article-main', 0)->innertext; + + foreach (array( + '
'); + + return $content; + } +} diff --git a/bridges/DavesTrailerPageBridge.php b/bridges/DavesTrailerPageBridge.php new file mode 100644 index 00000000..90afec46 --- /dev/null +++ b/bridges/DavesTrailerPageBridge.php @@ -0,0 +1,27 @@ +find('tr[!align]') as $tr) { + $item = array(); + + // title + $item['title'] = $tr->find('td', 0)->find('b', 0)->plaintext; + + // content + $item['content'] = $tr->find('ul', 1); + + // uri + $item['uri'] = $tr->find('a', 3)->getAttribute('href'); + + $this->items[] = $item; + } + } +} diff --git a/bridges/DealabsBridge.php b/bridges/DealabsBridge.php index b64bb1dc..1657b8b7 100644 --- a/bridges/DealabsBridge.php +++ b/bridges/DealabsBridge.php @@ -1145,7 +1145,7 @@ class PepperBridgeAbstract extends BridgeAbstract { } else { foreach ($list as $deal) { $item = array(); - $item['uri'] = $deal->find('div[class=threadGrid-title]', 0)->find('a', 0)->href; + $item['uri'] = $deal->find('div[class*=threadGrid-title]', 0)->find('a', 0)->href; $item['title'] = $deal->find('a[class*=' . $selectorLink . ']', 0 )->plaintext; $item['author'] = $deal->find('span.thread-username', 0)->plaintext; diff --git a/bridges/DemonoidBridge.php b/bridges/DemonoidBridge.php deleted file mode 100644 index 842b4214..00000000 --- a/bridges/DemonoidBridge.php +++ /dev/null @@ -1,169 +0,0 @@ - array( - 'q' => array( - 'name' => 'keywords', - 'exampleValue' => 'keyword1 keyword2…', - 'required' => true, - ), - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'All' => 0, - 'Movies' => 1, - 'Music' => 2, - 'TV' => 3, - 'Games' => 4, - 'Applications' => 5, - 'Pictures' => 8, - 'Anime' => 9, - 'Comics' => 10, - 'Books' => 11, - 'Audiobooks' => 17 - ) - ) - ), - 'Category Only' => array( - 'catOnly' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'All' => 0, - 'Movies' => 1, - 'Music' => 2, - 'TV' => 3, - 'Games' => 4, - 'Applications' => 5, - 'Pictures' => 8, - 'Anime' => 9, - 'Comics' => 10, - 'Books' => 11, - 'Audiobooks' => 17 - ) - ) - ), - 'User ID' => array( - 'userid' => array( - 'name' => 'user id', - 'exampleValue' => '00000', - 'required' => true, - 'type' => 'number' - ), - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'All' => 0, - 'Movies' => 1, - 'Music' => 2, - 'TV' => 3, - 'Games' => 4, - 'Applications' => 5, - 'Pictures' => 8, - 'Anime' => 9, - 'Comics' => 10, - 'Books' => 11, - 'Audiobooks' => 17 - ) - ) - ) - ); - - public function collectData() { - - if(!empty($this->getInput('q'))) { - - $html = getSimpleHTMLDOM( - self::URI . - 'files/?category=' . - rawurlencode($this->getInput('category')) . - '&subcategory=All&quality=All&seeded=2&external=2&query=' . - urlencode($this->getInput('q')) . - '&uid=0&sort=' - ) or returnServerError('Could not request Demonoid.'); - - } elseif(!empty($this->getInput('catOnly'))) { - - $html = getSimpleHTMLDOM( - self::URI . - 'files/?uid=0&category=' . - rawurlencode($this->getInput('catOnly')) . - '&subcategory=0&language=0&seeded=2&quality=0&query=&sort=' - ) or returnServerError('Could not request Demonoid.'); - - } elseif(!empty($this->getInput('userid'))) { - - $html = getSimpleHTMLDOM( - self::URI . - 'files/?uid=' . - rawurlencode($this->getInput('userid')) . - '&seeded=2' - ) or returnServerError('Could not request Demonoid.'); - - } else { - returnServerError('Invalid parameters !'); - } - - if(preg_match('~No torrents found~', $html)) { - return; - } - - $table = $html->find('td[class=ctable_content_no_pad]', 0); - $cursorCount = 4; - $elementCount = 0; - while($elementCount != 40) { - $elementCount++; - $currentElement = $table->find('tr', $cursorCount); - if(preg_match('~items total~', $currentElement)) { - break; - } - $item = array(); - //Do we have a date ? - if(preg_match('~Added.*?(.*)~', $currentElement->plaintext, $dateStr)) { - if(preg_match('~today~', $dateStr[0])) { - date_default_timezone_set('UTC'); - $timestamp = mktime(0, 0, 0, gmdate('n'), gmdate('j'), gmdate('Y')); - } else { - preg_match('~(?<=ed on ).*\d+~', $currentElement->plaintext, $fullDateStr); - date_default_timezone_set('UTC'); - $dateObj = strptime($fullDateStr[0], '%A, %b %d, %Y'); - $timestamp = mktime(0, 0, 0, $dateObj['tm_mon'] + 1, $dateObj['tm_mday'], 1900 + $dateObj['tm_year']); - } - $cursorCount++; - } - - $content = $table->find('tr', $cursorCount)->find('a', 1); - $cursorCount++; - $torrentInfo = $table->find('tr', $cursorCount); - $item['timestamp'] = $timestamp; - $item['title'] = $content->plaintext; - $item['id'] = self::URI . $content->href; - $item['uri'] = self::URI . $content->href; - $item['author'] = $torrentInfo->find('a[class=user]', 0)->plaintext; - $item['seeders'] = $torrentInfo->find('font[class=green]', 0)->plaintext; - $item['leechers'] = $torrentInfo->find('font[class=red]', 0)->plaintext; - $item['size'] = $torrentInfo->find('td', 3)->plaintext; - $item['content'] = 'Uploaded by ' . $item['author'] - . ' , Size ' . $item['size'] - . '
seeders: ' - . $item['seeders'] - . ' | leechers: ' - . $item['leechers'] - . '
info page'; - - $this->items[] = $item; - - $cursorCount++; - } - } -} diff --git a/bridges/DesoutterBridge.php b/bridges/DesoutterBridge.php index 4a7b0a94..38761ed8 100644 --- a/bridges/DesoutterBridge.php +++ b/bridges/DesoutterBridge.php @@ -116,6 +116,12 @@ class DesoutterBridge extends BridgeAbstract { 'name' => 'Load full articles', 'type' => 'checkbox', 'title' => 'Enable to load the full article for each item' + ), + 'limit' => array( + 'name' => 'Limit', + 'type' => 'number', + 'defaultValue' => 3, + 'title' => "Maximum number of items to return in the feed.\n0 = unlimited" ) ) ); @@ -156,19 +162,23 @@ class DesoutterBridge extends BridgeAbstract { $this->title = html_entity_decode($html->find('title', 0)->plaintext, ENT_QUOTES); + $limit = $this->getInput('limit') ?: 0; + foreach($html->find('article') as $article) { $item = array(); - $item['uri'] = $article->find('[itemprop="name"]', 0)->href; - $item['title'] = $article->find('[itemprop="name"]', 0)->title; + $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('[itemprop="description"]', 0)->plaintext; + $item['content'] = $article->find('div.tile-body p', 0)->plaintext; } $this->items[] = $item; + + if ($limit > 0 && count($this->items) >= $limit) break; } } diff --git a/bridges/DiarioDoAlentejoBridge.php b/bridges/DiarioDoAlentejoBridge.php new file mode 100644 index 00000000..806f8031 --- /dev/null +++ b/bridges/DiarioDoAlentejoBridge.php @@ -0,0 +1,60 @@ +30s!), keep the cache timeout high to avoid killing the host */ + $html = getSimpleHTMLDOMCached($this->getURI() . '/pt/noticias-listagem.aspx') + or returnServerError('Could not load content'); + + foreach($html->find('.list_news .item') as $element) { + $item = array(); + + $item_link = $element->find('.body h2.title a', 0); + /* Another broken URL, see also `bridges/ComboiosDePortugalBridge.php` */ + $item['uri'] = self::URI . implode('/', array_map('urlencode', explode('/', $item_link->href))); + $item['title'] = $item_link->innertext; + + $item['timestamp'] = str_ireplace( + array_map(function($name) { return ' ' . $name . ' '; }, self::PT_MONTH_NAMES), + array_map(function($num) { return sprintf('-%02d-', $num); }, range(1, sizeof(self::PT_MONTH_NAMES))), + $element->find('span.date', 0)->innertext); + + /* Fix the Image URL */ + $item_image = $element->find('img.thumb', 0); + $item_image->src = preg_replace('/.*&img=([^&]+).*/', '\1', $item_image->getAttribute('data-src')); + + /* Content: */ + /* - Image */ + /* - Category */ + $content = $item_image . + '
' . $element->find('a.category', 0) . '
'; + $item['content'] = defaultLinkTo($content, self::URI); + + $this->items[] = $item; + } + } +} diff --git a/bridges/DollbooruBridge.php b/bridges/DollbooruBridge.php deleted file mode 100644 index 5ed4119f..00000000 --- a/bridges/DollbooruBridge.php +++ /dev/null @@ -1,9 +0,0 @@ - array( + 'country' => array( + 'type' => 'list', + 'name' => 'Country', + 'values' => array( + 'Argentina' => 'https://downdetector.com.ar', + 'Australia' => 'https://downdetector.com.au', + 'België' => 'https://allestoringen.be', + 'Brasil' => 'https://downdetector.com.br', + 'Canada' => 'https://downdetector.ca', + 'Chile' => 'https://downdetector.cl', + 'Colombia' => 'https://downdetector.com.co', + 'Danmark' => 'https://downdetector.dk', + 'Deutschland' => 'https://allestörungen.de', + 'Ecuador' => 'https://downdetector.ec', + 'España' => 'https://downdetector.es', + 'France' => 'https://downdetector.fr', + 'Hong Kong' => 'https://downdetector.hk', + 'Hrvatska' => 'https://downdetector.hr', + 'India' => 'https://downdetector.in', + 'Indonesia' => 'https://downdetector.id', + 'Ireland' => 'https://downdetector.ie', + 'Italia' => 'https://downdetector.it', + 'Magyarország' => 'https://downdetector.hu', + 'Malaysia' => 'https://downdetector.my', + 'México' => 'https://downdetector.mx', + 'Nederland' => 'https://allestoringen.nl', + 'New Zealand' => 'https://downdetector.co.nz', + 'Norge' => 'https://downdetector.no', + 'Pakistan' => 'https://downdetector.pk', + 'Perú' => 'https://downdetector.pe', + 'Pilipinas' => 'https://downdetector.ph', + 'Polska' => 'https://downdetector.pl', + 'Portugal' => 'https://downdetector.pt', + 'România' => 'https://downdetector.ro', + 'Schweiz' => 'https://allestörungen.ch', + 'Singapore' => 'https://downdetector.sg', + 'Slovensko' => 'https://downdetector.sk', + 'South Africa' => 'https://downdetector.co.za', + 'Suomi' => 'https://downdetector.fi', + 'Sverige' => 'https://downdetector.se', + 'Türkiye' => 'https://downdetector.web.tr', + 'UAE' => 'https://downdetector.ae', + 'UK' => 'https://downdetector.co.uk', + 'United States' => 'https://downdetector.com', + 'Österreich' => 'https://allestörungen.at', + 'Česko' => 'https://downdetector.cz', + 'Ελλάς' => 'https://downdetector.gr', + 'Россия' => 'https://downdetector.ru', + '日本' => 'https://downdetector.jp' + ) + ) + ), + 'Specific Website' => array( + 'website' => array( + 'type' => 'list', + 'name' => 'Website', + 'values' => array( + 'Österreich' => array( + '1&1' => 35086, + '3 (Drei)' => 33546, + 'A1' => 33543, + 'Alexa' => 36919, + 'Amazon' => 33506, + 'Amazon Prime Video' => 35085, + 'Amino Apps' => 39034, + 'Anthem' => 38200, + 'Apex Legends' => 38117, + 'App Store' => 35584, + 'Bank Austria' => 34715, + 'Battlefield' => 38051, + 'BAWAG' => 34716, + 'Binance' => 36938, + 'Blizzard Battle.net' => 35087, + 'Bob' => 34953, + 'Boom Beach' => 34781, + 'Bwin' => 35071, + 'Call of Duty' => 34156, + 'Car2Go' => 34554, + 'Clash of Clans' => 35088, + 'Clash Royale' => 38357, + 'Coinbase' => 36804, + 'Counter-strike' => 35055, + 'Crunchyroll' => 38092, + 'Dazn' => 36508, + 'Dead By Daylight' => 37414, + 'Deezer' => 33832, + 'Destiny' => 34954, + 'DHL' => 36747, + 'Discord' => 36768, + 'Dota 2' => 35398, + 'Dropbox' => 33509, + 'EA' => 34502, + 'Easybank' => 36992, + 'eBay' => 33510, + 'Emerion' => 34613, + 'Epic Games Store' => 39021, + 'Erste Bank und Sparkasse' => 36724, + 'Facebook' => 33511, + 'Facebook Messenger' => 33512, + 'Fifa' => 37605, + 'Flickr' => 33513, + 'For Honor' => 35996, + 'Fortnite' => 36689, + 'Ghost Recon' => 36009, + 'Gmail' => 33514, + 'GMX' => 33515, + 'Google' => 33516, + 'Google Hangouts' => 33517, + 'Google Play' => 33518, + 'GTA 5' => 35082, + 'Guild Wars 2' => 36473, + 'Handy Parken' => 34316, + 'Hay Day' => 34854, + 'Hello Bank' => 37010, + 'HoT' => 38751, + 'iCloud' => 35501, + 'ICQ' => 33520, + 'ING DiBa' => 35129, + 'Instagram' => 33522, + 'iTunes' => 33523, + 'Kabelplus' => 34473, + 'Kik' => 33524, + 'Kraken' => 36770, + 'League of Legends' => 34350, + 'LinkedIn' => 33525, + 'Liwest' => 34471, + 'Lovoo' => 35079, + 'Magenta' => 38440, + 'Maxdome' => 35084, + 'Minecraft' => 36432, + 'Mittwald' => 36987, + 'N26' => 38834, + 'Netatmo' => 37956, + 'Netflix' => 34631, + 'Nintendo Network' => 35523, + 'Nitrado' => 35548, + 'NordVPN' => 38587, + 'Office 365' => 35120, + 'OneDrive' => 35412, + 'ORF' => 35600, + 'Origin' => 36889, + 'Outlook' => 35083, + 'Overwatch' => 36153, + 'Path of Exile' => 37730, + 'Paypal' => 35399, + 'Playerunknown\'s Battlegrounds' => 36488, + 'Playstation Network' => 33526, + 'Pokémon Go' => 35745, + 'Quizduell' => 34528, + 'Raiffeisen Bank' => 38750, + 'Rainbow Six' => 35563, + 'Red Dead Redemption' => 37739, + 'Reddit' => 36827, + 'Rocket League' => 35485, + 'roNET' => 37041, + 'Salzburg AG Cablelink' => 35601, + 'Shpock' => 38681, + 'Sky' => 35081, + 'Sky Ticket' => 35142, + 'Skype' => 33527, + 'Smart Hub' => 35400, + 'Snapchat' => 33528, + 'Spotify' => 33529, + 'Spusu' => 35598, + 'Steam' => 34117, + 'Teamviewer' => 35686, + 'Tele2' => 34339, + 'Telegram' => 34903, + 'Telering' => 34952, + 'The Division' => 35599, + 'The elder scrolls online' => 37160, + 'The Simpsons Tapped Out' => 37283, + 'Threema' => 34255, + 'Tinder' => 34243, + 'Tipico' => 36515, + 'Tumblr' => 33530, + 'Twitch' => 35024, + 'Twitter' => 33531, + 'Uplay PC' => 34689, + 'Viber' => 33532, + 'Viewster' => 34326, + 'Vimeo' => 33533, + 'Volksbank' => 34717, + 'Warface' => 37524, + 'Warframe' => 37136, + 'Waze' => 33534, + 'Whatsapp' => 33535, + 'Wikipedia' => 33536, + 'Willhaben.at' => 35859, + 'World of Tanks' => 36674, + 'World of Warcraft' => 36998, + 'World of Warships' => 38009, + 'Xbox Live' => 33538, + 'Yahoo Mail' => 33539, + 'Yahoo Messenger' => 33540, + 'Yesss' => 35346, + 'Youtube' => 33541, + 'Z1 Battle Royale' => 35489, + ), + 'Deutschland' => array( + '1&1' => 32554, + '1blu' => 37319, + '2k' => 37731, + '3CX' => 36439, + '3sat' => 35797, + 'Afterbuy' => 37015, + 'Airbnb' => 35422, + 'Albion Online' => 38799, + 'Aldi Talk' => 32579, + 'Alexa' => 35912, + 'All-inkl' => 35162, + 'Amazon' => 32572, + 'Amazon Prime Music' => 37584, + 'Amazon Prime Video' => 34495, + 'Amazon Web Services' => 36326, + 'Amino Apps' => 39033, + 'Amplus' => 35446, + 'Anthem' => 38055, + 'Anydesk' => 37487, + 'AOL' => 34377, + 'Apex Legends' => 38112, + 'App Store' => 35579, + 'Apple Music' => 35246, + 'Apple Store' => 34448, + 'Arche NetVision' => 35593, + 'ArcheAge' => 35062, + 'Arcor' => 34374, + 'ARK: Survival Evolved' => 39065, + 'Arma 3' => 35063, + 'Asana' => 38024, + 'Assassin\'s Creed' => 35010, + 'Badoo' => 35011, + 'Base' => 32568, + 'Battlefield' => 36092, + 'Baur' => 35043, + 'Bet3000' => 35035, + 'Bet365' => 35034, + 'Bethesda' => 38317, + 'BILDmobil' => 32609, + 'Binance' => 36942, + 'Bing' => 34708, + 'Bitfinex' => 36833, + 'Bitstamp' => 36808, + 'Blackberry' => 3, + 'Blade &Soul' => 35512, + 'Blau' => 37655, + 'Blau' => 34108, + 'Blizzard Battle.net' => 34483, + 'Blogger' => 32606, + 'BMW ConnectedDrive' => 38034, + 'Boom Beach' => 34365, + 'Bornet' => 34944, + 'Bwin' => 34491, + 'Call of Duty' => 34155, + 'Candy Crush' => 35148, + 'Candy Crush Soda Saga' => 35174, + 'Car-Net' => 37031, + 'Car2Go' => 34555, + 'Centurylink' => 35624, + 'Checkdomain' => 36096, + 'Clash of Clans' => 34289, + 'Clash Royale' => 35588, + 'Cloudflare' => 34881, + 'Coinbase' => 36798, + 'Colt' => 37653, + 'Comdirect' => 34774, + 'Comedy Central' => 34122, + 'Commerzbank' => 32607, + 'Comunio' => 34719, + 'Congstar' => 32590, + 'Consors Bank' => 35545, + 'CosmosDirect' => 35042, + 'Counter-strike' => 34887, + 'Crunchyroll' => 36718, + 'DABbank' => 35921, + 'Das Erste' => 34386, + 'Dazn' => 35956, + 'Dead By Daylight' => 35951, + 'Deezer' => 33831, + 'DeGiro' => 38203, + 'Deliveroo' => 37546, + 'Destiny' => 34895, + 'Deutsche Bahn' => 32569, + 'Deutsche Bank' => 32611, + 'Deutsche Glasfaser' => 36087, + 'Deutsche Telefon' => 35851, + 'DeutschlandSIM' => 34635, + 'DFP' => 35004, + 'DHL' => 34896, + 'Discord' => 35796, + 'DKB' => 32767, + 'DNSNET' => 35670, + 'DomainFactory' => 37381, + 'Dota 2' => 34900, + 'dpd' => 36120, + 'Dragon Ball' => 36380, + 'Drillisch' => 36019, + 'Driveclub' => 34928, + 'Dropbox' => 32585, + 'E-Plus' => 10121, + 'EA' => 34497, + 'Easybell' => 34370, + 'eBay' => 32567, + 'Ecotel' => 36794, + 'Elite: Dangerous' => 37077, + 'Emailn' => 34687, + 'Emerion' => 34614, + 'Entega' => 37039, + 'Epic Games Store' => 38827, + 'Escape from Tarkov' => 37977, + 'Etoro' => 36965, + 'Eurosport Player' => 36559, + 'Eventim' => 37621, + 'Evernote ' => 36506, + 'Ewe TEL' => 33559, + 'Facebook' => 32552, + 'Facebook Messenger' => 32560, + 'Faceit' => 37147, + 'Facetime' => 34603, + 'Fallout' => 35433, + 'Farm Heroes Saga' => 35175, + 'Fidor Bank' => 35894, + 'Fifa' => 35469, + 'Finya' => 37060, + 'Fitbit' => 37972, + 'Fl!nk' => 35170, + 'Flickr' => 32604, + 'Fonic' => 32594, + 'For Honor' => 35985, + 'Fortnite' => 36626, + 'Forza' => 37568, + 'Freenet' => 34354, + 'Friday the 13th The Game' => 37561, + 'Fyve' => 34378, + 'G-Portal' => 37155, + 'Game of war' => 35230, + 'Gameduell' => 37566, + 'Gardena Smart' => 37350, + 'Garmin' => 37049, + 'Gears of War' => 35940, + 'Gems of war' => 38802, + 'Geocaching' => 37178, + 'Ghost Recon' => 36001, + 'Giropay' => 38204, + 'GitHub' => 35348, + 'GLS' => 36119, + 'Gmail' => 32584, + 'GMX' => 32563, + 'Go Daddy' => 34874, + 'Goneo' => 37576, + 'Google' => 32553, + 'Google Drive' => 36603, + 'Google Hangouts' => 32600, + 'Google Kalender' => 38603, + 'Google Play' => 32593, + 'Gran Turismo' => 36917, + 'Grindr' => 35532, + 'GTA 5' => 34754, + 'Guild Wars 2' => 35061, + 'Halo' => 35419, + 'Halo Wars' => 36034, + 'Harry Potter: Wizards Unite' => 38686, + 'Hay Day' => 34366, + 'Helinet' => 34959, + 'Hermes' => 35196, + 'Hetzner ' => 35943, + 'Hipchat' => 34869, + 'Hitbox.TV' => 35932, + 'Hitman' => 35586, + 'Homematic' => 36795, + 'Hosteurope' => 37958, + 'Htp' => 34400, + 'Hue' => 37810, + 'Hunt: Showdown' => 38787, + 'HypoVereinsbank' => 34965, + 'iCloud' => 32549, + 'ICQ' => 33123, + 'Idealo' => 37156, + 'iMessage' => 32671, + 'Inexio' => 36371, + 'ING DiBa' => 35156, + 'Ingress' => 35765, + 'Innogy Highspeed' => 37459, + 'Instagram' => 32599, + 'Intercity Express (ICE)' => 32581, + 'iTunes' => 32672, + 'Jappy' => 32596, + 'Jira' => 36066, + 'Jobst DSL' => 36727, + 'Jodel' => 36793, + 'Joyn' => 38604, + 'Jurassic World Alive' => 37310, + 'K-Classic Mobil' => 33986, + 'Kabel eins‎' => 34392, + 'Kickbase' => 37451, + 'Kicker' => 37450, + 'Kicktipp' => 35824, + 'Kik' => 33353, + 'Klarmobil' => 32605, + 'KMS' => 34399, + 'Knuddels' => 35865, + 'Kraken' => 36749, + 'Ladbrokes' => 35161, + 'LastPass' => 34780, + 'League of Legends' => 34112, + 'Lebara' => 35791, + 'Line' => 34300, + 'LinkedIn' => 32557, + 'Lotto24' => 35040, + 'Lottohelden' => 36882, + 'Lovoo' => 34814, + 'Lufthansa' => 32574, + 'Lycamobile' => 34939, + 'M-net' => 32571, + 'Madden' => 38906, + 'Mail.de' => 34909, + 'Mailbox' => 34832, + 'Markt.de' => 37465, + 'Maxdome' => 34749, + 'MDCC' => 34760, + 'MDDSL' => 37056, + 'Mercedes Me' => 37153, + 'MiCoach' => 34936, + 'Microsoft Azure' => 36093, + 'Microsoft Teams' => 38185, + 'Minecraft' => 32556, + 'Mittwald' => 33412, + 'Mixer' => 38770, + 'Mobilcom Debitel' => 32610, + 'MTV' => 34120, + 'My Fitness Pal' => 37637, + 'Mybet' => 35036, + 'N26' => 35719, + 'NBA 2k' => 38595, + 'Need for Speed' => 35429, + 'Netatmo' => 37452, + 'Netbeat' => 35906, + 'NetCologne' => 32587, + 'netcombw' => 35638, + 'netcup' => 35632, + 'Netflix' => 34630, + 'Netkom' => 37201, + 'Neverwinter' => 35768, + 'Nfon' => 35792, + 'Nintendo Network' => 35519, + 'Nintendo Switch Online' => 37244, + 'Nitrado' => 34941, + 'No Man\'s Sky' => 35794, + 'NordVPN' => 38584, + 'Norisbank' => 34964, + 'Nvidia' => 37462, + 'Nvidia' => 39069, + 'O2' => 10122, + 'Office 365' => 34730, + 'OkCupid' => 37330, + 'OLB' => 37057, + 'OneDrive' => 35408, + 'Onleihe' => 35635, + 'Origin' => 34371, + 'Osnatel' => 33873, + 'Otelo' => 35480, + 'Otto' => 32598, + 'Outlook' => 32546, + 'Overwatch' => 35684, + 'Paladins' => 35925, + 'Path of Exile' => 36479, + 'Payback' => 37654, + 'Paypal' => 34375, + 'pcvisit' => 37643, + 'PES' => 37952, + 'Pet Rescue Saga' => 35176, + 'Pinterest' => 37585, + 'Placetel' => 35128, + 'Playerunknown\'s Battlegrounds' => 36332, + 'Playstation Network' => 32551, + 'Pokémon Duel' => 35976, + 'Pokémon Go' => 35724, + 'Pokerstars' => 37439, + 'Postbank' => 32589, + 'Posteo' => 34309, + 'Primacom' => 33502, + 'ProSieben' => 34390, + 'PŸUR' => 32592, + 'QSC' => 33560, + 'Quizduell' => 34292, + 'Rainbow Six' => 35479, + 'Razer' => 38699, + 'Realm Royale' => 37288, + 'Red Dead Redemption' => 37601, + 'Reddit' => 35882, + 'RFT Kabel' => 35831, + 'Roblox' => 35815, + 'Rocket League' => 35252, + 'Royal Games' => 38217, + 'RTL II' => 34393, + 'RTL Television' => 34387, + 'Runescape' => 35108, + 'RWE' => 36116, + 'RWW' => 37955, + 'Ryanair' => 37518, + 'Salesforce' => 34733, + 'Santander Consumer Bank' => 32603, + 'Sat.1' => 34389, + 'Save.TV' => 37193, + 'Sea of Thieves' => 37112, + 'Shpock' => 37311, + 'Signal' => 37072, + 'Simply' => 36376, + 'Simquadrat' => 34945, + 'Simsme' => 34795, + 'Simyo' => 32591, + 'Sipgate' => 34127, + 'SKL' => 35041, + 'Sky' => 32562, + 'Sky Ticket' => 35141, + 'Skype' => 32561, + 'Skype for Business' => 35351, + 'Slack' => 35936, + 'Smart Hub' => 35181, + 'Smite' => 34803, + 'Snapchat' => 33377, + 'Soundcloud' => 34353, + 'Sparda' => 35370, + 'Sparkasse' => 32757, + 'Speedtest' => 38780, + 'Spiegel' => 36050, + 'Spotify' => 32564, + 'Star Citizen' => 39035, + 'Star Wars Battlefront' => 35460, + 'Steam' => 32559, + 'Strato' => 33413, + 'Strava' => 38731, + 'Streetspotr' => 34920, + 'Summoners War' => 37420, + 'SWB' => 34515, + 'Tado' => 36634, + 'Tango' => 33731, + 'Targobank' => 34966, + 'Teamspeak' => 33346, + 'Teamviewer' => 34344, + 'Tele2' => 33503, + 'Tele5' => 35727, + 'Telegram' => 34229, + 'Telekom' => 10117, + 'Tellonym' => 37248, + 'The Crew 2' => 37252, + 'The Division' => 35570, + 'The elder scrolls online' => 34480, + 'The Simpsons Tapped Out' => 34701, + 'Threema' => 34253, + 'Tiktok' => 39044, + 'Tinder' => 34242, + 'Tipico' => 34714, + 'TNG' => 38772, + 'Todoist' => 36462, + 'TomTom' => 35813, + 'Tumblr' => 32588, + 'TuneIn' => 38763, + 'TV Now' => 35143, + 'Tweakbox' => 38349, + 'Twitch' => 34376, + 'Twitter' => 32583, + 'Udemy' => 38388, + 'Unitymedia' => 32548, + 'Uplay PC' => 34380, + 'Usenext' => 36117, + 'Vero' => 37064, + 'Versatel' => 33561, + 'Viber' => 33372, + 'Viewster' => 34324, + 'Vimeo' => 32582, + 'Visa' => 37238, + 'VIVA' => 34121, + 'Vodafone' => 10120, + 'Volksbanken und Raiffeisenbanken' => 32758, + 'VOX' => 34391, + 'Warface' => 35944, + 'Warframe' => 36366, + 'Watchbox' => 36537, + 'Wattpad' => 34700, + 'Waze' => 33411, + 'Web.de' => 32586, + 'WeChat' => 34301, + 'Weebly' => 34914, + 'Weight Watchers' => 35662, + 'WeTransfer' => 36433, + 'Wetter.com' => 36051, + 'WetterOnline' => 34810, + 'Whatsapp' => 32555, + 'Wikipedia' => 32565, + 'Wilhelm.tel' => 34398, + 'Wish' => 38153, + 'WiSoTEL' => 37543, + 'Wobcom' => 35991, + 'Wordpress' => 32570, + 'World of Tanks' => 35524, + 'World of Warcraft' => 34373, + 'World of Warships' => 36638, + 'Wüstenrot' => 36977, + 'Xbox Live' => 32573, + 'Xing' => 34822, + 'Yahoo Mail' => 32597, + 'Yahoo Messenger' => 32580, + 'Yourfone' => 33415, + 'Youtube' => 32578, + 'Youtube Music' => 37586, + 'Z1 Battle Royale' => 35147, + 'Zattoo' => 35367, + 'ZDF' => 34388, + 'Zynga' => 32608, + ), + 'Nederland' => array( + '112' => 10011, + '3FM' => 10174, + '9292.nl' => 33376, + 'ABN-Amro' => 29, + 'ABP pensioenfonds' => 35862, + 'Adobe Creative Cloud' => 34916, + 'ADP' => 32669, + 'Adyen' => 37610, + 'Aegon Bank' => 10132, + 'Afas' => 10093, + 'Afterpay' => 34769, + 'Airbnb' => 35423, + 'Airmiles' => 37516, + 'Albert Heijn' => 34010, + 'Alex' => 10056, + 'Algemeen Dagblad' => 1, + 'AliExpress' => 37384, + 'Amazon' => 37379, + 'Amazon Prime Video' => 36976, + 'AMSIX' => 10008, + 'Antagonist' => 35160, + 'Anthem' => 38054, + 'Apex Legends' => 38118, + 'App Store' => 35580, + 'Apple Music' => 35245, + 'Apple Store' => 34446, + 'Argenta' => 10158, + 'Argeweb' => 35073, + 'Arriva' => 2, + 'ASN Bank' => 10048, + 'Assassin\'s Creed' => 35009, + 'Badoo' => 37650, + 'Battlefield' => 37094, + 'Belastingdienst' => 10002, + 'Ben' => 10053, + 'Bibliotheek' => 34132, + 'Binance' => 36879, + 'Binck' => 10055, + 'Bing' => 34707, + 'BlaBlaCar' => 36088, + 'Bliep' => 33998, + 'Blizzard Battle.net' => 34485, + 'Bol.com' => 10112, + 'Booking.com' => 37574, + 'Boom Beach' => 34363, + 'Brabant Water' => 10071, + 'Brawl Stars' => 38818, + 'Budgetphone' => 35132, + 'Buienalarm' => 37245, + 'Buienradar' => 33565, + 'Bunq' => 35820, + 'Byte' => 32764, + 'Caiway' => 4, + 'CAK' => 37289, + 'Call of Duty' => 34154, + 'CanalDigitaal' => 31, + 'Candy Crush' => 34025, + 'Cbizz' => 37659, + 'Centraal Beheer Achmea' => 10157, + 'Centraal Bureau voor de Statistiek' => 10168, + 'CheapConnect' => 36122, + 'Chelloo' => 35546, + 'Choozze' => 35096, + 'Clash of Clans' => 34290, + 'Clash Royale' => 37522, + 'Cloudflare' => 38623, + 'Cloudhosting.nl' => 33486, + 'Coinbase' => 36236, + 'Comedy Central' => 34033, + 'Concepts' => 10043, + 'Connexxion' => 5, + 'Coolblue' => 35543, + 'Counter-strike' => 36644, + 'Credit Europe Bank' => 10194, + 'Crunchyroll' => 36722, + 'Dead By Daylight' => 37412, + 'Deezer' => 33769, + 'DeGiro' => 34019, + 'Delight Mobile' => 33987, + 'Delta' => 10123, + 'Destiny' => 34891, + 'Deutsche Bank' => 10119, + 'DFP' => 35003, + 'DHL' => 35223, + 'DigiD' => 10001, + 'Digipoort' => 34520, + 'Discord' => 36043, + 'Disney+' => 38319, + 'Dota 2' => 36397, + 'Dpd' => 35619, + 'Dropbox' => 10192, + 'Dumpert' => 33375, + 'Dunea' => 10074, + 'EA' => 34501, + 'Easynet' => 33997, + 'eBay' => 33857, + 'Elite: Dangerous' => 37777, + 'Enduris' => 35999, + 'Eneco' => 10124, + 'Energielabel voor Woningen' => 35117, + 'Enexis' => 10014, + 'Ennatuurlijk' => 35159, + 'Epic Games Store' => 39020, + 'Escape from Tarkov' => 39059, + 'Esprit Telecom' => 10150, + 'Essent' => 10114, + 'Etoro' => 36958, + 'Eurosport Player' => 35821, + 'Eventim' => 37622, + 'Evides' => 10072, + 'Eweka' => 10039, + 'Exact Online' => 34815, + 'Exchange Online' => 34728, + 'Facebook' => 6, + 'Facebook Messenger' => 10185, + 'Facetime' => 34607, + 'Fallout' => 35434, + 'FBTO' => 34015, + 'Feedly' => 34712, + 'Fiber' => 10103, + 'Fifa' => 35474, + 'Flickr' => 37948, + 'Flitsmeister' => 34369, + 'For Honor' => 35987, + 'Fortnite' => 36625, + 'Fox Sports' => 10096, + 'Friday the 13th The Game' => 37560, + 'Funda' => 32576, + 'Game of war' => 35231, + 'Garmin' => 38165, + 'Garmin Connect' => 38171, + 'Gatehub' => 36957, + 'Ghost Recon' => 36010, + 'GitHub' => 35347, + 'Glashart Media' => 10105, + 'GLS' => 36118, + 'Gmail' => 10041, + 'Google' => 10010, + 'Google Agenda' => 38602, + 'Google Drive' => 34276, + 'Google Hangouts' => 10062, + 'Google Play' => 10038, + 'Gran Turismo' => 36873, + 'Greenchoice' => 37959, + 'Greenwheels' => 34007, + 'Grindr' => 35531, + 'GTA 5' => 34753, + 'Guild Wars 2' => 36464, + 'GVB' => 7, + 'Halo' => 35420, + 'Happn' => 37014, + 'Harry Potter: Wizards Unite' => 38689, + 'Hay Day' => 34368, + 'Hearthstone' => 38564, + 'Hollandsnieuwe' => 10040, + 'Hosted.nl' => 33483, + 'Hostnet' => 32761, + 'HTM' => 8, + 'Hue' => 37812, + 'iCloud' => 30, + 'iDeal' => 10025, + 'IEX' => 35782, + 'IG' => 34307, + 'iMessage' => 10130, + 'Indeed' => 35941, + 'Infopact' => 37565, + 'ING' => 9, + 'Instagram' => 32558, + 'International Card Services (ICS)' => 10169, + 'InterNLnet' => 34252, + 'Interpolis' => 34017, + 'ITDev Solutions' => 35533, + 'iTunes' => 10151, + 'Jira' => 36067, + 'Jonaz' => 37348, + 'Jumbo' => 36184, + 'Jurassic World Alive' => 37334, + 'JustEat' => 10164, + 'Kabel Noord' => 10113, + 'Kabeltex' => 10138, + 'Kadaster' => 37652, + 'Kamer van Koophandel' => 10167, + 'KickXL' => 10098, + 'Kik' => 10191, + 'Kliksafe' => 10109, + 'KLM' => 10028, + 'Knab' => 10131, + 'KNMI' => 34013, + 'KPN' => 11, + 'Kraken' => 36748, + 'Kruidvat' => 37312, + 'LastPass' => 34779, + 'League of Legends' => 35169, + 'LeasePlan Bank' => 10160, + 'Leaseweb' => 32762, + 'Lebara' => 10128, + 'Lexa.nl' => 35838, + 'Liander' => 10013, + 'Lijbrandt' => 10045, + 'LinkedIn' => 10201, + 'Litebit' => 36836, + 'Lloyds Bank' => 10154, + 'LOI' => 34947, + 'Lotto' => 34923, + 'Lycamobile' => 10129, + 'Lynx' => 10085, + 'Magister' => 35274, + 'Marktplaats' => 10170, + 'Microsoft Azure' => 36114, + 'Microsoft Teams' => 38186, + 'Mijnbroker' => 10088, + 'Mijndomein' => 34348, + 'MijnOverheid' => 10015, + 'Minecraft' => 36173, + 'Mobicross' => 34898, + 'Moneyou' => 10155, + 'Motto' => 35482, + 'MTV' => 34031, + 'Multisafepay' => 34877, + 'My Fitness Pal' => 37636, + 'MyOrder' => 34291, + 'Nationale Nederlanden' => 10166, + 'Nest' => 35189, + 'Net 5' => 10181, + 'Netatmo' => 37453, + 'Netflix' => 33849, + 'Neverwinter' => 36676, + 'NIBC Direct' => 10159, + 'Nintendo eShop' => 34105, + 'Nintendo Network' => 35520, + 'Nintendo Switch Online' => 37242, + 'Nitrado' => 35549, + 'NLE' => 37651, + 'NLziet' => 34219, + 'NordVPN' => 38585, + 'NOS.nl' => 10063, + 'NPO 1' => 10179, + 'NPO 2' => 10180, + 'NPO 3' => 10182, + 'NPO Start' => 10064, + 'NS' => 21, + 'Nu.nl' => 12, + 'Nuon' => 10084, + 'Oasen' => 10078, + 'Office 365' => 35119, + 'OHRA' => 34819, + 'ON' => 10097, + 'OneDrive' => 32779, + 'Online.nl' => 10193, + 'OnlineWerkplekken.nl' => 33485, + 'Onsbrabantnet' => 10044, + 'Origin' => 34372, + 'Outlook' => 10042, + 'OV-chipkaart' => 10145, + 'Overwatch' => 35723, + 'Paladins' => 37407, + 'Park Mobile' => 10139, + 'Park-line' => 10137, + 'Path of Exile' => 37729, + 'Pathé Thuis' => 33848, + 'Paypal' => 10021, + 'Paysafecard' => 10152, + 'PCextreme' => 32763, + 'Picnic' => 37503, + 'PIN' => 10009, + 'Player Unknown\'s Battlegrounds' => 36336, + 'Playstation Network' => 10006, + 'Plinq' => 10108, + 'Pokémon Go' => 35731, + 'Pokerstars' => 37438, + 'PostNL' => 32766, + 'PWN Waterleidingbedrijf Noord-Holland' => 10073, + 'Q-Park' => 10111, + 'Quizduel' => 34293, + 'Qurrent' => 35729, + 'Rabobank' => 13, + 'Radio 1' => 10175, + 'Radio 2' => 10176, + 'Raet' => 32670, + 'Rainbow Six' => 35564, + 'Realm Royale' => 37315, + 'Red Dead Redemption' => 38286, + 'Reddit' => 36822, + 'Redworks' => 34773, + 'Reggefiber' => 10104, + 'RegioBank' => 10050, + 'Rekam' => 10195, + 'Rendo Netwerken' => 10126, + 'RET' => 14, + 'Reviced' => 36061, + 'Robeco' => 10153, + 'Robin Mobile' => 35695, + 'Roblox' => 35814, + 'Rocket League' => 35254, + 'RoutIT' => 34349, + 'RTL 5' => 10183, + 'RTL 8' => 10184, + 'RTL XL' => 10066, + 'RTL4' => 10178, + 'Runescape' => 35701, + 'Ruzzle' => 33968, + 'Ryanair' => 37517, + 'Salesforce' => 10016, + 'SBS6' => 10177, + 'Schiphol' => 10027, + 'Scorito' => 34726, + 'Sea of Thieves' => 37113, + 'SEOshop' => 35592, + 'Sharepoint Online' => 34732, + 'Signal' => 37073, + 'Signet' => 34407, + 'Simpel' => 10037, + 'Simyo' => 10054, + 'Skype' => 10024, + 'Skype for Business' => 34729, + 'Slack' => 37336, + 'Smart Hub' => 35180, + 'Snapchat' => 10187, + 'Snappet' => 37613, + 'SNS' => 10019, + 'Sociale Verzekeringsbank (SVB)' => 10186, + 'Sofort Banking' => 10149, + 'Solcon' => 10099, + 'Soundcloud' => 34352, + 'Sparql' => 34519, + 'Speurders' => 34805, + 'Spotify' => 10003, + 'Staatsloterij' => 10094, + 'Star Wars Battlefront' => 36918, + 'Steam' => 34106, + 'Stedin' => 10012, + 'Steep' => 37823, + 'Stemwijzer' => 35980, + 'Stichting Kabeltelevisie Pijnacker' => 34143, + 'Stipte' => 10102, + 'Strato' => 35045, + 'Strava' => 36046, + 'Studiemeter' => 37075, + 'Studystore' => 36600, + 'Surfnet' => 34396, + 'T-Mobile' => 19, + 'Tado' => 36633, + 'Tango' => 33740, + 'Teamviewer' => 34342, + 'Tele2' => 16, + 'Telegraaf' => 17, + 'Telegram' => 34221, + 'Telfort' => 18, + 'The Division' => 35569, + 'The elder scrolls online' => 37329, + 'Threema' => 34254, + 'Thuisbezorgd.nl' => 10163, + 'Ticketmaster' => 10133, + 'Tickney ' => 35923, + 'Tikkie' => 36737, + 'Tiktok' => 39074, + 'Tinder' => 34238, + 'Today\'s' => 10087, + 'TomTom Live' => 34917, + 'Toto' => 35590, + 'TradersOnly' => 10090, + 'Transavia' => 35168, + 'TransIP' => 32771, + 'Trined' => 10107, + 'Triodos Bank' => 10051, + 'True' => 36576, + 'Tumblr' => 36714, + 'TuneIn' => 38762, + 'Tweak' => 10100, + 'Tweakbox' => 38348, + 'Tweakers' => 37376, + 'Twinfield' => 36725, + 'Twitch' => 35025, + 'Twitter' => 22, + 'Unet' => 34408, + 'Uplay PC' => 34379, + 'UWV en Werk.nl' => 10017, + 'Van Lanschot Bankiers' => 10052, + 'VDX' => 35780, + 'Vectone' => 35237, + 'Veolia' => 32612, + 'Veronica' => 34493, + 'Versio' => 32765, + 'VGZ' => 34946, + 'Viber' => 10172, + 'Videoland' => 33850, + 'Vimeo' => 38534, + 'Vimexx' => 35924, + 'VirtualComputing.nl' => 35698, + 'Vitens' => 10070, + 'Vodafone' => 24, + 'VoIPmobiel' => 34692, + 'Volgjezorg' => 37645, + 'VPSHosting.nl' => 33487, + 'VPSServer.nl' => 33484, + 'VVV Cadeaubon' => 35134, + 'Warface' => 38764, + 'Warframe' => 36490, + 'Waterbedrijf Groningen' => 10077, + 'Waterleiding Maatschappij Limburg' => 10076, + 'Waterleidingmaatschappij Drenthe' => 10079, + 'Waternet' => 10075, + 'Waze' => 33409, + 'Webex' => 34164, + 'Webreus' => 32778, + 'Weebly' => 34913, + 'Weeronline.nl' => 34014, + 'Weerplaza' => 35115, + 'Wehkamp' => 33980, + 'Westland Infra' => 10125, + 'WeTransfer' => 10190, + 'Whatsapp' => 10000, + 'Wikipedia' => 10092, + 'Wish' => 38152, + 'Wisper' => 10101, + 'Woningnet' => 36758, + 'Wordfeud' => 25, + 'WordOn' => 34036, + 'World of Tanks' => 35526, + 'World of Warcraft' => 34382, + 'World of Warships' => 38010, + 'Wrts' => 34215, + 'Xbox Live' => 10148, + 'XS4ALL' => 26, + 'XSyou' => 10106, + 'Yahoo Mail' => 34020, + 'Yellowbrick' => 10140, + 'Youfone' => 10144, + 'Yourhosting' => 34360, + 'YouTube' => 10005, + 'Z1 Battle Royale' => 35495, + 'Zalando' => 33981, + 'Ziggo' => 28, + 'Zilveren Kruis' => 34727, + 'Zwitserleven' => 34262, + ), + 'UK' => array( + '123 Reg' => 35870, + '1and1' => 34818, + '2k' => 36427, + '3 (Three)' => 32674, + '4chan' => 38596, + 'Abebooks' => 36536, + 'Adobe Creative Cloud' => 34552, + 'Airbnb' => 35424, + 'Alexa' => 35911, + 'AliExpress' => 39062, + 'Amazon' => 32613, + 'Amazon Prime Music' => 36426, + 'Amazon Prime Video' => 34494, + 'Amazon Web Services' => 36327, + 'American Express' => 37398, + 'Amino Apps' => 38196, + 'Ancestry' => 38680, + 'Anthem' => 38053, + 'Anydesk' => 37489, + 'Apex Legends' => 38114, + 'App Store' => 35578, + 'Apple Music' => 35242, + 'Apple Store' => 34454, + 'Argos' => 34018, + 'ARK: Survival Evolved' => 36551, + 'Arlo' => 37076, + 'Asana' => 38023, + 'Asda' => 37979, + 'Ask4' => 34411, + 'Assassin\'s Creed' => 35006, + 'Autotrader' => 34793, + 'Badoo' => 37501, + 'Bank of Ireland UK' => 38228, + 'Bank of Scotland' => 32692, + 'Barclaycard' => 37762, + 'Barclays' => 32685, + 'Battlefield' => 36585, + 'Bet365' => 35030, + 'Betfair' => 35031, + 'Binance' => 36877, + 'Bing' => 33615, + 'Bitbucket' => 36683, + 'Black Desert Online' => 38492, + 'Blizzard Battle.net' => 34487, + 'Blogger' => 32617, + 'Booking.com' => 38703, + 'Boom Beach' => 34783, + 'Brawl Stars' => 38819, + 'British Airways' => 37644, + 'British Gas' => 35653, + 'BT' => 32673, + 'Bumble' => 36401, + 'Bwin' => 34490, + 'Cablecom' => 34413, + 'Cahoot' => 34826, + 'Call of Duty' => 34151, + 'Candy Crush' => 35094, + 'Capital One' => 37223, + 'Car-Net' => 37030, + 'Cdkeys' => 37575, + 'Centurylink' => 35625, + 'CEX' => 34897, + 'Channel 4' => 34948, + 'Chelsea Building Society' => 38223, + 'Clash of Clans' => 34444, + 'Clash Royale' => 36856, + 'Cloudflare' => 34880, + 'CMC Markets' => 37629, + 'Coinbase' => 36797, + 'Colt Group ' => 35961, + 'Conan Exiles' => 38392, + 'ConnectWise' => 38469, + 'Conventry Building Society' => 38225, + 'Counter-strike' => 35057, + 'Craigslist' => 32619, + 'Crunchyroll' => 36715, + 'Daisy' => 34410, + 'Danske Bank' => 38227, + 'Dark Souls' => 36953, + 'Dauntless' => 38541, + 'DayZ' => 38781, + 'Dazn' => 39010, + 'Dead By Daylight' => 36425, + 'Deezer' => 33829, + 'DeGiro' => 38202, + 'Deliveroo' => 37548, + 'Demon' => 34412, + 'Destiny' => 34893, + 'DeviantArt' => 38491, + 'DFP' => 34999, + 'Discord' => 36020, + 'Dota 2' => 35403, + 'Driveclub' => 34927, + 'Dropbox' => 32620, + 'EA' => 34506, + 'EA Sports UFC' => 38468, + 'Easynet' => 33881, + 'eBay' => 32621, + 'Eclipse' => 32680, + 'EE' => 32661, + 'Elite: Dangerous' => 36684, + 'Epic Games Store' => 38828, + 'Escape from Tarkov' => 38108, + 'Etoro' => 36959, + 'Etsy' => 36422, + 'Eurosport Player' => 35819, + 'Evernote' => 34705, + 'Exchange Online' => 34536, + 'Expedia' => 36421, + 'Experian' => 38156, + 'Exponential-E' => 34409, + 'Facebook' => 32622, + 'Facebook Messenger' => 32623, + 'Faceit' => 37145, + 'Facetime' => 34604, + 'Fallout' => 35431, + 'Fandom' => 38690, + 'Far Cry' => 37176, + 'Fasthosts' => 35705, + 'Fifa' => 35471, + 'Find my iPhone' => 36123, + 'First Direct' => 33978, + 'First Trust Bank' => 38226, + 'Fitbit' => 36630, + 'Flickr' => 32624, + 'Fling' => 35165, + 'For Honor' => 35988, + 'Fortnite' => 36624, + 'Forza' => 37569, + 'Freedompop' => 35639, + 'Freesat' => 37594, + 'Freeview' => 35771, + 'Friday the 13th The Game' => 37563, + 'Funimation' => 37745, + 'Game of war' => 35229, + 'Gamma' => 34899, + 'Garmin' => 36990, + 'Garmin Connect' => 37192, + 'Gears of War' => 35939, + 'Ghost Recon' => 36007, + 'Giffgaff' => 33985, + 'Gigler' => 35790, + 'GitHub' => 32625, + 'Glide' => 38901, + 'Gmail' => 32626, + 'Go Daddy' => 34873, + 'Google' => 32627, + 'Google Calendar' => 38598, + 'Google Cloud' => 38536, + 'Google Drive' => 34273, + 'Google Hangouts' => 32629, + 'Google Home' => 36138, + 'Google Play' => 32628, + 'GoToMeeting' => 36505, + 'Gran Turismo' => 36680, + 'Great Western Railway' => 37808, + 'Grindr' => 35530, + 'GTA 5' => 34752, + 'Guild Wars 2' => 36407, + 'Gumtree' => 32756, + 'Halifax' => 32689, + 'Halo' => 35451, + 'Harry Potter: Wizards Unite' => 38688, + 'Hay Day' => 34788, + 'Hayu' => 38052, + 'Heart Internet' => 37623, + 'Hearthstone' => 38563, + 'Hermes' => 34949, + 'Hipchat' => 34866, + 'Hitman' => 36880, + 'Hive' => 39072, + 'HMRC' => 36961, + 'Home telecom' => 35959, + 'Hootsuite' => 32630, + 'HQ Trivia' => 37161, + 'HSBC' => 32686, + 'Hue' => 37811, + 'Hunt: showdown' => 38823, + 'Hyperoptic' => 36520, + 'iCloud' => 32663, + 'IG' => 37630, + 'iMessage' => 32664, + 'Imgur' => 32632, + 'IMVU' => 37177, + 'Indeed' => 38589, + 'Instagram' => 32633, + 'Internet Movie Database (IMDb)' => 36752, + 'Iomart' => 37180, + 'iPlayer' => 32755, + 'iTunes' => 32665, + 'ITV' => 34302, + 'Janet' => 35452, + 'Jira' => 34871, + 'John Lewis' => 36782, + 'Jurassic World Alive' => 37332, + 'Just Eat' => 36031, + 'KC' => 34130, + 'Kik' => 33352, + 'Kraken' => 36751, + 'Ladbrokes' => 35033, + 'LastPass' => 34778, + 'League of Legends' => 35048, + 'LinkedIn' => 32634, + 'Litebit' => 36837, + 'Lloyds Bank' => 32691, + 'M&S Bank' => 35457, + 'M24Seven' => 37150, + 'Madden' => 38458, + 'Mail.com' => 34800, + 'Mailbox' => 34836, + 'Mailchimp' => 38728, + 'Manx Telecom' => 35534, + 'Mastercard' => 37250, + 'McDonalds app' => 37374, + 'Meetup' => 34246, + 'Metro Bank' => 34970, + 'Microsoft Azure' => 32651, + 'Microsoft Teams' => 38184, + 'Minecraft' => 32635, + 'Mixer' => 38768, + 'Monzo' => 37591, + 'Moonfruit' => 35456, + 'MTV' => 34124, + 'Musical.ly' => 37140, + 'My Fitness Pal' => 36956, + 'My Vue' => 36329, + 'Namesco Ltd' => 35900, + 'National Lottery' => 34950, + 'Nationwide' => 32690, + 'NatWest' => 32684, + 'NBA 2k' => 37721, + 'Nest' => 35192, + 'Netflix' => 32636, + 'Neverwinter' => 35767, + 'NikePlus' => 38783, + 'Nintendo eShop' => 34100, + 'Nintendo Network' => 35516, + 'Nintendo Switch Online' => 37241, + 'No Man\'s Sky' => 37405, + 'NordVPN' => 38248, + 'Now TV' => 34406, + 'Nvidia' => 39068, + 'O2' => 32660, + 'Office 365' => 34538, + 'OkCupid' => 37331, + 'Omegle' => 39043, + 'OneDrive' => 32781, + 'Online.net' => 36174, + 'ooVoo' => 36586, + 'Orange' => 32676, + 'Origin' => 36419, + 'Origin Broadband' => 36052, + 'Outlook' => 32631, + 'Overwatch' => 35681, + 'OVH' => 36160, + 'Paddy Power' => 35032, + 'Paladins' => 36377, + 'Path of Exile' => 36487, + 'Paypal' => 35557, + 'PES' => 38590, + 'Photobucket' => 32638, + 'Pinterest' => 32639, + 'Player Unknown\'s Battlegrounds' => 36335, + 'Playstation Network' => 32668, + 'PlentyOfFish' => 36023, + 'Plusnet' => 32683, + 'Pokémon Go' => 35730, + 'Pokerstars' => 37440, + 'Post Office' => 32682, + 'Project Online' => 34539, + 'ProtonMail' => 38963, + 'Quickbooks Online' => 36583, + 'Quizup' => 34298, + 'Rackspace' => 34610, + 'Rainbow Six' => 35476, + 'Rakuten TV' => 37597, + 'RBS (Royal Bank of Scotland)' => 32688, + 'Realm Royale' => 37309, + 'Red Dead Redemption' => 37743, + 'Redbox' => 38791, + 'Reddit' => 34844, + 'Relish' => 34797, + 'Revolut' => 37365, + 'Roblox' => 35786, + 'Rocket League' => 35355, + 'Royal Mail' => 35776, + 'Runescape' => 35107, + 'Ruzzle' => 34479, + 'Ryanair' => 36003, + 'Sagepay' => 36781, + 'Sainsbury\'s Bank' => 35116, + 'Salesforce' => 38495, + 'Santander' => 32687, + 'Sarahah' => 36373, + 'Scottish Power' => 36041, + 'Sea of Thieves' => 37110, + 'Sharepoint Online' => 34540, + 'Shopify' => 38538, + 'Shutterstock' => 37448, + 'Signal' => 37070, + 'Sky' => 32782, + 'Sky Bet' => 35076, + 'Skype' => 32640, + 'Skype for Business' => 34537, + 'Slack' => 35934, + 'Slideshare' => 32641, + 'Smart Hub' => 35179, + 'Smartsheet' => 38723, + 'Smile' => 36661, + 'Smite' => 35407, + 'Snapchat' => 32642, + 'Soundcloud' => 36954, + 'Sourceforge' => 32643, + 'South Western Railway' => 37809, + 'Spotify' => 32644, + 'Squarespace' => 38784, + 'SSE' => 37058, + 'Star Citizen' => 39036, + 'Star Wars Battlefront' => 35551, + 'Starling Bank' => 38719, + 'Steam' => 32667, + 'Steep' => 37822, + 'Strava' => 37046, + 'Streamcenter' => 37016, + 'Summoners War' => 37419, + 'T-Mobile' => 32679, + 'Tado' => 36632, + 'Talkmobile' => 33988, + 'TalkTalk' => 32677, + 'Tango' => 33739, + 'Teamviewer' => 34615, + 'Telegram' => 34222, + 'Tesco Bank' => 34969, + 'Tesco Broadband' => 34086, + 'Tesco Mobile' => 35149, + 'The Co-operative Bank' => 34971, + 'The Crew 2' => 36555, + 'The Division' => 35561, + 'The elder scrolls online' => 36400, + 'The People\'s Operator' => 35150, + 'The Simpsons Tapped out' => 35077, + 'Thinkmoney' => 36331, + 'Ticketmaster' => 36786, + 'Tiktok' => 38726, + 'Tinder' => 34240, + 'TomTom Live' => 34918, + 'Transferwise' => 38565, + 'Trove' => 38826, + 'TSB Bank' => 33970, + 'Tumblr' => 32645, + 'TuneIn' => 37973, + 'Tweakbox' => 38342, + 'Twitch' => 35022, + 'Twitter' => 32646, + 'Uber' => 37498, + 'Uber Eats' => 38271, + 'Udemy' => 37717, + 'UKfast' => 36838, + 'Ulster Bank' => 36071, + 'Unibet' => 36055, + 'Uplay PC' => 34691, + 'UPS' => 38744, + 'Viber' => 33349, + 'Vimeo' => 32647, + 'Virgin Media' => 32675, + 'Visa' => 37237, + 'Vistaprint' => 35130, + 'Vodafone' => 32659, + 'Waitrose' => 36964, + 'Warface' => 38766, + 'Warframe' => 36367, + 'Wattpad' => 34696, + 'Waze' => 33407, + 'Webex' => 34161, + 'Weebly' => 38547, + 'Weight Watchers' => 35663, + 'WeTransfer' => 32648, + 'Whatsapp' => 32649, + 'Whisper' => 37028, + 'Wikipedia' => 32650, + 'William Hill' => 35029, + 'Wish' => 39040, + 'Wix' => 37233, + 'Wordpress.com' => 32652, + 'World of Tanks' => 35525, + 'World of Warcraft' => 35052, + 'World of Warships' => 36639, + 'WWE Network' => 38085, + 'Xbox Live' => 32666, + 'Xero' => 37500, + 'XLN Telecom' => 33989, + 'Yahoo' => 32653, + 'Yahoo Mail' => 32654, + 'Yahoo Messenger' => 32655, + 'Yammer' => 36955, + 'Yelp' => 32656, + 'Yorkshire Bank' => 34968, + 'Yorkshire Building Society' => 38224, + 'Youtube' => 32657, + 'Youtube Music' => 37587, + 'Yubo' => 37246, + 'Z1 Battle Royale' => 35490, + 'Zen Internet' => 33874, + 'ZoHo' => 36039, + 'Zone Broadband' => 36994, + 'Zoom' => 37946, + 'Zynga' => 32658, + ), + 'United States' => array( + '1and1' => 34817, + '1Password' => 36441, + '2600hertz' => 37692, + '2k' => 34768, + '4chan' => 37567, + '7 Days to Die' => 35947, + '8x8' => 34314, + 'ABC' => 38902, + 'Abebooks' => 36535, + 'Absolver' => 37366, + 'Access One' => 36063, + 'Acorns' => 37647, + 'Adams Networks' => 33556, + 'Adobe Connect' => 34134, + 'Adobe Creative Cloud' => 34551, + 'ADP' => 37657, + 'ADT' => 38502, + 'Adyen' => 37611, + 'Agar.io' => 38216, + 'Aio Wireless' => 34098, + 'Air Canada' => 20003, + 'Airbnb' => 34335, + 'AireSpring' => 35893, + 'Airnow.gov' => 37658, + 'Akamai' => 35447, + 'Alaska Airlines' => 20004, + 'Alaska Communications' => 36040, + 'Albion Online' => 38795, + 'Alexa' => 35910, + 'AliExpress' => 37395, + 'Allegiant Air' => 20005, + 'Ally' => 34791, + 'Amazon' => 20006, + 'Amazon Prime Music' => 34710, + 'Amazon Prime Video' => 33370, + 'Amazon Web Services' => 10147, + 'American Airlines' => 20007, + 'American Express' => 35958, + 'American Messaging' => 34128, + 'Amino Apps' => 37290, + 'Ammyy' => 36575, + 'Amtrak' => 20008, + 'Ancestry' => 38296, + 'Anno 1800' => 38413, + 'Anthem' => 37746, + 'Antietam' => 37051, + 'Anydesk' => 37406, + 'AOL' => 33419, + 'Apex Legends' => 38111, + 'App Store' => 35575, + 'Apple Maps' => 36125, + 'Apple Music' => 35241, + 'Apple News' => 36126, + 'Apple Store' => 34447, + 'Apple TV' => 36058, + 'Apple TV+' => 38318, + 'Appriver' => 37074, + 'AppValley' => 37749, + 'Arbuckle' => 34489, + 'ArcheAge' => 34876, + 'ARK: Survival Evolved' => 36350, + 'Arlo' => 36779, + 'Armstrong' => 34027, + 'Arvest Bank' => 37148, + 'Arvig' => 34919, + 'Asana' => 35828, + 'Assassin\'s Creed' => 35005, + 'AT&T' => 20010, + 'Atlantic Broadband' => 33437, + 'Authorize.net' => 34821, + 'Autotask' => 37579, + 'BabyTEL' => 35631, + 'Backblaze' => 35483, + 'Badoo' => 35993, + 'Bandwidth' => 35667, + 'Bank of America' => 20011, + 'Barclays' => 37639, + 'BART' => 20012, + 'Batman The Telltale Series' => 35896, + 'Battalion 1944' => 37002, + 'Battleborn' => 35718, + 'Battlefield' => 36091, + 'Battlerite' => 36820, + 'BB&T' => 35658, + 'Beats Music' => 34294, + 'beIN' => 37355, + 'Bejeweled' => 35904, + 'Bendbroad' => 34884, + 'Best Buy' => 34329, + 'BET' => 34125, + 'Bethesda' => 38316, + 'Binance' => 36818, + 'Bing' => 20014, + 'Birch Communications' => 33549, + 'Bitbucket' => 35414, + 'Bitfinex' => 36787, + 'Bitflyer' => 36789, + 'Bitstamp' => 34023, + 'Bittrex' => 36788, + 'Black Desert Online' => 37108, + 'BlackBerry' => 20015, + 'Blade and Soul' => 35511, + 'Blizzard Battle.net' => 34486, + 'Blogger' => 20016, + 'Blue Jay Wireless' => 35464, + 'Bluebird' => 37362, + 'Bluehost' => 33418, + 'BlueJeans' => 37509, + 'BMW ConnectedDrive' => 38471, + 'BNY Mellon' => 35674, + 'Boingo' => 37377, + 'Booking.com' => 34330, + 'Boom Beach' => 34364, + 'Box' => 32775, + 'Brawl Stars' => 38312, + 'BT' => 38338, + 'Buckeye Cablesystem' => 34029, + 'Buffer' => 36013, + 'Bullet Force' => 37109, + 'Bumble' => 35823, + 'C Spire' => 38219, + 'Cable One' => 20019, + 'Call of Duty' => 34148, + 'CallTower' => 35365, + 'Caltrain' => 20021, + 'Candy Crush' => 34024, + 'Candy Crush Soda Saga' => 35171, + 'Canva' => 36778, + 'Capital One' => 20022, + 'Cash App' => 38685, + 'CBSSports' => 35826, + 'Century National Bank' => 37690, + 'CenturyLink' => 20023, + 'Change.org' => 35616, + 'Charles Schwab' => 20078, + 'Chase' => 20025, + 'Chegg' => 36054, + 'Chicago Transit Authority' => 20026, + 'Chime' => 39050, + 'Chrome Web Store' => 34116, + 'Cincinnati Bell' => 33422, + 'Cirra Networks' => 36357, + 'Citi' => 20027, + 'Citizens Bank' => 37748, + 'Civilization' => 35869, + 'Clash of Clans' => 33550, + 'Clash Royale' => 35587, + 'Classlink' => 36821, + 'Clear' => 33423, + 'Cloudflare' => 32542, + 'Cloudsmith' => 35946, + 'Cloudtrax' => 37528, + 'CNET' => 33341, + 'CNN' => 34141, + 'Cogent' => 34026, + 'Coinbase' => 36175, + 'Comcast' => 20029, + 'Comedy Central' => 34126, + 'Common App' => 36726, + 'Comporium' => 36032, + 'Conan Exiles' => 37195, + 'Concur' => 36730, + 'Confluence' => 37540, + 'ConnectWise' => 37493, + 'Consolidated' => 34030, + 'Consolidated Edison' => 35679, + 'Consumer Cellular' => 37761, + 'Copy.com' => 34231, + 'Coredial' => 35454, + 'Costco' => 37061, + 'Counter-strike' => 34886, + 'Cox' => 20030, + 'Crackle' => 35349, + 'Craigslist' => 20031, + 'Credit One Bank' => 37760, + 'Cricket Wireless' => 20032, + 'Crown Castle' => 35812, + 'Crunchyroll' => 35114, + 'Cryptopia' => 36973, + 'Dailymotion' => 37598, + 'Dark Souls.' => 35710, + 'Dauntless' => 37204, + 'DayZ' => 37751, + 'Dazn' => 37497, + 'DC Universe Online' => 37059, + 'Dead By Daylight' => 35760, + 'Deezer' => 36424, + 'Defiance 2050' => 37364, + 'Delta Air Lines' => 20033, + 'Destiny' => 34761, + 'DeviantArt' => 34888, + 'Di.fm' => 35645, + 'Diablo' => 34385, + 'DirecTV' => 20034, + 'Directv Now' => 35933, + 'Discord' => 35795, + 'Dish Network' => 20035, + 'Disney World' => 35837, + 'Disney+' => 38800, + 'Disqus' => 34145, + 'Diversity Lottery' => 38472, + 'Dlive' => 38470, + 'Docker' => 37817, + 'Doom' => 35717, + 'Doordash' => 37581, + 'Dota 2' => 34304, + 'Doublelist' => 37580, + 'Downforeveryoneorjustme' => 34758, + 'Draftkings' => 37036, + 'Dragon Ball' => 35858, + 'Dramafever' => 36064, + 'DreamHost' => 35926, + 'Driveclub' => 35891, + 'Dropbox' => 20036, + 'Duckduckgo' => 35650, + 'Duke Energy' => 35871, + 'Duolingo' => 35942, + 'Dyl' => 37383, + 'Dyn' => 35707, + 'E-Trade' => 20037, + 'EA' => 34499, + 'EA Sports UFC' => 35715, + 'Eagle Communications ' => 36027, + 'Earthlink' => 32776, + 'Eatel' => 35448, + 'eBay' => 20038, + 'Ecobee' => 37163, + 'eFax' => 38664, + 'Electric Power Board' => 35778, + 'Elite: Dangerous' => 36478, + 'Ello' => 34906, + 'Endicia' => 36839, + 'Engine Yard' => 33966, + 'eNom' => 35901, + 'EPB Chattanooga' => 35513, + 'Epic Games Store' => 37756, + 'Escape from Tarkov' => 37424, + 'ESPN' => 33335, + 'ESPN Plus' => 38320, + 'Etherdelta' => 36858, + 'Etsy' => 33366, + 'Eve Online' => 35721, + 'Eve Valkyrie' => 35884, + 'Evernote' => 34706, + 'Everquest' => 38337, + 'Exchange Online' => 34534, + 'Exede' => 33557, + 'Expedia' => 34332, + 'Experian' => 38155, + 'ExpressVPN' => 37544, + 'Faceapp' => 38695, + 'Facebook' => 10198, + 'Facebook Messenger' => 20039, + 'Faceit' => 38543, + 'Facetime' => 34606, + 'FAFSA' => 37691, + 'FairPoint' => 33994, + 'Fallout' => 35430, + 'Family Search' => 33439, + 'Fandango' => 34481, + 'Fandom' => 34317, + 'Fanduel' => 37035, + 'Fanfiction' => 37101, + 'Far Cry' => 35713, + 'FastMail' => 35779, + 'Fatcow' => 34882, + 'Fax2mail' => 38665, + 'FedEx' => 35843, + 'Feedly' => 34704, + 'Fidelity' => 36738, + 'Fifa' => 35470, + 'Final Fantasy' => 35860, + 'Find my iPhone' => 38662, + 'First Communications ' => 36062, + 'FirstEnergy' => 35994, + 'FirstLight' => 34879, + 'Fitbit' => 35800, + 'Fite' => 38794, + 'Fiverr' => 37054, + 'Flickr' => 20040, + 'Fling' => 37353, + 'Flipboard' => 33491, + 'Florida Power & Light' => 35699, + 'Fonality' => 34312, + 'For Honor' => 35984, + 'Forge Of Empires' => 35886, + 'Fortnite' => 36375, + 'Forza' => 36817, + 'Fox News' => 35880, + 'Fox Sports Go' => 37164, + 'Frankfort PlantBoard' => 35466, + 'Freedompop' => 35657, + 'Freepik' => 36739, + 'Friday the 13th The Game' => 36348, + 'Frii' => 35668, + 'Frontier' => 20041, + 'Frontier Airlines' => 20042, + 'FuboTV' => 36658, + 'Funimation' => 36665, + 'Fuze' => 35847, + 'FXNOW' => 37504, + 'G-Portal' => 37154, + 'G2A.com' => 35898, + 'Game of Thrones Conquest' => 37669, + 'Game of war' => 35228, + 'Gamebattles' => 37753, + 'Gameloft' => 35964, + 'Gamestop' => 36780, + 'Gang Beasts' => 36926, + 'Garmin' => 35554, + 'Garmin Connect' => 35801, + 'Gatehub' => 36841, + 'GCI' => 35633, + 'GDAX' => 36791, + 'Gears of War' => 35848, + 'Geeking' => 37251, + 'Gemini' => 36811, + 'Gems of war' => 38801, + 'Geocaching' => 35770, + 'Gfycat' => 38504, + 'Ghost' => 38720, + 'Ghost Recon' => 36000, + 'Ghosttunes' => 34883, + 'GitHub' => 10004, + 'Gitlab' => 37146, + 'Glassdoor' => 35740, + 'Glide' => 34016, + 'Glitch' => 38721, + 'Gmail' => 20043, + 'Go Daddy' => 33416, + 'GOG.com' => 34287, + 'Google' => 10200, + 'Google Calendar' => 38597, + 'Google Cloud' => 37380, + 'Google Drive' => 34271, + 'Google Fiber' => 33421, + 'Google Hangouts' => 20046, + 'Google Home' => 36137, + 'Google Play' => 20045, + 'GoToMeeting' => 33489, + 'Gran Turismo' => 36347, + 'Grande' => 34404, + 'Greenlight' => 37628, + 'Grindr' => 35529, + 'Groove Music' => 33972, + 'Groupme' => 35739, + 'Groupon' => 35709, + 'Growtopia' => 36835, + 'Grubhub' => 37693, + 'GTA 5' => 33995, + 'Guild Wars 2' => 35060, + 'H&R Block' => 34328, + 'Halo' => 35418, + 'Halo Wars' => 36035, + 'Happn' => 37013, + 'Hargray' => 34142, + 'Harry Potter: Wizards Unite' => 38593, + 'Hashflare' => 37038, + 'Hawaiian Airlines' => 20047, + 'Hawaiian Telcom' => 33436, + 'Hay Day' => 34367, + 'Hayu' => 37198, + 'HBO Go' => 34340, + 'HBO Now' => 38697, + 'Healthcare.gov' => 34011, + 'Hearthstone' => 36834, + 'Heartland' => 38803, + 'Heroku' => 33965, + 'HiDive' => 37527, + 'HInge' => 37625, + 'Hipchat' => 34865, + 'HitBTC' => 36925, + 'Hitman' => 35585, + 'Hive' => 37571, + 'Honeywell' => 38110, + 'Hootsuite' => 20048, + 'Hostgator' => 33417, + 'Hostmonster' => 35931, + 'Hotels.com' => 34334, + 'Hotwire' => 34331, + 'HouseParty' => 37356, + 'HQ Trivia' => 36815, + 'HSBC' => 35673, + 'Hue' => 37514, + 'HughesNet' => 32787, + 'Hulu' => 33331, + 'Humanity' => 36735, + 'Hunt: showdown' => 37006, + 'Hurricane Electric' => 35811, + 'I3 Broadband' => 36286, + 'IBM Cloud' => 37649, + 'iCloud' => 20049, + 'ICQ' => 34766, + 'iFunny' => 38294, + 'IGTV' => 37317, + 'iHeartRadio‎' => 33369, + 'Illinois Century Network' => 36086, + 'IMDb Freedrive' => 37960, + 'iMessage' => 20050, + 'Imgur' => 20051, + 'IMVU' => 36523, + 'Inbox' => 34318, + 'inContact' => 35836, + 'Indeed' => 35864, + 'Ingress' => 35764, + 'Injustice 2' => 36526, + 'Inmotion' => 34759, + 'Instagram' => 20052, + 'Integra' => 33554, + 'Interactive Brokers' => 34115, + 'Intermedia' => 33973, + 'Internet Movie Database (IMDb)' => 33337, + 'Intralinks' => 38667, + 'Ipage' => 35659, + 'iRacing' => 35957, + 'Iridium' => 35652, + 'Ironsight' => 37142, + 'IRS' => 35542, + 'iTunes' => 20053, + 'iTunes Connect' => 36489, + 'iTunes Match' => 36059, + 'iWork' => 36124, + 'Jabber' => 36524, + 'Jackbox' => 38790, + 'JetBlue Airways' => 20054, + 'Jira' => 34870, + 'Jive' => 35453, + 'JP Morgan' => 37034, + 'Juno' => 32786, + 'Jurassic World Alive' => 37256, + 'Kayak' => 34338, + 'Keek' => 33770, + 'Kik' => 32770, + 'Kraken' => 36660, + 'Kucoin' => 36962, + 'last.fm' => 35634, + 'LastPass' => 35541, + 'LawBreakers' => 36527, + 'Layer3 TV' => 37378, + 'Leaco' => 35665, + 'League of Legends' => 34111, + 'Lifesize' => 37003, + 'Lightpath' => 35685, + 'Limebike' => 37205, + 'Limelight Networks' => 35462, + 'Line' => 33738, + 'LinkedIn' => 20055, + 'Linode' => 35907, + 'Liquid Web' => 34825, + 'LiveLeak' => 38273, + 'Liveperson' => 35417, + 'Logix' => 38295, + 'Logmein' => 33488, + 'Lola Wireless' => 35905, + 'Lowe\'s' => 38297, + 'LS Networks' => 35703, + 'Lumos Networks' => 34217, + 'Lyft' => 36571, + 'Lynda' => 35781, + 'Madden' => 37747, + 'Maguss' => 37047, + 'Mail.com' => 34798, + 'Mailbox' => 34831, + 'Mailchimp' => 36017, + 'MARTA' => 20056, + 'Marvel' => 38792, + 'Marvel Contest of Champions' => 37005, + 'Maryland Transit Administration (MTA)' => 20017, + 'Mass Effect ' => 36057, + 'Mastercard' => 37249, + 'MaxxSouth ' => 36095, + 'MBTA' => 20057, + 'McDonalds app' => 36369, + 'Media Temple' => 35950, + 'Mediacom' => 20058, + 'Meetme' => 37042, + 'Meetup' => 34245, + 'Megapath' => 33420, + 'Merrill Lynch' => 37032, + 'Metra' => 20059, + 'Metro PCS' => 20060, + 'MetroCast' => 34405, + 'Metrolink' => 20061, + 'Metronetinc' => 35890, + 'MHz Choice' => 37297, + 'Miami Dade Transit' => 20062, + 'Microsoft Azure' => 32547, + 'Microsoft Teams' => 38018, + 'Microsoft VLSC' => 38187, + 'Midcontinent Media' => 33552, + 'Mimecast' => 35783, + 'Minecraft' => 20063, + 'Mint' => 36589, + 'MintSim' => 36438, + 'Mitel' => 36005, + 'Mixer' => 36525, + 'MLB The Show' => 35711, + 'MLB TV' => 35712, + 'Mobile legends' => 37394, + 'Mobile Strike' => 36476, + 'Momentum Telecom' => 36733, + 'Moneylion' => 38303, + 'Monster Hunter' => 37007, + 'Mordhau' => 38497, + 'Moviepass' => 37004, + 'Movies Anywhere' => 38771, + 'MovieTickets' => 36178, + 'MSL Live' => 37107, + 'MTV' => 34133, + 'MU Legend' => 36792, + 'Music Unlimited' => 35909, + 'Musical.ly' => 36659, + 'My Fitness Pal' => 35803, + 'My Social Security' => 37627, + 'Naruto-Storm' => 35845, + 'National Grid NY' => 36024, + 'NBA 2k' => 35714, + 'NBC Sports Live Extra' => 34295, + 'NBCNews.com' => 33340, + 'NCTC' => 36344, + 'Need for Speed' => 35428, + 'NEMR' => 35514, + 'Nest' => 35190, + 'Net10 Wireless' => 34090, + 'netBlazr' => 35656, + 'Neteller' => 38321, + 'Netflix' => 20065, + 'Netsuite' => 35743, + 'Nettalk' => 36732, + 'Network Solutions' => 33548, + 'NetZero' => 32785, + 'Neverwinter' => 35603, + 'New Jersey Transit' => 20066, + 'New York MTA' => 20067, + 'New York Times' => 33339, + 'Newegg' => 35438, + 'NewWave Communications' => 36026, + 'Nextiva' => 34850, + 'NFL Network' => 36572, + 'NHL.tv' => 36816, + 'Nicehash' => 37037, + 'NikePlus' => 35805, + 'Nintendo eShop' => 34099, + 'Nintendo Network' => 35515, + 'Nintendo Switch Online' => 37240, + 'No Man\'s Sky' => 35793, + 'NordVPN' => 37545, + 'North State' => 36840, + 'Northland Communications' => 38035, + 'Norwood Light' => 35738, + 'NTT Communications' => 35856, + 'NuGet' => 36740, + 'Oculus' => 38253, + 'Office 365' => 34532, + 'OkCupid' => 35902, + 'Okta' => 35742, + 'Omegle' => 36522, + 'OneDrive' => 32780, + 'Onelogin' => 38439, + 'Ooma' => 35628, + 'ooVoo' => 34085, + 'Opera' => 38406, + 'Optimum / Cablevision' => 20020, + 'OptionsHouse' => 20068, + 'optionsXpress' => 20069, + 'Oracle Cloud' => 38033, + 'Orbitz' => 34337, + 'Origin' => 34113, + 'OS X Update' => 36127, + 'Outlook' => 10205, + 'Overdrive' => 35855, + 'Overwatch' => 35680, + 'Pacific Northern Gas ' => 35807, + 'Page Plus' => 34012, + 'Pagely' => 33974, + 'Paladins' => 35857, + 'Pandora' => 20070, + 'Paperspace' => 37200, + 'Paragon' => 36359, + 'PATCO' => 20071, + 'PATH' => 20072, + 'Path of Exile' => 34823, + 'Patreon' => 38503, + 'Paypal' => 20073, + 'pCloud' => 36588, + 'PenTeleData' => 34401, + 'Periscope' => 35595, + 'Personal Capital' => 36969, + 'PES' => 37951, + 'PG&E' => 35677, + 'PGA Tour Live' => 37235, + 'Phonepower' => 35361, + 'Photobucket' => 20074, + 'Pinterest' => 20075, + 'Planetside2' => 35787, + 'Player Unknown\'s Battlegrounds' => 36135, + 'Playstation Network' => 20076, + 'Playstation Vue' => 35188, + 'PlentyOfFish' => 35799, + 'Plex' => 35573, + 'PNC' => 33333, + 'Pokémon Duel' => 35966, + 'Pokémon Go' => 35725, + 'Pokerstars' => 38749, + 'Poloniex' => 36349, + 'Postmates' => 36774, + 'Powerschool' => 35867, + 'Prey' => 37295, + 'Priceline' => 34333, + 'Project Online' => 34535, + 'Proofpoint' => 38666, + 'ProtonMail' => 36136, + 'Quake Champions' => 36773, + 'Quickbooks Online' => 35761, + 'Quicken' => 36614, + 'Quizlet' => 34322, + 'Quizup' => 34296, + 'Quora' => 34341, + 'Rabb.it' => 36819, + 'Rackspace' => 34609, + 'Rain World Game' => 36042, + 'Rainbow Six' => 35463, + 'Razer' => 38698, + 'RCN' => 32784, + 'Realm Royale' => 37287, + 'Red Dead Redemption' => 37600, + 'Redbox' => 37213, + 'Reddit' => 33342, + 'Reflexion' => 38733, + 'RingCentral' => 34311, + 'Rise Broadband' => 35458, + 'Robinhood' => 35706, + 'Roblox' => 34820, + 'Rocket League' => 35251, + 'RocketRez' => 35784, + 'Roku' => 37194, + 'Royal Games' => 38218, + 'Runescape' => 34804, + 'Runkeeper' => 35802, + 'Runtastic' => 35804, + 'Rust' => 37755, + 'Ruzzle' => 33969, + 'Safelink Wireless' => 35467, + 'Safenet' => 37538, + 'Salesforce' => 32773, + 'Santander Bank' => 38656, + 'Sarahah' => 36365, + 'Schooldesk' => 35468, + 'Scottrade' => 20079, + 'ScreenConnect' => 35661, + 'Scum' => 37464, + 'Sea of Thieves' => 36995, + 'SEC Network' => 37757, + 'Secom' => 36972, + 'Second Life' => 37612, + 'SEPTA' => 20080, + 'Service Electric' => 33553, + 'Sharebuilder' => 20081, + 'Sharefile' => 37513, + 'Sharepoint Online' => 34533, + 'Shentel' => 35666, + 'Shopify' => 37555, + 'Shoretel' => 35846, + 'Showtime Anytime' => 35591, + 'Shutterstock' => 35704, + 'Signal' => 37071, + 'SignupGenius' => 35636, + 'Simple' => 34790, + 'Simple Mobile' => 33979, + 'Siri' => 38339, + 'SiriusXM' => 37048, + 'Skillshare' => 35775, + 'Skrill' => 37257, + 'Skype' => 20082, + 'Skype for Business' => 35350, + 'Skyscanner' => 38645, + 'SkySwitch' => 36664, + 'SkyWest' => 20084, + 'Slack' => 35437, + 'Slashdot' => 36615, + 'Sleeper' => 37455, + 'Slideshare' => 20085, + 'Sling' => 35155, + 'Smart Hub' => 35178, + 'Smartsheet' => 38310, + 'SmartThings' => 37102, + 'Smite' => 34802, + 'SNAP EBT' => 34006, + 'Snapchat' => 20086, + 'Socket' => 37411, + 'Sonic.net' => 34403, + 'Soundcloud' => 34351, + 'Sourceforge' => 20087, + 'South Central Communications' => 35759, + 'Southern California Edison' => 35700, + 'Southwest Airlines' => 20088, + 'Spectrum' => 20024, + 'Speedtest' => 37354, + 'Spirit Communications' => 35808, + 'SplashID' => 35594, + 'Spotify' => 20090, + 'Sprint' => 20091, + 'Square' => 37053, + 'Squarespace' => 36573, + 'Stack Exchange' => 34319, + 'Stackoverflow' => 34258, + 'Stadia' => 38302, + 'Staminus' => 34608, + 'Stamps.com' => 36662, + 'Star Citizen' => 36477, + 'Star Wars Battlefront' => 35459, + 'Starbucks' => 37210, + 'Starz' => 38291, + 'Steam' => 20092, + 'Steep' => 36011, + 'Straight Talk' => 34097, + 'Strava' => 36047, + 'Streamlabs' => 37206, + 'Strife' => 35853, + 'Stripe' => 38668, + 'Suddenlink' => 20093, + 'Suitebox' => 37211, + 'Summit Broadband' => 35617, + 'Summoners War' => 37409, + 'SunTrust Bank' => 37346, + 'Surfline' => 36341, + 'SurveyMonkey' => 35963, + 'T-Mobile' => 20097, + 'T. Rowe Price' => 37033, + 'Talkray' => 34750, + 'Tango' => 33728, + 'Target' => 34320, + 'Taxslayer' => 36018, + 'TD Ameritrade' => 20094, + 'TD Bank' => 35672, + 'TDS Telecom' => 33558, + 'Teamviewer' => 34343, + 'Tekken' => 37196, + 'Telecharge' => 37220, + 'Telegram' => 34227, + 'Tennis TV' => 37040, + 'TERA' => 37105, + 'Tesla' => 37152, + 'The Crew 2' => 35809, + 'The Culling' => 37752, + 'The Division' => 35558, + 'The elder scrolls online' => 35535, + 'The Huffington Post' => 33334, + 'The Simpsons Tapped out' => 34702, + 'The Weather Channel' => 33336, + 'Thingiverse' => 38732, + 'Threads' => 38983, + 'Threema' => 35892, + 'TIAA ' => 35597, + 'Ticketmaster' => 36534, + 'Tidal' => 37512, + 'Tiktok' => 38367, + 'Tinder' => 34237, + 'Titanfall' => 35897, + 'Tivo' => 37001, + 'Todoist' => 36461, + 'Toggl' => 36993, + 'TPx Communications' => 34402, + 'TracFone Wireless' => 20099, + 'Tradeking' => 33768, + 'TradeSatoshi' => 37106, + 'TradeStation' => 20100, + 'Transferwise' => 37199, + 'Travelocity' => 34336, + 'Trello' => 34796, + 'Trove' => 35363, + 'Trusted Id' => 36636, + 'TSYS' => 36517, + 'Tumblr' => 20101, + 'TuneIn' => 35221, + 'TurboTax' => 34327, + 'TV Time' => 37197, + 'TW Telecom' => 32783, + 'Tweakbox' => 37750, + 'Tweetdeck' => 39009, + 'Twitch' => 34308, + 'Twitter' => 10204, + 'Uber' => 36570, + 'Uber Eats' => 37648, + 'Udacity' => 35842, + 'Udemy' => 35644, + 'UFC' => 36736, + 'UMG Gaming' => 36528, + 'Uncharted' => 35716, + 'United Airlines' => 20104, + 'Untappd' => 37363, + 'Uplay PC' => 34381, + 'UPS' => 34321, + 'Upwork' => 35436, + 'US Bank' => 20106, + 'US Cellular' => 20103, + 'USA Mobility' => 34129, + 'USPS' => 34259, + 'Utah Broadband' => 36342, + 'Vainglory' => 37203, + 'Vanguard' => 37011, + 'Vectren' => 35872, + 'Venmo' => 37209, + 'Verizon' => 20107, + 'Vero' => 37062, + 'Viaero' => 37508, + 'Viasat' => 38298, + 'Viber' => 32769, + 'Viewster' => 34323, + 'Viki' => 36065, + 'Vimeo' => 20110, + 'Vine' => 33368, + 'Virgin Mobile' => 34724, + 'Visa' => 37239, + 'Visible' => 38605, + 'Visual Studio Team Services' => 37463, + 'Vlive' => 36574, + 'Vonage' => 34315, + 'Voxer' => 33492, + 'VRChat' => 36991, + 'VRV' => 36640, + 'Vudu' => 34109, + 'Vyve Broadband' => 36012, + 'W3Schools' => 35887, + 'Waiter.com' => 36776, + 'Walmart Family' => 35465, + 'Walmart.com' => 33338, + 'War Thunder' => 37318, + 'Warface' => 37510, + 'Warframe' => 35825, + 'Washington Metropolitan Area Transit Authority' => 20111, + 'Wattpad' => 34693, + 'Wave Broadband' => 34028, + 'Waze' => 33382, + 'Webassign' => 35873, + 'Webex' => 33490, + 'Webhosting.net' => 35435, + 'Webs' => 35895, + 'Weebly' => 34910, + 'Weight Watchers' => 35646, + 'Wells Fargo' => 20112, + 'Wemo' => 37593, + 'Westman' => 35359, + 'WeTransfer' => 20113, + 'Whatsapp' => 10136, + 'Whisper' => 36881, + 'Wikipedia' => 20115, + 'Wilcoinc' => 35696, + 'Wildstar' => 36475, + 'William Hill' => 38900, + 'Windstream' => 20117, + 'Wish' => 38154, + 'Wix' => 35596, + 'Wordpress.com' => 20118, + 'Workday' => 37314, + 'World of Tanks' => 35357, + 'World of Warcraft' => 34263, + 'World of Warships' => 36637, + 'World War 3' => 37599, + 'World War Z' => 38405, + 'WOW' => 20114, + 'WP Engine' => 33975, + 'Wunderground' => 35948, + 'Wunderlist' => 35839, + 'WWE Network' => 36056, + 'Xbox Live' => 20119, + 'Xfinity Flex' => 38412, + 'XO' => 32777, + 'Yahoo' => 10203, + 'Yahoo Mail' => 20120, + 'Yahoo Messenger' => 20121, + 'Yammer' => 35559, + 'Yelp' => 20122, + 'Youtube' => 34651, + 'Youtube Music' => 37461, + 'Youtube TV' => 37296, + 'Yubo' => 37720, + 'Z1 Battle Royale' => 35146, + 'Zayo' => 35866, + 'Zelle' => 38683, + 'Zendesk' => 35854, + 'Zillow' => 33367, + 'ZoHo' => 34144, + 'Zoom' => 37349, + 'Zwift' => 38167, + 'Zynga' => 20123, + ), + 'New Zealand' => array( + '2degrees' => 33886, + '4chan' => 38270, + 'Alexa' => 36920, + 'Amurit.net' => 37710, + 'Anthem' => 38210, + 'ANZ' => 33888, + 'Apex Legends' => 38134, + 'Apple Store' => 34457, + 'ASB' => 33905, + 'beIN' => 38493, + 'Bigpipe' => 37704, + 'Bing' => 33889, + 'Blizzard Battle.net' => 38465, + 'BNZ' => 33907, + 'Call of Duty' => 34150, + 'CallPlus' => 33885, + 'CCL' => 37705, + 'Counter-strike' => 36854, + 'Crunchyroll' => 38146, + 'Cryptopia' => 36963, + 'Dead By Daylight' => 37434, + 'Destiny' => 38942, + 'Discord' => 38609, + 'Dota 2' => 36415, + 'EA' => 34505, + 'eBay' => 33891, + 'Evernote' => 35914, + 'Facebook' => 33892, + 'Facebook Messenger' => 33893, + 'Facetime' => 34587, + 'Fifa' => 38400, + 'Flip' => 37706, + 'Fortnite' => 36655, + 'Gmail' => 33894, + 'Google' => 33895, + 'Google Drive' => 36759, + 'Google Hangouts' => 33896, + 'Google Play' => 38525, + 'Gran Turismo' => 36948, + 'Grindr' => 38402, + 'GTA 5' => 37277, + 'iCloud' => 33921, + 'iMessage' => 33922, + 'Inspire Net' => 35445, + 'Instagram' => 33915, + 'iTunes' => 33923, + 'Kik' => 33914, + 'Kiwibank' => 33906, + 'League of Legends' => 38692, + 'Lightbox' => 35504, + 'LinkedIn' => 33897, + 'Minecraft' => 38617, + 'My Fitness Pal' => 37635, + 'myob' => 38926, + 'MyRepublic' => 34931, + 'NBA 2k' => 38785, + 'Neon' => 37707, + 'Netflix' => 34857, + 'Nzdating' => 37708, + 'Office 365' => 37171, + 'OneDrive' => 36863, + 'Orcon' => 33883, + 'Origin' => 36887, + 'Outlook' => 33918, + 'Overwatch' => 36416, + 'Pandora' => 33919, + 'Pinterest' => 33898, + 'Player Unknown\'s Battlegrounds' => 36362, + 'Playstation Network' => 34858, + 'Pokémon Go' => 35733, + 'Rabodirect' => 34980, + 'Rainbow Six' => 36610, + 'Reddit' => 34860, + 'Roblox' => 37662, + 'Skinny' => 37709, + 'Sky TV' => 33887, + 'Skype' => 33912, + 'Slingshot' => 34737, + 'Snapchat' => 33908, + 'Spark' => 33884, + 'Spotify' => 33910, + 'Steam' => 34225, + 'Tinder' => 34859, + 'Trade Me' => 33916, + 'Trustpower' => 37703, + 'Twitch' => 36540, + 'Twitter' => 33899, + 'Viber' => 33909, + 'Vodafone' => 33882, + 'Voxer' => 33913, + 'Wattpad' => 34697, + 'WeChat' => 33920, + 'Westpac' => 33900, + 'Whatsapp' => 33917, + 'Wikipedia' => 33901, + 'World of Warcraft' => 38814, + 'Xbox Live' => 34284, + 'Xero' => 35929, + 'Yahoo' => 33902, + 'Yahoo Mail' => 33903, + 'Youtube' => 33904, + 'Z1 Battle Royale' => 37266, + ), + 'België' => array( + '2dehands.be' => 34257, + 'Apex Legends' => 38141, + 'App Store' => 38555, + 'Apple Store' => 38925, + 'Argenta' => 32491, + 'Bancontact Mister Cash' => 33976, + 'Base' => 10207, + 'Battlefield' => 38444, + 'Belfius' => 32545, + 'Binance' => 36934, + 'Binck' => 32388, + 'Blizzard Battle.net' => 38810, + 'BNP Paribas Fortis' => 32543, + 'Bolero' => 37619, + 'Bpost' => 37157, + 'Bwin' => 34872, + 'Call of Duty' => 35536, + 'Clash Royale' => 38948, + 'Counter-strike' => 38245, + 'Crelan' => 37614, + 'Dead By Daylight' => 37445, + 'Deezer' => 33828, + 'Destiny' => 38559, + 'Deutsche Bank' => 34140, + 'Discord' => 37391, + 'Dommel' => 33977, + 'Dota 2' => 38478, + 'Dropbox' => 32525, + 'EA' => 34841, + 'eBay' => 33858, + 'EDPnet' => 32855, + 'Engie Electrabel' => 37616, + 'Epic Games Store' => 39027, + 'Facebook' => 22339, + 'Facebook Messenger' => 34851, + 'Facetime' => 34605, + 'Fifa' => 36906, + 'Fintro' => 37617, + 'Fluvius' => 37615, + 'Fortnite' => 36696, + 'Ghost Recon' => 39001, + 'Gmail' => 32374, + 'Google' => 32343, + 'Google Hangouts' => 32395, + 'Google Play' => 32371, + 'Gran Turismo' => 36913, + 'Grindr' => 38743, + 'GTA 5' => 35500, + 'Guild Wars 2' => 36474, + 'iCloud' => 22363, + 'iMessage' => 32463, + 'ING' => 22342, + 'Instagram' => 34286, + 'Isabel' => 34004, + 'Itsme' => 37454, + 'iTunes' => 32484, + 'Jim Mobile' => 37618, + 'KBC' => 32544, + 'Kik' => 32524, + 'Kraken' => 36828, + 'League of Legends' => 38631, + 'Lebara' => 32461, + 'LinkedIn' => 32534, + 'Lycamobile' => 32462, + 'Mobile Vikings' => 32854, + 'Moneyou' => 32488, + 'Multisafepay' => 34878, + 'Nest' => 38517, + 'Netflix' => 34629, + 'Numéricable' => 32856, + 'Office 365' => 36409, + 'OneDrive' => 35410, + 'Orange' => 10208, + 'Origin' => 36888, + 'Outlook' => 32375, + 'Overwatch' => 36383, + 'OVH' => 36746, + 'Paypal' => 32354, + 'Paysafecard' => 32485, + 'Play Sports' => 37582, + 'Player Unknown\'s Battlegrounds' => 36568, + 'Playstation Network' => 32339, + 'Pokémon Go' => 35736, + 'Proximus' => 10206, + 'Rainbow Six' => 36180, + 'Reddit' => 37474, + 'Roblox' => 38757, + 'Rocket League' => 35978, + 'Runescape' => 38654, + 'Salesforce' => 32349, + 'Scarlet' => 32435, + 'Simyo' => 32387, + 'Skype' => 32357, + 'Smartschool' => 37620, + 'Snapchat' => 32520, + 'Sofort Banking' => 32482, + 'Spotify' => 32336, + 'Steam' => 34278, + 'Strava' => 38161, + 'Teamviewer' => 34907, + 'Telegram' => 35971, + 'Telenet' => 10196, + 'Tinder' => 35499, + 'TuneIn' => 38759, + 'TV Vlaanderen' => 22364, + 'Tweakbox' => 38786, + 'Twitch' => 36542, + 'Twitter' => 22355, + 'Unibet' => 38830, + 'Uplay PC' => 35614, + 'Viber' => 32505, + 'Voo' => 32853, + 'VTM' => 38394, + 'Waze' => 36657, + 'WeTransfer' => 32523, + 'Whatsapp' => 32333, + 'Wikipedia' => 32425, + 'Wordfeud' => 22358, + 'Xbox Live' => 32481, + 'Yahoo Mail' => 34840, + 'Yellowbrick' => 37609, + 'Yeloplay' => 37359, + 'YouTube' => 32338, + 'Z1 Battle Royale' => 37258, + ), + 'Australia' => array( + '2k' => 36402, + '4chan' => 38290, + 'AAPT telecommunications' => 35849, + 'ABC' => 37712, + 'Activ8me' => 36814, + 'Adam Internet' => 33877, + 'Adelaide Bank' => 36729, + 'Adobe Creative Cloud' => 34553, + 'Airbnb' => 36403, + 'Alexa' => 36922, + 'Amaysim' => 35654, + 'Amazon' => 36404, + 'Amazon Prime Video' => 36968, + 'Amazon Web Services' => 34093, + 'American Express' => 38173, + 'Amino Apps' => 39045, + 'Amnet' => 33878, + 'Anthem' => 38176, + 'ANZ' => 33364, + 'Apex Legends' => 38125, + 'App Store' => 35577, + 'Apple Music' => 35243, + 'Apple Store' => 34455, + 'ARK: Survival Evolved' => 38746, + 'Assassin\'s Creed' => 35008, + 'AusBBS' => 36812, + 'Aussie Broadband' => 37219, + 'Australia Post' => 37052, + 'Bank Australia' => 38682, + 'Bank of Melbourne' => 35356, + 'Bank of Queensland' => 37458, + 'Bank SA' => 35354, + 'Bankwest' => 34972, + 'Battlefield' => 37468, + 'Belong' => 37179, + 'Bendigo Bank' => 37715, + 'BigPond' => 33993, + 'Binance' => 36878, + 'Bing' => 32874, + 'Blizzard Battle.net' => 35053, + 'Blogger' => 32875, + 'Boom Beach' => 36871, + 'Box' => 32876, + 'Brawl Stars' => 38816, + 'Bumble' => 36809, + 'Call of Duty' => 36405, + 'Candy Crush' => 35092, + 'Centrelink' => 37549, + 'Centurylink' => 34131, + 'Citibank' => 36507, + 'Clash of Clans' => 37302, + 'Clash Royale' => 38953, + 'Cloudflare' => 32877, + 'ClubTelco' => 34792, + 'Coinbase' => 36802, + 'Coinspot' => 36927, + 'Commander' => 37714, + 'Commonwealth Bank' => 33362, + 'Counter-strike' => 35059, + 'Crackle' => 35112, + 'Craigslist' => 32878, + 'Crunchyroll' => 36717, + 'Dead By Daylight' => 37415, + 'Deezer' => 33833, + 'Deliveroo' => 38197, + 'Destiny' => 34894, + 'DFP' => 35002, + 'Discord' => 36022, + 'Dodo' => 34003, + 'Doordash' => 39006, + 'Dota 2' => 35405, + 'Dropbox' => 32879, + 'EA' => 34509, + 'eBay' => 32880, + 'Eftel' => 35553, + 'Elite: Dangerous' => 37104, + 'Escape from Tarkov' => 38626, + 'ESPN' => 38387, + 'Etsy' => 37481, + 'Exchange Online' => 34545, + 'Exetel' => 34478, + 'Facebook' => 32881, + 'Facebook Messenger' => 32882, + 'Facetime' => 34601, + 'Fallout' => 35432, + 'Far Cry' => 37190, + 'Fifa' => 35473, + 'Fitbit' => 38358, + 'Flickr' => 32883, + 'Fonality' => 34313, + 'For Honor' => 35989, + 'Fortnite' => 36622, + 'Foxtel' => 33355, + 'Funimation' => 38583, + 'Game of war' => 35416, + 'Gears of War' => 38894, + 'Ghost Recon' => 36008, + 'GitHub' => 36529, + 'Gmail' => 32885, + 'Go Daddy' => 36619, + 'Google' => 32886, + 'Google Drive' => 34274, + 'Google Hangouts' => 32887, + 'Google Home' => 37343, + 'Google Play' => 32888, + 'Gran Turismo' => 36875, + 'Grindr' => 35834, + 'GTA 5' => 34755, + 'Guild Wars 2' => 36408, + 'Gumtree' => 33360, + 'Halo' => 35449, + 'Hay Day' => 34786, + 'Hayu' => 38366, + 'Hearthstone' => 38660, + 'Heronet' => 35949, + 'Hipchat' => 34867, + 'Hootsuite' => 32889, + 'iCloud' => 32890, + 'iiNet' => 33555, + 'iMessage' => 32891, + 'Imgur' => 32892, + 'Instagram' => 32893, + 'Internode' => 34477, + 'iPrimus' => 33876, + 'iTunes' => 32894, + 'Kik' => 32895, + 'Kraken' => 36849, + 'Ladbrokes' => 37640, + 'LastPass' => 34776, + 'League of Legends' => 35049, + 'LinkedIn' => 32896, + 'Mail.com' => 34799, + 'Mailbox' => 34834, + 'Mate Communicate' => 36813, + 'McDonalds app' => 37525, + 'ME Bank' => 34973, + 'Microsoft Azure' => 32917, + 'Microsoft Teams' => 38694, + 'Minecraft' => 32897, + 'My Fitness Pal' => 37633, + 'My Republic' => 35982, + 'MyGov' => 37550, + 'MyNetFone' => 35481, + 'myob' => 36343, + 'NAB' => 33365, + 'National Broadband Network, NBN' => 35992, + 'NBA 2k' => 37736, + 'Netflix' => 35177, + 'Netregistry' => 36060, + 'Netspeed' => 34492, + 'Nine' => 37000, + 'Nintendo eShop' => 34102, + 'Nintendo Network' => 35517, + 'Nintendo Switch Online' => 37243, + 'No Man\'s Sky' => 37404, + 'Office 365' => 34543, + 'OkCupid' => 38489, + 'OneDrive' => 32904, + 'Optus' => 33356, + 'Origin' => 35091, + 'Outlook' => 32899, + 'Overwatch' => 35683, + 'Paladins' => 37656, + 'Pandora' => 33343, + 'Panthur' => 36346, + 'Path of Exile' => 36498, + 'Paypal' => 35607, + 'Photobucket' => 32901, + 'Pinterest' => 32902, + 'Player Unknown\'s Battlegrounds' => 36356, + 'Playstation Network' => 34842, + 'PlentyOfFish' => 37416, + 'Pokémon Go' => 35732, + 'Project Online' => 34542, + 'Quickbooks Online' => 38220, + 'Rabobank' => 34974, + 'Rabodirect' => 34975, + 'Rainbow Six' => 35478, + 'Red Dead Redemption' => 37740, + 'Reddit' => 34843, + 'Roblox' => 36832, + 'Rocket League' => 35497, + 'Runescape' => 35106, + 'Salesforce' => 32903, + 'Sarahah' => 36374, + 'SBS' => 34703, + 'Sea of Thieves' => 37115, + 'Sharepoint Online' => 34541, + 'Skype' => 32905, + 'Skype for Business' => 34544, + 'Slack' => 35937, + 'Slideshare' => 32906, + 'Smite' => 35406, + 'Snapchat' => 32907, + 'Soundcloud' => 37230, + 'Spintel' => 35962, + 'Sportsbet' => 37713, + 'Spotify' => 33225, + 'St. George Bank' => 35353, + 'Stan' => 35503, + 'Star Wars Battlefront' => 35552, + 'Steam' => 34114, + 'Steep' => 37922, + 'Summoners War' => 37418, + 'Suncorp Bank' => 34432, + 'Sure Telecom' => 34394, + 'Tango' => 33742, + 'Teamviewer' => 34617, + 'Telegram' => 34220, + 'Telstra' => 33354, + 'The Division' => 35572, + 'The elder scrolls online' => 36554, + 'The Simpsons Tapped out' => 38166, + 'Think Mobile' => 34527, + 'Tiktok' => 39061, + 'Tinder' => 34241, + 'TPG Telecom' => 33359, + 'Tumblr' => 32910, + 'TuneIn' => 38761, + 'Tweakbox' => 38346, + 'Twitch' => 35026, + 'Twitter' => 32911, + 'Uber' => 37766, + 'Uber Eats' => 38272, + 'Uberglobal' => 35669, + 'Udemy' => 37719, + 'Uplay PC' => 36211, + 'V4 Telecom' => 37357, + 'Vaya' => 34518, + 'Viber' => 32912, + 'Vimeo' => 32913, + 'Virgin Mobile' => 34516, + 'Vocus' => 37711, + 'Vodafone' => 33357, + 'War Thunder' => 38361, + 'Warframe' => 36495, + 'Wattpad' => 34694, + 'Waze' => 33405, + 'Webex' => 34162, + 'Weebly' => 34911, + 'Westnet' => 35163, + 'Westpac' => 33363, + 'WeTransfer' => 32914, + 'Whatsapp' => 32915, + 'Wikipedia' => 32916, + 'Wish' => 39042, + 'Wix' => 37255, + 'Wordpress.com' => 32918, + 'World of Tanks' => 37191, + 'World of Warcraft' => 35054, + 'WWE Network' => 38722, + 'Xbox Live' => 34283, + 'Xero' => 36177, + 'Yahoo' => 32919, + 'Yahoo Mail' => 32920, + 'Yahoo Messenger' => 32921, + 'Yelp' => 32922, + 'Youtube' => 32923, + 'Youtube Music' => 37590, + 'Z1 Battle Royale' => 35494, + 'Zettanet' => 35655, + 'ZoHo' => 37551, + ), + 'Canada' => array( + '2k' => 36723, + 'Acanac' => 34107, + 'Access' => 33069, + 'Aeroplan' => 35789, + 'Air Canada' => 37029, + 'Air Miles' => 35788, + 'Airbnb' => 37423, + 'Alaska Airlines' => 32926, + 'Alexa' => 37067, + 'Allegiant Air' => 32927, + 'Allstream' => 34001, + 'Altima Telecom' => 37247, + 'Amazon' => 32928, + 'Amazon Prime Music' => 38907, + 'Amazon Prime Video' => 37393, + 'Amazon Web Services' => 32929, + 'American Airlines' => 32930, + 'American Express' => 38172, + 'Amino Apps' => 38716, + 'Anthem' => 38056, + 'Anydesk' => 37488, + 'Apex Legends' => 38113, + 'App Store' => 35576, + 'Apple Music' => 35244, + 'Apple Store' => 34456, + 'ARK: Survival Evolved' => 38747, + 'Assassin\'s Creed' => 35007, + 'BabyTEL' => 35360, + 'Battlefield' => 36923, + 'BC Hydro' => 35806, + 'Beanfield' => 36663, + 'beIN' => 38788, + 'Bell' => 32936, + 'Bell Aliant' => 32937, + 'Bet365' => 39064, + 'Binance' => 36876, + 'Bing' => 32938, + 'Black Desert Online' => 38829, + 'Blizzard Battle.net' => 34488, + 'Blogger' => 32940, + 'BMO' => 34525, + 'Boom Beach' => 36558, + 'Box' => 32941, + 'Brama Telecom' => 34146, + 'Brawl Stars' => 38833, + 'Bumble' => 36330, + 'Call of Duty' => 34149, + 'Candy Crush' => 35093, + 'Capital One' => 36410, + 'Carry Telecom' => 37695, + 'CBC' => 34267, + 'Centurylink' => 35627, + 'Chatr' => 34005, + 'CIBC' => 34526, + 'CIK Telecom‎' => 34428, + 'Clash of Clans' => 34445, + 'Clash Royale' => 37523, + 'Cloudflare' => 32952, + 'CNN' => 37372, + 'Cogeco' => 32953, + 'Cogent' => 35556, + 'Coinbase' => 36800, + 'Colbanet' => 37542, + 'CommStream' => 36028, + 'Comwave' => 36641, + 'Counter-strike' => 35056, + 'Crackle' => 35111, + 'Craigslist' => 32956, + 'Crave TV' => 35621, + 'Crunchyroll' => 36716, + 'Dark Souls' => 36557, + 'DayZ' => 39007, + 'Dazn' => 36510, + 'Dead By Daylight' => 36595, + 'Delta Air Lines' => 32958, + 'Destiny' => 34892, + 'DFP' => 35001, + 'Diablo' => 39071, + 'Discord' => 36411, + 'Distributel' => 33870, + 'Doordash' => 38385, + 'Dota 2' => 35404, + 'Dropbox' => 32961, + 'EA' => 34504, + 'EA Sports UFC' => 36552, + 'Eastlink' => 33414, + 'eBay' => 32964, + 'Ebox' => 35555, + 'Ecobee' => 38021, + 'Elite: Dangerous' => 37103, + 'Epic Games Store' => 38767, + 'Equitable Bank' => 36731, + 'Escape from Tarkov' => 37975, + 'Etsy' => 38265, + 'Exchange Online' => 34546, + 'Execulink' => 34942, + 'Expedia' => 37401, + 'Facebook' => 32965, + 'Facebook Messenger' => 32966, + 'Facetime' => 34595, + 'Fallout' => 37663, + 'FedEx' => 36500, + 'Fido' => 34000, + 'Fifa' => 35472, + 'Fitbit' => 38359, + 'Flickr' => 32967, + 'For Honor' => 35986, + 'Fortnite' => 36623, + 'Forza' => 38836, + 'Fox News' => 37665, + 'Freedom Mobile' => 33869, + 'Friday the 13th The Game' => 37562, + 'Funimation' => 37744, + 'Game of war' => 35415, + 'Garmin Connect' => 38540, + 'Gears of War' => 35938, + 'Ghost Recon' => 37772, + 'GitHub' => 32970, + 'Gmail' => 32971, + 'Go Daddy' => 34943, + 'GO Transit' => 32972, + 'Google' => 32973, + 'Google Calendar' => 38599, + 'Google Cloud' => 38537, + 'Google Drive' => 34272, + 'Google Hangouts' => 32974, + 'Google Home' => 36141, + 'Google Play' => 32975, + 'GoToMeeting' => 36504, + 'Gran Turismo' => 36679, + 'Grindr' => 35835, + 'GTA 5' => 34756, + 'Guild Wars 2' => 36406, + 'Halo' => 35450, + 'Harry Potter: Wizards Unite' => 38687, + 'Hay Day' => 34789, + 'Hayu' => 38789, + 'Hearthstone' => 38289, + 'Hipchat' => 34868, + 'Hootsuite' => 32977, + 'HQ Trivia' => 37027, + 'HSBC' => 39048, + 'Hue' => 37814, + 'HughesNet' => 32978, + 'iCloud' => 32979, + 'iMessage' => 33249, + 'Imgur' => 32981, + 'IMVU' => 37472, + 'Indeed' => 38782, + 'Instagram' => 32982, + 'Interac' => 39049, + 'iTunes' => 32983, + 'JetBlue Airways' => 32984, + 'Jira' => 38545, + 'Juno' => 32985, + 'Kijiji' => 34303, + 'Kik' => 32986, + 'Koodo' => 34002, + 'Kraken' => 36750, + 'LastPass' => 36785, + 'League of Legends' => 35047, + 'Lightspeed' => 37066, + 'LinkedIn' => 32987, + 'Madden' => 38459, + 'Mail.com' => 34801, + 'Mailbox' => 34837, + 'McDonalds app' => 37642, + 'MCSNet' => 35889, + 'Metro Loop' => 37646, + 'Microsoft Azure' => 33057, + 'Minecraft' => 32996, + 'Mixer' => 38769, + 'MLB The Show' => 36989, + 'MLB TV' => 37189, + 'Montréal Metro' => 32997, + 'Mordhau' => 38734, + 'MTS' => 33868, + 'My Fitness Pal' => 37632, + 'NBA 2k' => 37722, + 'Nest' => 35191, + 'Netflix' => 32998, + 'NetZero' => 32999, + 'Neverwinter' => 35766, + 'Nintendo eShop' => 34101, + 'Nintendo Network' => 35518, + 'No Man\'s Sky' => 38779, + 'NorthernTel' => 34636, + 'Northwestel' => 35104, + 'Office 365' => 34548, + 'OkCupid' => 36678, + 'OneDrive' => 33020, + 'Oricom Internet' => 35829, + 'Origin' => 36584, + 'Outlook' => 33005, + 'Overwatch' => 35682, + 'Paladins' => 37664, + 'Path of Exile' => 36481, + 'Paypal' => 33010, + 'PC Optimum' => 37216, + 'Peer 1' => 34824, + 'Photobucket' => 33011, + 'Pinterest' => 33012, + 'Piper' => 38323, + 'Player Unknown\'s Battlegrounds' => 36337, + 'Playstation Network' => 33013, + 'PlentyOfFish' => 35798, + 'Pokémon Go' => 35734, + 'Pokerstars' => 38174, + 'Primus' => 33872, + 'Qtrade Financial Group' => 36025, + 'Quickbooks Online' => 37607, + 'Quizup' => 34297, + 'Rabb.it' => 37974, + 'Rainbow Six' => 35477, + 'RBC' => 34521, + 'Red Dead Redemption' => 37742, + 'Reddit' => 34845, + 'Roblox' => 35785, + 'Rocket League' => 35498, + 'Rogers' => 33015, + 'Runescape' => 35105, + 'Salesforce' => 33016, + 'Sarahah' => 36514, + 'Sasktel' => 33871, + 'Scotiabank' => 34523, + 'Sea of Thieves' => 37111, + 'Sharepoint Online' => 34550, + 'Shaw' => 33551, + 'Shopify' => 35863, + 'Signal' => 36328, + 'Simplii' => 37541, + 'SiriusXM' => 38561, + 'SkipTheDishes' => 38322, + 'Skype' => 33021, + 'Skype for Business' => 34547, + 'Slack' => 35935, + 'Slideshare' => 33023, + 'Smartsheet' => 38724, + 'Smite' => 37026, + 'Snapchat' => 33024, + 'Soundcloud' => 37229, + 'Sourceforge' => 33025, + 'Spotify' => 33028, + 'Star Wars Battlefront' => 35550, + 'Start Communications' => 34426, + 'Steam' => 33030, + 'Steep' => 37923, + 'Strava' => 38745, + 'Summoners War' => 37422, + 'Tangerine' => 37694, + 'Tango' => 33741, + 'Tbaytel' => 34612, + 'TD Canada Trust' => 34522, + 'Teamviewer' => 34618, + 'TekSavvy' => 33867, + 'Télébec' => 34110, + 'Telegram' => 34228, + 'Telnet' => 34158, + 'Telus' => 33034, + 'TeraGo' => 34427, + 'Tesla' => 38835, + 'The Division' => 35571, + 'The elder scrolls online' => 36399, + 'The Simpsons Tapped out' => 35109, + 'The Weather Channel' => 38837, + 'Ticketmaster' => 39008, + 'Tiktok' => 38727, + 'Tinder' => 34239, + 'Toronto Transit Commission' => 33036, + 'Translink' => 33039, + 'Trello' => 39066, + 'Trove' => 38825, + 'Tumblr' => 33040, + 'TuneIn' => 36037, + 'TurboTax' => 38213, + 'Tweakbox' => 38343, + 'Twitch' => 35023, + 'Twitter' => 33042, + 'Uber' => 37765, + 'Uber Eats' => 38022, + 'Udemy' => 37718, + 'United Airlines' => 33044, + 'Uplay PC' => 34688, + 'UPS' => 36831, + 'Vancity' => 37570, + 'Velcom' => 36978, + 'Viber' => 33348, + 'Vidéotron' => 33050, + 'Vimeo' => 33051, + 'Virgin Mobile' => 34096, + 'Visa' => 38693, + 'VMedia' => 34265, + 'Warface' => 38777, + 'Warframe' => 36370, + 'Wattpad' => 34695, + 'Waveapps' => 35960, + 'Waze' => 33406, + 'Webex' => 34160, + 'Weight Watchers' => 35664, + 'WestJet' => 35648, + 'WeTransfer' => 33054, + 'Whatsapp' => 33055, + 'Whisper' => 34625, + 'Wiband' => 34771, + 'Wikipedia' => 33056, + 'Wish' => 39041, + 'Wisp' => 37218, + 'Wordpress.com' => 33059, + 'World of Tanks' => 35527, + 'World of Warcraft' => 34264, + 'WWE Network' => 38717, + 'Xbox Live' => 33061, + 'Xplornet' => 34270, + 'Yahoo' => 33063, + 'Yahoo Mail' => 33064, + 'Yahoo Messenger' => 33065, + 'Yak' => 34269, + 'Yelp' => 33066, + 'Youtube' => 33067, + 'Youtube Music' => 37588, + 'Yubo' => 38961, + 'Z1 Battle Royale' => 35491, + 'ZoHo' => 36038, + 'Zoom' => 37947, + 'Zynga' => 33068, + ), + 'Hong Kong' => array( + '3 (Three)' => 36202, + 'Apex Legends' => 38178, + 'App Store' => 37734, + 'Binance' => 37023, + 'Blizzard Battle.net' => 36186, + 'China Mobile' => 36199, + 'Cloudflare' => 38641, + 'Counter-strike' => 38725, + 'CSL' => 36200, + 'Discord' => 38608, + 'EA' => 36700, + 'eBay' => 36207, + 'Facebook' => 36185, + 'Fifa' => 37604, + 'For Honor' => 36187, + 'Ghost Recon' => 38990, + 'Gmail' => 36188, + 'Google' => 36210, + 'Google Drive' => 38264, + 'Google Play' => 38526, + 'GTA 5' => 37279, + 'Hong Kong Broadband Network' => 36204, + 'iCloud' => 38842, + 'Instagram' => 36392, + 'Kraken' => 36984, + 'Netflix' => 36213, + 'Netvigator' => 36203, + 'Office 365' => 37172, + 'Origin' => 38396, + 'Outlook' => 36189, + 'Overwatch' => 36190, + 'PCCW' => 36205, + 'Player Unknown\'s Battlegrounds' => 36617, + 'Playstation Network' => 36191, + 'Pokémon Go' => 36192, + 'Rainbow Six' => 36193, + 'Reddit' => 37479, + 'Skype' => 36194, + 'Smartone' => 36198, + 'Snapchat' => 36208, + 'Steam' => 38650, + 'Telegram' => 36195, + 'Three Home Broadband' => 36206, + 'Tinder' => 36196, + 'TuneIn' => 36036, + 'Twitter' => 38671, + 'Uplay PC' => 36212, + 'Whatsapp' => 36197, + 'Yahoo Mail' => 36769, + 'Youtube' => 36209, + ), + 'Ireland' => array( + '3 (Three)' => 34214, + 'AIB (Allied Irish Banks)' => 34209, + 'Amazon' => 34165, + 'Apex Legends' => 38133, + 'App Store' => 38557, + 'Apple Store' => 34460, + 'Bank of Ireland' => 37754, + 'Battlefield' => 38451, + 'Binance' => 36931, + 'Bing' => 34166, + 'Boom Beach' => 35153, + 'Call of Duty' => 36709, + 'Counter-strike' => 38233, + 'Craigslist' => 34167, + 'Crunchyroll' => 38094, + 'Dead By Daylight' => 37444, + 'Deezer' => 34168, + 'Destiny' => 38615, + 'Digiweb' => 34875, + 'Discord' => 37390, + 'DoneDeal' => 34208, + 'EA' => 36413, + 'eBay' => 34169, + 'Eir' => 34210, + 'eMobile' => 34211, + 'Facebook' => 34170, + 'Facebook Messenger' => 34171, + 'Facetime' => 34584, + 'Fifa' => 36907, + 'Fortnite' => 36692, + 'Ghost Recon' => 38992, + 'Gmail' => 34172, + 'Google' => 34173, + 'Google Hangouts' => 34174, + 'Google Play' => 34175, + 'Gran Turismo' => 36949, + 'Grindr' => 36755, + 'GTA 5' => 37275, + 'Hootsuite' => 34176, + 'iCloud' => 34177, + 'Imagine' => 34425, + 'iMessage' => 34178, + 'Imgur' => 34179, + 'Instagram' => 34180, + 'iTunes' => 34181, + 'Kik' => 34182, + 'Kraken' => 36850, + 'Ladbrokes' => 37469, + 'LinkedIn' => 34183, + 'Magnet' => 34424, + 'Meteor' => 34213, + 'My Fitness Pal' => 37634, + 'Nest' => 35193, + 'Netflix' => 34184, + 'Now TV' => 38403, + 'O2' => 34206, + 'Office 365' => 35441, + 'Origin' => 36890, + 'Outlook' => 34185, + 'Overwatch' => 36484, + 'Paddy Power' => 38360, + 'Pinterest' => 34186, + 'Playstation Network' => 34187, + 'PlentyOfFish' => 37417, + 'Pokémon Go' => 35758, + 'Rabodirect' => 34979, + 'Rainbow Six' => 36414, + 'Red Dead Redemption' => 38288, + 'Reddit' => 36826, + 'Roblox' => 38756, + 'Rocket League' => 36670, + 'Ryanair' => 37253, + 'Sky' => 34212, + 'Skype' => 37018, + 'Sleepless' => 38314, + 'Snapchat' => 34188, + 'Spotify' => 34189, + 'Steam' => 34190, + 'Strava' => 38643, + 'Teamviewer' => 37631, + 'Telegram' => 37226, + 'Tinder' => 34864, + 'Tumblr' => 34191, + 'TuneIn' => 38760, + 'Twitch' => 36538, + 'Twitter' => 34192, + 'Ulster Bank' => 36070, + 'Viber' => 34193, + 'Vimeo' => 34194, + 'Virgin Media' => 34207, + 'Vodafone' => 34196, + 'Wattpad' => 34699, + 'Waze' => 34197, + 'Webex' => 34198, + 'Whatsapp' => 34199, + 'Wikipedia' => 34200, + 'Xbox Live' => 34201, + 'Yahoo' => 34202, + 'Yahoo Mail' => 34203, + 'Yahoo Messenger' => 34204, + 'Youtube' => 34205, + 'Youtube Music' => 37589, + 'Z1 Battle Royale' => 37261, + ), + 'Danmark' => array( + '3 (Tre)' => 33694, + 'Amazon' => 33651, + 'Anthem' => 38212, + 'Apex Legends' => 38128, + 'App Store' => 38554, + 'Apple Store' => 34452, + 'Battlefield' => 37097, + 'Bet365' => 38813, + 'Bibob' => 33841, + 'Binance' => 36930, + 'Blizzard Battle.net' => 36162, + 'Call of Duty' => 36707, + 'CBB Mobil' => 33840, + 'Counter-strike' => 36171, + 'Crunchyroll' => 36721, + 'Danske Bank' => 33687, + 'DBA' => 33700, + 'Dead By Daylight' => 37427, + 'Deezer' => 33835, + 'Destiny' => 36593, + 'Discord' => 36682, + 'DMI' => 36121, + 'Dota 2' => 36398, + 'EA' => 35496, + 'eBay' => 34862, + 'EnergiMidt' => 34435, + 'Epic Games Store' => 39025, + 'Ewii' => 34436, + 'Facebook' => 33652, + 'Facebook Messenger' => 33653, + 'Faceit' => 38542, + 'Facetime' => 34590, + 'Fifa' => 36621, + 'For Honor' => 36163, + 'Fortnite' => 36690, + 'Fullrate' => 34433, + 'Ghost Recon' => 39002, + 'Gmail' => 33654, + 'Google' => 33655, + 'Google Drive' => 36606, + 'Google Hangouts' => 33656, + 'Google Play' => 33657, + 'Gran Turismo' => 36915, + 'GTA 5' => 36170, + 'Guild Wars 2' => 36469, + 'HBO Nordic' => 35605, + 'iCloud' => 33658, + 'iMessage' => 33659, + 'Instagram' => 33660, + 'iTunes' => 33661, + 'Kik' => 33705, + 'Kraken' => 36851, + 'Kviknet' => 38016, + 'League of Legends' => 36166, + 'LinkedIn' => 33701, + 'Minecraft' => 37282, + 'Net 1' => 33725, + 'Netflix' => 33689, + 'Nordea' => 33688, + 'Office 365' => 35440, + 'OneDrive' => 36867, + 'Origin' => 36894, + 'Outlook' => 33662, + 'Overwatch' => 36165, + 'Path of Exile' => 36482, + 'Playerunknown\'s Battlegrounds' => 36334, + 'Playstation Network' => 33663, + 'Pokémon Go' => 35753, + 'Rainbow Six' => 36161, + 'Realm Royale' => 37306, + 'Red Dead Redemption' => 38306, + 'Reddit' => 36824, + 'Roblox' => 36612, + 'Rocket League' => 35977, + 'Sea of Thieves' => 37122, + 'Skype' => 33686, + 'Snapchat' => 33696, + 'Spotify' => 33697, + 'Steam' => 34277, + 'Stofa' => 34434, + 'TDC' => 33691, + 'Teamviewer' => 35693, + 'Telegram' => 37181, + 'Telenor' => 33692, + 'Telia' => 33693, + 'Tinder' => 34861, + 'Twitch' => 35508, + 'Twitter' => 33664, + 'Uplay PC' => 35610, + 'Viaplay' => 37292, + 'Viber' => 33698, + 'Warframe' => 36703, + 'Whatsapp' => 33703, + 'Wikipedia' => 33665, + 'Wordfeud' => 33702, + 'World of Warcraft' => 36168, + 'World of Warships' => 38013, + 'Xbox Live' => 33666, + 'Yahoo Mail' => 37043, + 'Youmusic' => 33699, + 'Yousee' => 33695, + 'Youtube' => 33667, + 'Z1 Battle Royale' => 37265, + ), + 'Sverige' => array( + '3 (Tre)' => 33722, + 'A3' => 38249, + 'Amazon' => 33668, + 'Anthem' => 38190, + 'Apex Legends' => 38115, + 'App Store' => 38101, + 'Apple Store' => 34450, + 'Bahnhof' => 34417, + 'BankID' => 33845, + 'Battlefield' => 37093, + 'Bet365' => 38812, + 'Binance' => 36933, + 'Blizzard Battle.net' => 34484, + 'Blocket' => 33716, + 'Boxer' => 37572, + 'Bredband2' => 34418, + 'Bredbandsbolaget' => 33843, + 'Bredbandsson' => 36049, + 'Call of Duty' => 35539, + 'Com Hem' => 34416, + 'Counter-strike' => 36172, + 'Crunchyroll' => 36719, + 'Dead By Daylight' => 36598, + 'Destiny' => 35510, + 'Discord' => 36764, + 'Dota 2' => 35876, + 'EA' => 35509, + 'eBay' => 34863, + 'Epic Games Store' => 39023, + 'Escape from Tarkov' => 38107, + 'Eurosport Player' => 36613, + 'Facebook' => 33669, + 'Facebook Messenger' => 33670, + 'Faceit' => 39039, + 'Facetime' => 34589, + 'Fifa' => 36909, + 'For Honor' => 35998, + 'Fortnite' => 36685, + 'Ghost Recon' => 39003, + 'Glocalnet' => 33842, + 'Gmail' => 33671, + 'Google' => 33672, + 'Google Drive' => 36604, + 'Google Hangouts' => 33673, + 'Google Play' => 33674, + 'Gran Turismo' => 36914, + 'Grindr' => 38739, + 'GTA 5' => 35528, + 'Guild Wars 2' => 36466, + 'Halebop' => 33726, + 'Handelsbanken' => 33718, + 'Hay Day' => 38214, + 'HBO Nordic' => 35606, + 'Hearthstone' => 38659, + 'Hue' => 37816, + 'iCloud' => 33675, + 'Ikano Bank' => 36578, + 'iMessage' => 33676, + 'Instagram' => 33677, + 'iTunes' => 33678, + 'Kik' => 33704, + 'Kraken' => 36829, + 'League of Legends' => 36167, + 'LinkedIn' => 33714, + 'Minecraft' => 37281, + 'Net 1' => 33724, + 'Netflix' => 33706, + 'Nordea' => 33717, + 'Office 365' => 35439, + 'OneDrive' => 36869, + 'Origin' => 36898, + 'Outlook' => 33679, + 'Overwatch' => 36150, + 'Path of Exile' => 36480, + 'Playerunknown\'s Battlegrounds' => 36333, + 'Playstation Network' => 33707, + 'Pokémon Go' => 35749, + 'Rainbow Six' => 36148, + 'Realm Royale' => 37307, + 'Red Dead Redemption' => 38285, + 'Reddit' => 35883, + 'Riksnet' => 37486, + 'Roblox' => 36611, + 'Rocket League' => 35487, + 'Sea of Thieves' => 37114, + 'SEB' => 33719, + 'SF Anytime' => 37573, + 'SkandiaBanken' => 34021, + 'Skype' => 33708, + 'Slack' => 37495, + 'Snapchat' => 33709, + 'Soundcloud' => 37228, + 'Sparbanken Öresund' => 34310, + 'Spotify' => 33838, + 'Steam' => 34135, + 'Swedbank' => 33713, + 'Swish' => 35777, + 'Teamviewer' => 35692, + 'Tele2' => 33721, + 'Telegram' => 35969, + 'Telenor' => 33723, + 'Telia' => 33720, + 'Telldus' => 36653, + 'The Elder Scrolls Online' => 36033, + 'Tinder' => 36458, + 'Tradera' => 33715, + 'Tumblr' => 36713, + 'Tweakbox' => 38905, + 'Twitch' => 35507, + 'Twitter' => 33710, + 'Uplay PC' => 35609, + 'Viaplay' => 37291, + 'Viber' => 37502, + 'Warframe' => 36701, + 'Whatsapp' => 33711, + 'Wikipedia' => 33682, + 'Wordfeud' => 33712, + 'World of Tanks' => 35488, + 'World of Warcraft' => 36169, + 'World of Warships' => 38011, + 'Xbox Live' => 33683, + 'Yahoo Mail' => 37044, + 'Youtube' => 33684, + 'Z1 Battle Royale' => 35493, + ), + 'Italia' => array( + '3 Italia' => 33154, + 'Airbnb' => 37345, + 'Aircomm' => 36015, + 'Alexa' => 38031, + 'AlternatYva' => 37556, + 'Amazon' => 33174, + 'Amazon Prime Video' => 36975, + 'Anthem' => 38189, + 'Anydesk' => 37491, + 'Apex Legends' => 38119, + 'App Store' => 37733, + 'Apple Store' => 34468, + 'Aruba' => 37217, + 'Banco di Napoli' => 33181, + 'Battlefield' => 37095, + 'Betclic' => 39047, + 'Binance' => 36937, + 'Blizzard Battle.net' => 37092, + 'BNL' => 33180, + 'Brawl Stars' => 38821, + 'Bwin' => 35072, + 'Call of Duty' => 35537, + 'Clash of Clans' => 34361, + 'Clash Royale' => 38956, + 'Cloudflare' => 38620, + 'Coinbase' => 36801, + 'Coopvoce' => 34921, + 'Counter-strike' => 36650, + 'Credito Emiliano - Credem' => 33182, + 'Crunchyroll' => 38095, + 'Dazn' => 37442, + 'Dead By Daylight' => 36627, + 'Deezer' => 33839, + 'Destiny' => 36590, + 'Discord' => 36767, + 'Dota 2' => 35878, + 'Dropbox' => 33175, + 'EA' => 34498, + 'EA Sports UFC' => 36553, + 'eBay' => 32816, + 'Elite: Dangerous' => 37774, + 'Eolo' => 37763, + 'Epic Games Store' => 39024, + 'Escape from Tarkov' => 39056, + 'Facebook' => 32812, + 'Facebook Messenger' => 32813, + 'Facetime' => 34598, + 'Fallout' => 37668, + 'Fastweb' => 33160, + 'Fifa' => 36910, + 'Fineco' => 33167, + 'Flickr' => 37949, + 'For Honor' => 35990, + 'Fortnite' => 36686, + 'Friday the 13th The Game' => 37564, + 'Ghost Recon' => 38999, + 'Gmail' => 32815, + 'Google' => 32805, + 'Google Drive' => 36607, + 'Google Hangouts' => 32806, + 'Google Play' => 32807, + 'Gran Turismo' => 36874, + 'Grindr' => 36757, + 'GTA5 ' => 35918, + 'Guild Wars 2' => 36472, + 'Ho' => 37408, + 'Hue' => 37813, + 'iCloud' => 33169, + 'Iliad' => 37214, + 'iMessage' => 33164, + 'Infinity' => 35972, + 'Infostrada' => 34711, + 'ING Direct' => 33168, + 'Instagram' => 33170, + 'Intesa Sanpaolo' => 33179, + 'iTunes' => 33159, + 'Kena Mobile' => 36364, + 'Kraken' => 36771, + 'League of Legends' => 37759, + 'Libero' => 33178, + 'LinkedIn' => 33177, + 'Linkem' => 34475, + 'Lottomatica' => 35068, + 'Lycamobile' => 38691, + 'Mc-link' => 34476, + 'Mediaset Premium' => 33162, + 'Mediaset TV Free' => 35973, + 'My Fitness Pal' => 37638, + 'Netflix' => 35568, + 'Neverwinter' => 36675, + 'NGI' => 34474, + 'Office 365' => 35443, + 'Onedrive' => 35411, + 'Origin' => 36897, + 'Outlook' => 33163, + 'Overwatch' => 36384, + 'Paddy Power' => 35069, + 'Paladins' => 36379, + 'Path of Exile' => 37726, + 'Paypal' => 33166, + 'Player Unknown\'s Battlegrounds' => 36567, + 'Playstation Network' => 32818, + 'Pokémon Go' => 35737, + 'Poste Italiane' => 37361, + 'PosteMobile' => 37360, + 'Quizduello' => 34299, + 'Rainbow Six' => 36045, + 'Red Dead Redemption' => 38284, + 'Reddit' => 37367, + 'Roblox' => 38916, + 'Rocket League' => 35979, + 'Ruzzle' => 33967, + 'Ryanair' => 37519, + 'Sea of Thieves' => 37121, + 'Sisal' => 35066, + 'Sky' => 33155, + 'Skype' => 32821, + 'Slack' => 37341, + 'SNAI' => 35067, + 'Snapchat' => 36446, + 'Spotify' => 33176, + 'Steam' => 34136, + 'Tango' => 33743, + 'Teamviewer' => 35455, + 'Telegram' => 34904, + 'The elder scrolls online' => 37327, + 'TIM' => 33158, + 'TimMusic' => 35913, + 'TimVision' => 35908, + 'Tinder' => 36549, + 'Tiscali' => 33161, + 'Tumblr' => 36712, + 'Tweakbox' => 38344, + 'Twitch' => 35604, + 'Twitter' => 32814, + 'UniCredit' => 33171, + 'Uplay PC' => 35613, + 'Viber' => 33403, + 'Virgilio' => 36459, + 'Vodafone' => 33156, + 'Warface' => 38765, + 'Warframe' => 36492, + 'WeBank' => 33173, + 'WeTransfer' => 36445, + 'Whatsapp' => 32820, + 'Wifi Trenitalia' => 35974, + 'Wikipedia' => 33172, + 'William Hill' => 35070, + 'Wind' => 33157, + 'World of Warships' => 38012, + 'Xbox Live' => 32819, + 'Yahoo' => 32809, + 'Yahoo Mail' => 32810, + 'Yahoo Messenger' => 32811, + 'Youtube' => 32808, + 'Z1 Battle Royale' => 37254, + 'ZoHo' => 37554, + ), + 'South Africa' => array( + 'ABSA' => 33599, + 'Afrihost' => 35660, + 'Amazon' => 33578, + 'Apex Legends' => 38157, + 'Apple Store' => 34458, + 'Axxess' => 35618, + 'Bidorbuy' => 33596, + 'Binance' => 36941, + 'Bing' => 33579, + 'Blizzard Battle.net' => 37661, + 'Call of Duty' => 34152, + 'Capitec' => 36966, + 'Cell C' => 33602, + 'Clash Royale' => 38946, + 'Cool Ideas' => 38326, + 'Counter-strike' => 36512, + 'Cybersmart' => 34619, + 'Dead By Daylight' => 37435, + 'Deezer' => 33834, + 'Destiny' => 38981, + 'DirecTV Now' => 35915, + 'Discord' => 38611, + 'Discovery' => 38334, + 'Dota 2' => 36428, + 'DSTv' => 33607, + 'EA' => 34507, + 'eBay' => 33581, + 'Eskom' => 38333, + 'Facebook' => 33582, + 'Facebook Messenger' => 37737, + 'Fifa' => 38436, + 'First National Bank (FNB)' => 33597, + 'Fortnite' => 36710, + 'Ghost Recon' => 38994, + 'Gmail' => 33583, + 'Google' => 33584, + 'Google Hangouts' => 33585, + 'Google Play' => 33586, + 'Gov.za' => 38328, + 'GTA 5' => 37273, + 'Guild Wars 2' => 36501, + 'Gumtree' => 33592, + 'iBurst' => 34620, + 'iCloud' => 33587, + 'Imaginet' => 38325, + 'Instagram' => 33588, + 'Internet Solutions' => 38332, + 'iTunes' => 33589, + 'Kik' => 33590, + 'Kraken' => 36985, + 'League of Legends' => 38633, + 'LinkedIn' => 33591, + 'Luno' => 37012, + 'Metrofibre' => 38335, + 'MTN' => 33601, + 'MWEB' => 33606, + 'Nedbank' => 35461, + 'Neotel' => 35426, + 'Netflix' => 36390, + 'Octotel' => 38327, + 'Office 365' => 35444, + 'OLX' => 33255, + 'Openserve' => 38331, + 'Origin' => 36886, + 'Outlook' => 33566, + 'Overwatch' => 36149, + 'Paypal' => 33567, + 'Pinterest' => 33568, + 'Player Unknown\'s Battlegrounds' => 36642, + 'Playstation Network' => 33569, + 'Pokémon Go' => 35756, + 'Rain' => 37397, + 'Rainbow Six' => 37738, + 'Reddit' => 38362, + 'Safricom' => 35139, + 'SARS' => 38336, + 'Seacom' => 38329, + 'Showmax' => 37149, + 'Skype' => 33570, + 'Snapchat' => 33571, + 'Standard Bank' => 33598, + 'Steam' => 34223, + 'Takealot' => 36777, + 'Teamviewer' => 37480, + 'Telegram' => 37184, + 'Telkom' => 33603, + 'The elder scrolls online' => 37325, + 'The Simpsons Tapped out' => 35903, + 'Tinder' => 37166, + 'Twitch' => 38702, + 'Twitter' => 33572, + 'Viber' => 33595, + 'Virgin Mobile' => 38324, + 'Vodacom' => 33600, + 'Vox' => 35368, + 'Vumatel' => 37660, + 'Warframe' => 37125, + 'Webafrica' => 38330, + 'WeChat' => 33594, + 'Whatsapp' => 33573, + 'Wikipedia' => 33574, + 'Xbox Live' => 34285, + 'Yahoo' => 33575, + 'Yahoo Mail' => 33576, + 'Youtube' => 33577, + ), + 'India' => array( + 'ACT' => 34764, + 'Aircel' => 33257, + 'Airtel' => 33233, + 'Alexa' => 36921, + 'AliExpress' => 37824, + 'Amazon' => 35118, + 'Amazon Prime Music' => 37583, + 'Amazon Prime Video' => 36974, + 'Amazon Web Services' => 38411, + 'Apex Legends' => 38180, + 'App Store' => 38556, + 'Apple Store' => 34459, + 'Bank of Baroda' => 37676, + 'Bank of India' => 33250, + 'Bharat Sanchar Nigam Limited (BSNL)' => 33232, + 'Binance' => 36932, + 'Bitbucket' => 36988, + 'Boom Beach' => 34782, + 'Brawl Stars' => 38822, + 'Call of Duty' => 34153, + 'Cherrinet' => 37677, + 'Clash of Clans' => 36872, + 'Clash Royale' => 38955, + 'Cloudflare' => 38652, + 'Counter-strike' => 38240, + 'Destiny' => 38989, + 'Discord' => 38106, + 'Dota 2' => 36393, + 'Dropbox' => 33237, + 'EA' => 34508, + 'eBay' => 32863, + 'Facebook' => 32860, + 'Facebook Messenger' => 33238, + 'Facetime' => 34596, + 'Fifa' => 38399, + 'Flipkart' => 33252, + 'Fortnite' => 36843, + 'GitHub' => 37606, + 'Gmail' => 32862, + 'Go Daddy' => 37499, + 'Google' => 32857, + 'Google Drive' => 38261, + 'Google Hangouts' => 32869, + 'Google Home' => 37344, + 'Google Play' => 33239, + 'GTA 5' => 37272, + 'Haptik' => 34762, + 'Hathway' => 37673, + 'Hay Day' => 34787, + 'HDFC Bank' => 33231, + 'ICICI Bank' => 37674, + 'iCloud' => 33240, + 'IDBI Bank' => 37678, + 'Idea Cellular' => 33234, + 'Idian Bank' => 37670, + 'iMessage' => 33248, + 'Instagram' => 33241, + 'IRCTC' => 37671, + 'iTunes' => 33230, + 'Jio' => 37460, + 'Kik' => 33351, + 'Line' => 33400, + 'LinkedIn' => 33242, + 'Mahanagar Telephone Nigam Limited (MTNL)' => 33258, + 'Microsoft Azure' => 36112, + 'MTS' => 33260, + 'Naukri' => 33253, + 'Netflix' => 37299, + 'Office 365' => 37174, + 'OLX' => 33593, + 'ooVoo' => 34084, + 'Origin' => 38378, + 'Outlook' => 33243, + 'Paypal' => 33244, + 'Paytm' => 33401, + 'Pinterest' => 37231, + 'Player Unknown\'s Battlegrounds' => 36643, + 'Playstation Network' => 32865, + 'Pokémon Go' => 35755, + 'Quora' => 37473, + 'Rainbow Six' => 36412, + 'Reddit' => 38061, + 'Reliance' => 33236, + 'Roblox' => 38921, + 'Salesforce' => 34734, + 'Sarahah' => 36513, + 'Skype' => 32868, + 'Snapchat' => 36741, + 'Spotify' => 37079, + 'Standard Chartered' => 33235, + 'State Bank of India (SBI)' => 33251, + 'Steam' => 35930, + 'Swiggy' => 37675, + 'Tango' => 33744, + 'Tata Docomo' => 33256, + 'Teamviewer' => 34346, + 'Telegram' => 36582, + 'Tiktok' => 38661, + 'Tinder' => 37167, + 'Tumblr' => 37641, + 'Twitch' => 38355, + 'Twitter' => 32861, + 'Uber Eats' => 38898, + 'Udemy' => 38389, + 'Uninor' => 33259, + 'Uplay PC' => 36628, + 'Viber' => 33049, + 'Videocon' => 33261, + 'Vimeo' => 37232, + 'Vodafone' => 33254, + 'Warframe' => 37124, + 'Wattpad' => 34698, + 'Waze' => 33408, + 'Webex' => 34163, + 'WeChat' => 33396, + 'Whatsapp' => 32867, + 'Wikipedia' => 33245, + 'Xbox Live' => 32866, + 'Yahoo' => 32859, + 'Yahoo Mail' => 33246, + 'Yahoo Messenger' => 33247, + 'You Broadband' => 34763, + 'Youtube' => 32858, + 'Zee5' => 38647, + 'Zerodha' => 37207, + 'ZoHo' => 37552, + 'Zomato' => 37672, + ), + 'Portugal' => array( + 'ActivoBank' => 33959, + 'Apex Legends' => 38127, + 'Apple Store' => 34464, + 'Banco Espírito Santo' => 33961, + 'Banco Santander Totta' => 33960, + 'Binance' => 36945, + 'Bing' => 33925, + 'Blizzard Battle.net' => 37225, + 'Cabovisão' => 33956, + 'Caixa Geral de Depósitos' => 33957, + 'Call of Duty' => 38982, + 'Clash Royale' => 38957, + 'Counter-Strike' => 36651, + 'Dead By Daylight' => 37429, + 'Deezer' => 33927, + 'Destiny' => 38964, + 'Discord' => 37388, + 'Dota 2' => 38479, + 'EA' => 36452, + 'eBay' => 33928, + 'Facebook' => 33929, + 'Facebook Messenger' => 34847, + 'Facetime' => 34586, + 'Fifa' => 36905, + 'Fortnite' => 36695, + 'Gmail' => 33930, + 'Go Daddy' => 34849, + 'Google' => 33931, + 'Google Play' => 33932, + 'GTA 5' => 36857, + 'HBO' => 38379, + 'iCloud' => 33933, + 'iMessage' => 33934, + 'Instagram' => 33963, + 'iTunes' => 33935, + 'Kik' => 33943, + 'Kraken' => 36980, + 'League of Legends' => 38635, + 'LinkedIn' => 33936, + 'MEO' => 33950, + 'Millennium Bcp' => 33958, + 'Montepio' => 33962, + 'Netflix' => 37301, + 'NOS' => 33952, + 'Office 365' => 37175, + 'OLX' => 33949, + 'OneDrive' => 36870, + 'Optimus' => 33951, + 'Origin' => 36892, + 'Outlook' => 33937, + 'Overwatch' => 37082, + 'Paypal' => 33938, + 'Pinterest' => 37373, + 'Player Unknown\'s Battlegrounds' => 36566, + 'Playstation Network' => 33939, + 'Pokémon Go' => 35754, + 'Rainbow Six' => 36454, + 'Reddit' => 38068, + 'Roblox' => 38920, + 'Rocket League' => 36668, + 'Skype' => 33940, + 'Snapchat' => 36742, + 'Spotify' => 33964, + 'Steam' => 34138, + 'Teamviewer' => 35691, + 'Telegram' => 37186, + 'Tinder' => 37921, + 'Twitch' => 36453, + 'Twitter' => 33941, + 'Uplay PC' => 38651, + 'Uzo' => 33954, + 'Viber' => 33942, + 'Vodafone' => 33953, + 'Warframe' => 37135, + 'Whatsapp' => 34855, + 'Wikipedia' => 33944, + 'Xbox Live' => 33945, + 'Yahoo' => 33946, + 'Yahoo Mail' => 33947, + 'Youtube' => 33948, + 'Z1 Battle Royale' => 37264, + ), + 'Schweiz' => array( + 'Airbnb' => 37666, + 'Anthem' => 38205, + 'Apex Legends' => 38130, + 'App Store' => 35583, + 'Apple Store' => 34462, + 'Battlefield' => 38050, + 'Bet365' => 38831, + 'Binance' => 36935, + 'Blizzard Battle.net' => 36569, + 'Call of Duty' => 34157, + 'Clash Royale' => 38958, + 'Coinbase' => 36805, + 'Counter-strike' => 36646, + 'Crunchyroll' => 38144, + 'Dazn' => 36509, + 'Dead By Daylight' => 37413, + 'Deezer' => 34038, + 'Destiny' => 36591, + 'Discord' => 37389, + 'Dota 2' => 35402, + 'Dropbox' => 34039, + 'EA' => 34503, + 'eBay' => 34040, + 'Epic Games Store' => 39026, + 'Evard' => 35642, + 'Facebook' => 34041, + 'Facebook Messenger' => 34885, + 'Facetime' => 34585, + 'Fifa' => 37089, + 'flashcable' => 37202, + 'Flickr' => 34042, + 'For Honor' => 35997, + 'Fortnite' => 36688, + 'Ghost Recon' => 36006, + 'Gmail' => 34043, + 'GMX' => 34044, + 'Google' => 34045, + 'Google Hangouts' => 34046, + 'Google Play' => 34047, + 'Green' => 36971, + 'Grindr' => 38742, + 'GTA 5' => 35502, + 'Guild Wars 2' => 36465, + 'Hay Day' => 34853, + 'iCloud' => 34048, + 'ICQ' => 34049, + 'iMessage' => 34050, + 'Instagram' => 34051, + 'iTunes' => 34052, + 'Kik' => 34054, + 'Kraken' => 36807, + 'League of Legends' => 35574, + 'LinkedIn' => 34055, + 'Lovoo' => 35401, + 'Mail.de' => 38629, + 'Netatmo' => 38467, + 'Netflix' => 34632, + 'Nitrado' => 35547, + 'Office 365' => 36581, + 'OneDrive' => 35413, + 'Origin' => 36891, + 'Outlook' => 34057, + 'Overwatch' => 36152, + 'Peoplefone' => 36602, + 'Playerunknown\'s Battlegrounds' => 36560, + 'Playstation Network' => 34087, + 'Pokémon Go' => 35744, + 'PostFinance' => 36048, + 'Protonmail' => 35643, + 'Quickline' => 36970, + 'Quizduell' => 34529, + 'Rainbow Six' => 36147, + 'Red Dead Redemption' => 37741, + 'Reddit' => 38067, + 'Ricardo' => 34079, + 'Rocket League' => 35486, + 'Salt' => 35602, + 'Search.ch' => 34080, + 'Sky Sport' => 37069, + 'Skype' => 34059, + 'Snapchat' => 34060, + 'Spotify' => 34061, + 'SRF' => 34081, + 'Steam' => 34226, + 'Sunrise' => 34078, + 'Swisscom' => 34077, + 'Tango' => 34062, + 'Teamviewer' => 34616, + 'Telegram' => 35968, + 'The elder scrolls online' => 35763, + 'Threema' => 34256, + 'Tinder' => 34244, + 'Tumblr' => 34063, + 'TuneIn' => 38775, + 'Tutti' => 36014, + 'Tweakbox' => 38904, + 'Twint' => 38183, + 'Twitch' => 35505, + 'Twitter' => 34064, + 'UBS' => 34082, + 'UPC' => 34075, + 'Uplay PC' => 34690, + 'Viber' => 34065, + 'Viewster' => 34325, + 'Vimeo' => 34066, + 'Waze' => 34067, + 'Web.de' => 34068, + 'Whatsapp' => 34069, + 'Wikipedia' => 34070, + 'World of Warships' => 38015, + 'Xbox Live' => 34088, + 'Yahoo Mail' => 34072, + 'Yahoo Messenger' => 34073, + 'Yallo' => 37298, + 'Youtube' => 34074, + 'Z1 Battle Royale' => 35492, + 'Zattoo' => 35366, + ), + 'Singapore' => array( + 'Airbnb' => 37608, + 'Apex Legends' => 38182, + 'App Store' => 37735, + 'Apple Store' => 34682, + 'Binance' => 36944, + 'Bing' => 34652, + 'Blizzard Battle.net' => 36431, + 'Brawl Stars' => 38820, + 'Call of Duty' => 39051, + 'Clash of Clans' => 34667, + 'Clash Royale' => 38952, + 'Counter-strike' => 36556, + 'DBS' => 34658, + 'Destiny' => 38986, + 'Discord' => 37100, + 'Dota 2' => 35875, + 'Dropbox' => 34671, + 'EA' => 36429, + 'Facebook' => 34645, + 'Facebook Messenger' => 34646, + 'Facetime' => 34681, + 'Fifa' => 38398, + 'Fortnite' => 37321, + 'Ghost Recon' => 38995, + 'Gmail' => 34647, + 'Google' => 34648, + 'Google Drive' => 34649, + 'Google Hangouts' => 38364, + 'Google Play' => 38519, + 'Grindr' => 36754, + 'GTA 5' => 37278, + 'HSBC' => 34672, + 'iCloud' => 34685, + 'iMessage' => 34684, + 'Instagram' => 34665, + 'iTunes' => 34683, + 'League of Legends' => 38748, + 'Line' => 34740, + 'LinkedIn' => 34656, + 'M1' => 34662, + 'Microsoft Azure' => 36113, + 'Minecraft' => 38738, + 'My Republic' => 36463, + 'Netflix' => 36391, + 'Office 365' => 37173, + 'OkCupid' => 38490, + 'Origin' => 38377, + 'Outlook' => 34673, + 'Overwatch' => 36389, + 'Path of Exile' => 37723, + 'Player Unknown\'s Battlegrounds' => 36361, + 'Playstation Network' => 34668, + 'Pokémon Go' => 35757, + 'POSB' => 34738, + 'Rainbow Six' => 36382, + 'Reddit' => 37475, + 'Roblox' => 37286, + 'SingTel' => 34661, + 'Skype' => 36418, + 'Snapchat' => 34666, + 'Spotify' => 34739, + 'Standard Chartered' => 34743, + 'Starhub' => 34663, + 'Steam' => 36430, + 'Summoners War' => 37421, + 'Taobao' => 34660, + 'Telegram' => 36381, + 'Tinder' => 37137, + 'Tumblr' => 36711, + 'Tweakbox' => 38345, + 'Twitch' => 36539, + 'Twitter' => 34659, + 'Uplay PC' => 37521, + 'Viber' => 34670, + 'Viewqwest' => 37506, + 'Warframe' => 36494, + 'WeChat' => 34674, + 'Whatsapp' => 34664, + 'Whisper' => 34624, + 'Wikipedia' => 34655, + 'Xbox Live' => 34669, + 'Yahoo' => 34654, + 'Yahoo Mail' => 34653, + 'Youtube' => 34650, + ), + 'Türkiye' => array( + 'Akbank' => 33783, + 'Anthem' => 38195, + 'Apex Legends' => 38121, + 'Apple Store' => 34451, + 'Battlefield' => 38449, + 'Binance' => 36952, + 'Blizzard Battle.net' => 37224, + 'Call of Duty' => 38940, + 'Counter-strike' => 36652, + 'D-Smart' => 34422, + 'Dead By Daylight' => 37433, + 'Denizbank' => 33788, + 'Destiny' => 38984, + 'Discord' => 36766, + 'Dota 2' => 36396, + 'EA' => 36157, + 'Facebook' => 33748, + 'Facebook Messenger' => 33749, + 'Facetime' => 34588, + 'Fifa' => 37087, + 'Finansbank' => 33786, + 'For Honor' => 37369, + 'Fortnite' => 36698, + 'Garanti' => 33780, + 'GittiGidiyor' => 33779, + 'Gmail' => 33750, + 'Google' => 33751, + 'Google Hangouts' => 33752, + 'Google Play' => 33753, + 'Gran Turismo' => 36947, + 'GTA 5' => 37276, + 'HalkBank' => 33784, + 'HSBC' => 33790, + 'iCloud' => 33754, + 'iMessage' => 33755, + 'ING Bank' => 33789, + 'Instagram' => 33732, + 'iTunes' => 33756, + 'Kik' => 33757, + 'Kraken' => 36986, + 'LinkedIn' => 33758, + 'Messageme' => 33727, + 'Netflix' => 37300, + 'Office 365' => 38058, + 'Origin' => 36901, + 'Outlook' => 33759, + 'Overwatch' => 36386, + 'Player Unknown\'s Battlegrounds' => 36456, + 'Playstation Network' => 33760, + 'Rainbow Six' => 36154, + 'Reddit' => 38064, + 'Roblox' => 38915, + 'Rocket League' => 36669, + 'Sahibinden' => 33778, + 'Skype' => 33761, + 'Snapchat' => 33762, + 'Steam' => 37322, + 'Tango' => 33730, + 'Telegram' => 37090, + 'Tinder' => 37911, + 'Ttnet' => 33776, + 'Türk Ekonomi Bankası (TEB)' => 33791, + 'Türk Telekom' => 33771, + 'Turkcell' => 33774, + 'Türkiye İş Bankası' => 33781, + 'Türksat Kablo' => 33772, + 'Twitch' => 35506, + 'Twitter' => 33613, + 'Uplay PC' => 36156, + 'VakıfBank' => 33785, + 'Viber' => 33733, + 'Vodafone' => 33775, + 'Warframe' => 37128, + 'Whatsapp' => 33764, + 'Xbox Live' => 33766, + 'Yahoo Mail' => 38885, + 'Yandex' => 33777, + 'Yapı Kredi' => 33782, + 'Youtube' => 33767, + 'Ziraat Bankası' => 33787, + ), + 'Suomi' => array( + 'Aktia' => 36308, + 'Apex Legends' => 38123, + 'Battlefield' => 37096, + 'Binance' => 36929, + 'Blizzard Battle.net' => 36299, + 'Call of Duty' => 36706, + 'Clash Royale' => 38947, + 'Counter-strike' => 36645, + 'Crunchyroll' => 38097, + 'Dankse Bank' => 36307, + 'Dead By Daylight' => 37428, + 'Destiny' => 38531, + 'Discord' => 36681, + 'DNA' => 36304, + 'Dota 2' => 36434, + 'EA' => 36295, + 'Elisa' => 36303, + 'Epic Games Store' => 39030, + 'Escape from Tarkov' => 39057, + 'Facebook' => 36237, + 'Facebook Messenger' => 36240, + 'Faceit' => 39038, + 'Fifa' => 37603, + 'Fortnite' => 36691, + 'Ghost Recon' => 38998, + 'Gmail' => 36254, + 'Google' => 36253, + 'Google Play' => 38522, + 'Gran Turismo' => 36912, + 'GTA 5' => 36844, + 'Guild Wars 2' => 36471, + 'HBO Nordic' => 36302, + 'iCloud' => 38841, + 'Instagram' => 36435, + 'Kraken' => 36852, + 'League of Legends' => 38632, + 'Minecraft' => 38616, + 'Netflix' => 36252, + 'Nordea' => 36306, + 'Office 365' => 36297, + 'OP' => 36305, + 'Origin' => 36904, + 'Outlook' => 36251, + 'Overwatch' => 36250, + 'Path of Exile' => 36530, + 'Paypal' => 37347, + 'Playerunknown\'s Battlegrounds' => 36339, + 'Playstation Network' => 36249, + 'Pokémon Go' => 36248, + 'Rainbow Six' => 36247, + 'Reddit' => 36823, + 'Roblox' => 38913, + 'Rocket League' => 36300, + 'Runescape' => 38655, + 'Sea of Thieves' => 37118, + 'Skype' => 36246, + 'Slack' => 37496, + 'Snapchat' => 36245, + 'Spotify' => 37078, + 'Steam' => 36296, + 'Telegram' => 37091, + 'Telia' => 36287, + 'Tinder' => 36298, + 'Twitch' => 36436, + 'Twitter' => 36677, + 'Uplay PC' => 36244, + 'Viaplay' => 37294, + 'Viber' => 37494, + 'Warframe' => 36704, + 'Whatsapp' => 36243, + 'Wikipedia' => 38871, + 'World of Tanks' => 36673, + 'Xbox Live' => 36301, + 'Yahoo Mail' => 38883, + 'Youtube' => 36242, + 'Z1 Battle Royale' => 37259, + ), + 'France' => array( + 'Albion Online' => 38796, + 'Alexa' => 38030, + 'Alice' => 34009, + 'Amazon' => 32693, + 'Amazon Web Services' => 38109, + 'Ameli' => 38705, + 'Amen' => 35888, + 'Anthem' => 38207, + 'Apex Legends' => 38116, + 'App Store' => 35582, + 'Apple Store' => 34469, + 'AXA Banque' => 33427, + 'Bankin\'' => 37592, + 'Banque Populaire' => 33432, + 'Battlefield' => 38057, + 'Betclic' => 39046, + 'Binance' => 36939, + 'Bing' => 32696, + 'BlaBlaCar' => 36089, + 'Blizzard Battle.net' => 36437, + 'Blogger' => 32697, + 'BNP Paribas' => 32749, + 'Boom Beach' => 35154, + 'Bouygues Télécom' => 32747, + 'Brawl Stars' => 38815, + 'Caisse d\'allocations familiales' => 38708, + 'Caisse d\'Epargne' => 33428, + 'Call of Duty' => 35037, + 'CanalSat' => 34034, + 'Candy Crush' => 35881, + 'Cdiscount' => 35038, + 'CIC' => 33431, + 'Clash Royale' => 38151, + 'Cloudflare' => 38622, + 'Coinbase' => 36830, + 'Completel' => 33982, + 'Compte Nickel' => 35983, + 'Counter-Strike' => 36649, + 'Crédit Agricole' => 32753, + 'Crédit Mutuel' => 33429, + 'Crunchyroll' => 38096, + 'Dartybox' => 32848, + 'Dead By Daylight' => 37425, + 'Deezer' => 32843, + 'Destiny' => 34951, + 'Discord' => 36763, + 'Dofus Touch' => 38711, + 'Dota 2' => 35877, + 'Dropbox' => 32700, + 'Duel Quiz' => 34530, + 'EA' => 32844, + 'eBay' => 32701, + 'Elite: Dangerous' => 37776, + 'Epic Games Store' => 39029, + 'Escape from Tarkov' => 39058, + 'Facebook' => 32702, + 'Facebook Messenger' => 32703, + 'Facetime' => 34602, + 'Fallout' => 37667, + 'Fifa' => 35475, + 'Flickr' => 32704, + 'Fnac' => 35065, + 'For Honor' => 35995, + 'Fortnite' => 36656, + 'Free' => 32744, + 'Gandi' => 38712, + 'Ghost Recon' => 38959, + 'GitHub' => 32705, + 'Gmail' => 32706, + 'Google' => 32707, + 'Google Agenda' => 38601, + 'Google Drive' => 36608, + 'Google Hangouts' => 32709, + 'Google Play' => 32708, + 'Gran Turismo' => 36911, + 'Grindr' => 36756, + 'GTA 5' => 35039, + 'Guild Wars 2' => 36468, + 'Hearthstone' => 38562, + 'Hootsuite' => 32710, + 'Hue' => 37815, + 'Hunt: showdown' => 38824, + 'iCloud' => 32712, + 'Idealo' => 34890, + 'iMessage' => 32713, + 'ING Direct' => 33426, + 'Instagram' => 32715, + 'iTunes' => 32716, + 'JeuxVidéo' => 32847, + 'K-net' => 37790, + 'Kraken' => 36753, + 'La Banque Postale' => 32754, + 'La Poste' => 37215, + 'La Poste Mobile' => 32752, + 'LCL (Crédit Lyonnais)' => 33424, + 'Le Bon Coin' => 32840, + 'League of Legends' => 35369, + 'LinkedIn' => 32717, + 'LycaMobile' => 32849, + 'M6 Mobile' => 32845, + 'Magic' => 37456, + 'Météo France' => 38706, + 'Microsoft Azure' => 36115, + 'Minecraft' => 32718, + 'Molotov.TV' => 35965, + 'myCanal' => 38710, + 'Netatmo' => 37957, + 'Netflix' => 34633, + 'Nintendo eShop' => 34103, + 'Nordnet' => 38709, + 'NRJ Mobile' => 32846, + 'Numéricable' => 32748, + 'OCS' => 38707, + 'Office 365' => 35126, + 'Onedrive' => 35409, + 'Online.net' => 36159, + 'Orange' => 32745, + 'Orange Bank' => 37511, + 'Origin' => 36896, + 'Outlook' => 32711, + 'Overwatch' => 36151, + 'OVH' => 34744, + 'Paladins' => 36378, + 'Path of Exile' => 36855, + 'Paypal' => 32842, + 'Photobucket' => 32721, + 'Pinterest' => 32722, + 'Player Unknown\'s Battlegrounds' => 36363, + 'Playstation Network' => 32741, + 'Pokémon Go' => 35735, + 'Prime Video' => 38457, + 'Rainbow Six' => 35562, + 'Rakuten TV' => 37596, + 'Realm Royale' => 37308, + 'Red Dead Redemption' => 38287, + 'Reddit' => 37368, + 'Roblox' => 38755, + 'Rocket League' => 35253, + 'Salesforce' => 38501, + 'Sea of Thieves' => 37117, + 'SFR' => 32746, + 'Shadow' => 37212, + 'Skype' => 32723, + 'Skyrock.com' => 33430, + 'Slack' => 37337, + 'Slideshare' => 32724, + 'Snapchat' => 33378, + 'Société Générale' => 33425, + 'Sosh' => 34830, + 'Spotify' => 32727, + 'Star Citizen' => 39037, + 'Steam' => 32742, + 'Streamlabs' => 38927, + 'Syma' => 37449, + 'Tango' => 34482, + 'Teamviewer' => 34908, + 'Telegram' => 34230, + 'The elder scrolls online' => 37326, + 'Tinder' => 34808, + 'Tumblr' => 32728, + 'TuneIn' => 38758, + 'Tweakbox' => 38347, + 'Twitch' => 35027, + 'Twitter' => 32729, + 'Uber Eats' => 38897, + 'Unibet' => 38274, + 'Uplay PC' => 35544, + 'Viber' => 33373, + 'Vimeo' => 32730, + 'Warface' => 38776, + 'Warframe' => 36368, + 'Waze' => 33410, + 'WeTransfer' => 32731, + 'Whatsapp' => 32575, + 'Wibox' => 37055, + 'Wikipedia' => 32732, + 'Wordpress.com' => 32734, + 'World of Tanks' => 36030, + 'Xbox Live' => 32743, + 'Yahoo' => 32735, + 'Yahoo Mail' => 32736, + 'Yahoo Messenger' => 32737, + 'Youtube' => 32739, + 'Z1 Battle Royale' => 37260, + 'Zynga' => 32740, + ), + 'Brasil' => array( + 'Albion Online' => 38797, + 'Alelo' => 37410, + 'Algar' => 37507, + 'Alog' => 34746, + 'Amazon' => 33206, + 'Amazon Prime Video' => 38754, + 'Amazon Web Services' => 34091, + 'America Net' => 35127, + 'Anthem' => 38211, + 'Anydesk' => 37492, + 'Apex Legends' => 38126, + 'App Store' => 39054, + 'Apple Store' => 34449, + 'Avianca' => 36519, + 'Azul' => 36358, + 'Banco Central do Brasil' => 36002, + 'Banco do Brasil' => 34037, + 'Banco Inter' => 37559, + 'Banco Itaú' => 33205, + 'Banco Safra' => 37151, + 'Banco Santander' => 33381, + 'Banestes' => 37065, + 'Banrisul' => 35425, + 'Battlefield' => 38447, + 'Betfair' => 38188, + 'Binance' => 36940, + 'Bing' => 33215, + 'Blizzard Battle.net' => 36671, + 'Bradesco' => 33197, + 'Brisanet' => 36967, + 'Buscapé' => 35167, + 'C6 Bank' => 38773, + 'Cabo Telecom' => 33991, + 'Cabonnet' => 35919, + 'Caixa Econômica Federal' => 33191, + 'Call of Duty' => 38163, + 'Claro' => 33199, + 'Clash of Clans' => 37303, + 'Clash Royale' => 36053, + 'Clear' => 37557, + 'Cloudflare' => 38621, + 'Clusterweb' => 35967, + 'Copel Telecom' => 35671, + 'Correios' => 34611, + 'Counter-Strike' => 38162, + 'Credit Suisse' => 37978, + 'Crunchyroll' => 37138, + 'Dataprev' => 35137, + 'Dead By Daylight' => 36596, + 'Deezer' => 33385, + 'Destiny' => 38943, + 'Discord' => 37392, + 'Dota 2' => 35879, + 'Dropbox' => 33214, + 'EA' => 34500, + 'eBay' => 33207, + 'eCAC' => 34961, + 'Embratel' => 33999, + 'Enem' => 35136, + 'Epic Games Store' => 39031, + 'eSocial' => 35427, + 'Faceapp' => 38696, + 'Facebook' => 33184, + 'Facebook Messenger' => 34846, + 'Facetime' => 34597, + 'Feedly' => 34723, + 'Fifa' => 37769, + 'For Honor' => 37371, + 'Fortnite' => 36699, + 'Free Fire' => 38804, + 'Garena' => 38301, + 'Getnet' => 36745, + 'GitHub' => 38591, + 'Globo' => 34721, + 'Gmail' => 33194, + 'Go Daddy' => 34848, + 'Gol' => 38600, + 'Google' => 33186, + 'Google Cloud' => 38535, + 'Google Play' => 33211, + 'GTA 5' => 34757, + 'GVT' => 33562, + 'HBO' => 36354, + 'Hostgator' => 34745, + 'Hostnet' => 35850, + 'HSBC' => 34644, + 'iCloud' => 33219, + 'ICQ' => 34765, + 'iFood ' => 36960, + 'iMessage' => 33165, + 'Instagram' => 33204, + 'ITMNetworks' => 36094, + 'iTunes' => 33208, + 'Jurassic World Alive' => 37335, + 'Kik' => 34856, + 'KingHost' => 34957, + 'Kraken' => 36979, + 'League of Legends' => 39018, + 'Ligue Telecom' => 37526, + 'Line' => 33736, + 'LinkedIn' => 33210, + 'Locaweb' => 34722, + 'Mandic' => 36090, + 'Mercado Bitcoin' => 36790, + 'Mercado Livre' => 34234, + 'Microsoft Azure' => 36111, + 'Multiplay' => 37221, + 'NET' => 33190, + 'Netflix' => 33222, + 'Neverwinter' => 36158, + 'Nextel' => 33202, + 'Nota fiscal eletrônica' => 34718, + 'Nubank' => 37063, + 'Office 365' => 37170, + 'Oi' => 33196, + 'OLX' => 34235, + 'OneDrive' => 36866, + 'Origin' => 36903, + 'Outlook' => 33221, + 'Overwatch' => 36502, + 'PagSeguro' => 37313, + 'Path of Exile' => 37725, + 'Paypal' => 33213, + 'Pinterest' => 38164, + 'Player Unknown\'s Battlegrounds' => 36565, + 'Playstation Network' => 33217, + 'Pokémon Go' => 35751, + 'Polícia Federal' => 35135, + 'Porto Seguro Conecta' => 35981, + 'QConcursos' => 36601, + 'Rainbow Six' => 36451, + 'Receita Federal' => 34960, + 'Red Dead Redemption' => 38909, + 'Reddit' => 37477, + 'Roblox' => 37399, + 'Rocket League' => 38558, + 'Salesforce' => 38494, + 'Sefaz' => 37626, + 'Sercomtel' => 33990, + 'Sicoob' => 37624, + 'Sicredi' => 37505, + 'SiSU' => 38029, + 'SKY' => 33875, + 'Skype' => 33203, + 'Slack' => 37339, + 'Snapchat' => 33379, + 'Spotify' => 36594, + 'Steam' => 34137, + 'Submarino' => 35074, + 'SuperDigital' => 36744, + 'Teamviewer' => 35690, + 'Telegram' => 34937, + 'Terra' => 33193, + 'TIM' => 34686, + 'Tinder' => 36550, + 'Tribunal Superior Eleitoral' => 37578, + 'Twitch' => 36372, + 'Twitter' => 33195, + 'Uber' => 37767, + 'Udemy' => 38313, + 'Umbler' => 37577, + 'UOL' => 33189, + 'UOLHost' => 34748, + 'Uplay PC' => 38636, + 'Viber' => 33386, + 'Vimeo' => 38533, + 'Vivo' => 33192, + 'Vono' => 35138, + 'Warframe' => 36702, + 'Waze' => 33383, + 'WeChat' => 33735, + 'Whatsapp' => 32837, + 'Wikipedia' => 33198, + 'World of Warcraft' => 38879, + 'Xbox Live' => 33216, + 'Yahoo' => 33187, + 'Yahoo Mail' => 33201, + 'Youtube' => 33185, + 'Zello' => 33387, + ), + 'Россия' => array( + 'Albion Online' => 38798, + 'Anthem' => 38191, + 'Apex Legends' => 38138, + 'Apple Music' => 39012, + 'Apple Store' => 34470, + 'avito' => 37696, + 'Battlefield' => 38448, + 'Binance' => 37021, + 'Blizzard Battle.net' => 36784, + 'Call of Duty' => 39052, + 'Citilink' => 37700, + 'Cloudflare' => 38619, + 'Counter-strike' => 38234, + 'Dead By Daylight' => 37430, + 'Destiny' => 38941, + 'DNS Shop' => 37701, + 'Dota 2' => 36395, + 'EA' => 36924, + 'eBay' => 32799, + 'Elite: Dangerous' => 37775, + 'Facetime' => 34599, + 'Fifa' => 38428, + 'For Honor' => 37370, + 'Fortnite' => 37143, + 'GitHub' => 38713, + 'Gmail' => 32798, + 'GTA 5' => 36845, + 'iCloud' => 38840, + 'ICQ' => 33122, + 'iMessage' => 33115, + 'Interzet' => 33563, + 'iTunes' => 33114, + 'Ivi' => 34807, + 'Kraken' => 36982, + 'Last FM' => 36533, + 'Mail.Ru' => 33113, + 'Megogo' => 38198, + 'NetByNet' => 35927, + 'Okko' => 38793, + 'Origin' => 36900, + 'Outlook' => 33116, + 'Overwatch' => 36385, + 'Ozon' => 37702, + 'Path of Exile' => 37724, + 'Paypal' => 33118, + 'Player Unknown\'s Battlegrounds' => 36455, + 'Playstation Network' => 32801, + 'Qip' => 37697, + 'Qiwi' => 33504, + 'Rainbow Six' => 36457, + 'Reddit' => 38066, + 'Roblox' => 38922, + 'SkyNet' => 35103, + 'Slack' => 37340, + 'Snapchat' => 37144, + 'Teamviewer' => 34347, + 'Telegram' => 36734, + 'The elder scrolls online' => 37323, + 'Tumblr' => 37284, + 'Twitter' => 32797, + 'Uplay PC' => 35611, + 'Viber ' => 33402, + 'Warframe' => 37126, + 'WebMoney' => 33119, + 'Wildberries' => 37699, + 'World of Tanks' => 35051, + 'Xbox Live' => 32802, + 'Yahoo' => 32792, + 'Yahoo Mail' => 32793, + 'Yahoo Messenger' => 32794, + 'Yota' => 34356, + 'Акадо' => 34429, + 'Альфа-Банк' => 33110, + 'Банк ДОМ.РФ' => 38704, + 'Билайн' => 32850, + 'Ватсап' => 32803, + 'Википедия' => 33120, + 'ВКонтакте' => 32851, + 'ВТБ 24' => 33109, + 'ГИС ЖКХ' => 35822, + 'Гугл' => 32788, + 'Гугл Hangouts' => 32789, + 'Гугл Плей' => 32790, + 'Дискорд ' => 37386, + 'Дом.ru' => 34395, + 'Инстаграм' => 36069, + 'Кинопаб' => 38199, + 'Летай' => 34430, + 'МГТС' => 33108, + 'МегаФон' => 33104, + 'Мотив' => 33864, + 'МТС' => 33121, + 'НСПК ' => 35608, + 'Одноклассники' => 33112, + 'Почта России' => 36587, + 'Рамблер' => 37441, + 'Релком' => 34357, + 'Росреестр' => 38311, + 'Ростелеком' => 33105, + 'Сбербанк' => 33106, + 'СДЭК' => 38729, + 'Скай Линк' => 34358, + 'Скайп' => 32804, + 'Стим' => 34266, + 'Твич' => 36543, + 'Теле2' => 33818, + 'Тиндер' => 37919, + 'Тинькофф Банк ' => 37698, + 'ТТК' => 34359, + 'Уфанет' => 34431, + 'Фейсбук' => 32795, + 'Фейсбук Мессенджер' => 32796, + 'ФНС' => 38648, + 'Хоум Кредит' => 38627, + 'ЭлЖур ' => 37515, + 'Ютуб' => 32791, + 'Яндекс' => 33111, + 'Яндекс.Музыка' => 39013, + 'Яндекс.Навигатор' => 39014, + ), + 'España' => array( + 'Alexa' => 38032, + 'Amazon' => 33140, + 'Amazon Prime Video' => 38753, + 'Anthem' => 38201, + 'Anydesk' => 37490, + 'Apex Legends' => 38122, + 'App Store' => 38553, + 'Apple Store' => 34465, + 'Banco Popular' => 33135, + 'Banco Sabadell' => 33151, + 'Banco Santander' => 33132, + 'Bankia' => 33134, + 'Bankinter' => 33143, + 'Battlefield' => 39017, + 'BBVA' => 33133, + 'Binance' => 36936, + 'Blizzard Battle.net' => 36105, + 'Brawl Stars' => 38832, + 'Cableworld' => 37457, + 'Call of Duty' => 35538, + 'Clash of Clans' => 34362, + 'Clash Royale' => 38951, + 'Coinbase' => 36803, + 'Counter Strike' => 38239, + 'Crunchyroll' => 38147, + 'Dazn' => 39005, + 'Dead By Daylight' => 36597, + 'Deezer' => 33830, + 'Destiny' => 38944, + 'Discord' => 37387, + 'Dota 2' => 37188, + 'Dropbox' => 33142, + 'EA' => 36449, + 'eBay' => 33267, + 'Epic Games Store' => 39032, + 'Euskaltel' => 34442, + 'Facebook' => 32829, + 'Facebook Messenger' => 32830, + 'Facetime' => 34600, + 'Fifa' => 37088, + 'For Honor' => 36182, + 'Fortnite' => 36694, + 'Ghost Recon' => 38996, + 'Github' => 36521, + 'Gmail' => 32832, + 'Google' => 32822, + 'Google Drive' => 36605, + 'Google Hangouts' => 32823, + 'Google Play' => 33226, + 'Gran Turismo' => 36950, + 'Grindr' => 38401, + 'GTA 5' => 37274, + 'Guild Wars 2' => 36485, + 'HBO' => 36497, + 'Ibercaja' => 33153, + 'iCloud' => 33227, + 'iMessage' => 32980, + 'ING Direct' => 33148, + 'Instagram' => 33147, + 'iTunes' => 33144, + 'Jazztel' => 33129, + 'Kraken' => 36806, + 'League of Legends' => 38634, + 'Line' => 33394, + 'LinkedIn' => 33150, + 'Llamaya' => 38315, + 'Lowi' => 35832, + 'MásMóvil' => 36068, + 'Mil Anuncios' => 33141, + 'Movistar' => 33124, + 'Nest' => 38551, + 'Netflix' => 35565, + 'Office 365' => 35442, + 'OneDrive' => 36864, + 'ONO' => 33128, + 'Orange' => 33126, + 'Origin' => 36895, + 'Outlook' => 33220, + 'Overwatch' => 36387, + 'Pasion.com' => 37141, + 'Path of Exile' => 37770, + 'Paypal' => 33145, + 'Pepephone' => 33130, + 'Player Unknown\'s Battlegrounds' => 36561, + 'Playstation Network' => 32835, + 'Pokémon Go' => 35750, + 'Quantis' => 37050, + 'R' => 34441, + 'Rainbow Six' => 36145, + 'Rakuten TV' => 37595, + 'Reddit' => 37476, + 'Roblox' => 38928, + 'Rocket League' => 36181, + 'Sea of Thieves' => 37119, + 'Segunda Mano' => 33138, + 'Simyo' => 33131, + 'Sky' => 37520, + 'Skype' => 32838, + 'Slack' => 37342, + 'Snapchat' => 33380, + 'Spotify' => 33146, + 'Steam' => 34280, + 'Tango' => 33746, + 'Teamviewer' => 35688, + 'Telecable' => 34443, + 'Telegram' => 35352, + 'Tinder' => 36548, + 'Tuenti' => 33137, + 'Tweakbox' => 38350, + 'Twitch' => 36545, + 'Twitter' => 32831, + 'Unicaja' => 33149, + 'Uplay PC' => 35615, + 'Vibbo' => 36183, + 'Viber' => 33374, + 'Vodafone' => 33125, + 'Warframe' => 36705, + 'Whatsapp' => 33263, + 'Wikipedia' => 33139, + 'Xbox Live' => 32836, + 'Yahoo' => 32826, + 'Yahoo Mail' => 32827, + 'Yahoo Messenger' => 32828, + 'Yoigo' => 33127, + 'Youtube' => 32825, + 'Z1 Battle Royale' => 37263, + ), + 'Polska' => array( + 'Allegro' => 33799, + 'Amazon' => 33633, + 'Apex Legends' => 38124, + 'Apple Store' => 34453, + 'Bank Millennium' => 37535, + 'Bank Pekao' => 37534, + 'Battlefield' => 37471, + 'BGŻ BNP Paribas' => 37530, + 'Binance' => 36946, + 'Blizzard Battle.net' => 36783, + 'Call of Duty' => 38923, + 'Chomikuj' => 33803, + 'Clash Royale' => 38950, + 'Cloudflare' => 38624, + 'Counter-Strike' => 36648, + 'Cyfrowy Polsat' => 33866, + 'Dead By Daylight' => 36599, + 'Deezer' => 33837, + 'Destiny' => 38945, + 'Deutsche bank Polska' => 36176, + 'Discord' => 36761, + 'Dota 2' => 36447, + 'Dropbox' => 33796, + 'EA' => 36448, + 'East&West' => 35641, + 'eBay' => 38579, + 'Elite: Dangerous' => 37778, + 'Epic Games Store' => 39022, + 'Escape from Tarkov' => 39060, + 'Eurobank' => 37008, + 'Facebook' => 33634, + 'Facebook Messenger' => 33635, + 'Facetime' => 34591, + 'Fifa' => 37086, + 'For Honor' => 37305, + 'Fortnite' => 36693, + 'Get in Bank' => 37532, + 'GG (Gadu Gadu)' => 33809, + 'Ghost Recon' => 38997, + 'GitHub' => 38466, + 'Gmail' => 33636, + 'Google' => 33637, + 'Google Hangouts' => 33638, + 'Google Play' => 33639, + 'Gran Turismo' => 36916, + 'Grindr' => 38741, + 'GTA 5' => 36848, + 'Guild Wars 2' => 36467, + 'Gumtree' => 33805, + 'HBO' => 36355, + 'Heyah' => 33812, + 'Home.pl' => 37537, + 'iCloud' => 33640, + 'Idea Bank' => 37533, + 'iMessage' => 33641, + 'Inea' => 34415, + 'ING Bank' => 33807, + 'Instagram' => 33642, + 'iTunes' => 33643, + 'Kraken' => 36983, + 'League of Legends' => 36629, + 'LinkedIn' => 33806, + 'Mbank' => 33800, + 'Multimedia Polska' => 33879, + 'Nazwa' => 37320, + 'Nest Bank' => 37529, + 'Netflix' => 35728, + 'Netia' => 33815, + 'NK.pl (Nasza-klasa)' => 33808, + 'O2' => 33801, + 'Office 365' => 35125, + 'OLX' => 33802, + 'OneDrive' => 36868, + 'Orange' => 33810, + 'Origin' => 36899, + 'Outlook' => 33644, + 'Overwatch' => 36388, + 'Path of Exile' => 37728, + 'PKO Bank Polski' => 36044, + 'Play' => 33813, + 'Player Unknown\'s Battlegrounds' => 36580, + 'Playstation Network' => 33645, + 'Plus' => 33814, + 'Pokémon Go' => 35752, + 'Rainbow Six' => 36394, + 'Reddit' => 37485, + 'Roblox' => 37400, + 'Rocket League' => 36667, + 'Santander' => 37531, + 'Sea of Thieves' => 37120, + 'Skype' => 33793, + 'Slack' => 37338, + 'Snapchat' => 33797, + 'Spotify' => 33836, + 'Steam' => 34279, + 'T-Mobile' => 33811, + 'Teamviewer' => 35694, + 'Telegram' => 35970, + 'The elder scrolls online' => 37328, + 'Tinder' => 36547, + 'Tumblr' => 37285, + 'Twitch' => 36450, + 'Twitter' => 33646, + 'UPC' => 33816, + 'Uplay PC' => 35612, + 'Vectra' => 33817, + 'Viber' => 33794, + 'Warframe' => 36493, + 'Whatsapp' => 33792, + 'Wikipedia' => 33647, + 'World of Tanks' => 36029, + 'World of Warcraft' => 37446, + 'World of Warships' => 38008, + 'Xbox Live' => 33648, + 'Yahoo Mail' => 38887, + 'Youtube' => 33649, + 'Z1 Battle Royale' => 37267, + ), + 'Norge' => array( + 'Altibox' => 36318, + 'Anthem' => 38206, + 'Apex Legends' => 38120, + 'App Store' => 38100, + 'Battlefield' => 37098, + 'Binance' => 36928, + 'Blizzard Battle.net' => 36309, + 'Call of Duty' => 36708, + 'Coinbase' => 36799, + 'Counter-strike' => 36647, + 'Crunchyroll' => 36720, + 'Danske Bank' => 36321, + 'Dead By Daylight' => 37426, + 'Destiny' => 36592, + 'Discord' => 36762, + 'Dota 2' => 36442, + 'EA' => 36310, + 'Epic Games Store' => 39028, + 'Facebook' => 36238, + 'Facebook Messenger' => 36269, + 'Fifa' => 36908, + 'Fortnite' => 36687, + 'Get' => 36319, + 'Ghost Recon' => 39000, + 'Gmail' => 36255, + 'Google' => 36256, + 'Google Drive' => 36760, + 'Google Play' => 38528, + 'GTA 5' => 36847, + 'Guild Wars 2' => 36470, + 'HBO Nordic' => 36316, + 'iCloud' => 38844, + 'Ikano Bank' => 36577, + 'Instagram' => 36443, + 'Kraken' => 36853, + 'League of Legends' => 38630, + 'Minecraft' => 37280, + 'Netflix' => 36257, + 'NexGenTel' => 36320, + 'Nordea' => 36325, + 'Norges Bank' => 36324, + 'Office 365' => 36311, + 'OneDrive' => 36865, + 'Origin' => 36893, + 'Outlook' => 36258, + 'Overwatch' => 36259, + 'Path of Exile' => 37727, + 'Playerunknown\'s Battlegrounds' => 36338, + 'Playstation Network' => 36260, + 'Pokémon Go' => 36261, + 'Rainbow Six' => 36262, + 'Realm Royale' => 37316, + 'Red Dead Redemption' => 38283, + 'Reddit' => 36825, + 'Roblox' => 38919, + 'Rocket League' => 36312, + 'Sea of Thieves' => 37116, + 'Skype' => 36263, + 'Snapchat' => 36264, + 'Sparebanken 1' => 36323, + 'Spotify' => 36796, + 'Steam' => 36313, + 'Strava' => 38730, + 'Tele2' => 36317, + 'Telegram' => 37183, + 'Telenor' => 36288, + 'The elder scrolls online' => 37324, + 'Tinder' => 36314, + 'Twitch' => 36444, + 'Twitter' => 37268, + 'Uplay PC' => 36265, + 'Viaplay' => 37293, + 'Warframe' => 37129, + 'Whatsapp' => 36266, + 'Wikipedia' => 38872, + 'World of Warcraft' => 37447, + 'Xbox Live' => 36315, + 'Yahoo' => 36268, + 'Yahoo Mail' => 38890, + 'Youtube' => 36267, + 'Z1 Battle Royale' => 37262, + ), + 'México' => array( + 'Amazon' => 33292, + 'Amazon Prime Video' => 38752, + 'Anthem' => 38192, + 'Apex Legends' => 38139, + 'App Store' => 37732, + 'Apple Store' => 34466, + 'AT&T' => 35861, + 'Axtel' => 34439, + 'Banamex' => 33322, + 'Banco Santander' => 36772, + 'Banorte' => 33326, + 'Battlefield' => 38445, + 'BBVA Bancomer' => 33321, + 'Binance' => 37019, + 'Blizzard Battle.net' => 36106, + 'Cablecom' => 34035, + 'Cablemás' => 34438, + 'Call of Duty' => 35540, + 'Clash of Clans' => 37099, + 'Crunchyroll' => 37139, + 'Dead By Daylight' => 37432, + 'Deezer' => 33397, + 'Destiny' => 38966, + 'Discord' => 38606, + 'Dropbox' => 33294, + 'EA' => 36609, + 'eBay' => 33295, + 'Facebook' => 33296, + 'Facebook Messenger' => 33297, + 'Facetime' => 34594, + 'Fifa' => 38307, + 'Fortnite' => 36842, + 'Gears of War' => 38893, + 'Gmail' => 33298, + 'Go Daddy' => 36620, + 'Google' => 33299, + 'Google Drive' => 38260, + 'Google Hangouts' => 33300, + 'Google Play' => 33301, + 'Gran Turismo' => 36951, + 'Grindr' => 38740, + 'GTA 5' => 38275, + 'HBO' => 36351, + 'iCloud' => 33302, + 'iMessage' => 33303, + 'Instagram' => 33304, + 'iTunes' => 33305, + 'Izzi' => 37685, + 'League of Legends' => 39019, + 'Line' => 33395, + 'LinkedIn' => 33306, + 'Megacable' => 34437, + 'Mercado Libre' => 33320, + 'Microsoft Azure' => 37466, + 'Minecraft' => 38737, + 'Movistar' => 33307, + 'Netflix' => 33325, + 'Origin' => 38103, + 'Outlook' => 33308, + 'Overwatch' => 36483, + 'Paypal' => 33309, + 'Player Unknown\'s Battlegrounds' => 36562, + 'Playstation Network' => 33310, + 'Pokémon Go' => 36360, + 'Rainbow Six' => 36146, + 'Reddit' => 37482, + 'Roblox' => 38917, + 'Salesforce' => 38496, + 'Scotiabank' => 37686, + 'Segunda Mano' => 33327, + 'SKY México' => 33328, + 'Skype' => 33311, + 'Snapchat' => 34288, + 'SPEI' => 37689, + 'Spotify' => 33398, + 'Steam' => 34281, + 'Tango' => 33745, + 'Teamviewer' => 35689, + 'Telcel' => 33319, + 'Telegram' => 35852, + 'Telmex' => 33324, + 'Telnor' => 34440, + 'Tinder' => 37165, + 'Totalplay' => 37687, + 'Tweakbox' => 38903, + 'Twitch' => 36546, + 'Twitter' => 33312, + 'Unefon' => 37688, + 'WeChat' => 33399, + 'Whatsapp' => 33313, + 'Wikipedia' => 33314, + 'World of Warcraft' => 38880, + 'Xbox Live' => 33315, + 'Yahoo' => 33316, + 'Yahoo Mail' => 33317, + 'Yahoo Messenger' => 33318, + 'Youtube' => 33361, + 'Zello' => 33388, + ), + '日本' => array( + 'Amazon' => 33464, + 'Amazon Web Services' => 34094, + 'Amazon インスタント・ビデオ' => 34496, + 'Apex Legends' => 38177, + 'App Store' => 35581, + 'Apple Store' => 34467, + 'ASAHI ネット' => 33859, + 'Au' => 33465, + 'Biglobe' => 33479, + 'Blizzard Battle.net' => 36672, + 'bmobile' => 33466, + 'Call of Duty' => 39053, + 'Dazn' => 36511, + 'Discord' => 38613, + 'Disney Mobile' => 33467, + 'DMM' => 35818, + 'Dropbox' => 33441, + 'EA' => 37080, + 'eBay' => 33442, + 'EO Net' => 33863, + 'Facebook' => 33443, + 'Facebook Messenger' => 33444, + 'Facetime' => 34593, + 'FC2' => 34510, + 'Fortnite' => 37162, + 'Freetel' => 35620, + 'Ghost Recon' => 38993, + 'Github' => 37375, + 'Gmail' => 33445, + 'Google' => 33446, + 'Google Hangouts' => 33447, + 'Google Play' => 33448, + 'Hulu' => 33480, + 'iCloud' => 33449, + 'IIJ' => 34511, + 'iMessage' => 33450, + 'Instagram' => 33451, + 'iTunes' => 33452, + 'Jcom' => 33478, + 'League of Legends' => 38899, + 'Line' => 33453, + 'LinkedIn' => 33454, + 'Livedoor' => 33472, + 'Microsoft Azure' => 36110, + 'Netflix' => 36728, + 'Nifty' => 33473, + 'Nintendo Network' => 35521, + 'NTT Docomo' => 33474, + 'NTT東日本' => 33475, + 'NTT西日本' => 33476, + 'OCN' => 33564, + 'Office 365' => 35928, + 'OneDrive' => 36496, + 'Outlook' => 33455, + 'Paypal' => 33456, + 'Player Unknown\'s Battlegrounds' => 36616, + 'Playstation Network' => 33457, + 'Rakuten' => 34513, + 'Reddit' => 37478, + 'Skype' => 33458, + 'Slack' => 37358, + 'Snapchat' => 38511, + 'So-net' => 33862, + 'Steam' => 38649, + 'Telegram' => 37227, + 'Tinder' => 37916, + 'Twitter' => 33459, + 'UQ Wimax' => 33482, + 'Whatsapp' => 38255, + 'Xbox Live' => 33471, + 'Yahoo' => 33460, + 'Yahoo BB' => 33860, + 'Yahoo Mail' => 33461, + 'Yammer' => 35560, + 'Youtube' => 33462, + 'じぶん銀行' => 33500, + 'ぷらら' => 33861, + 'みずほ銀行' => 33493, + 'りそな銀行' => 33496, + 'アメーバブログ' => 34514, + 'カカオトーク' => 33469, + 'ジャパンネット銀行' => 33497, + 'スカパー' => 33477, + 'スポナビLive' => 36516, + 'ソニー銀行' => 33498, + 'ソフトバンク' => 33470, + 'ポケモン go' => 35746, + 'ワイモバイル' => 36503, + '三井住友銀行' => 33495, + '三菱東京UFJ銀行' => 33494, + '埼玉りそな銀行' => 36532, + '楽天銀行' => 33499, + '近畿大阪銀行' => 36531, + ), + 'Pilipinas' => array( + 'Anthem' => 38193, + 'Apex Legends' => 38158, + 'Battlefield' => 37925, + 'Blizzard Battle.net' => 37834, + 'Converge' => 38714, + 'Counter-strike' => 38241, + 'Destiny' => 38987, + 'Discord' => 38453, + 'Dota 2' => 37835, + 'EA' => 37931, + 'Facebook' => 37836, + 'Facebook Messenger' => 37837, + 'Fortnite' => 37930, + 'Globe' => 37941, + 'Gmail' => 38080, + 'Google' => 37838, + 'Google Drive' => 38259, + 'Google Hangouts' => 38365, + 'Google Play' => 38521, + 'GTA 5' => 38718, + 'iCloud' => 38853, + 'Instagram' => 37839, + 'League of Legends' => 38911, + 'Mobile legends' => 38175, + 'Netflix' => 37840, + 'Office 365' => 37936, + 'Origin' => 38397, + 'Outlook' => 37935, + 'Overwatch' => 37932, + 'Paypal' => 38463, + 'Player Unknown\'s Battlegrounds' => 37841, + 'Playstation Network' => 38736, + 'PLDT' => 37938, + 'Rainbow Six' => 37933, + 'Reddit' => 38089, + 'Roblox' => 37927, + 'Sky Cable' => 37942, + 'Skype' => 37929, + 'Smart' => 37940, + 'Spotify' => 38644, + 'Steam' => 37924, + 'Telegram' => 38019, + 'Tinder' => 37912, + 'TNT' => 37943, + 'Twitter' => 37928, + 'Warframe' => 37934, + 'Wattpad' => 38962, + 'Waze' => 38267, + 'Whatsapp' => 38254, + 'Yahoo' => 37926, + 'Yahoo Mail' => 38456, + 'Youtube' => 37842, + ), + 'Indonesia' => array( + 'Anthem' => 38194, + 'Apex Legends' => 38181, + 'Biznet' => 38975, + 'Blizzard Battle.net' => 37825, + 'Clash of Clans' => 37961, + 'Counter-strike' => 38410, + 'Discord' => 38084, + 'Dota 2' => 37826, + 'EA' => 38419, + 'Facebook' => 37827, + 'Fifa' => 38427, + 'First Media' => 38974, + 'Fortnite' => 38532, + 'Gmail' => 38250, + 'Google' => 37828, + 'Google Drive' => 38262, + 'Google Play' => 38518, + 'GTA 5' => 38700, + 'iCloud' => 38851, + 'Indosat Ooredoo' => 38978, + 'Instagram' => 37829, + 'Myrepublic' => 38972, + 'Netflix' => 37830, + 'Origin' => 38376, + 'Pinterest' => 37950, + 'Player Unknown\'s Battlegrounds' => 37831, + 'Playstation Network' => 38735, + 'Reddit' => 38498, + 'Roblox' => 38914, + 'Steam' => 38098, + 'Telegram' => 38020, + 'Telkom' => 38973, + 'Telkomsel' => 38976, + 'Tinder' => 37915, + 'Twitter' => 38160, + 'Uplay PC' => 37962, + 'Whatsapp' => 37832, + 'XL' => 38977, + 'Yahoo Mail' => 38889, + 'Youtube' => 37833, + ), + 'Pakistan' => array( + 'Apex Legends' => 38414, + 'App Store' => 38960, + 'Battlefield' => 38002, + 'Blizzard Battle.net' => 37992, + 'Call of Duty' => 38939, + 'Discord' => 37993, + 'Dota 2' => 38476, + 'EA' => 38418, + 'Facebook' => 37994, + 'Facebook Messenger' => 37995, + 'Fiberlink' => 38006, + 'Fifa' => 38425, + 'Fortnite' => 38252, + 'Google' => 37996, + 'iCloud' => 38715, + 'Instagram' => 37997, + 'Kik' => 38539, + 'Netflix' => 37998, + 'Pinterest' => 38679, + 'Player Unknown\'s Battlegrounds' => 37999, + 'Playstation Network' => 38571, + 'PTLC' => 38004, + 'Quora' => 38908, + 'Snapchat' => 38508, + 'Steam' => 38341, + 'Tinder' => 38000, + 'Tumblr' => 38546, + 'Twitter' => 38678, + 'Wateen' => 38007, + 'Wattpad' => 38544, + 'Whatsapp' => 38003, + 'Yahoo Mail' => 38882, + 'Youtube' => 38001, + 'Zong' => 38005, + ), + 'UAE' => array( + 'Apex Legends' => 38159, + 'beIN' => 39073, + 'Binance' => 37022, + 'Blizzard Battle.net' => 36214, + 'Botim' => 38393, + 'Call of Duty' => 38935, + 'Cloudflare' => 38642, + 'Counter-strike' => 38269, + 'Dead By Daylight' => 37437, + 'Discord' => 37773, + 'Dota 2' => 36423, + 'Du' => 36235, + 'EA' => 36883, + 'Etisalat' => 36234, + 'Facebook' => 36215, + 'Facebook Messenger' => 36233, + 'Fifa' => 38433, + 'For Honor' => 36216, + 'Fortnite' => 36996, + 'Gmail' => 36217, + 'Google' => 36218, + 'iCloud' => 38843, + 'Instagram' => 36420, + 'Netflix' => 36219, + 'Office 365' => 38041, + 'Origin' => 38422, + 'Outlook' => 36220, + 'Overwatch' => 36221, + 'Player Unknown\'s Battlegrounds' => 36618, + 'Playstation Network' => 36222, + 'Pokémon Go' => 36223, + 'Rainbow Six' => 36224, + 'Reddit' => 38499, + 'Roblox' => 37222, + 'Rocket League' => 36231, + 'Skype' => 36225, + 'Snapchat' => 36226, + 'Steam' => 38340, + 'Telegram' => 37185, + 'Tinder' => 36227, + 'Twitter' => 38677, + 'Uplay PC' => 36228, + 'Whatsapp' => 36229, + 'Wikipedia' => 38878, + 'Xbox Live' => 38404, + 'Yahoo' => 36232, + 'Yahoo Mail' => 38888, + 'Youtube' => 36230, + 'ZoHo' => 37553, + ), + 'Malaysia' => array( + 'Apex Legends' => 38179, + 'Blizzard Battle.net' => 37891, + 'Counter-strike' => 38409, + 'Destiny' => 38985, + 'Discord' => 37976, + 'Dota 2' => 38017, + 'EA' => 38420, + 'Facebook' => 37892, + 'Facebook Messenger' => 37893, + 'Fifa' => 38429, + 'Gmail' => 38251, + 'Google' => 37894, + 'Google Drive' => 38263, + 'Google Play' => 38529, + 'GTA 5' => 38488, + 'iCloud' => 38850, + 'Instagram' => 37895, + 'League of Legends' => 38910, + 'Line' => 38386, + 'LinkedIn' => 38150, + 'Maxis' => 37964, + 'Netflix' => 37896, + 'Origin' => 38395, + 'Overwatch' => 38895, + 'Paypal' => 38461, + 'Player Unknown\'s Battlegrounds' => 37897, + 'Playstation Network' => 38570, + 'Reddit' => 38363, + 'Roblox' => 38912, + 'Snapchat' => 38548, + 'Steam' => 38081, + 'TIME' => 37965, + 'Tinder' => 37917, + 'Twitter' => 38481, + 'Unifi' => 37963, + 'Uplay PC' => 38896, + 'Waze' => 38268, + 'Whatsapp' => 38028, + 'Yahoo Mail' => 38891, + 'Youtube' => 37898, + ), + 'Perú' => array( + 'Apex Legends' => 39015, + 'Bitel' => 37982, + 'Claro' => 37981, + 'Crunchyroll' => 38550, + 'Dota 2' => 38353, + 'EA' => 38415, + 'Econocable' => 37983, + 'Entel' => 37980, + 'Facebook' => 37791, + 'Facebook Messenger' => 38258, + 'Fortnite' => 37792, + 'Gmail' => 37944, + 'Google' => 37945, + 'HBO' => 38369, + 'iCloud' => 38855, + 'Instagram' => 37793, + 'Movistar' => 37794, + 'Netflix' => 38594, + 'Origin' => 38423, + 'Outlook' => 38581, + 'Playstation Network' => 38568, + 'Spotify' => 38580, + 'Steam' => 38099, + 'Telegram' => 38483, + 'Tinder' => 37909, + 'Twitter' => 38105, + 'Whatsapp' => 37795, + 'Youtube' => 37796, + ), + 'Argentina' => array( + 'Apex Legends' => 38129, + 'Arnet' => 37969, + 'Banco Galicia' => 36132, + 'Banco Nación' => 36131, + 'Banco Provincia' => 36133, + 'Banco Santander Río ' => 36130, + 'BBVA Francés' => 36134, + 'Binance' => 37020, + 'Blizzard Battle.net' => 36104, + 'Call of Duty' => 36743, + 'Claro' => 36100, + 'Clash of Clans' => 36128, + 'Correo Argentino' => 36999, + 'Counter-Strike' => 38247, + 'Crunchyroll' => 38091, + 'Dead By Daylight' => 37431, + 'Destiny' => 38965, + 'Discord' => 38614, + 'Dota 2' => 37187, + 'EA' => 36885, + 'Facebook' => 36072, + 'Facebook Messenger' => 36073, + 'Fibertel' => 36635, + 'Fifa' => 38435, + 'Flow' => 38309, + 'Fortnite' => 37017, + 'Free Fire' => 38806, + 'Gigared' => 38215, + 'Gmail' => 37716, + 'Google' => 36074, + 'Google Play' => 38520, + 'GTA 5' => 38276, + 'HBO' => 36352, + 'ICBC' => 38646, + 'iCloud' => 38858, + 'Instagram' => 36075, + 'Mercado Libre' => 36076, + 'Microsoft Azure' => 37467, + 'Movistar' => 36077, + 'Netflix' => 36078, + 'Nextel' => 36103, + 'OLX' => 37045, + 'Origin' => 36902, + 'Outlook' => 36079, + 'Overwatch' => 38774, + 'Path of Exile' => 37771, + 'Personal' => 37970, + 'Player Unknown\'s Battlegrounds' => 36563, + 'Playstation Network' => 36080, + 'Pokémon Go' => 36294, + 'Rainbow Six' => 36144, + 'Reddit' => 37483, + 'Roblox' => 38929, + 'Skype' => 36081, + 'Snapchat' => 36082, + 'Spotify' => 36107, + 'Steam' => 36129, + 'Teamviewer' => 36142, + 'Telecentro' => 36810, + 'Telecom' => 36097, + 'Telefónica' => 36099, + 'Telegram' => 38485, + 'Telered' => 37971, + 'Tinder' => 37169, + 'Tuenti' => 38628, + 'Twitch' => 36544, + 'Twitter' => 36109, + 'Uplay PC' => 36143, + 'Waze' => 37470, + 'Whatsapp' => 36083, + 'Wikipedia' => 37352, + 'World of Warcraft' => 38778, + 'Xbox Live' => 37443, + 'Yahoo Mail' => 36084, + 'Youtube' => 36085, + ), + 'Slovensko' => array( + 'Apex Legends' => 38131, + 'Blizzard Battle.net' => 37875, + 'Counter-strike' => 38243, + 'Discord' => 38612, + 'Dota 2' => 38480, + 'EA' => 38417, + 'Facebook' => 37876, + 'Facebook Messenger' => 37877, + 'Fifa' => 38426, + 'Fortnite' => 38807, + 'Google' => 37878, + 'GTA 5' => 38279, + 'HBO Go' => 38381, + 'iCloud' => 38848, + 'Instagram' => 37879, + 'Netflix' => 37880, + 'Origin' => 38374, + 'Player Unknown\'s Battlegrounds' => 37881, + 'Playstation Network' => 38572, + 'Reddit' => 38809, + 'Snapchat' => 38513, + 'Steam' => 38230, + 'Twitter' => 38669, + 'Whatsapp' => 38371, + 'Wikipedia' => 38877, + 'Xbox Live' => 38088, + 'Yahoo Mail' => 38881, + 'Youtube' => 37882, + ), + 'Ελλάς' => array( + 'Apex Legends' => 38132, + 'Bet365' => 38811, + 'Blizzard Battle.net' => 37843, + 'Clash Royale' => 38949, + 'Counter-strike' => 38235, + 'Destiny' => 38980, + 'Discord' => 38607, + 'Dota 2' => 38352, + 'EA' => 38416, + 'eBay' => 38578, + 'Facebook' => 37844, + 'Facebook Messenger' => 37845, + 'Fifa' => 38434, + 'Fortnite' => 38087, + 'Gmail' => 38079, + 'Google' => 37846, + 'Google Play' => 38527, + 'GTA 5' => 38701, + 'iCloud' => 38846, + 'Instagram' => 37847, + 'League of Legends' => 38618, + 'Netflix' => 37848, + 'Office 365' => 38076, + 'Origin' => 38375, + 'Paypal' => 38462, + 'Player Unknown\'s Battlegrounds' => 37849, + 'Playstation Network' => 38575, + 'Pokémon Go' => 38639, + 'Reddit' => 38516, + 'Snapchat' => 38505, + 'Steam' => 38231, + 'Twitch' => 38582, + 'Twitter' => 38676, + 'Viber' => 38588, + 'Whatsapp' => 38373, + 'Yahoo Mail' => 38892, + 'Youtube' => 37850, + 'Βικιπαίδεια' => 38873, + ), + 'Hrvatska' => array( + 'Apex Legends' => 38135, + 'Battlefield' => 38442, + 'Blizzard Battle.net' => 37859, + 'Clash Royale' => 38356, + 'Counter-strike' => 38244, + 'Destiny' => 39011, + 'Discord' => 38610, + 'Dota 2' => 38351, + 'EA' => 38036, + 'Facebook' => 37860, + 'Facebook Messenger' => 37861, + 'Fifa' => 38438, + 'Fortnite' => 38069, + 'Google' => 37862, + 'GTA 5' => 38278, + 'HBO Go' => 38384, + 'iCloud' => 38847, + 'Instagram' => 37863, + 'Netflix' => 37864, + 'Office 365' => 38075, + 'Origin' => 38149, + 'Player Unknown\'s Battlegrounds' => 37865, + 'Playstation Network' => 38574, + 'Reddit' => 38090, + 'Snapchat' => 38509, + 'Steam' => 38229, + 'Tinder' => 37920, + 'Twitter' => 38672, + 'Whatsapp' => 38026, + 'Wikipedia' => 38874, + 'Yahoo Mail' => 38884, + 'Youtube' => 37866, + ), + 'Česko' => array( + 'Apex Legends' => 38136, + 'Battlefield' => 38446, + 'Blizzard Battle.net' => 37851, + 'Counter-strike' => 38236, + 'Discord' => 38452, + 'Dota 2' => 38221, + 'EA' => 38037, + 'Facebook' => 37852, + 'Facebook Messenger' => 37853, + 'Fifa' => 38430, + 'Fortnite' => 38086, + 'Gmail' => 38515, + 'Google' => 37854, + 'Google Play' => 38523, + 'GTA 5' => 38304, + 'HBO Go' => 38380, + 'iCloud' => 38845, + 'Instagram' => 37855, + 'Netflix' => 37856, + 'Office 365' => 38042, + 'Origin' => 38043, + 'Player Unknown\'s Battlegrounds' => 37857, + 'Playstation Network' => 38573, + 'Reddit' => 38062, + 'Snapchat' => 38512, + 'Steam' => 38082, + 'Tinder' => 37914, + 'Twitch' => 38530, + 'Twitter' => 38675, + 'Uplay PC' => 38638, + 'Whatsapp' => 38025, + 'Xbox Live' => 38072, + 'Youtube' => 37858, + ), + 'Chile' => array( + 'Apex Legends' => 38137, + 'Banco Estado' => 37682, + 'Banco Santander' => 37351, + 'Blizzard Battle.net' => 38104, + 'Call of Duty' => 38938, + 'Claro' => 36290, + 'Clash of Clans' => 37304, + 'Correos' => 37025, + 'Counter-Strike' => 38246, + 'Crunchyroll' => 38093, + 'Dead By Daylight' => 37436, + 'Discord' => 37758, + 'Dota 2' => 38477, + 'EA' => 36884, + 'Entel' => 37680, + 'Facebook' => 36239, + 'Facebook Messenger' => 36285, + 'Falabella' => 37681, + 'Fifa' => 38432, + 'Fortnite' => 36997, + 'Gmail' => 36499, + 'Google' => 36284, + 'Google Drive' => 38560, + 'GTA 5' => 38305, + 'Gtd Manquehue' => 37683, + 'HBO' => 36353, + 'iCloud' => 38854, + 'Instagram' => 36283, + 'Itaú' => 38473, + 'LinkedIn' => 38625, + 'Movistar' => 36289, + 'Mundo Pacifico' => 37684, + 'Netflix' => 36282, + 'Office 365' => 38077, + 'Origin' => 38102, + 'Outlook' => 36281, + 'Player Unknown\'s Battlegrounds' => 36564, + 'Playstation Network' => 36280, + 'Pokémon Go' => 36293, + 'Rainbow Six' => 36270, + 'Reddit' => 37484, + 'Roblox' => 38930, + 'SII' => 37679, + 'Skype' => 36279, + 'Snapchat' => 36278, + 'Spotify' => 36274, + 'Steam' => 36272, + 'Teamviewer' => 37068, + 'Telegram' => 38170, + 'Telsur' => 36292, + 'Tinder' => 37907, + 'Twitch' => 36541, + 'Twitter' => 36273, + 'Uplay PC' => 36271, + 'VTR' => 36291, + 'Warframe' => 37123, + 'Whatsapp' => 36277, + 'Wom' => 38308, + 'Xbox Live' => 37024, + 'Yahoo Mail' => 36276, + 'Youtube' => 36275, + ), + 'Colombia' => array( + 'Apex Legends' => 38140, + 'BBVA' => 37781, + 'Blizzard Battle.net' => 38148, + 'Call of Duty' => 38937, + 'Claro' => 37788, + 'Directv' => 37968, + 'Discord' => 38552, + 'EA' => 38282, + 'ETB' => 37967, + 'Facebook' => 37782, + 'Facebook Messenger' => 38257, + 'Fifa' => 38431, + 'Fortnite' => 37783, + 'Free Fire' => 38805, + 'Gmail' => 37779, + 'Google' => 37780, + 'Google Drive' => 39067, + 'GTA 5' => 38277, + 'HBO' => 38370, + 'iCloud' => 38857, + 'Instagram' => 37784, + 'Movistar' => 37789, + 'Netflix' => 37820, + 'Origin' => 38424, + 'Outlook' => 38464, + 'Player Unknown\'s Battlegrounds' => 37785, + 'Playstation Network' => 38391, + 'Snapchat' => 38506, + 'Spotify' => 38168, + 'Steam' => 38933, + 'Teamviewer' => 38460, + 'Telegram' => 38484, + 'Tigo' => 37966, + 'Tinder' => 37908, + 'Twitter' => 38169, + 'Waze' => 39070, + 'Whatsapp' => 37786, + 'Xbox Live' => 38071, + 'Yahoo Mail' => 38924, + 'Youtube' => 37787, + ), + 'România' => array( + 'Apex Legends' => 38142, + 'Battlefield' => 38441, + 'Blizzard Battle.net' => 37867, + 'Brawl Stars' => 38817, + 'Call of Duty' => 38934, + 'Counter-strike' => 38237, + 'Destiny' => 38988, + 'Digi' => 38971, + 'Discord' => 37953, + 'Dota 2' => 38222, + 'EA' => 38039, + 'Facebook' => 37868, + 'Facebook Messenger' => 37869, + 'Fifa' => 38046, + 'Fortnite' => 38070, + 'Gmail' => 38549, + 'Google' => 37870, + 'Google Play' => 38524, + 'GTA 5' => 38281, + 'HBO Go' => 38383, + 'iCloud' => 38849, + 'Instagram' => 37871, + 'Netflix' => 37872, + 'Orange' => 38970, + 'Origin' => 38045, + 'Player Unknown\'s Battlegrounds' => 37873, + 'Playstation Network' => 38407, + 'Reddit' => 38063, + 'Roblox' => 38918, + 'Snapchat' => 38510, + 'Steam' => 38083, + 'Telekom' => 38931, + 'Tinder' => 37913, + 'Twitter' => 38674, + 'UPC' => 38968, + 'Uplay PC' => 38637, + 'Vodafone' => 38967, + 'Whatsapp' => 38027, + 'Wikipedia' => 38875, + 'Xbox Live' => 38073, + 'Yahoo Mail' => 38455, + 'Youtube' => 37874, + ), + 'Magyarország' => array( + 'Apex Legends' => 38143, + 'Battlefield' => 38450, + 'Blizzard Battle.net' => 37883, + 'Call of Duty' => 39004, + 'Counter-strike' => 38242, + 'Destiny' => 38979, + 'Discord' => 38454, + 'Dota 2' => 38475, + 'EA' => 38038, + 'Escape from Tarkov' => 39055, + 'Facebook' => 37884, + 'Facebook Messenger' => 37885, + 'Fifa' => 38437, + 'Fortnite' => 38808, + 'Ghost Recon' => 38991, + 'Gmail' => 38514, + 'Google' => 37886, + 'GTA 5' => 38280, + 'HBO Go' => 38382, + 'iCloud' => 38852, + 'Instagram' => 37887, + 'Mastercard' => 38592, + 'Netflix' => 37888, + 'Office 365' => 38040, + 'Origin' => 38044, + 'Outlook' => 38078, + 'Pinterest' => 37954, + 'Player Unknown\'s Battlegrounds' => 37889, + 'Playstation Network' => 38408, + 'Rainbow Six' => 38486, + 'Reddit' => 38065, + 'Snapchat' => 38507, + 'Steam' => 38232, + 'Tinder' => 37910, + 'Twitch' => 39016, + 'Twitter' => 38673, + 'Uplay PC' => 38354, + 'Whatsapp' => 38372, + 'Wikipedia' => 38876, + 'Xbox Live' => 38074, + 'Yahoo Mail' => 38886, + 'Youtube' => 37890, + ), + 'Ecuador' => array( + 'Call of Duty' => 38936, + 'Claro' => 37985, + 'CNT' => 37990, + 'EA' => 38421, + 'Facebook' => 37797, + 'Facebook Messenger' => 38256, + 'Fortnite' => 37798, + 'HBO' => 38368, + 'iCloud' => 38856, + 'Instagram' => 37799, + 'iPlanet' => 37986, + 'Movistar' => 37984, + 'Nedetel' => 37987, + 'Netflix' => 37819, + 'Netlife' => 37991, + 'Playstation Network' => 38577, + 'Puntonet' => 37988, + 'Steam' => 38932, + 'Telegram' => 38482, + 'TVCable' => 37989, + 'Twitter' => 38670, + 'Whatsapp' => 37800, + 'Youtube' => 37801, + ), + ) + ) + ), + ); + + const API_TOKEN = 'YW5kcm9pZF9hcGlfdXNlcl92MTpxTkRyenZSczY1bW1ESlk0ZVNIWmtobFY='; + + public function collectData(){ + + if($this->queriedContext == 'All Websites') { + $html = getSimpleHTMLDOM($this->getURI() . '/archive/') + or returnClientError('Impossible to query website !.'); + + $table = $html->find('table.table-striped', 0); + + $maxCount = 10; + foreach ($table->find('tr') as $downEvent) { + $downLink = $downEvent->find('td', 1)->find('a', 1); + $item = $this->collectArticleData($downLink->getAttribute('href')); + $this->items[] = $item; + if($maxCount == 0) break; + $maxCount -= 1; + } + } else { + $this->items = $this->collectCompanyEvents($this->getInput('website')); + } + } + + protected function collectArticleData($link) { + + preg_match('/\/([0-9]{3,})/', $link, $matches); + $eventId = $matches[1]; + + $header = array( + 'Authorization: Basic ' . self::API_TOKEN + ); + + $article = getContents('https://downdetectorapi.com/v1/events/' . $eventId, $header) + or returnServerError('Could not request DownDetector API.'); + $article_json = json_decode($article); + + $item = array(); + $item['uri'] = $this->getURI() . $link; + $item['id'] = $article_json->id; + $item['title'] = $article_json->title; + $item['content'] = $article_json->body; + $item['timestamp'] = (new DateTime($article_json->started_at))->getTimestamp(); + return $item; + + } + + protected function collectCompanyEvents($companyId) { + + $header = array( + 'Authorization: Basic ' . self::API_TOKEN + ); + + $events = getContents('https://downdetectorapi.com/v1/companies/' . $companyId . '/events/', $header) + or returnServerError('Could not request DownDetector API.'); + $events_json = json_decode($events); + + $items = array(); + + foreach($events_json as $event) { + $item = array(); + $item['id'] = $event->id; + $item['title'] = $event->title; + $item['content'] = $event->body; + $item['timestamp'] = (new DateTime($event->started_at))->getTimestamp(); + $items[] = $item; + } + + return $items; + + } + + public function getURI() { + if($this->getInput('country') !== null) { + return $this->getInput('country'); + } else { + return self::URI; + } + } +} diff --git a/bridges/DribbbleBridge.php b/bridges/DribbbleBridge.php index 5058da63..b1193c90 100644 --- a/bridges/DribbbleBridge.php +++ b/bridges/DribbbleBridge.php @@ -19,7 +19,7 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico'; $json = $this->loadEmbeddedJsonData($html); foreach($html->find('li[id^="screenshot-"]') as $shot) { - $item = []; + $item = array(); $additional_data = $this->findJsonForShot($shot, $json); if ($additional_data === null) { @@ -38,14 +38,14 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico'; $preview_path = $shot->find('picture source', 0)->attr['srcset']; $item['content'] .= $this->getImageTag($preview_path, $item['title']); - $item['enclosures'] = [$this->getFullSizeImagePath($preview_path)]; + $item['enclosures'] = array($this->getFullSizeImagePath($preview_path)); $this->items[] = $item; } } private function loadEmbeddedJsonData($html){ - $json = []; + $json = array(); $scripts = $html->find('script'); foreach($scripts as $script) { diff --git a/bridges/EconomistBridge.php b/bridges/EconomistBridge.php index 1256be45..94121ac3 100644 --- a/bridges/EconomistBridge.php +++ b/bridges/EconomistBridge.php @@ -40,7 +40,7 @@ class EconomistBridge extends BridgeAbstract { if ($nextprev) $nextprev->outertext = ''; - $section = [ $article->find('h3[itemprop="articleSection"]', 0)->plaintext ]; + $section = array( $article->find('h3[itemprop="articleSection"]', 0)->plaintext ); $item = array(); $item['title'] = $header->find('span', 0)->innertext . ': ' diff --git a/bridges/EliteDangerousGalnetBridge.php b/bridges/EliteDangerousGalnetBridge.php index dc6077b3..1afa0423 100644 --- a/bridges/EliteDangerousGalnetBridge.php +++ b/bridges/EliteDangerousGalnetBridge.php @@ -47,5 +47,8 @@ class EliteDangerousGalnetBridge extends BridgeAbstract { $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 1f66edc3..8bcfa922 100644 --- a/bridges/ElloBridge.php +++ b/bridges/ElloBridge.php @@ -95,7 +95,7 @@ class ElloBridge extends BridgeAbstract { private function getEnclosures($post, $postData) { - $assets = []; + $assets = array(); foreach($post->links->assets as $asset) { foreach($postData->linked->assets as $assetLink) { if($asset == $assetLink->id) { @@ -120,9 +120,11 @@ class ElloBridge extends BridgeAbstract { } private function getAPIKey() { - $cache = Cache::create(Configuration::getConfig('cache', 'type')); + $cacheFac = new CacheFactory(); + $cacheFac->setWorkingDir(PATH_LIB_CACHES); + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); $cache->setScope(get_called_class()); - $cache->setKey(['key']); + $cache->setKey(array('key')); $key = $cache->loadData(); if($key == null) { diff --git a/bridges/ElsevierBridge.php b/bridges/ElsevierBridge.php index 080fe00f..01230a9e 100644 --- a/bridges/ElsevierBridge.php +++ b/bridges/ElsevierBridge.php @@ -3,7 +3,7 @@ class ElsevierBridge extends BridgeAbstract { const MAINTAINER = 'Pierre Mazière'; const NAME = 'Elsevier journals recent articles'; - const URI = 'http://www.journals.elsevier.com/'; + const URI = 'https://www.journals.elsevier.com/'; const CACHE_TIMEOUT = 43200; //12h const DESCRIPTION = 'Returns the recent articles published in Elsevier journals'; diff --git a/bridges/EngadgetBridge.php b/bridges/EngadgetBridge.php new file mode 100644 index 00000000..cf200fa4 --- /dev/null +++ b/bridges/EngadgetBridge.php @@ -0,0 +1,26 @@ +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; + } +} diff --git a/bridges/EsquerdaNetBridge.php b/bridges/EsquerdaNetBridge.php new file mode 100644 index 00000000..f459eb23 --- /dev/null +++ b/bridges/EsquerdaNetBridge.php @@ -0,0 +1,70 @@ + array( + 'name' => 'Feed', + 'type' => 'list', + 'defaultValue' => 'Geral', + 'values' => array( + 'Geral' => 'geral', + 'Dossier' => 'artigos-dossier', + 'Vídeo' => 'video', + 'Opinião' => 'opinioes', + 'Rádio' => 'radio', + ) + ) + ) + ); + + public function getURI() { + $type = $this->getInput('feed'); + return self::URI . '/rss/' . $type; + } + + public function getIcon() { + return 'https://www.esquerda.net/sites/default/files/favicon_0.ico'; + } + + public function collectData(){ + parent::collectExpandableDatas($this->getURI()); + } + + protected function parseItem($newsItem){ + # Fix Publish date + $badDate = $newsItem->pubDate; + preg_match('|(?P\d\d)/(?P\d\d)/(?P\d\d\d\d) - (?P\d\d):(?P\d\d)|', $badDate, $d); + $newsItem->pubDate = sprintf('%s-%s-%sT%s:%s', $d['year'], $d['month'], $d['day'], $d['hour'], $d['minute']); + $item = parent::parseItem($newsItem); + # Include all the content + $uri = $item['uri']; + $html = getSimpleHTMLDOMCached($uri) + or returnServerError('Could not load content for ' . $uri); + $content = $html->find('div#content div.content', 0); + ## Fix author + $authorHTML = $html->find('.field-name-field-op-author a', 0); + if ($authorHTML) { + $item['author'] = $authorHTML->innertext; + $authorHTML->remove(); + } + ## Remove crap + $content->find('.field-name-addtoany', 0)->remove(); + ## Fix links + $content = defaultLinkTo($content, self::URI); + ## Fix Images + foreach($content->find('img') as $img) { + $altSrc = $img->getAttribute('data-src'); + if ($altSrc) { + $img->setAttribute('src', $altSrc); + } + $img->width = null; + $img->height = null; + } + $item['content'] = $content; + return $item; + } +} diff --git a/bridges/ExtremeDownloadBridge.php b/bridges/ExtremeDownloadBridge.php index acdf6301..1b4aa9a9 100644 --- a/bridges/ExtremeDownloadBridge.php +++ b/bridges/ExtremeDownloadBridge.php @@ -1,7 +1,7 @@ array( 'name' => 'Username', 'required' => true - ) + ), + 'abbrev_name' => array( + 'name' => 'Abbreviate author name in title', + 'type' => 'checkbox', + 'defaultValue' => true, + ), )); public function getIcon() { @@ -102,12 +107,12 @@ EOD else $timestamp = 0; - $item['uri'] = html_entity_decode('http://touch.facebook.com' + $item['uri'] = html_entity_decode('https://touch.facebook.com' . $content->find("div[class='_52jc _5qc4 _78cz _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href'), ENT_QUOTES); //Decode images $imagecleaned = preg_replace_callback('/]* style="[^"]*url\(\'(.*?)\'\).*?><\/i>/m', function ($matches) { - return ""; + return ""; }, $content); $content = str_get_html($imagecleaned); @@ -159,7 +164,11 @@ EOD $content = preg_replace('//m', '', $content); //Remove the double section tags - $content = str_replace(['
', '
'], ['
', '
'], $content); + $content = str_replace( + array('
', '
'), + array('
', '
'), + $content + ); //Move the section tag link upper, if it is down $content = str_get_html($content); @@ -182,8 +191,10 @@ EOD $item['content'] = html_entity_decode($content, ENT_QUOTES); $title = $author; - if (strlen($title) > 24) - $title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...'; + if ($this->getInput('abbrev_name') === true) { + if (strlen($title) > 24) + $title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...'; + } $title = $title . ' | ' . strip_tags($content); if (strlen($title) > 64) $title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...'; @@ -281,10 +292,20 @@ EOD } public function getName(){ - return (isset($this->name) ? $this->name . ' - ' : '') . 'Facebook Bridge'; + $username = $this->getInput('u'); + if (isset($username)) { + return $this->getInput('u') . ' | Facebook'; + } else { + return self::NAME; + } } public function getURI(){ - return 'http://facebook.com'; + $username = $this->getInput('u'); + if (isset($username)) { + return 'https://facebook.com/' . $this->getInput('u') . '/posts'; + } else { + return self::URI; + } } } diff --git a/bridges/FabriceBellardBridge.php b/bridges/FabriceBellardBridge.php new file mode 100644 index 00000000..2c24b5ea --- /dev/null +++ b/bridges/FabriceBellardBridge.php @@ -0,0 +1,36 @@ +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; + } + } +} diff --git a/bridges/FacebookBridge.php b/bridges/FacebookBridge.php index c0901072..5ce67f94 100644 --- a/bridges/FacebookBridge.php +++ b/bridges/FacebookBridge.php @@ -2,7 +2,7 @@ class FacebookBridge extends BridgeAbstract { const MAINTAINER = 'teromene, logmanoriginal'; - const NAME = 'Facebook Bridge'; + const NAME = 'Facebook Bridge | Main Site'; const URI = 'https://www.facebook.com/'; const CACHE_TIMEOUT = 300; // 5min const DESCRIPTION = 'Input a page title or a profile log. For a profile log, @@ -66,14 +66,13 @@ class FacebookBridge extends BridgeAbstract { case 'User': if(!empty($this->authorName)) { - return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName - . ' - ' . static::NAME; + return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName; } break; case 'Group': if(!empty($this->groupName)) { - return $this->groupName . ' - ' . static::NAME; + return $this->groupName; } break; @@ -82,6 +81,34 @@ class FacebookBridge extends BridgeAbstract { return parent::getName(); } + public function detectParameters($url){ + $params = array(); + + // By profile + $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/profile\.php\?id\=([^\/?&\n]+)?(.*)/'; + if(preg_match($regex, $url, $matches) > 0) { + $params['u'] = urldecode($matches[3]); + return $params; + } + + // By group + $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/groups\/([^\/?\n]+)?(.*)/'; + if(preg_match($regex, $url, $matches) > 0) { + $params['g'] = urldecode($matches[3]); + return $params; + } + + // By username + $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/([^\/?\n]+)/'; + + if(preg_match($regex, $url, $matches) > 0) { + $params['u'] = urldecode($matches[3]); + return $params; + } + + return null; + } + public function getURI() { $uri = self::URI; @@ -142,7 +169,11 @@ class FacebookBridge extends BridgeAbstract { private function collectGroupData() { - $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n"); + if(getEnv('HTTP_ACCEPT_LANGUAGE')) { + $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE')); + } else { + $header = array(); + } $html = getSimpleHTMLDOM($this->getURI(), $header) or returnServerError('Failed loading facebook page: ' . $this->getURI()); @@ -505,7 +536,11 @@ EOD; // Retrieve page contents if(is_null($html)) { - $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE')); + if(getEnv('HTTP_ACCEPT_LANGUAGE')) { + $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE')); + } else { + $header = array(); + } $html = getSimpleHTMLDOM($this->getURI(), $header) or returnServerError('No results for this query.'); @@ -580,6 +615,8 @@ EOD; '._5mly', // Remove embedded videos (the preview image remains) '._2ezg', // Remove "Views ..." '.hidden_elem', // Remove hidden elements (they are hidden anyway) + '.timestampContent', // Remove relative timestamp + '._6spk', // Remove redundant separator ); foreach($content_filters as $filter) { @@ -664,8 +701,15 @@ EOD; $uri = $post->find('abbr')[0]->parent()->getAttribute('href'); - if (false !== strpos($uri, '?')) { - $uri = substr($uri, 0, strpos($uri, '?')); + // Extract fbid and patch link + if (strpos($uri, '?') !== false) { + $query = substr($uri, strpos($uri, '?') + 1); + parse_str($query, $query_params); + if (isset($query_params['story_fbid'])) { + $uri = self::URI . $query_params['story_fbid']; + } else { + $uri = substr($uri, 0, strpos($uri, '?')); + } } //Build and add final item diff --git a/bridges/FicbookBridge.php b/bridges/FicbookBridge.php new file mode 100644 index 00000000..8b8a57fc --- /dev/null +++ b/bridges/FicbookBridge.php @@ -0,0 +1,164 @@ + 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', + ), + ), + ); + + 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 collectData() { + + $header = array('Accept-Language: en-US'); + + $html = getSimpleHTMLDOM($this->getURI(), $header) + or returnServerError('Could not request ' . $this->getURI()); + + $html = defaultLinkTo($html, self::URI); + + 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.post') 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.table-of-contents > 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; + + } +} diff --git a/bridges/FindACrewBridge.php b/bridges/FindACrewBridge.php index 1dac775a..abab6e13 100644 --- a/bridges/FindACrewBridge.php +++ b/bridges/FindACrewBridge.php @@ -62,11 +62,16 @@ class FindACrewBridge extends BridgeAbstract { foreach ($annonces as $annonce) { $item = array(); - $img = parent::getURI() . $annonce->find('.lst-pic img', 0)->getAttribute('src'); + $link = parent::getURI() . $annonce->find('.lst-ctrls a', 0)->href; + $htmlDetail = getSimpleHTMLDOMCached($link . '?mdl=2'); // add ?mdl=2 for xhr content not full html page + + $img = parent::getURI() . $htmlDetail->find('img.img-responsive', 0)->getAttribute('src'); $item['title'] = $annonce->find('.lst-tags span', 0)->plaintext; - $item['uri'] = parent::getURI() . $annonce->find('.lst-ctrls a', 0)->href; - $content = $annonce->find('.lst-dtl', 0)->innertext; - $item['content'] = "
$content"; + $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; diff --git a/bridges/FreeCodeCampBridge.php b/bridges/FreeCodeCampBridge.php new file mode 100644 index 00000000..da0b5c7d --- /dev/null +++ b/bridges/FreeCodeCampBridge.php @@ -0,0 +1,27 @@ +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; + } +} diff --git a/bridges/FurAffinityBridge.php b/bridges/FurAffinityBridge.php new file mode 100644 index 00000000..2f78ee4f --- /dev/null +++ b/bridges/FurAffinityBridge.php @@ -0,0 +1,918 @@ + array( + 'q' => array( + 'name' => 'Query', + 'required' => true + ), + 'rating-general' => array( + 'name' => 'General', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'rating-mature' => array( + 'name' => 'Mature', + 'type' => 'checkbox', + ), + 'rating-adult' => array( + 'name' => 'Adult', + 'type' => 'checkbox', + ), + 'range' => array( + 'name' => 'Time range', + 'type' => 'list', + 'values' => array( + 'A Day' => 'day', + '3 Days' => '3days', + 'A Week' => 'week', + 'A Month' => 'month', + 'All time' => 'all' + ), + 'defaultValue' => 'all' + ), + 'type-art' => array( + 'name' => 'Art', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'type-flash' => array( + 'name' => 'Flash', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'type-photo' => array( + 'name' => 'Photography', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'type-music' => array( + 'name' => 'Music', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'type-story' => array( + 'name' => 'Story', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'type-poetry' => array( + 'name' => 'Poetry', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'mode' => array( + 'name' => 'Match mode', + 'type' => 'list', + 'values' => array( + 'All of the words' => 'all', + 'Any of the words' => 'any', + 'Extended' => 'extended' + ), + 'defaultValue' => 'extended' + ), + 'limit' => array( + 'name' => 'Limit', + 'type' => 'number', + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ), + 'full' => array( + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'cache' => array( + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ) + ), + 'Browse' => array( + 'cat' => array( + 'name' => 'Category', + 'type' => 'list', + 'values' => array( + 'Visual Art' => array( + 'All' => 1, + 'Artwork (Digital)' => 2, + 'Artwork (Traditional)' => 3, + 'Cellshading' => 4, + 'Crafting' => 5, + 'Designs' => 6, + 'Flash' => 7, + 'Fursuiting' => 8, + 'Icons' => 9, + 'Mosaics' => 10, + 'Photography' => 11, + 'Sculpting' => 12 + ), + 'Readable Art' => array( + 'Story' => 13, + 'Poetry' => 14, + 'Prose' => 15 + ), + 'Audio Art' => array( + 'Music' => 16, + 'Podcasts' => 17 + ), + 'Downloadable' => array( + 'Skins' => 18, + 'Handhelds' => 19, + 'Resources' => 20 + ), + 'Other Stuff' => array( + 'Adoptables' => 21, + 'Auctions' => 22, + 'Contests' => 23, + 'Current Events' => 24, + 'Desktops' => 25, + 'Stockart' => 26, + 'Screenshots' => 27, + 'Scraps' => 28, + 'Wallpaper' => 29, + 'YCH / Sale' => 30, + 'Other' => 31 + ) + ), + 'defaultValue' => 1 + ), + 'atype' => array( + 'name' => 'Type', + 'type' => 'list', + 'values' => array( + 'General Things' => array( + 'All' => 1, + 'Abstract' => 2, + 'Animal related (non-anthro)' => 3, + 'Anime' => 4, + 'Comics' => 5, + 'Doodle' => 6, + 'Fanart' => 7, + 'Fantasy' => 8, + 'Human' => 9, + 'Portraits' => 10, + 'Scenery' => 11, + 'Still Life' => 12, + 'Tutorials' => 13, + 'Miscellaneous' => 14 + ), + 'Fetish / Furry specialty' => array( + 'Baby fur' => 101, + 'Bondage' => 102, + 'Digimon' => 103, + 'Fat Furs' => 104, + 'Fetish Other' => 105, + 'Fursuit' => 106, + 'Gore / Macabre Art' => 119, + 'Hyper' => 107, + 'Inflation' => 108, + 'Macro / Micro' => 109, + 'Muscle' => 110, + 'My Little Pony / Brony' => 111, + 'Paw' => 112, + 'Pokemon' => 113, + 'Pregnancy' => 114, + 'Sonic' => 115, + 'Transformation' => 116, + 'Vore' => 117, + 'Water Sports' => 118, + 'General Furry Art' => 100 + ), + 'Music' => array( + 'Techno' => 201, + 'Trance' => 202, + 'House' => 203, + '90s' => 204, + '80s' => 205, + '70s' => 206, + '60s' => 207, + 'Pre-60s' => 208, + 'Classical' => 209, + 'Game Music' => 210, + 'Rock' => 211, + 'Pop' => 212, + 'Rap' => 213, + 'Industrial' => 214, + 'Other Music' => 200 + ) + ), + 'defaultValue' => 1 + ), + 'species' => array( + 'name' => 'Species', + 'type' => 'list', + 'values' => array( + 'Unspecified / Any' => 1, + 'Amphibian' => array( + 'Frog' => 1001, + 'Newt' => 1002, + 'Salamander' => 1003, + 'Amphibian (Other)' => 1000 + ), + 'Aquatic' => array( + 'Cephalopod' => 2001, + 'Dolphin' => 2002, + 'Fish' => 2005, + 'Porpoise' => 2004, + 'Seal' => 6068, + 'Shark' => 2006, + 'Whale' => 2003, + 'Aquatic (Other)' => 2000 + ), + 'Avian' => array( + 'Corvid' => 3001, + 'Crow' => 3002, + 'Duck' => 3003, + 'Eagle' => 3004, + 'Falcon' => 3005, + 'Goose' => 3006, + 'Gryphon' => 3007, + 'Hawk' => 3008, + 'Owl' => 3009, + 'Phoenix' => 3010, + 'Swan' => 3011, + 'Avian (Other)' => 3000 + ), + 'Bears & Ursines' => array( + 'Bear' => 6002 + ), + 'Camelids' => array( + 'Camel' => 6074, + 'Llama' => 6036 + ), + 'Canines & 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 & 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 & 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 & Crocodile' => 7001, + 'Gecko' => 7003, + 'Iguana' => 7004, + 'Lizard' => 7005, + 'Snakes & Serpents' => 7006, + 'Turtle' => 7007, + 'Reptilian (Other)' => 7000 + ), + 'Rodents' => array( + 'Beaver' => 6064, + 'Mouse' => 6065, + 'Rat' => 6061, + 'Squirrel' => 6070, + 'Rodent (Other)' => 6067 + ), + 'Vulpines' => array( + 'Fennec' => 6072, + 'Fox' => 6075, + 'Vulpine (Other)' => 6015 + ), + 'Other' => array( + 'Dinosaur' => 8001, + 'Wolverine' => 6050 + ) + ), + 'defaultValue' => 1 + ), + 'gender' => array( + 'name' => 'Gender', + 'type' => 'list', + 'values' => array( + 'Any' => 0, + 'Male' => 2, + 'Female' => 3, + 'Herm' => 4, + 'Transgender' => 5, + 'Multiple characters' => 6, + 'Other / Not Specified' => 7 + ), + 'defaultValue' => 0 + ), + 'rating_general' => array( + 'name' => 'General', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'rating_mature' => array( + 'name' => 'Mature', + 'type' => 'checkbox', + ), + 'rating_adult' => array( + 'name' => 'Adult', + 'type' => 'checkbox', + ), + 'limit-browse' => array( + 'name' => 'Limit', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ), + 'full' => array( + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'cache' => array( + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ) + + ), + 'Journals' => array( + 'username-journals' => array( + 'name' => 'Username', + 'required' => true, + 'title' => 'Lowercase username as seen in URLs' + ), + 'limit' => array( + 'name' => 'Limit', + 'type' => 'number', + 'defaultValue' => -1, + 'title' => 'Limit number of journals to return. -1 for unlimited.' + ) + + ), + 'Single Journal' => array( + 'journal-id' => array( + 'name' => 'Journal ID', + 'required' => true, + 'type' => 'number', + 'title' => 'Number seen in journal URL' + ) + ), + 'Gallery' => array( + 'username-gallery' => array( + 'name' => 'Username', + 'required' => true, + 'title' => 'Lowercase username as seen in URLs' + ), + 'limit' => array( + 'name' => 'Limit', + 'type' => 'number', + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ), + 'full' => array( + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'cache' => array( + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ) + ), + 'Scraps' => array( + 'username-scraps' => array( + 'name' => 'Username', + 'required' => true, + 'title' => 'Lowercase username as seen in URLs' + ), + 'limit' => array( + 'name' => 'Limit', + 'type' => 'number', + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ), + 'full' => array( + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'cache' => array( + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ) + ), + 'Favorites' => array( + 'username-favorites' => array( + 'name' => 'Username', + 'required' => true, + 'title' => 'Lowercase username as seen in URLs' + ), + 'limit' => array( + 'name' => 'Limit', + 'type' => 'number', + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ), + 'full' => array( + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'cache' => array( + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ) + ), + 'Gallery Folder' => array( + 'username-folder' => array( + 'name' => 'Username', + 'required' => true, + 'title' => 'Lowercase username as seen in URLs' + ), + 'folder-id' => array( + 'name' => 'Folder ID', + 'required' => true, + 'type' => 'number', + 'title' => 'Number seen in folder URL' + ), + 'limit' => array( + 'name' => 'Limit', + 'type' => 'number', + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ), + 'full' => array( + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ), + 'cache' => array( + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ) + ) + ); + + /* + * This was aquired by creating a new user on FA then + * extracting the cookie from the browsers dev console. + */ + const FA_AUTH_COOKIE = 'b=4ce65691-b50f-4742-a990-bf28d6de16ee; a=ca6e4566-9d81-4263-9444-653b142e35f8'; + + public function detectParameters($url) { + $params = array(); + + // Single journal + $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journal\/(\d+)/'; + if(preg_match($regex, $url, $matches) > 0) { + $params['journal-id'] = urldecode($matches[3]); + return $params; + } + + // Journals + $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journals\/([^\/&?\n]+)/'; + if(preg_match($regex, $url, $matches) > 0) { + $params['username-journals'] = urldecode($matches[3]); + return $params; + } + + // Gallery folder + $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/gallery\/([^\/&?\n]+)\/folder\/(\d+)/'; + if(preg_match($regex, $url, $matches) > 0) { + $params['username-folder'] = urldecode($matches[3]); + $params['folder-id'] = urldecode($matches[4]); + $params['full'] = 'on'; + return $params; + } + + // Gallery (must be after gallery folder) + $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/(gallery|scraps|favorites)\/([^\/&?\n]+)/'; + if(preg_match($regex, $url, $matches) > 0) { + $params['username-' . $matches[3]] = urldecode($matches[4]); + $params['full'] = 'on'; + return $params; + } + + return null; + } + + public function getName() { + switch($this->queriedContext) { + case 'Search': + return 'Search For ' + . $this->getInput('q'); + case 'Browse': + return 'Browse'; + case 'Journals': + return $this->getInput('username-journals'); + case 'Single Journal': + return 'Journal ' + . $this->getInput('journal-id'); + case 'Gallery': + return $this->getInput('username-gallery'); + case 'Scraps': + return $this->getInput('username-scraps'); + case 'Favorites': + return $this->getInput('username-favorites'); + case 'Gallery Folder': + return $this->getInput('username-folder') + . '\'s Folder ' + . $this->getInput('folder-id'); + default: return parent::getName(); + } + } + + public function getDescription() { + switch($this->queriedContext) { + case 'Search': + return 'FurAffinity Search For ' + . $this->getInput('q'); + case 'Browse': + return 'FurAffinity Browse'; + case 'Journals': + return 'FurAffinity Journals By ' + . $this->getInput('username-journals'); + case 'Single Journal': + return 'FurAffinity Journal ' + . $this->getInput('journal-id'); + case 'Gallery': + return 'FurAffinity Gallery By ' + . $this->getInput('username-gallery'); + case 'Scraps': + return 'FurAffinity Scraps By ' + . $this->getInput('username-scraps'); + case 'Favorites': + return 'FurAffinity Favorites By ' + . $this->getInput('username-favorites'); + case 'Gallery Folder': + return 'FurAffinity Gallery Folder ' + . $this->getInput('folder-id') + . ' By ' + . $this->getInput('username-folder'); + default: return parent::getDescription(); + } + } + + public function getURI() { + switch($this->queriedContext) { + case 'Search': + return SELF::URI + . '/search'; + case 'Browse': + return SELF::URI + . '/browse'; + case 'Journals': + return SELF::URI + . '/journals/' + . $this->getInput('username-journals'); + case 'Single Journal': + return SELF::URI + . '/journal/' + . $this->getInput('journal-id'); + case 'Gallery': + return SELF::URI + . '/gallery/' + . $this->getInput('username-gallery'); + case 'Scraps': + return SELF::URI + . '/scraps/' + . $this->getInput('username-scraps'); + case 'Favorites': + return SELF::URI + . '/favorites/' + . $this->getInput('username-favorites'); + case 'Gallery Folder': + return SELF::URI + . '/gallery/' + . $this->getInput('username-folder') + . '/folder/' + . $this->getInput('folder-id'); + default: return parent::getURI(); + } + } + + public function collectData() { + switch($this->queriedContext) { + case 'Search': + $data = array( + 'q' => $this->getInput('q'), + 'perpage' => 72, + 'rating-general' => ($this->getInput('rating-general') === true ? 'on' : 0), + 'rating-mature' => ($this->getInput('rating-mature') === true ? 'on' : 0), + 'rating-adult' => ($this->getInput('rating-adult') === true ? 'on' : 0), + 'range' => $this->getInput('range'), + 'type-art' => ($this->getInput('type-art') === true ? 'on' : 0), + 'type-flash' => ($this->getInput('type-flash') === true ? 'on' : 0), + 'type-photo' => ($this->getInput('type-photo') === true ? 'on' : 0), + 'type-music' => ($this->getInput('type-music') === true ? 'on' : 0), + 'type-story' => ($this->getInput('type-story') === true ? 'on' : 0), + 'type-poetry' => ($this->getInput('type-poetry') === true ? 'on' : 0), + 'mode' => $this->getInput('mode') + ); + $html = $this->postFASimpleHTMLDOM($data); + $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10); + $this->itemsFromSubmissionList($html, $limit); + break; + case 'Browse': + $data = array( + 'cat' => $this->getInput('cat'), + 'atype' => $this->getInput('atype'), + 'species' => $this->getInput('species'), + 'gender' => $this->getInput('gender'), + 'perpage' => 72, + 'rating_general' => ($this->getInput('rating_general') === true ? 'on' : 0), + 'rating_mature' => ($this->getInput('rating_mature') === true ? 'on' : 0), + 'rating_adult' => ($this->getInput('rating_adult') === true ? 'on' : 0) + ); + $html = $this->postFASimpleHTMLDOM($data); + $limit = (is_int($this->getInput('limit-browse')) ? $this->getInput('limit-browse') : 10); + $this->itemsFromSubmissionList($html, $limit); + break; + case 'Journals': + $html = $this->getFASimpleHTMLDOM($this->getURI()); + $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : -1); + $this->itemsFromJournalList($html, $limit); + break; + case 'Single Journal': + $html = $this->getFASimpleHTMLDOM($this->getURI()); + $this->itemsFromJournal($html); + break; + case 'Gallery': + case 'Scraps': + case 'Favorites': + case 'Gallery Folder': + $html = $this->getFASimpleHTMLDOM($this->getURI()); + $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10); + $this->itemsFromSubmissionList($html, $limit); + break; + } + } + + private function postFASimpleHTMLDOM($data) { + $opts = array( + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => http_build_query($data) + ); + $header = array( + 'Host: ' . parse_url(self::URI, PHP_URL_HOST), + 'Content-Type: application/x-www-form-urlencoded', + 'Cookie: ' . self::FA_AUTH_COOKIE + ); + + $html = getSimpleHTMLDOM($this->getURI(), $header, $opts); + $html = defaultLinkTo($html, $this->getURI()); + + return $html; + } + + private function getFASimpleHTMLDOM($url, $cache = false) { + $header = array( + 'Cookie: ' . self::FA_AUTH_COOKIE + ); + + if($cache) { + $html = getSimpleHTMLDOMCached($url, 86400, $header); // 24 hours + } else { + $html = getSimpleHTMLDOM($url, $header); + } + + $html = defaultLinkTo($html, $url); + + return $html; + } + + private function itemsFromJournalList($html, $limit) { + foreach($html->find('table[id^=jid:]') as $journal) { + # allows limit = -1 to mean 'unlimited' + if($limit-- === 0) break; + + $item = array(); + + $this->setReferrerPolicy($journal); + + $item['uri'] = $journal->find('a', 0)->href; + $item['title'] = html_entity_decode($journal->find('a', 0)->plaintext); + $item['author'] = $this->getInput('username-journals'); + $item['timestamp'] = strtotime( + $journal->find('span.popup_date', 0)->plaintext); + $item['content'] = $journal + ->find('.alt1 table div.no_overflow', 0) + ->innertext; + + $this->items[] = $item; + } + } + + private function itemsFromJournal($html) { + $this->setReferrerPolicy($html); + $item = array(); + + $item['uri'] = $this->getURI(); + + $title = $html->find('.journal-title-box .no_overflow', 0)->plaintext; + $title = html_entity_decode($title); + $title = trim($title, " \t\n\r\0\x0B" . chr(0xC2) . chr(0xA0)); + $item['title'] = $title; + + $item['author'] = $html->find('.journal-title-box a', 0)->plaintext; + $item['timestamp'] = strtotime( + $html->find('.journal-title-box span.popup_date', 0)->plaintext); + $item['content'] = $html->find('.journal-body', 0)->innertext; + + $this->items[] = $item; + } + + private function itemsFromSubmissionList($html, $limit) { + $cache = ($this->getInput('cache') === true); + + foreach($html->find('section.gallery figure') as $figure) { + # allows limit = -1 to mean 'unlimited' + if($limit-- === 0) break; + + $item = array(); + + $submissionURL = $figure->find('b u a', 0)->href; + $imgURL = 'https:' . $figure->find('b u a img', 0)->src; + + $item['uri'] = $submissionURL; + $item['title'] = html_entity_decode( + $figure->find('figcaption p a[href*=/view/]', 0)->title); + $item['author'] = $figure->find('figcaption p a[href*=/user/]', 0)->title; + + if($this->getInput('full') === true) { + $submissionHTML = $this->getFASimpleHTMLDOM($submissionURL, $cache); + + $stats = $submissionHTML->find('.stats-container', 0); + $item['timestamp'] = strtotime($stats->find('.popup_date', 0)->title); + $item['enclosures'] = array( + $submissionHTML->find('.actions a[href^=https://d.facdn]', 0)->href + ); + foreach($stats->find('#keywords a') as $keyword) { + $item['categories'][] = $keyword->plaintext; + } + + $previewSrc = $submissionHTML->find('#submissionImg', 0) + ->{'data-preview-src'}; + if($previewSrc) { + $imgURL = 'https:' . $previewSrc; + } + + $description = $submissionHTML + ->find('.maintable .maintable tr td.alt1', -1); + $this->setReferrerPolicy($description); + $description = $description->innertext; + + $item['content'] = << + + +

+{$description} +

+EOD; + } else { + $item['content'] = << + + +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'; + } + } +} diff --git a/bridges/FurAffinityUserBridge.php b/bridges/FurAffinityUserBridge.php new file mode 100644 index 00000000..fd8a61f0 --- /dev/null +++ b/bridges/FurAffinityUserBridge.php @@ -0,0 +1,110 @@ + array( + 'name' => 'Search Username', + 'type' => 'text', + 'required' => true, + 'title' => 'Username to fetch the gallery for' + ), + 'loginUsername' => array( + 'name' => 'Login Username', + 'type' => 'text', + 'required' => true + ), + 'loginPassword' => array( + 'name' => 'Login Password', + 'type' => 'text', + 'required' => true + ) + ) + ); + + public function collectData() { + $cookies = self::login(); + $url = self::URI . '/gallery/' . $this->getInput('searchUsername'); + + $html = getSimpleHTMLDOM($url, $cookies) + or returnServerError('Could not load the user\'s galary page.'); + + $submissions = $html->find('section[id=gallery-gallery]', 0)->find('figure'); + foreach($submissions as $submission) { + $item = array(); + $item['title'] = $submission->find('figcaption', 0)->find('a', 0)->plaintext; + + $thumbnail = $submission->find('a', 0); + $thumbnail->href = self::URI . $thumbnail->href; + + $item['content'] = $submission->find('a', 0); + + $this->items[] = $item; + } + } + + public function getName() { + return self::NAME . ' for ' . $this->getInput('searchUsername'); + } + + public function getURI() { + return self::URI . '/user/' . $this->getInput('searchUsername'); + } + + private function login() { + $ch = curl_init(self::URI . '/login/'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + + curl_setopt($ch, CURLOPT_USERAGENT, ini_get('user_agent')); + curl_setopt($ch, CURLOPT_ENCODING, ''); + curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + + $fields = implode('&', array( + 'action=login', + 'retard_protection=1', + 'name=' . urlencode($this->getInput('loginUsername')), + 'pass=' . urlencode($this->getInput('loginPassword')), + 'login=Login to Faraffinity' + )); + + curl_setopt($ch, CURLOPT_POST, 5); + curl_setopt($ch, CURLOPT_POSTFIELDS, $fields); + + if(defined('PROXY_URL') && !defined('NOPROXY')) { + curl_setopt($ch, CURLOPT_PROXY, PROXY_URL); + } + + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLINFO_HEADER_OUT, true); + + $data = curl_exec($ch); + + $errorCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + $curlError = curl_error($ch); + $curlErrno = curl_errno($ch); + $curlInfo = curl_getinfo($ch); + + if($data === false) + fDebug::log("Cant't download {$url} cUrl error: {$curlError} ({$curlErrno})"); + + curl_close($ch); + + if($errorCode != 200) { + returnServerError(error_get_last()); + } else { + preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $data, $matches); + $cookies = array(); + + foreach($matches[1] as $item) { + parse_str($item, $cookie); + $cookies = array_merge($cookies, $cookie); + } + + return $cookies; + } + } +} diff --git a/bridges/GQMagazineBridge.php b/bridges/GQMagazineBridge.php index 961b3a09..14a9a56c 100644 --- a/bridges/GQMagazineBridge.php +++ b/bridges/GQMagazineBridge.php @@ -40,6 +40,11 @@ class GQMagazineBridge extends BridgeAbstract 'data-original' => 'src' ); + const POSSIBLE_TITLES = array( + 'h2', + 'h3' + ); + private function getDomain() { $domain = $this->getInput('domain'); if (empty($domain)) @@ -54,6 +59,17 @@ class GQMagazineBridge extends BridgeAbstract 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()) or returnServerError('Could not request ' . $this->getURI()); @@ -61,31 +77,36 @@ class GQMagazineBridge extends BridgeAbstract // Since GQ don't want simple class scrapping, let's do it the hard way and ... discover content ! $main = $html->find('main', 0); foreach ($main->find('a') as $link) { + if(strpos($link, $this->getInput('page'))) + continue; $uri = $link->href; - $title = $link->find('h2', 0); - $date = $link->find('time', 0); + $date = $link->parent()->find('time', 0); $item = array(); - $author = $link->find('span[itemprop=name]', 0); - $item['author'] = $author->plaintext; - $item['title'] = $title->plaintext; - if(substr($uri, 0, 1) === 'h') { // absolute uri - $item['uri'] = $uri; - } else if(substr($uri, 0, 1) === '/') { // domain relative url - $item['uri'] = $this->getDomain() . $uri; - } else { - $item['uri'] = $this->getDomain() . '/' . $uri; + $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'] = "Article body couldn't be loaded. It must be a bug!"; + } + $short_date = $date->datetime; + $item['timestamp'] = strtotime($short_date); + $this->items[] = $item; } - - $article = $this->loadFullArticle($item['uri']); - if($article) { - $item['content'] = $this->replaceUriInHtmlElement($article); - } else { - $item['content'] = "Article body couldn't be loaded. It must be a bug!"; - } - $short_date = $date->datetime; - $item['timestamp'] = strtotime($short_date); - $this->items[] = $item; } } @@ -96,16 +117,7 @@ class GQMagazineBridge extends BridgeAbstract */ private function loadFullArticle($uri){ $html = getSimpleHTMLDOMCached($uri); - // Once again, that generated css classes madness is an obstacle ... which i can go over easily - foreach($html->find('div') as $div) { - // List the CSS classes of that div - $classes = $div->class; - // I can't directly lookup that class since GQ since to generate random names like "ArticleBodySection-fkggUW" - if(strpos($classes, 'ArticleBodySection') !== false) { - return $div; - } - } - return null; + return $html->find('section[data-test-id=MainContentWrapper]', 0); } /** diff --git a/bridges/GiphyBridge.php b/bridges/GiphyBridge.php index 26d1ebaf..202bbbbd 100644 --- a/bridges/GiphyBridge.php +++ b/bridges/GiphyBridge.php @@ -5,7 +5,7 @@ class GiphyBridge extends BridgeAbstract { const MAINTAINER = 'kraoc'; const NAME = 'Giphy Bridge'; - const URI = 'http://giphy.com/'; + const URI = 'https://giphy.com/'; const CACHE_TIMEOUT = 300; //5min const DESCRIPTION = 'Bridge for giphy.com'; diff --git a/bridges/GiteaBridge.php b/bridges/GiteaBridge.php new file mode 100644 index 00000000..33247873 --- /dev/null +++ b/bridges/GiteaBridge.php @@ -0,0 +1,27 @@ +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('h3', 0)->plaintext, + ); + } + } +} diff --git a/bridges/GithubIssueBridge.php b/bridges/GithubIssueBridge.php index 91dd45ec..2eddeb2e 100644 --- a/bridges/GithubIssueBridge.php +++ b/bridges/GithubIssueBridge.php @@ -66,10 +66,21 @@ class GithubIssueBridge extends BridgeAbstract { return parent::getURI(); } - protected function extractIssueEvent($issueNbr, $title, $comment){ - $comment = $comment->firstChild(); - $uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p') - . '/issues/' . $issueNbr . '#' . $comment->getAttribute('id'); + private function buildGitHubIssueCommentUri($issue_number, $comment_id) { + // https://github.com///issues/# + return static::URI + . $this->getInput('u') + . '/' + . $this->getInput('p') + . '/issues/' + . $issue_number + . '#' + . $comment_id; + } + + private function extractIssueEvent($issueNbr, $title, $comment){ + + $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id); $author = $comment->find('.author', 0)->plaintext; @@ -94,22 +105,21 @@ class GithubIssueBridge extends BridgeAbstract { return $item; } - protected function extractIssueComment($issueNbr, $title, $comment){ - $uri = static::URI . $this->getInput('u') . '/' - . $this->getInput('p') . '/issues/' . $issueNbr; + private function extractIssueComment($issueNbr, $title, $comment){ + + $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->parent->id); $author = $comment->find('.author', 0)->plaintext; $title .= ' / ' . trim( - $comment->find('.comment .timeline-comment-header-text', 0)->plaintext + $comment->find('.timeline-comment-header-text', 0)->plaintext ); $content = $comment->find('.comment-body', 0)->innertext; $item = array(); $item['author'] = $author; - $item['uri'] = $uri - . '#' . $comment->firstChild()->nextSibling()->getAttribute('id'); + $item['uri'] = $uri; $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8'); $item['timestamp'] = strtotime( $comment->find('relative-time', 0)->getAttribute('datetime') @@ -118,25 +128,32 @@ class GithubIssueBridge extends BridgeAbstract { return $item; } - protected function extractIssueComments($issue){ + 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('.js-discussion', 0); - foreach($comments->children() as $comment) { + + $comments = $issue->find(' + [id^="issue-"] > .comment, + [id^="issuecomment-"] > .comment, + [id^="event-"], + [id^="ref-"] + '); + foreach($comments as $comment) { + if (!$comment->hasChildNodes()) { continue; } - $comment = $comment->firstChild(); - $classes = explode(' ', $comment->getAttribute('class')); - if (in_array('timeline-comment-wrapper', $classes)) { + + if (!$comment->hasClass('discussion-item-header')) { $item = $this->extractIssueComment($issueNbr, $title, $comment); $items[] = $item; continue; } - while (in_array('discussion-item', $classes)) { + + while ($comment->hasClass('discussion-item-header')) { $item = $this->extractIssueEvent($issueNbr, $title, $comment); $items[] = $item; $comment = $comment->nextSibling(); @@ -145,6 +162,7 @@ class GithubIssueBridge extends BridgeAbstract { } $classes = explode(' ', $comment->getAttribute('class')); } + } return $items; } @@ -192,8 +210,13 @@ class GithubIssueBridge extends BridgeAbstract { ENT_QUOTES, 'UTF-8' ); - $comments = trim($issue->find('.col-5', 0)->plaintext); - $item['content'] .= "\n" . 'Comments: ' . ($comments ? $comments : '0'); + + $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 . $issue->find('.js-navigation-open', 0)->getAttribute('href'); $this->items[] = $item; @@ -216,4 +239,43 @@ class GithubIssueBridge extends BridgeAbstract { $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] !== 'issues') { + 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, + ); + + } } diff --git a/bridges/GlassdoorBridge.php b/bridges/GlassdoorBridge.php index 68b6a137..308859d7 100644 --- a/bridges/GlassdoorBridge.php +++ b/bridges/GlassdoorBridge.php @@ -141,7 +141,7 @@ class GlassdoorBridge extends BridgeAbstract { } private function collectReviewData($html, $limit) { - $reviews = $html->find('#EmployerReviews li[id^="empReview]') + $reviews = $html->find('#ReviewsFeed li[id^="empReview]') or returnServerError('Unable to find reviews!'); foreach($reviews as $review) { @@ -153,7 +153,19 @@ class GlassdoorBridge extends BridgeAbstract { $item['timestamp'] = strtotime($review->find('time', 0)->datetime); $mainText = $review->find('p.mainText', 0)->plaintext; - $description = $review->find('div.prosConsAdvice', 0)->innertext; + + $description = ''; + foreach($review->find('div.description p') as $p) { + + if ($p->hasClass('strong')) { + $p->tag = 'strong'; + $p->removeClass('strong'); + } + + $description .= $p; + + } + $item['content'] = "

{$mainText}

{$description}

"; $this->items[] = $item; diff --git a/bridges/GogsBridge.php b/bridges/GogsBridge.php new file mode 100644 index 00000000..a08bcc0e --- /dev/null +++ b/bridges/GogsBridge.php @@ -0,0 +1,206 @@ + array( + 'host' => array( + 'name' => 'Host', + 'exampleValue' => 'https://gogs.io', + 'required' => true, + 'title' => 'Host name without trailing slash', + ), + 'user' => array( + 'name' => 'Username', + 'exampleValue' => 'gogs', + 'required' => true, + 'title' => 'User name as it appears in the URL', + ), + 'project' => array( + 'name' => 'Project name', + 'exampleValue' => 'gogs', + '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' => 102, + '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'); + } break; + case 'Issues': { + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/issues/'; + } break; + case 'Single issue': { + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/issues/' . $this->getInput('issue'); + } break; + case 'Releases': { + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/releases/'; + } break; + 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()) + 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 '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, + ); + } + } +} diff --git a/bridges/GoogleSearchBridge.php b/bridges/GoogleSearchBridge.php index c3d9561f..e02aaeba 100644 --- a/bridges/GoogleSearchBridge.php +++ b/bridges/GoogleSearchBridge.php @@ -25,13 +25,10 @@ class GoogleSearchBridge extends BridgeAbstract { public function collectData(){ $html = ''; - $html = getSimpleHTMLDOM(self::URI - . 'search?q=' - . urlencode($this->getInput('q')) - . '&num=100&complete=0&tbs=qdr:y,sbd:1') + $html = getSimpleHTMLDOM($this->getURI()) or returnServerError('No results for this query.'); - $emIsRes = $html->find('div[id=ires]', 0); + $emIsRes = $html->find('div[id=res]', 0); if(!is_null($emIsRes)) { foreach($emIsRes->find('div[class=g]') as $element) { @@ -54,6 +51,17 @@ class GoogleSearchBridge extends BridgeAbstract { } } + public function getURI() { + if (!is_null($this->getInput('q'))) { + return self::URI + . 'search?q=' + . urlencode($this->getInput('q')) + . '&num=100&complete=0&tbs=qdr:y,sbd:1'; + } + + return parent::getURI(); + } + public function getName(){ if(!is_null($this->getInput('q'))) { return $this->getInput('q') . ' - Google search'; diff --git a/bridges/HDWallpapersBridge.php b/bridges/HDWallpapersBridge.php index f1579e02..16c08e75 100644 --- a/bridges/HDWallpapersBridge.php +++ b/bridges/HDWallpapersBridge.php @@ -2,7 +2,7 @@ class HDWallpapersBridge extends BridgeAbstract { const MAINTAINER = 'nel50n'; const NAME = 'HD Wallpapers Bridge'; - const URI = 'http://www.hdwallpapers.in/'; + const URI = 'https://www.hdwallpapers.in/'; const CACHE_TIMEOUT = 43200; //12h const DESCRIPTION = 'Returns the latests wallpapers from HDWallpapers'; @@ -72,7 +72,7 @@ class HDWallpapersBridge extends BridgeAbstract { public function getName(){ if(!is_null($this->getInput('c')) && !is_null($this->getInput('r'))) { return 'HDWallpapers - ' - . str_replace(['__', '_'], [' & ', ' '], $this->getInput('c')) + . str_replace(array('__', '_'), array(' & ', ' '), $this->getInput('c')) . ' [' . $this->getInput('r') . ']'; diff --git a/bridges/HaveIBeenPwnedBridge.php b/bridges/HaveIBeenPwnedBridge.php index f256623a..96dc7b2a 100644 --- a/bridges/HaveIBeenPwnedBridge.php +++ b/bridges/HaveIBeenPwnedBridge.php @@ -13,6 +13,11 @@ class HaveIBeenPwnedBridge extends BridgeAbstract { 'Date added to HIBP' => 'dateAdded', ), 'defaultValue' => 'dateAdded', + ), + 'item_limit' => array( + 'name' => 'Limit number of returned items', + 'type' => 'number', + 'defaultValue' => 20, ) )); @@ -52,13 +57,15 @@ class HaveIBeenPwnedBridge extends BridgeAbstract { // Remove permalink $breach->find('p', 1)->find('a', 0)->outertext = ''; - $item['title'] = $breach->find('h3', 0)->plaintext . ' - ' . $accounts[1] . ' breached accounts'; + $item['title'] = html_entity_decode($breach->find('h3', 0)->plaintext, ENT_QUOTES) + . ' - ' . $accounts[1] . ' breached accounts'; $item['dateAdded'] = strtotime($dateAdded[1]); $item['breachDate'] = strtotime($breachDate[1]); $item['uri'] = self::URI . '/PwnedWebsites' . $permalink; - $item['content'] = '

' . $breach->find('p', 0)->innertext . '

'; - $item['content'] .= '

' . $breach->find('p', 1)->innertext . '

'; + $item['content'] = '

' . $breach->find('p', 0)->innertext . '

'; + $item['content'] .= '

' . $this->breachType($breach) . '

'; + $item['content'] .= '

' . $breach->find('p', 1)->innertext . '

'; $this->breaches[] = $item; } @@ -67,6 +74,25 @@ class HaveIBeenPwnedBridge extends BridgeAbstract { $this->createItems(); } + /** + * Extract data breach type(s) + */ + private function breachType($breach) { + + $content = ''; + + if ($breach->find('h3 > i', 0)) { + + foreach ($breach->find('h3 > i') as $i) { + $content .= $i->title . '.
'; + } + + } + + return $content; + + } + /** * Order Breaches by date added or date breached */ @@ -88,6 +114,12 @@ class HaveIBeenPwnedBridge extends BridgeAbstract { */ private function createItems() { + $limit = $this->getInput('item_limit'); + + if ($limit < 1) { + $limit = 20; + } + foreach ($this->breaches as $breach) { $item = array(); @@ -97,6 +129,10 @@ class HaveIBeenPwnedBridge extends BridgeAbstract { $item['content'] = $breach['content']; $this->items[] = $item; + + if (count($this->items) >= $limit) { + break; + } } } } diff --git a/bridges/HentaiHavenBridge.php b/bridges/HentaiHavenBridge.php index 21a0ff5e..0e4fda46 100644 --- a/bridges/HentaiHavenBridge.php +++ b/bridges/HentaiHavenBridge.php @@ -3,7 +3,7 @@ class HentaiHavenBridge extends BridgeAbstract { const MAINTAINER = 'albirew'; const NAME = 'Hentai Haven'; - const URI = 'http://hentaihaven.org/'; + const URI = 'https://hentaihaven.org/'; const CACHE_TIMEOUT = 21600; // 6h const DESCRIPTION = 'Returns releases from Hentai Haven'; diff --git a/bridges/IGNBridge.php b/bridges/IGNBridge.php new file mode 100644 index 00000000..6a254b37 --- /dev/null +++ b/bridges/IGNBridge.php @@ -0,0 +1,55 @@ +collectExpandableDatas('http://feeds.ign.com/ign/all', 15); + } + + // IGNs feed is both hidden and incomplete. This bridge tries to fix this. + + protected function parseItem($newsItem){ + $item = parent::parseItem($newsItem); + + // $articlePage gets the entire page's contents + $articlePage = getSimpleHTMLDOM($newsItem->link); + + /* + * NOTE: Though articles and wiki/howtos have seperate styles of pages, there is no mechanism + * for handling them seperately as it just ignores the DOM querys which it does not find. + * (and their scraping) + */ + + // For Articles + $article = $articlePage->find('section.article-page', 0); + // add in verdicts in articles, reviews etc + foreach($articlePage->find('div.article-section') as $element) { + $article = $article . $element; + } + + // For Wikis and HowTos + $uselessWikiElements = array( + '.wiki-page-tools', + '.feedback-container', + '.paging-container' + ); + foreach($articlePage->find('.wiki-page') as $wikiContents) { + $copy = clone $wikiContents; + // Remove useless elements present in IGN wiki/howtos + foreach($uselessWikiElements as $uslElement) { + $toRemove = $wikiContents->find($uslElement, 0); + $copy = str_replace($toRemove, '', $copy); + } + $article = $article . $copy; + } + + // Add content to feed + $item['content'] = $article; + return $item; + } +} diff --git a/bridges/IndeedBridge.php b/bridges/IndeedBridge.php new file mode 100644 index 00000000..c1d0cfd3 --- /dev/null +++ b/bridges/IndeedBridge.php @@ -0,0 +1,245 @@ + 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', + '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) + or returnServerError('Could not request ' . $url); + + $html = defaultLinkTo($html, $url); + + $this->title = $html->find('h1', 0)->innertext; + + // Use local translation of the word "Rating" + $rating_local = $html->find('a[data-id="rating_desc"]', 0)->plaintext; + + foreach($html->find('#cmp-content [id^="cmp-review-"]') as $review) { + $item = array(); + + $rating = $review->find('.cmp-ratingNumber', 0)->plaintext; + $title = $review->find('.cmp-review-title > span', 0)->plaintext; + $comment = $this->beautifyComment($review->find('.cmp-review-content-container', 0)); + + $item['uri'] = $review->find('.cmp-review-share-popup-item-link--copylink', 0)->href; + $item['title'] = "{$rating_local} {$rating} / {$title}"; + $item['timestamp'] = $review->find('.cmp-review-date-created', 0)->plaintext; + $item['author'] = $review->find('.cmp-reviewer', 0)->plaintext; + $item['content'] = $comment; + //$item['enclosures'] + $item['categories'][] = $review->find('.cmp-reviewer-job-location', 0)->plaintext; + //$item['uid'] + + $this->items[] = $item; + + if(count($this->items) >= $limit) { + break; + } + } + + // Break if no more pages available. + if($next = $html->find('a[data-tn-element="next-page"]', 0)) { + $url = $next->href; + } else { + 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/[/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, + ); + } + + private function beautifyComment($comment) { + foreach($comment->find('.cmp-bold') as $bold) { + $bold->tag = 'strong'; + $bold->removeClass('cmp-bold'); + } + + return $comment; + } +} diff --git a/bridges/InstagramBridge.php b/bridges/InstagramBridge.php index 1bf86072..679c4c0e 100644 --- a/bridges/InstagramBridge.php +++ b/bridges/InstagramBridge.php @@ -32,28 +32,61 @@ class InstagramBridge extends BridgeAbstract { 'required' => false, 'values' => array( 'All' => 'all', - 'Story' => 'story', 'Video' => 'video', 'Picture' => 'picture', + 'Multiple' => 'multiple', ), 'defaultValue' => 'all' + ), + 'direct_links' => array( + 'name' => 'Use direct media links', + 'type' => 'checkbox', ) ) ); - public function collectData(){ + const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951'; + const TAG_QUERY_HASH = '174a5243287c5f3a7de741089750ab3b'; + const SHORTCODE_QUERY_HASH = '865589822932d1b43dfe312121dd353a'; - if(is_null($this->getInput('u')) && $this->getInput('media_type') == 'story') { - returnClientError('Stories are not supported for hashtags nor locations!'); + protected function getInstagramUserId($username) { + + if(is_numeric($username)) return $username; + + $cacheFac = new CacheFactory(); + $cacheFac->setWorkingDir(PATH_LIB_CACHES); + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $cache->setScope(get_called_class()); + $cache->setKey(array($username)); + $key = $cache->loadData(); + + if($key == null) { + $data = getContents(self::URI . 'web/search/topsearch/?query=' . $username); + + foreach(json_decode($data)->users as $user) { + if($user->user->username === $username) { + $key = $user->user->pk; + } + } + if($key == null) { + returnServerError('Unable to find username in search result.'); + } + $cache->saveData($key); } + return $key; + + } + + public function collectData(){ + $directLink = !is_null($this->getInput('direct_links')) && $this->getInput('direct_links'); $data = $this->getInstagramJSON($this->getURI()); if(!is_null($this->getInput('u'))) { - $userMedia = $data->entry_data->ProfilePage[0]->graphql->user->edge_owner_to_timeline_media->edges; + $userMedia = $data->data->user->edge_owner_to_timeline_media->edges; } elseif(!is_null($this->getInput('h'))) { - $userMedia = $data->entry_data->TagPage[0]->graphql->hashtag->edge_hashtag_to_media->edges; + $userMedia = $data->data->hashtag->edge_hashtag_to_media->edges; } elseif(!is_null($this->getInput('l'))) { $userMedia = $data->entry_data->LocationsPage[0]->graphql->location->edge_location_to_media->edges; } @@ -61,22 +94,18 @@ class InstagramBridge extends BridgeAbstract { foreach($userMedia as $media) { $media = $media->node; - if(!is_null($this->getInput('u'))) { - switch($this->getInput('media_type')) { - case 'all': break; - case 'video': - if($media->__typename != 'GraphVideo') continue 2; - break; - case 'picture': - if($media->__typename != 'GraphImage') continue 2; - break; - case 'story': - if($media->__typename != 'GraphSidecar') continue 2; - break; - default: break; - } - } else { - if($this->getInput('media_type') == 'video' && !$media->is_video) continue; + 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(); @@ -86,72 +115,141 @@ class InstagramBridge extends BridgeAbstract { $item['author'] = $media->owner->username; } - if (isset($media->edge_media_to_caption->edges[0]->node->text)) { - $textContent = $media->edge_media_to_caption->edges[0]->node->text; - } else { - $textContent = '(no text)'; - } + $textContent = $this->getTextContent($media); - $item['title'] = ($media->is_video ? '▶ ' : '') . trim($textContent); + $item['title'] = ($media->is_video ? '▶ ' : '') . $textContent; $titleLinePos = strpos(wordwrap($item['title'], 120), "\n"); if ($titleLinePos != false) { $item['title'] = substr($item['title'], 0, $titleLinePos) . '...'; } - if(!is_null($this->getInput('u')) && $media->__typename == 'GraphSidecar') { - $data = $this->getInstagramStory($item['uri']); - $item['content'] = $data[0]; - $item['enclosures'] = $data[1]; - } else { - $mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l'; - $item['content'] = ''; - $item['content'] .= '' . $item['title'] . ''; - $item['content'] .= '

' . nl2br(htmlentities($textContent)); - $item['enclosures'] = array($mediaURI); + switch($media->__typename) { + case 'GraphSidecar': + $data = $this->getInstagramSidecarData($item['uri'], $item['title']); + $item['content'] = $data[0]; + $item['enclosures'] = $data[1]; + break; + case 'GraphImage': + if($directLink) { + $mediaURI = $media->display_url; + } else { + $mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l'; + } + $item['content'] = ''; + $item['content'] .= '' . $item['title'] . ''; + $item['content'] .= '

' . nl2br(htmlentities($textContent)); + $item['enclosures'] = array($mediaURI); + break; + case 'GraphVideo': + $data = $this->getInstagramVideoData($item['uri']); + $item['content'] = $data[0]; + if($directLink) { + $item['enclosures'] = $data[1]; + } else { + $item['enclosures'] = array(self::URI . 'p/' . $media->shortcode . '/media?size=l'); + } + break; + default: break; } - $item['timestamp'] = $media->taken_at_timestamp; $this->items[] = $item; } } - protected function getInstagramStory($uri) { + // returns Sidecar(a post which has multiple media)'s contents and enclosures + protected function getInstagramSidecarData($uri, $postTitle) { + $mediaInfo = $this->getSinglePostData($uri); - $data = $this->getInstagramJSON($uri); - $mediaInfo = $data->entry_data->PostPage[0]->graphql->shortcode_media; + $textContent = $this->getTextContent($mediaInfo); - //Process the first element, that isn't in the node graph - if (count($mediaInfo->edge_media_to_caption->edges) > 0) { - $caption = $mediaInfo->edge_media_to_caption->edges[0]->node->text; - } else { - $caption = ''; - } - - $enclosures = [$mediaInfo->display_url]; - $content = '' . $caption . ''; - - foreach($mediaInfo->edge_sidecar_to_children->edges as $media) { - $display_url = $media->node->display_url; - if(!in_array($display_url, $enclosures)) { // add only if not added yet - $content .= '' . $caption . ''; - $enclosures[] = $display_url; + $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 .= '
'; + array_push($enclosures, $singleMedia->video_url); + } else { + if(in_array($singleMedia->display_url, $enclosures)) continue; // check if not added yet + $content .= ''; + $content .= '' . $postTitle . ''; + $content .= '
'; + array_push($enclosures, $singleMedia->display_url); } } + $content .= '
' . nl2br(htmlentities($textContent)); - return [$content, $enclosures]; + return array($content, $enclosures); + } + // returns Video post's contents and enclosures + protected function getInstagramVideoData($uri) { + $mediaInfo = $this->getSinglePostData($uri); + + $textContent = $this->getTextContent($mediaInfo); + $content = '
'; + $content .= '
' . nl2br(htmlentities($textContent)); + + return array($content, array($mediaInfo->video_url)); + } + + protected function getTextContent($media) { + $textContent = '(no text)'; + //Process the first element, that isn't in the node graph + if (count($media->edge_media_to_caption->edges) > 0) { + $textContent = trim($media->edge_media_to_caption->edges[0]->node->text); + } + return $textContent; + } + + protected function getSinglePostData($uri) { + $shortcode = explode('/', $uri)[4]; + $data = getContents(self::URI . + 'graphql/query/?query_hash=' . + self::SHORTCODE_QUERY_HASH . + '&variables={"shortcode"%3A"' . + $shortcode . + '"}'); + + return json_decode($data)->data->shortcode_media; } protected function getInstagramJSON($uri) { - $html = getContents($uri) - or returnServerError('Could not request Instagram.'); - $scriptRegex = '/window\._sharedData = (.*);<\/script>/'; + if(!is_null($this->getInput('u'))) { - preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0); + $userId = $this->getInstagramUserId($this->getInput('u')); - return json_decode($matches[1][0]); + $data = getContents(self::URI . + 'graphql/query/?query_hash=' . + self::USER_QUERY_HASH . + '&variables={"id"%3A"' . + $userId . + '"%2C"first"%3A10}'); + return json_decode($data); + + } elseif(!is_null($this->getInput('h'))) { + $data = getContents(self::URI . + 'graphql/query/?query_hash=' . + self::TAG_QUERY_HASH . + '&variables={"tag_name"%3A"' . + $this->getInput('h') . + '"%2C"first"%3A10}'); + return json_decode($data); + + } else { + + $html = getContents($uri) + or returnServerError('Could not request Instagram.'); + $scriptRegex = '/window\._sharedData = (.*);<\/script>/'; + + preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0); + + return json_decode($matches[1][0]); + + } } diff --git a/bridges/InstructablesBridge.php b/bridges/InstructablesBridge.php index 7e7d5428..e28c34b3 100644 --- a/bridges/InstructablesBridge.php +++ b/bridges/InstructablesBridge.php @@ -1,8 +1,7 @@ 'Category', 'type' => 'list', 'values' => array( - 'Play' => array( - 'All' => '/play/', - 'KNEX' => '/play/knex/', - 'Offbeat' => '/play/offbeat/', - 'Lego' => '/play/lego/', - 'Airsoft' => '/play/airsoft/', - 'Card Games' => '/play/card-games/', - 'Guitars' => '/play/guitars/', - 'Instruments' => '/play/instruments/', - 'Magic Tricks' => '/play/magic-tricks/', - 'Minecraft' => '/play/minecraft/', - 'Music' => '/play/music/', - 'Nerf' => '/play/nerf/', - 'Nintendo' => '/play/nintendo/', - 'Office Supplies' => '/play/office-supplies/', - 'Paintball' => '/play/paintball/', - 'Paper Airplanes' => '/play/paper-airplanes/', - 'Party Tricks' => '/play/party-tricks/', - 'PlayStation' => '/play/playstation/', - 'Pranks and Humor' => '/play/pranks-and-humor/', - 'Puzzles' => '/play/puzzles/', - 'Siege Engines' => '/play/siege-engines/', - 'Sports' => '/play/sports/', - 'Table Top' => '/play/table-top/', - 'Toys' => '/play/toys/', - 'Video Games' => '/play/video-games/', - 'Wii' => '/play/wii/', - 'Xbox' => '/play/xbox/', - 'Yo-Yo' => '/play/yo-yo/', - ), - 'Craft' => array( - 'All' => '/craft/', - 'Art' => '/craft/art/', - 'Sewing' => '/craft/sewing/', - 'Paper' => '/craft/paper/', - 'Jewelry' => '/craft/jewelry/', - 'Fashion' => '/craft/fashion/', - 'Books & Journals' => '/craft/books-and-journals/', - 'Cards' => '/craft/cards/', - 'Clay' => '/craft/clay/', - 'Duct Tape' => '/craft/duct-tape/', - 'Embroidery' => '/craft/embroidery/', - 'Felt' => '/craft/felt/', - 'Fiber Arts' => '/craft/fiber-arts/', - 'Gifts & Wrapping' => '/craft/gifts-and-wrapping/', - 'Knitting & Crocheting' => '/craft/knitting-and-crocheting/', - 'Leather' => '/craft/leather/', - 'Mason Jars' => '/craft/mason-jars/', - 'No-Sew' => '/craft/no-sew/', - 'Parties & Weddings' => '/craft/parties-and-weddings/', - 'Print Making' => '/craft/print-making/', - 'Soap' => '/craft/soap/', - 'Wallets' => '/craft/wallets/', - ), - 'Technology' => array( - 'All' => '/technology/', - 'Electronics' => '/technology/electronics/', - 'Arduino' => '/technology/arduino/', - 'Photography' => '/technology/photography/', - 'Leds' => '/technology/leds/', - 'Science' => '/technology/science/', - 'Reuse' => '/technology/reuse/', - 'Apple' => '/technology/apple/', - 'Computers' => '/technology/computers/', - '3D Printing' => '/technology/3D-Printing/', - 'Robots' => '/technology/robots/', - 'Art' => '/technology/art/', - 'Assistive Tech' => '/technology/assistive-technology/', - 'Audio' => '/technology/audio/', - 'Clocks' => '/technology/clocks/', - 'CNC' => '/technology/cnc/', - 'Digital Graphics' => '/technology/digital-graphics/', - 'Gadgets' => '/technology/gadgets/', - 'Kits' => '/technology/kits/', - 'Laptops' => '/technology/laptops/', - 'Lasers' => '/technology/lasers/', - 'Linux' => '/technology/linux/', - 'Microcontrollers' => '/technology/microcontrollers/', - 'Microsoft' => '/technology/microsoft/', - 'Mobile' => '/technology/mobile/', - 'Raspberry Pi' => '/technology/raspberry-pi/', - 'Remote Control' => '/technology/remote-control/', - 'Sensors' => '/technology/sensors/', - 'Software' => '/technology/software/', - 'Soldering' => '/technology/soldering/', - 'Speakers' => '/technology/speakers/', - 'Steampunk' => '/technology/steampunk/', - 'Tools' => '/technology/tools/', - 'USB' => '/technology/usb/', - 'Wearables' => '/technology/wearables/', - 'Websites' => '/technology/websites/', - 'Wireless' => '/technology/wireless/', + '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/', - 'Woodworking' => '/workshop/woodworking/', - 'Tools' => '/workshop/tools/', - 'Gardening' => '/workshop/gardening/', - 'Cars' => '/workshop/cars/', - 'Metalworking' => '/workshop/metalworking/', - 'Cardboard' => '/workshop/cardboard/', - 'Electric Vehicles' => '/workshop/electric-vehicles/', - 'Energy' => '/workshop/energy/', - 'Furniture' => '/workshop/furniture/', - 'Home Improvement' => '/workshop/home-improvement/', - 'Home Theater' => '/workshop/home-theater/', - 'Hydroponics' => '/workshop/hydroponics/', - 'Laser Cutting' => '/workshop/laser-cutting/', - 'Lighting' => '/workshop/lighting/', - 'Molds & Casting' => '/workshop/molds-and-casting/', - 'Motorcycles' => '/workshop/motorcycles/', - 'Organizing' => '/workshop/organizing/', - 'Pallets' => '/workshop/pallets/', - 'Repair' => '/workshop/repair/', - 'Shelves' => '/workshop/shelves/', - 'Solar' => '/workshop/solar/', - 'Workbenches' => '/workshop/workbenches/', + '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/', ), - 'Home' => array( - 'All' => '/home/', - 'Halloween' => '/home/halloween/', - 'Decorating' => '/home/decorating/', - 'Organizing' => '/home/organizing/', - 'Pets' => '/home/pets/', - 'Life Hacks' => '/home/life-hacks/', - 'Beauty' => '/home/beauty/', - 'Christmas' => '/home/christmas/', - 'Cleaning' => '/home/cleaning/', - 'Education' => '/home/education/', - 'Finances' => '/home/finances/', - 'Gardening' => '/home/gardening/', - 'Green' => '/home/green/', - 'Health' => '/home/health/', - 'Hiding Places' => '/home/hiding-places/', - 'Holidays' => '/home/holidays/', - 'Homesteading' => '/home/homesteading/', - 'Kids' => '/home/kids/', - 'Kitchen' => '/home/kitchen/', - 'Life Skills' => '/home/life-skills/', - 'Parenting' => '/home/parenting/', - 'Pest Control' => '/home/pest-control/', - 'Relationships' => '/home/relationships/', - 'Reuse' => '/home/reuse/', - 'Travel' => '/home/travel/', + '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/', - 'Bikes' => '/outside/bikes/', - 'Survival' => '/outside/survival/', - 'Backyard' => '/outside/backyard/', - 'Beach' => '/outside/beach/', - 'Birding' => '/outside/birding/', - 'Boats' => '/outside/boats/', - 'Camping' => '/outside/camping/', - 'Climbing' => '/outside/climbing/', - 'Fire' => '/outside/fire/', - 'Fishing' => '/outside/fishing/', - 'Hunting' => '/outside/hunting/', - 'Kites' => '/outside/kites/', - 'Knives' => '/outside/knives/', - 'Knots' => '/outside/knots/', - 'Paracord' => '/outside/paracord/', - 'Rockets' => '/outside/rockets/', - 'Skateboarding' => '/outside/skateboarding/', - 'Snow' => '/outside/snow/', - 'Water' => '/outside/water/', + '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/', ), - 'Food' => array( - 'All' => '/food/', - 'Dessert' => '/food/dessert/', - 'Snacks & Appetizers' => '/food/snacks-and-appetizers/', - 'Bacon' => '/food/bacon/', - 'BBQ & Grilling' => '/food/bbq-and-grilling/', - 'Beverages' => '/food/beverages/', - 'Bread' => '/food/bread/', - 'Breakfast' => '/food/breakfast/', - 'Cake' => '/food/cake/', - 'Candy' => '/food/candy/', - 'Canning & Preserves' => '/food/canning-and-preserves/', - 'Cocktails & Mocktails' => '/food/cocktails-and-mocktails/', - 'Coffee' => '/food/coffee/', - 'Cookies' => '/food/cookies/', - 'Cupcakes' => '/food/cupcakes/', - 'Homebrew' => '/food/homebrew/', - 'Main Course' => '/food/main-course/', - 'Pasta' => '/food/pasta/', - 'Pie' => '/food/pie/', - 'Pizza' => '/food/pizza/', - 'Salad' => '/food/salad/', - 'Sandwiches' => '/food/sandwiches/', - 'Soups & Stews' => '/food/soups-and-stews/', - 'Vegetarian & Vegan' => '/food/vegetarian-and-vegan/', + '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/', ), - 'Costumes' => array( - 'All' => '/costumes/', - 'Props' => '/costumes/props-and-accessories/', - 'Animals' => '/costumes/animals/', - 'Comics' => '/costumes/comics/', - 'Fantasy' => '/costumes/fantasy/', - 'For Kids' => '/costumes/for-kids/', - 'For Pets' => '/costumes/for-pets/', - 'Funny' => '/costumes/funny/', - 'Games' => '/costumes/games/', - 'Historic & Futuristic' => '/costumes/historic-and-futuristic/', - 'Makeup' => '/costumes/makeup/', - 'Masks' => '/costumes/masks/', - 'Scary' => '/costumes/scary/', - 'TV & Movies' => '/costumes/tv-and-movies/', - 'Weapons & Armor' => '/costumes/weapons-and-armor/', - ) ), 'title' => 'Select your category (required)', - 'defaultValue' => 'Technology' + 'defaultValue' => 'Circuits' ), 'filter' => array( 'name' => 'Filter', @@ -252,65 +233,70 @@ class InstructablesBridge extends BridgeAbstract { ) ); - private $uri; - public function collectData() { // Enable the following line to get the category list (dev mode) // $this->listCategories(); - $this->uri = static::URI; + $html = getSimpleHTMLDOM($this->getURI()) + or returnServerError('Error loading category ' . $this->getURI()); + $html = defaultLinkTo($html, $this->getURI()); - switch($this->queriedContext) { - case 'Category': $this->uri .= $this->getInput('category') . $this->getInput('filter'); - } + $covers = $html->find(' + .category-projects-list > div, + .category-landing-projects-list > div, + '); - $html = getSimpleHTMLDOM($this->uri) - or returnServerError('Error loading category ' . $this->uri); - - foreach($html->find('ul.explore-covers-list li') as $cover) { + foreach($covers as $cover) { $item = array(); - $item['uri'] = static::URI . $cover->find('a.cover-image', 0)->href; - $item['title'] = $cover->find('.title', 0)->innertext; + $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'] = 'src + . $cover->find('img', 0)->getAttribute('data-src') . '>'; - $image = str_replace('.RECTANGLE1', '.LARGE', $cover->find('a.cover-image img', 0)->src); - $item['enclosures'] = [$image]; + $item['enclosures'][] = str_replace( + '.RECTANGLE1', + '.LARGE', + $cover->find('img', 0)->getAttribute('data-src') + ); $this->items[] = $item; } } public function getName() { - if(!is_null($this->getInput('category')) - && !is_null($this->getInput('filter'))) { - foreach(self::PARAMETERS[$this->queriedContext]['category']['values'] as $key => $value) { - $subcategory = array_search($this->getInput('category'), $value); + switch($this->queriedContext) { + case 'Category': { + foreach(self::PARAMETERS[$this->queriedContext]['category']['values'] as $key => $value) { + $subcategory = array_search($this->getInput('category'), $value); - if($subcategory !== false) - break; - } + if($subcategory !== false) + break; + } - $filter = array_search( - $this->getInput('filter'), - self::PARAMETERS[$this->queriedContext]['filter']['values'] - ); + $filter = array_search( + $this->getInput('filter'), + self::PARAMETERS[$this->queriedContext]['filter']['values'] + ); - return $subcategory . ' (' . $filter . ') - ' . static::NAME; + return $subcategory . ' (' . $filter . ') - ' . static::NAME; + } break; } return parent::getName(); } public function getURI() { - if(!is_null($this->getInput('category')) - && !is_null($this->getInput('filter'))) { - return $this->uri; + switch($this->queriedContext) { + case 'Category': { + return self::URI + . $this->getInput('category') + . $this->getInput('filter'); + } break; } return parent::getURI(); @@ -321,24 +307,32 @@ class InstructablesBridge extends BridgeAbstract { * parameters list) */ private function listCategories(){ - // Use arbitrary category to receive full list - $html = getSimpleHTMLDOM(self::URI . '/technology/'); - foreach($html->find('.channel a') as $channel) { - $name = html_entity_decode(trim($channel->innertext)); + // Use home page to acquire main categories + $html = getSimpleHTMLDOM(self::URI); + $html = defaultLinkTo($html, self::URI); - // Remove unwanted entities - $name = str_replace("'", '', $name); - $name = str_replace(''', '', $name); + foreach($html->find('.home-content-explore-link') as $category) { - $uri = $channel->href; + // Use arbitrary category to receive full list + $html = getSimpleHTMLDOM($category->href); - $category = explode('/', $uri)[1]; + foreach($html->find('.channel-thumbnail a') as $channel) { + $name = html_entity_decode(trim($channel->title)); - if(!isset($categories) - || !array_key_exists($category, $categories) - || !in_array($uri, $categories[$category])) - $categories[$category][$name] = $uri; + // Remove unwanted entities + $name = str_replace("'", '', $name); + $name = str_replace(''', '', $name); + + $uri = $channel->href; + + $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; + } } // Build PHP array manually @@ -360,9 +354,9 @@ class InstructablesBridge extends BridgeAbstract { */ private function getCategoryAuthor($cover) { return 'href + . $cover->find('.ible-author a', 0)->href . '>' - . $cover->find('span.author a', 0)->innertext + . $cover->find('.ible-author a', 0)->innertext . ''; } } diff --git a/bridges/InternetArchiveBridge.php b/bridges/InternetArchiveBridge.php new file mode 100644 index 00000000..dca1c32a --- /dev/null +++ b/bridges/InternetArchiveBridge.php @@ -0,0 +1,293 @@ + 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', + ) + ) + ); + + const CACHE_TIMEOUT = 900; // 15 mins + + private $skipClasses = array( + 'item-ia mobile-header hidden-tiles', + 'item-ia account-ia' + ); + + public function collectData() { + + $html = getSimpleHTMLDOM($this->getURI()) + or returnServerError('Could not request: ' . $this->getURI()); + + $html = defaultLinkTo($html, $this->getURI()); + + if ($this->getInput('content') !== 'posts') { + + $detailsDivNumber = 0; + + foreach ($html->find('div.results > div[data-id]') 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++; + } + } + + 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 = self::URI . $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'] = <<Media Type: {$result->attr['data-mediatype']}
+Collection: {$collectionTitle}

+EOD; + + $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source; + + return $item; + } + + private function processReview($result) { + + $item = array(); + + $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; + } + + $item['content'] = <<Subject: {$result->find('div.review-title', 0)->plaintext}

+

{$result->find('div.hidden-lists.review' , 0)->children(1)->plaintext}

+EOD; + + $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source; + + return $item; + } + + private function processWebArchives($result) { + + $item = array(); + + $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'] = <<processUsername()} archived {$result->find('div.ttl', 0)->plaintext} +EOD; + + $item['enclosures'][] = $result->find('img.item-img', 0)->source; + + return $item; + } + + private function processCollection($result) { + + $item = array(); + + $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['content'] = ''; + + if ($result->find('img.item-img', 0)) { + $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source; + } + + return $item; + } + + 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 ($detailsDiv->find('div.C234', 0)->children(0)) { + $description = $detailsDiv->find('div.C234', 0)->children(0)->plaintext; + + $detailsDiv->find('div.C234', 0)->children(0)->innertext = ''; + } + + $topics = trim($detailsDiv->find('div.C234', 0)->plaintext); + + if (!empty($topics)) { + $topics = trim($detailsDiv->find('div.C234', 0)->plaintext); + $topics = trim(substr($topics, 7)); + + $item['categories'] = explode(',', $topics); + } + + $item['content'] = '

' . $description . '

' . $item['content']; + } + + return $item; + } + + private function processPosts($html) { + + $items = array(); + + foreach ($html->find('table.forumTable > tr') as $index => $tr) { + $item = array(); + + 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; + + $formLink = <<{$tr->find('td', 2)->children(0)->plaintext} +EOD; + + $postDate = $tr->find('td', 4)->children(0)->plaintext; + + $postPageHtml = getSimpleHTMLDOMCached($item['uri'], 3600) + or returnServerError('Could not request: ' . $item['uri']); + + $postPageHtml = defaultLinkTo($postPageHtml, $this->getURI()); + + $post = $postPageHtml->find('div.box.well.well-sm', 0); + + $parentLink = ''; + $replyLink = <<Reply +EOD; + + if ($post->find('a', 1)->innertext = 'See parent post') { + $parentLink = <<View parent post +EOD; + } + + $post->find('h1', 0)->outertext = ''; + $post->find('h2', 0)->outertext = ''; + + $item['content'] = <<{$post->innertext}

{$replyLink} - {$parentLink} - Posted in {$formLink} on {$postDate} +EOD; + + $items[] = $item; + + if (count($items) >= 10) { + break; + } + } + return $items; + } +} diff --git a/bridges/JapanExpoBridge.php b/bridges/JapanExpoBridge.php index 17901711..7906ec0d 100644 --- a/bridges/JapanExpoBridge.php +++ b/bridges/JapanExpoBridge.php @@ -19,28 +19,6 @@ class JapanExpoBridge extends BridgeAbstract { public function collectData(){ - function frenchPubDateToTimestamp($date_to_parse) { - return strtotime( - strtr( - strtolower(str_replace('Publié le ', '', $date_to_parse)), - array( - 'janvier' => 'jan', - 'février' => 'feb', - 'mars' => 'march', - 'avril' => 'apr', - 'mai' => 'may', - 'juin' => 'jun', - 'juillet' => 'jul', - 'août' => 'aug', - 'septembre' => 'sep', - 'octobre' => 'oct', - 'novembre' => 'nov', - 'décembre' => 'dec' - ) - ) - ); - } - $convert_article_images = function($matches){ if(is_array($matches) && count($matches) > 1) { return ''; @@ -82,7 +60,7 @@ class JapanExpoBridge extends BridgeAbstract { $content = $headings . $article; } else { $date_text = $element->find('span.date', 0)->plaintext; - $timestamp = frenchPubDateToTimestamp($date_text); + $timestamp = $this->frenchPubDateToTimestamp($date_text); $title = trim($element->find('span._title', 0)->plaintext); $content = 'find('article') or returnServerError('Unable to find articles!'); + $limit = $this->getInput('limit') ?: 0; + // Go through all articles foreach($articles as $article) { @@ -116,7 +134,7 @@ class KununuBridge extends BridgeAbstract { $item = array(); $item['author'] = $this->extractArticleAuthorPosition($article); - $item['timestamp'] = strtotime($date); + $item['timestamp'] = strtotime($date->content); $item['title'] = $rating->getAttribute('aria-label') . ' : ' . strip_tags($summary->innertext); @@ -131,6 +149,8 @@ class KununuBridge extends BridgeAbstract { $this->items[] = $item; + if ($limit > 0 && count($this->items) >= $limit) break; + } } @@ -175,7 +195,32 @@ class KununuBridge extends BridgeAbstract { $description = $article->find('[itemprop=reviewBody]', 0) or returnServerError('Cannot find article description!'); - return $description->innertext; + $retVal = $description->innertext; + + if($this->getInput('include_ratings') + && ($ratings = $article->find('.review-ratings .rating-group'))) { + $retVal .= (empty($retVal) ? '' : '
') . ''; + foreach($ratings as $rating) { + $retVal .= << + +EOD; + } + $retVal .= '
{$rating->find('.rating-title', 0)->plaintext} + {$rating->find('.rating-badge', 0)->plaintext} +
'; + } + + if($this->getInput('include_benefits') + && ($benefits = $article->find('benefit'))) { + $retVal .= (empty($retVal) ? '' : '
') . '
    '; + foreach($benefits as $benefit) { + $retVal .= "
  • {$benefit->plaintext}
  • "; + } + $retVal .= '
'; + } + + return $retVal; } /** diff --git a/bridges/LaCentraleBridge.php b/bridges/LaCentraleBridge.php new file mode 100644 index 00000000..baaaa582 --- /dev/null +++ b/bridges/LaCentraleBridge.php @@ -0,0 +1,477 @@ + array( + 'name' => 'Type de véhicule', + 'type' => 'list', + 'values' => array( + 'Voiture' => 'car', + 'Camion/Pickup' => 'truck', + 'Moto' => 'moto', + 'Scooter' => 'scooter', + 'Quad' => 'quad', + 'Caravane/Camping-car' => 'mobileHome' + ) + ), + 'brand' => array( + 'name' => 'Marque', + 'type' => 'list', + 'values' => array( + '' => '', + 'ABARTH' => 'ABARTH', + 'AC' => 'AC', + 'AIXAM' => 'AIXAM', + 'ALFA ROMEO' => 'ALFA ROMEO', + 'ALKE' => 'ALKE', + 'ALPINA' => 'ALPINA', + 'ALPINE' => 'ALPINE', + 'AMC' => 'AMC', + 'ANAIG' => 'ANAIG', + 'APRILIA' => 'APRILIA', + 'ARIEL' => 'ARIEL', + 'ASTON MARTIN' => 'ASTON MARTIN', + 'AUDI' => 'AUDI', + 'AUSTIN HEALEY' => 'AUSTIN HEALEY', + 'AUSTIN' => 'AUSTIN', + 'AUTOBIANCHI' => 'AUTOBIANCHI', + 'AVINTON' => 'AVINTON', + 'BELLIER' => 'BELLIER', + 'BENELLI' => 'BENELLI', + 'BENTLEY' => 'BENTLEY', + 'BETA' => 'BETA', + 'BMW' => 'BMW', + 'BOLLORE' => 'BOLLORE', + 'BRIXTON' => 'BRIXTON', + 'BUELL' => 'BUELL', + 'BUGATTI' => 'BUGATTI', + 'BUICK' => 'BUICK', + 'BULLIT' => 'BULLIT', + 'CADILLAC' => 'CADILLAC', + 'CASALINI' => 'CASALINI', + 'CATERHAM' => 'CATERHAM', + 'CHATENET' => 'CHATENET', + 'CHEVROLET' => 'CHEVROLET', + 'CHRYSLER' => 'CHRYSLER', + 'CHUNLAN' => 'CHUNLAN', + 'CITROEN' => 'CITROEN', + 'COURB' => 'COURB', + 'CR&S' => 'CR&S', + 'CUPRA' => 'CUPRA', + 'CYCLONE' => 'CYCLONE', + 'DACIA' => 'DACIA', + 'DAELIM' => 'DAELIM', + 'DAEWOO' => 'DAEWOO', + 'DAF' => 'DAF', + 'DAIHATSU' => 'DAIHATSU', + 'DANGEL' => 'DANGEL', + 'DATSUN' => 'DATSUN', + 'DE SOTO' => 'DE SOTO', + 'DE TOMASO' => 'DE TOMASO', + 'DERBI' => 'DERBI', + 'DEVINCI' => 'DEVINCI', + 'DODGE' => 'DODGE', + 'DONKERVOORT' => 'DONKERVOORT', + 'DS' => 'DS', + 'DUCATI' => 'DUCATI', + 'DUCATY' => 'DUCATY', + 'DUE' => 'DUE', + 'ENFIELD' => 'ENFIELD', + 'EXCALIBUR' => 'EXCALIBUR', + 'FACEL VEGA' => 'FACEL VEGA', + 'FANTIC MOTOR' => 'FANTIC MOTOR', + 'FERRARI' => 'FERRARI', + 'FIAT' => 'FIAT', + 'FISKER' => 'FISKER', + 'FORD' => 'FORD', + 'FUSO' => 'FUSO', + 'GAS GAS' => 'GAS GAS', + 'GILERA' => 'GILERA', + 'GMC' => 'GMC', + 'GOWINN' => 'GOWINN', + 'GRANDIN' => 'GRANDIN', + 'HARLEY DAVIDSON' => 'HARLEY DAVIDSON', + 'HOMMELL' => 'HOMMELL', + 'HONDA' => 'HONDA', + 'HUMMER' => 'HUMMER', + 'HUSABERG' => 'HUSABERG', + 'HUSQVARNA' => 'HUSQVARNA', + 'HYOSUNG' => 'HYOSUNG', + 'HYUNDAI' => 'HYUNDAI', + 'INDIAN' => 'INDIAN', + 'INFINITI' => 'INFINITI', + 'INNOCENTI' => 'INNOCENTI', + 'ISUZU' => 'ISUZU', + 'IVECO' => 'IVECO', + 'JAGUAR' => 'JAGUAR', + 'JDM SIMPA' => 'JDM SIMPA', + 'JEEP' => 'JEEP', + 'JENSEN' => 'JENSEN', + 'JIAYUAN' => 'JIAYUAN', + 'KAWASAKI' => 'KAWASAKI', + 'KEEWAY' => 'KEEWAY', + 'KIA' => 'KIA', + 'KSR' => 'KSR', + 'KTM' => 'KTM', + 'KYMCO' => 'KYMCO', + 'LADA' => 'LADA', + 'LAMBORGHINI' => 'LAMBORGHINI', + 'LANCIA' => 'LANCIA', + 'LAND ROVER' => 'LAND ROVER', + 'LEXUS' => 'LEXUS', + 'LIGIER' => 'LIGIER', + 'LINCOLN' => 'LINCOLN', + 'LONDON TAXI COMPANY' => 'LONDON TAXI COMPANY', + 'LOTUS' => 'LOTUS', + 'MAGPOWER' => 'MAGPOWER', + 'MAN' => 'MAN', + 'MASAI' => 'MASAI', + 'MASERATI' => 'MASERATI', + 'MASH' => 'MASH', + 'MATRA' => 'MATRA', + 'MAYBACH' => 'MAYBACH', + 'MAZDA' => 'MAZDA', + 'MCLAREN' => 'MCLAREN', + 'MEGA' => 'MEGA', + 'MERCEDES' => 'MERCEDES', + 'MERCEDES-AMG' => 'MERCEDES-AMG', + 'MERCURY' => 'MERCURY', + 'MEYERS MANX' => 'MEYERS MANX', + 'MG' => 'MG', + 'MIA ELECTRIC' => 'MIA ELECTRIC', + 'MICROCAR' => 'MICROCAR', + 'MINAUTO' => 'MINAUTO', + 'MINI' => 'MINI', + 'MITSUBISHI' => 'MITSUBISHI', + 'MORGAN' => 'MORGAN', + 'MORRIS' => 'MORRIS', + 'MOTO GUZZI' => 'MOTO GUZZI', + 'MOTO MORINI' => 'MOTO MORINI', + 'MOTOBECANE' => 'MOTOBECANE', + 'MPM MOTORS' => 'MPM MOTORS', + 'MV AGUSTA' => 'MV AGUSTA', + 'NISSAN' => 'NISSAN', + 'NORTON' => 'NORTON', + 'NSU' => 'NSU', + 'OLDSMOBILE' => 'OLDSMOBILE', + 'OPEL' => 'OPEL', + 'ORCAL' => 'ORCAL', + 'OSSA' => 'OSSA', + 'PACKARD' => 'PACKARD', + 'PANTHER' => 'PANTHER', + 'PEUGEOT' => 'PEUGEOT', + 'PGO' => 'PGO', + 'PIAGGIO' => 'PIAGGIO', + 'PLYMOUTH' => 'PLYMOUTH', + 'POLARIS' => 'POLARIS', + 'PONTIAC' => 'PONTIAC', + 'PORSCHE' => 'PORSCHE', + 'REALM' => 'REALM', + 'REGAL RAPTOR' => 'REGAL RAPTOR', + 'RENAULT' => 'RENAULT', + 'RIEJU' => 'RIEJU', + 'ROLLS ROYCE' => 'ROLLS ROYCE', + 'ROVER' => 'ROVER', + 'ROYAL ENFIELD' => 'ROYAL ENFIELD', + 'SAAB' => 'SAAB', + 'SANTANA' => 'SANTANA', + 'SCANIA' => 'SCANIA', + 'SEAT' => 'SEAT', + 'SECMA' => 'SECMA', + 'SHELBY' => 'SHELBY', + 'SHERCO' => 'SHERCO', + 'SIMCA' => 'SIMCA', + 'SKODA' => 'SKODA', + 'SMART' => 'SMART', + 'SPYKER' => 'SPYKER', + 'SSANGYONG' => 'SSANGYONG', + 'STUDEBAKER' => 'STUDEBAKER', + 'SUBARU' => 'SUBARU', + 'SUNBEAM' => 'SUNBEAM', + 'SUZUKI' => 'SUZUKI', + 'SWM' => 'SWM', + 'SYM' => 'SYM', + 'TALBOT SIMCA' => 'TALBOT SIMCA', + 'TALBOT' => 'TALBOT', + 'TEILHOL' => 'TEILHOL', + 'TESLA' => 'TESLA', + 'TM' => 'TM', + 'TNT MOTOR' => 'TNT MOTOR', + 'TOYOTA' => 'TOYOTA', + 'TRIUMPH' => 'TRIUMPH', + 'TVR' => 'TVR', + 'VAUXHALL' => 'VAUXHALL', + 'VESPA' => 'VESPA', + 'VICTORY' => 'VICTORY', + 'VOLKSWAGEN' => 'VOLKSWAGEN', + 'VOLVO' => 'VOLVO', + 'VOXAN' => 'VOXAN', + 'WIESMANN' => 'WIESMANN', + 'YAMAHA' => 'YAMAHA', + 'YCF' => 'YCF', + 'ZERO' => 'ZERO', + 'ZONGSHEN' => 'ZONGSHEN' + ) + ), + 'model' => array( + 'name' => 'Modèle', + 'type' => 'text', + 'title' => 'Get the exact name on LaCentrale' + ), + 'versions' => array( + 'name' => 'Version(s)', + 'type' => 'text', + 'title' => 'Get the exact name(s) on LaCentrale. Separate by comma' + ), + 'category' => array( + 'name' => 'Catégorie', + 'type' => 'list', + 'values' => array( + '' => '', + 'Voiture' => array( + '4x4, SUV & Crossover' => '47', + 'Citadine' => '40', + 'Berline' => '41_42', + 'Break' => '43', + 'Cabriolet' => '46', + 'Coupé' => '45', + 'Monospace' => '44', + 'Bus et minibus' => '82', + 'Fourgonnette' => '85', + 'Fourgon (< 3,5 tonnes)' => '81', + 'Pick-up' => '50', + 'Voiture société, commerciale' => '80', + 'Sans permis' => '48', + 'Camion (> 3,5 tonnes)' => '83', + ), + 'Camion/Pickup' => array( + 'Camion (> 3,5 tonnes)' => '83', + 'Fourgon (< 3,5 tonnes)' => '81', + 'Bus et minibus' => '82', + 'Fourgonnette' => '85', + 'Pick-up' => '50', + 'Voiture société, commerciale' => '80' + ), + 'Moto' => array( + 'Custom' => '60', + 'Offroad' => '61', + 'Roadster' => '62', + 'GT' => '63', + 'Mini moto' => '64', + 'Mobylette' => '65', + 'Supermotard' => '66', + 'Trail' => '67', + 'Side-car' => '69', + 'Sportive' => '68' + ), + 'Caravane/Camping-car' => array( + 'Caravane' => '423', + 'Profilé' => '506', + 'Fourgon aménagé' => '507', + 'Intégral' => '508', + 'Capucine' => '510' + ) + ) + ), + 'pricemin' => array( + 'name' => 'Prix min', + 'type' => 'number' + ), + 'pricemax' => array( + 'name' => 'Prix max', + 'type' => 'number' + ), + 'location' => array( + 'name' => 'CP ou département', + 'type' => 'number', + 'title' => 'Only one' + ), + 'distance' => array( + 'name' => 'Rayon de recherche', + 'type' => 'list', + 'values' => array( + '' => '', + '10 km' => '1', + '20 km' => '2', + '50 km' => '3', + '100 km' => '4', + '200 km' => '5' + ) + ), + 'region' => array( + 'name' => 'Région', + 'type' => 'list', + 'values' => array( + '' => '', + 'Auvergne-Rhône-Alpes' => 'FR-ARA', + 'Bourgogne-Franche-Comté' => 'FR-BFC', + 'Bretagne' => 'FR-BRE', + 'Centre-Val de Loire' => 'FR-CVL', + 'Corse' => 'FR-COR', + 'Grand Est' => 'FR-GES', + 'Hauts-de-France' => 'FR-HDF', + 'Île-de-France' => 'FR-IDF', + 'Normandie' => 'FR-NOR', + 'Nouvelle-Aquitaine' => 'FR-PAC', + 'Occitanie' => 'FR-PDL', + 'Pays de la Loire' => 'FR-OCC', + 'Provence-Alpes-Côte d\'Azur' => 'FR-NAQ' + ) + ), + 'mileagemin' => array( + 'name' => 'Kilométrage min', + 'type' => 'number' + ), + 'mileagemax' => array( + 'name' => 'Kilométrage max', + 'type' => 'number' + ), + 'yearmin' => array( + 'name' => 'Année min', + 'type' => 'number' + ), + 'yearmax' => array( + 'name' => 'Année max', + 'type' => 'number' + ), + 'cubiccapacitymin' => array( + 'name' => 'Cylindrée min', + 'type' => 'number' + ), + 'cubiccapacitymax' => array( + 'name' => 'Cylindrée max', + 'type' => 'number' + ), + 'fuel' => array( + 'name' => 'Énergie', + 'type' => 'list', + 'values' => array( + '' => '', + 'Diesel' => 'dies', + 'Essence' => 'ess', + 'Électrique' => 'elec', + 'Hybride' => 'hyb', + 'GPL' => 'gpl', + 'Bioéthanol' => 'eth', + 'Autre' => 'alt' + ) + ), + 'gearbox' => array( + 'name' => 'Boite de vitesse', + 'type' => 'list', + 'values' => array( + '' => '', + 'Boite automatique' => 'AUTO', + 'Boite mécanique' => 'MANUAL' + ) + ), + 'doors' => array( + 'name' => 'Nombre de portes', + 'type' => 'list', + 'values' => array( + '' => '', + '2 portes' => '2', + '3 portes' => '3', + '4 portes' => '4', + '5 portes' => '5', + '6 portes ou plus' => '6' + ) + ), + 'firsthand' => array( + 'name' => 'Première main', + 'type' => 'checkbox' + ), + 'seller' => array( + 'name' => 'Vendeur', + 'type' => 'list', + 'values' => array( + '' => '', + 'Particulier' => 'PART', + 'Professionel' => 'PRO' + ) + ), + 'sort' => array( + 'name' => 'Tri', + 'type' => 'list', + 'values' => array( + 'Prix (croissant)' => 'priceAsc', + 'Prix (décroissant)' => 'priceDesc', + 'Marque (croissant)' => 'makeAsc', + 'Marque (décroissant)' => 'makeDesc', + 'Kilométrage (croissant)' => 'mileageAsc', + 'Kilométrage (décroissant)' => 'mileageDesc', + 'Année (croissant)' => 'yearAsc', + 'Année (décroissant)' => 'yearDesc', + 'Département (croissant)' => 'visitPlaceAsc', + 'Département (décroissant)' => 'visitPlaceDesc' + ) + ), + )); + + public function collectData(){ + // check data + if(!empty($this->getInput('distance')) + && is_null($this->getInput('location'))) { + returnClientError('You need a place ("CP ou département") to search arround.'); + } + + $params = array( + 'vertical' => $this->getInput('type'), + 'makesModelsCommercialNames' => $this->getInput('brand') . ':' . $this->getInput('model'), + 'versions' => $this->getInput('versions'), + 'categories' => $this->getInput('category'), + 'priceMin' => $this->getInput('pricemin'), + 'priceMax' => $this->getInput('pricemax'), + 'dptCp' => $this->getInput('location'), + 'distance' => $this->getInput('distance'), + 'regions' => $this->getInput('region'), + 'mileageMin' => $this->getInput('mileagemin'), + 'mileageMax' => $this->getInput('mileagemax'), + 'yearMin' => $this->getInput('yearmin'), + 'yearMax' => $this->getInput('yearmax'), + 'cubicMin' => $this->getInput('cubiccapacitymin'), + 'cubicMax' => $this->getInput('cubiccapacitymax'), + 'energies' => $this->getInput('fuel'), + 'firstHand' => $this->getInput('firsthand') ? 'true' : 'false', + 'gearbox' => $this->getInput('gearbox'), + 'doors' => $this->getInput('doors'), + 'sortBy' => $this->getInput('sort') + ); + $url = self::URI . 'listing?' . http_build_query($params); + $html = getSimpleHTMLDOM($url) + or returnServerError('Could not request LaCentrale.'); + + foreach($html->find('.linkAd') as $element) { + + $item = array(); + $item['uri'] = trim(self::URI, '/') . $element->href; + $item['title'] = $element->find('.brandModel', 0)->plaintext; + $item['sellerType'] = $element->find('.typeSeller', 0)->plaintext; + $item['author'] = $item['sellerType']; + $item['version'] = $element->find('.version', 0)->plaintext; + $item['price'] = $element->find('.fieldPrice', 0)->plaintext; + $item['year'] = $element->find('.fieldYear', 0)->plaintext; + $item['mileage'] = $element->find('.fieldMileage', 0)->plaintext; + $item['departement'] = str_replace(',', '', $element->find('.dptCont', 0)->plaintext); + $item['thumbnail'] = $element->find('.imgContent img', 0)->src; + $item['enclosures'] = array($item['thumbnail']); + + $item['content'] = ' + +
Variation : ' . $item['version'] + . '
Prix : ' . $item['price'] + . '
Année : ' . $item['year'] + . '
Kilométrage : ' . $item['mileage'] + . '
Département : ' . $item['departement'] + . '
Type de vendeur : ' . $item['sellerType']; + + $this->items[] = $item; + + } + } +} diff --git a/bridges/LeBonCoinBridge.php b/bridges/LeBonCoinBridge.php index 36196cb1..fc1432e3 100644 --- a/bridges/LeBonCoinBridge.php +++ b/bridges/LeBonCoinBridge.php @@ -356,6 +356,7 @@ class LeBonCoinBridge extends BridgeAbstract { $data = $this->buildRequestJson(); $header = array( + 'User-Agent: LBC;Android;Null;Null;Null;Null;Null;Null;Null;Null', 'Content-Type: application/json', 'Content-Length: ' . strlen($data), 'api_key: ' . self::$LBC_API_KEY @@ -430,11 +431,11 @@ class LeBonCoinBridge extends BridgeAbstract { ); if($this->getInput('region') != '') { - $requestJson->filters->location['regions'] = [$this->getInput('region')]; + $requestJson->filters->location['regions'] = array($this->getInput('region')); } if($this->getInput('department') != '') { - $requestJson->filters->location['departments'] = [$this->getInput('department')]; + $requestJson->filters->location['departments'] = array($this->getInput('department')); } if($this->getInput('cities') != '') { @@ -466,7 +467,7 @@ class LeBonCoinBridge extends BridgeAbstract { } if($this->getInput('estate') != '') { - $requestJson->filters->enums['real_estate_type'] = [$this->getInput('estate')]; + $requestJson->filters->enums['real_estate_type'] = array($this->getInput('estate')); } if($this->getInput('roomsmin') != '' @@ -525,7 +526,7 @@ class LeBonCoinBridge extends BridgeAbstract { } if($this->getInput('fuel') != '') { - $requestJson->filters->enums['fuel'] = [$this->getInput('fuel')]; + $requestJson->filters->enums['fuel'] = array($this->getInput('fuel')); } $requestJson->limit = 30; diff --git a/bridges/ListverseBridge.php b/bridges/ListverseBridge.php new file mode 100644 index 00000000..f597c0b4 --- /dev/null +++ b/bridges/ListverseBridge.php @@ -0,0 +1,22 @@ +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; + } +} diff --git a/bridges/MangareaderBridge.php b/bridges/MangareaderBridge.php index 9ecb0feb..a41113a3 100644 --- a/bridges/MangareaderBridge.php +++ b/bridges/MangareaderBridge.php @@ -3,7 +3,7 @@ class MangareaderBridge extends BridgeAbstract { const MAINTAINER = 'logmanoriginal'; const NAME = 'Mangareader Bridge'; - const URI = 'http://www.mangareader.net'; + const URI = 'https://www.mangareader.net'; const CACHE_TIMEOUT = 10800; // 3h const DESCRIPTION = 'Returns the latest updates, popular mangas or manga updates (new chapters)'; diff --git a/bridges/MastodonBridge.php b/bridges/MastodonBridge.php new file mode 100644 index 00000000..9e131b7d --- /dev/null +++ b/bridges/MastodonBridge.php @@ -0,0 +1,89 @@ + array( + 'name' => 'Canonical username (ex : @sebsauvage@framapiaf.org)', + 'required' => true, + ), + 'norep' => array( + 'name' => 'Without replies', + 'type' => 'checkbox', + 'title' => 'Only return initial toots' + ), + 'noboost' => array( + 'name' => 'Without boosts', + 'required' => false, + 'type' => 'checkbox', + 'title' => 'Hide boosts' + ) + )); + + public function getName(){ + switch($this->queriedContext) { + case 'By username': + return $this->getInput('canusername'); + default: return parent::getName(); + } + } + + protected function parseItem($newItem){ + $item = parent::parseItem($newItem); + + $content = str_get_html($item['content']); + $title = str_get_html($item['title']); + + $item['title'] = $content->plaintext; + + if(strlen($item['title']) > 75) { + $item['title'] = substr($item['title'], 0, strpos(wordwrap($item['title'], 75), "\n")) . '...'; + } + + if(strpos($title, 'shared a status by') !== false) { + if($this->getInput('noboost')) { + return null; + } + + preg_match('/shared a status by (\S{0,})/', $title, $matches); + $item['title'] = 'Boost ' . $matches[1] . ' ' . $item['title']; + $item['author'] = $matches[1]; + } else { + $item['author'] = $this->getInput('canusername'); + } + + // Check if it's a initial toot or a response + if($this->getInput('norep') && preg_match('/^@.+/', trim($content->plaintext))) { + return null; + } + + return $item; + } + + 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]; + } + + public function getURI(){ + if($this->getInput('canusername')) + return 'https://' . $this->getInstance() . '/users/' . $this->getUsername() . '.atom'; + + return parent::getURI(); + } + + public function collectData(){ + return $this->collectExpandableDatas($this->getURI()); + } +} diff --git a/bridges/MediapartBridge.php b/bridges/MediapartBridge.php index 15d1d3ea..f7fff4ab 100644 --- a/bridges/MediapartBridge.php +++ b/bridges/MediapartBridge.php @@ -30,29 +30,34 @@ class MediapartBridge extends FeedExpander { protected function parseItem($newsItem) { $item = parent::parseItem($newsItem); - // 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 = array(); + $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', + array(), + $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; diff --git a/bridges/N26Bridge.php b/bridges/N26Bridge.php index dd1c423c..ac43756b 100644 --- a/bridges/N26Bridge.php +++ b/bridges/N26Bridge.php @@ -15,11 +15,11 @@ class N26Bridge extends BridgeAbstract public function collectData() { - $html = getSimpleHTMLDOM(self::URI . '/en-fr/blog-archive') + $html = getSimpleHTMLDOM(self::URI . '/en-eu/blog-archive') or returnServerError('Error while downloading the website content'); - foreach($html->find('div.ga') as $article) { - $item = []; + foreach($html->find('div[class="ag ah ai aj bs bt dx ea fo gx ie if ih ii ij ik s"]') as $article) { + $item = array(); $item['uri'] = self::URI . $article->find('h2 a', 0)->href; $item['title'] = $article->find('h2 a', 0)->plaintext; @@ -27,9 +27,9 @@ class N26Bridge extends BridgeAbstract $fullArticle = getSimpleHTMLDOM($item['uri']) or returnServerError('Error while downloading the full article'); - $dateElement = $fullArticle->find('span[class="fk fl de ch fm by"]', 0); + $dateElement = $fullArticle->find('time', 0); $item['timestamp'] = strtotime($dateElement->plaintext); - $item['content'] = $fullArticle->find('main article', 0)->innertext; + $item['content'] = $fullArticle->find('div[class="af ag ah ai an"]', 1)->innertext; $this->items[] = $item; } diff --git a/bridges/NFLRUSBridge.php b/bridges/NFLRUSBridge.php new file mode 100644 index 00000000..739f4ab5 --- /dev/null +++ b/bridges/NFLRUSBridge.php @@ -0,0 +1,60 @@ + 'January', + 'Февраля' => 'February', + 'Марта' => 'March', + 'Апреля' => 'April', + 'Мая' => 'May', + 'Июня' => 'June', + 'Июля' => 'July', + 'Августа' => 'August', + 'Сентября' => 'September', + 'Октября' => 'October', + 'Ноября' => 'November', + 'Декабря' => 'December', + ); + + if (isset($months[$month])) { + return $months[$month]; + } + return false; + } + + private function extractArticleTimestamp($article) { + $time = $article->find('time', 0); + if($time) { + $timestring = trim($time->plaintext); + $parts = explode(' ', $timestring); + $month = $this->getEnglishMonth($parts[1]); + if ($month) { + $timestring = $parts[0] . ' ' . $month . ' ' . $parts[2]; + return strtotime($timestring); + } + } + return 0; + } + + public function collectData() { + $html = getSimpleHTMLDOM(self::URI) + or returnServerError('Unable to get any articles from NFLRUS'); + $html = defaultLinkTo($html, self::URI); + + foreach($html->find('article') as $article) { + $item = array(); + $item['uri'] = $article->find('.b-article__title a', 0)->href; + $item['title'] = $article->find('.b-article__title a', 0)->plaintext; + $item['author'] = $article->find('.link-author', 0)->plaintext; + $item['timestamp'] = $this->extractArticleTimestamp($article); + $item['content'] = $article->find('div', 0)->innertext; + $this->items[] = $item; + } + } +} diff --git a/bridges/NYTBridge.php b/bridges/NYTBridge.php new file mode 100644 index 00000000..687d0889 --- /dev/null +++ b/bridges/NYTBridge.php @@ -0,0 +1,26 @@ +collectExpandableDatas('https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', 15); + } + + protected function parseItem($newsItem){ + $item = parent::parseItem($newsItem); + // $articlePage gets the entire page's contents + $articlePage = getSimpleHTMLDOM($newsItem->link); + // figure contain's the main article image + $article = $articlePage->find('figure', 0); + // p > css-exrw3m has the actual article + foreach($articlePage->find('p.css-exrw3m') as $element) + $article = $article . $element; + $item['content'] = $article; + return $item; + } +} diff --git a/bridges/NationalGeographicBridge.php b/bridges/NationalGeographicBridge.php new file mode 100644 index 00000000..dfccd25c --- /dev/null +++ b/bridges/NationalGeographicBridge.php @@ -0,0 +1,194 @@ + 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 (takes longer)' + ) + ) + ); + + private $topicName = ''; + + public function getURI() { + switch ($this->queriedContext) { + case self::CONTEXT_BY_TOPIC: { + return self::URI . $this->getInput(self::PARAMETER_TOPIC); + } break; + default: { + return parent::getURI(); + } + } + } + + public function collectData() { + $this->topicName = $this->getTopicName($this->getInput(self::PARAMETER_TOPIC)); + + switch($this->topicName) { + case self::TOPIC_MAGAZINE: { + return $this->collectMagazine(); + } break; + case self::TOPIC_LATEST_STORIES: { + return $this->collectLatestStories(); + } break; + default: { + returnServerError('Unknown topic: "' . $this->topicName . '"'); + } + } + } + + public function getName() { + switch ($this->queriedContext) { + case self::CONTEXT_BY_TOPIC: { + return static::NAME . ': ' . $this->topicName; + } break; + 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() { + $uri = $this->getURI(); + + $html = getSimpleHTMLDOM($uri) + or returnServerError('Could not request ' . $uri); + + $script = $html->find('#lead-component script')[0]; + + $json = json_decode($script->innertext, true); + + // This is probably going to break in the future, fix it then :) + foreach($json['body']['0']['multilayout_promo_beta']['stories'] as $story) { + $this->addStory($story); + } + } + + private function collectLatestStories() { + $uri = self::URI . 'latest-stories/_jcr_content/content/hubfeed.promo-hub-feed-all-stories.json'; + + $json_raw = getContents($uri) + or returnServerError('Could not request ' . $uri); + + foreach(json_decode($json_raw, true) as $story) { + $this->addStory($story); + } + } + + private function addStory($story) { + $title = 'Unknown title'; + $content = ''; + + foreach($story['components'] as $component) { + switch($component['content_type']) { + case 'title': { + $title = $component['title']['text']; + } break; + case 'dek': { + $content = $component['dek']['text']; + } break; + } + } + + $item = array(); + + $item['uri'] = $story['uri']; + $item['title'] = $title; + + // if full article is requested! + if ($this->getInput(self::PARAMETER_FULL_ARTICLE)) + $item['content'] = $this->getFullArticle($item['uri']); + else + $item['content'] = $content; + + if (isset($story['promo_image'])) { + switch($story['promo_image']['content_type']) { + case 'image': { + $item['enclosures'][] = $story['promo_image']['image']['uri']; + } break; + } + } + + if (isset($story['lead_media'])) { + $media = $story['lead_media']; + switch($media['content_type']) { + case 'image': { + // Don't add if promo_image was added + if (empty($item['enclosures'])) + $item['enclosures'][] = $media['image']['uri']; + } break; + case 'image_gallery': { + foreach($media['image_gallery']['images'] as $image) { + $item['enclosures'][] = $image['uri']; + } + } break; + } + } + + $this->items[] = $item; + } + + private function getFullArticle($uri) { + $html = getSimpleHTMLDOMCached($uri) + or returnServerError('Could not load ' . $uri); + + $html = defaultLinkTo($html, $uri); + + $content = ''; + + foreach($html->find(' + .content > .smartbody.text, + .content > .section.image script[type="text/json"], + .content > .section.image span[itemprop="caption"], + .content > .section.inline script[type="text/json"] + ') as $element) { + if ($element->tag === 'script') { + $json = json_decode($element->innertext, true); + if (isset($json['src'])) { + $content .= '' . $json['alt'] . ''; + } elseif (isset($json['galleryType']) && isset($json['endpoint'])) { + $doc = getContents($json['endpoint']) + or returnServerError('Could not load ' . $json['endpoint']); + $json = json_decode($doc, true); + foreach($json['items'] as $item) { + $content .= '

' . $item['caption'] . '

'; + $content .= '' . $item['caption'] . ''; + } + } + } else { + $content .= $element->outertext; + } + } + + return $content; + } +} diff --git a/bridges/NiceMatinBridge.php b/bridges/NiceMatinBridge.php index 117c7794..b0af7608 100644 --- a/bridges/NiceMatinBridge.php +++ b/bridges/NiceMatinBridge.php @@ -3,7 +3,7 @@ class NiceMatinBridge extends FeedExpander { const MAINTAINER = 'pit-fgfjiudghdf'; const NAME = 'NiceMatin'; - const URI = 'http://www.nicematin.com/'; + const URI = 'https://www.nicematin.com/'; const DESCRIPTION = 'Returns the 10 newest posts from NiceMatin (full text)'; public function collectData(){ diff --git a/bridges/NineGagBridge.php b/bridges/NineGagBridge.php index e726c732..939ff387 100644 --- a/bridges/NineGagBridge.php +++ b/bridges/NineGagBridge.php @@ -17,6 +17,15 @@ class NineGagBridge extends BridgeAbstract { 'Fresh' => 'fresh', ), ), + 'video' => array( + 'name' => 'Filter Video', + 'type' => 'list', + 'values' => array( + 'NotFiltred' => 'none', + 'VideoFiltred' => 'without', + 'VideoOnly' => 'only', + ), + ), 'p' => array( 'name' => 'Pages', 'type' => 'number', @@ -121,13 +130,32 @@ class NineGagBridge extends BridgeAbstract { } foreach ($posts as $post) { - $item['uri'] = $post['url']; - $item['title'] = $post['title']; - $item['content'] = self::getContent($post); - $item['categories'] = self::getCategories($post); - $item['timestamp'] = self::getTimestamp($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; + } - $this->items[] = $item; + if (!$AvoidElement) { + $item['uri'] = $post['url']; + $item['title'] = $post['title']; + $item['content'] = self::getContent($post); + $item['categories'] = self::getCategories($post); + $item['timestamp'] = self::getTimestamp($post); + + $this->items[] = $item; + } } } diff --git a/bridges/NovelUpdatesBridge.php b/bridges/NovelUpdatesBridge.php index 729eb485..05acd8ef 100644 --- a/bridges/NovelUpdatesBridge.php +++ b/bridges/NovelUpdatesBridge.php @@ -3,7 +3,7 @@ class NovelUpdatesBridge extends BridgeAbstract { const MAINTAINER = 'albirew'; const NAME = 'Novel Updates'; - const URI = 'http://www.novelupdates.com/'; + const URI = 'https://www.novelupdates.com/'; const CACHE_TIMEOUT = 21600; // 6h const DESCRIPTION = 'Returns releases from Novel Updates'; const PARAMETERS = array( array( diff --git a/bridges/WhydBridge.php b/bridges/OpenwhydBridge.php similarity index 81% rename from bridges/WhydBridge.php rename to bridges/OpenwhydBridge.php index 04d0b30e..f80cb066 100644 --- a/bridges/WhydBridge.php +++ b/bridges/OpenwhydBridge.php @@ -1,9 +1,9 @@ getInput('u'))) == 24) { // is input the userid ? $html = getSimpleHTMLDOM( - self::URI . 'u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u')) + self::URI . '/u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u')) ) or returnServerError('No results for this query.'); } else { // input may be the username $html = getSimpleHTMLDOM( - self::URI . 'search?q=' . urlencode($this->getInput('u')) + self::URI . '/search?q=' . urlencode($this->getInput('u')) ) or returnServerError('No results for this query.'); for($j = 0; $j < 5; $j++) { @@ -57,6 +56,6 @@ class WhydBridge extends BridgeAbstract { } public function getName(){ - return (!empty($this->userName) ? $this->userName . ' - ' : '') . 'Whyd Bridge'; + return (!empty($this->userName) ? $this->userName . ' - ' : '') . 'Openwhyd Bridge'; } } diff --git a/bridges/ParuVenduImmoBridge.php b/bridges/ParuVenduImmoBridge.php index a2e2b335..7b2825bc 100644 --- a/bridges/ParuVenduImmoBridge.php +++ b/bridges/ParuVenduImmoBridge.php @@ -3,7 +3,7 @@ class ParuVenduImmoBridge extends BridgeAbstract { const MAINTAINER = 'polo2ro'; const NAME = 'Paru Vendu Immobilier'; - const URI = 'http://www.paruvendu.fr'; + const URI = 'https://www.paruvendu.fr'; const CACHE_TIMEOUT = 10800; // 3h const DESCRIPTION = 'Returns the ads from the first page of search result.'; diff --git a/bridges/PatreonBridge.php b/bridges/PatreonBridge.php new file mode 100644 index 00000000..57727a3e --- /dev/null +++ b/bridges/PatreonBridge.php @@ -0,0 +1,203 @@ + array( + 'name' => 'Creator', + 'type' => 'text', + 'required' => true, + 'title' => 'Creator name as seen in their page URL' + ) + )); + + public function collectData(){ + $html = getSimpleHTMLDOMCached($this->getURI(), 86400) + or returnServerError('Failed to load creator page at ' . $this->getURI()); + $regex = '#/api/campaigns/([0-9]+)#'; + if(preg_match($regex, $html->save(), $matches) > 0) { + $campaign_id = $matches[1]; + } else { + returnServerError('Could not find campaign ID'); + } + + $query = array( + 'include' => implode(',', array( + 'user', + 'attachments', + 'user_defined_tags', + //'campaign', + //'poll.choices', + //'poll.current_user_responses.user', + //'poll.current_user_responses.choice', + //'poll.current_user_responses.poll', + //'access_rules.tier.null', + //'images.null', + //'audio.null' + )), + 'fields' => array( + 'post' => implode(',', array( + //'change_visibility_at', + //'comment_count', + 'content', + //'current_user_can_delete', + //'current_user_can_view', + //'current_user_has_liked', + //'embed', + 'image', + //'is_paid', + //'like_count', + //'min_cents_pledged_to_view', + //'patreon_url', + //'patron_count', + //'pledge_url', + //'post_file', + //'post_metadata', + //'post_type', + 'published_at', + 'teaser_text', + //'thumbnail_url', + 'title', + //'upgrade_url', + 'url', + //'was_posted_by_campaign_owner' + )), + 'user' => implode(',', array( + //'image_url', + 'full_name', + //'url' + )) + ), + 'filter' => array( + 'contains_exclusive_posts' => true, + 'is_draft' => false, + 'campaign_id' => $campaign_id + ), + 'sort' => '-published_at' + ); + $posts = $this->apiGet('posts', $query); + + foreach($posts->data as $post) { + $item = array( + 'uri' => $post->attributes->url, + 'title' => $post->attributes->title, + 'timestamp' => $post->attributes->published_at, + 'content' => '', + 'uid' => 'patreon.com/' . $post->id + ); + + $user = $this->findInclude($posts, + 'user', + $post->relationships->user->data->id); + $item['author'] = $user->full_name; + + if(isset($post->attributes->image)) + $item['content'] .= '

'; + + if(isset($post->attributes->content)) { + $item['content'] .= $post->attributes->content; + } elseif (isset($post->attributes->teaser_text)) { + $item['content'] .= '

' + . $post->attributes->teaser_text + . '

'; + } + + if(isset($post->relationships->user_defined_tags)) { + $item['categories'] = array(); + foreach($post->relationships->user_defined_tags->data as $tag) { + $attrs = $this->findInclude($posts, 'post_tag', $tag->id); + $item['categories'][] = $attrs->value; + } + } + + if(isset($post->relationships->attachments)) { + $item['enclosures'] = array(); + foreach($post->relationships->attachments->data as $attachment) { + $attrs = $this->findInclude($posts, 'attachment', $attachment->id); + $item['enclosures'][] = $attrs->url; + } + } + + $this->items[] = $item; + } + } + + /* + * Searches the "included" array in an API response and returns attributes + * for the first match. + */ + private function findInclude($data, $type, $id) { + foreach($data->included as $include) + if($include->type === $type && $include->id === $id) + return $include->attributes; + } + + private function apiGet($endpoint, $query_data = array()) { + $query_data['json-api-version'] = 1.0; + $query_data['json-api-use-default-includes'] = 0; + + $url = 'https://www.patreon.com/api/' + . $endpoint + . '?' + . http_build_query($query_data); + + /* + * Accept-Language header and the CURL cipher list are for bypassing the + * Cloudflare anti-bot protection on the Patreon API. If this ever breaks, + * here are some other project that also deal with this: + * https://github.com/mikf/gallery-dl/issues/342 + * https://github.com/daemionfox/patreon-feed/issues/7 + * https://www.patreondevelopers.com/t/api-returning-cloudflare-challenge/2025 + * https://github.com/splitbrain/patreon-rss/issues/4 + */ + $header = array( + 'Accept-Language: en-US', + 'Content-Type: application/json' + ); + $opts = array( + CURLOPT_SSL_CIPHER_LIST => implode(':', array( + 'DEFAULT', + '!DHE-RSA-CHACHA20-POLY1305' + )) + ); + + $data = json_decode(getContents($url, $header, $opts)) + or returnServerError('API request to "' . $url . '" failed.'); + + return $data; + } + + public function getName(){ + if(!is_null($this->getInput('creator'))) + return $this->getInput('creator') . ' posts'; + + return parent::getName(); + } + + public function getURI(){ + if(!is_null($this->getInput('creator'))) + return self::URI . $this->getInput('creator'); + + return parent::getURI(); + } + + public function detectParameters($url){ + $params = array(); + + // Matches e.g. https://www.patreon.com/SomeCreator + $regex = '/^(https?:\/\/)?(www\.)?patreon\.com\/([^\/&?\n]+)/'; + if(preg_match($regex, $url, $matches) > 0) { + $params['creator'] = urldecode($matches[3]); + return $params; + } + + return null; + } +} diff --git a/bridges/PickyWallpapersBridge.php b/bridges/PickyWallpapersBridge.php index 6c26df7a..488b4483 100644 --- a/bridges/PickyWallpapersBridge.php +++ b/bridges/PickyWallpapersBridge.php @@ -3,7 +3,7 @@ class PickyWallpapersBridge extends BridgeAbstract { const MAINTAINER = 'nel50n'; const NAME = 'PickyWallpapers Bridge'; - const URI = 'http://www.pickywallpapers.com/'; + const URI = 'https://www.pickywallpapers.com/'; const CACHE_TIMEOUT = 43200; // 12h const DESCRIPTION = 'Returns the latests wallpapers from PickyWallpapers'; diff --git a/bridges/PikabuBridge.php b/bridges/PikabuBridge.php index 362b87dc..a54f6bf5 100644 --- a/bridges/PikabuBridge.php +++ b/bridges/PikabuBridge.php @@ -32,6 +32,13 @@ class PikabuBridge extends BridgeAbstract { 'required' => true ), 'filter' => self::PARAMETERS_FILTER + ), + 'По пользователю' => array( + 'user' => array( + 'name' => 'Пользователь', + 'exampleValue' => 'admin', + 'required' => true + ) ) ); @@ -40,6 +47,8 @@ class PikabuBridge extends BridgeAbstract { public function getURI() { if ($this->getInput('tag')) { return self::URI . '/tag/' . rawurlencode($this->getInput('tag')) . '/' . rawurlencode($this->getInput('filter')); + } else if ($this->getInput('user')) { + return self::URI . '/@' . rawurlencode($this->getInput('user')); } else if ($this->getInput('community')) { $uri = self::URI . '/community/' . rawurlencode($this->getInput('community')); if ($this->getInput('filter') != 'hot') { @@ -101,6 +110,10 @@ class PikabuBridge extends BridgeAbstract { } } $img->outertext = ''; + + // it is assumed, that img's parents are links to post itself + // we don't need them + $img->parent()->outertext = $img->outertext; } $categories = array(); @@ -116,7 +129,10 @@ class PikabuBridge extends BridgeAbstract { $item['categories'] = $categories; $item['author'] = $post->find('.user__nick', 0)->innertext; $item['title'] = $title->plaintext; - $item['content'] = strip_tags(backgroundToImg($post->find('.story__content-inner', 0)->innertext), '

'); + $item['content'] = strip_tags( + backgroundToImg($post->find('.story__content-inner', 0)->innertext), + '

+ '); $item['uri'] = $title->href; $item['timestamp'] = strtotime($time->getAttribute('datetime')); $this->items[] = $item; diff --git a/bridges/PinterestBridge.php b/bridges/PinterestBridge.php index 2917b267..48c0cfca 100644 --- a/bridges/PinterestBridge.php +++ b/bridges/PinterestBridge.php @@ -16,12 +16,6 @@ class PinterestBridge extends FeedExpander { 'name' => 'board', 'required' => true ) - ), - 'From search' => array( - 'q' => array( - 'name' => 'Keyword', - 'required' => true - ) ) ); @@ -29,22 +23,14 @@ class PinterestBridge extends FeedExpander { return 'https://s.pinimg.com/webapp/style/images/favicon-9f8f9adf.png'; } - public function collectData(){ - switch($this->queriedContext) { - case 'By username and board': - $this->collectExpandableDatas($this->getURI() . '.rss'); - $this->fixLowRes(); - break; - case 'From search': - default: - $html = getSimpleHTMLDOMCached($this->getURI()); - $this->getSearchResults($html); - } + public function collectData() { + $this->collectExpandableDatas($this->getURI() . '.rss'); + $this->fixLowRes(); } private function fixLowRes() { - $newitems = []; + $newitems = array(); $pattern = '/https\:\/\/i\.pinimg\.com\/[a-zA-Z0-9]*x\//'; foreach($this->items as $item) { @@ -55,71 +41,21 @@ class PinterestBridge extends FeedExpander { } - private function getSearchResults($html){ - $json = json_decode($html->find('#jsInit1', 0)->innertext, true); - $results = $json['resourceDataCache'][0]['data']['results']; + public function getURI() { - foreach($results as $result) { - $item = array(); - - $item['uri'] = self::URI . $result['board']['url']; - - // Some use regular titles, others provide 'advanced' infos, a few - // provide even less info. Thus we attempt multiple options. - $item['title'] = trim($result['title']); - - if($item['title'] === '') - $item['title'] = trim($result['rich_summary']['display_name']); - - if($item['title'] === '') - $item['title'] = trim($result['grid_description']); - - $item['timestamp'] = strtotime($result['created_at']); - $item['username'] = $result['pinner']['username']; - $item['fullname'] = $result['pinner']['full_name']; - $item['avatar'] = $result['pinner']['image_small_url']; - $item['author'] = $item['username'] . ' (' . $item['fullname'] . ')'; - $item['content'] = '

' - . $item['username'] - . '
' - . $item['fullname'] - . '



' - . $result['description'] - . '

'; - - $item['enclosures'] = array($result['images']['orig']['url']); - - $this->items[] = $item; + if ($this->queriedContext === 'By username and board') { + return self::URI . '/' . urlencode($this->getInput('u')) . '/' . urlencode($this->getInput('b')); } + + return parent::getURI(); } - public function getURI(){ - switch($this->queriedContext) { - case 'By username and board': - $uri = self::URI . '/' . urlencode($this->getInput('u')) . '/' . urlencode($this->getInput('b'));// . '.rss'; - break; - case 'From search': - $uri = self::URI . '/search/?q=' . urlencode($this->getInput('q')); - break; - default: return parent::getURI(); - } - return $uri; - } + public function getName() { - public function getName(){ - switch($this->queriedContext) { - case 'By username and board': - $specific = $this->getInput('u') . ' - ' . $this->getInput('b'); - break; - case 'From search': - $specific = $this->getInput('q'); - break; - default: return parent::getName(); + if ($this->queriedContext === 'By username and board') { + return $this->getInput('u') . ' - ' . $this->getInput('b') . ' - ' . self::NAME; } - return $specific . ' - ' . self::NAME; + + return parent::getName(); } } diff --git a/bridges/PirateCommunityBridge.php b/bridges/PirateCommunityBridge.php new file mode 100644 index 00000000..fcf97b9c --- /dev/null +++ b/bridges/PirateCommunityBridge.php @@ -0,0 +1,88 @@ + array( + 'name' => 'Topic ID', + 'type' => 'number', + 'title' => 'Topic ID from topic URL. If the URL contains t=12 the ID is 12.', + 'required' => true + ))); + + private $feedName = ''; + + public function detectParameters($url){ + $parsed_url = parse_url($url); + + if($parsed_url['host'] !== 'raymanpc.com') + return null; + + parse_str($parsed_url['query'], $parsed_query); + + if($parsed_url['path'] === '/forum/viewtopic.php' + && array_key_exists('t', $parsed_query)) { + return array('t' => $parsed_query['t']); + } + + return null; + } + + public function getName() { + if(!empty($this->feedName)) + return $this->feedName; + + return parent::getName(); + } + + public function getURI(){ + if(!is_null($this->getInput('t'))) { + return self::URI + . 'forum/viewtopic.php?t=' + . $this->getInput('t') + . '&sd=d'; // sort posts decending by ate so first page has latest posts + } + + return parent::getURI(); + } + + public function collectData(){ + $html = getSimpleHTMLDOM($this->getURI()) + or returnServerError('Could not retrieve topic page at ' . $this->getURI()); + + $this->feedName = $html->find('head title', 0)->plaintext; + + foreach($html->find('.post') as $reply) { + $item = array(); + + $item['uri'] = $this->getURI() + . $reply->find('h3 a', 0)->getAttribute('href'); + + $item['title'] = $reply->find('h3 a', 0)->plaintext; + + $author_html = $reply->find('.author', 0); + // author_html contains the timestamp as text directly inside it, + // so delete all other child elements + foreach($author_html->children as $child) + $child->outertext = ''; + // Timestamps are always in UTC+1 + $item['timestamp'] = trim($author_html->innertext) . ' +01:00'; + + $item['author'] = $reply + ->find('.username, .username-coloured', 0) + ->plaintext; + + $item['content'] = defaultLinkTo($reply->find('.content', 0)->innertext, + $this->getURI()); + + $item['enclosures'] = array(); + foreach($reply->find('.attachbox img.postimage') as $img) + $item['enclosures'][] = urljoin($this->getURI(), $img->src); + + $this->items[] = $item; + } + } +} diff --git a/bridges/PlantUMLReleasesBridge.php b/bridges/PlantUMLReleasesBridge.php new file mode 100644 index 00000000..66480560 --- /dev/null +++ b/bridges/PlantUMLReleasesBridge.php @@ -0,0 +1,67 @@ + 'href', + 'src' => 'src', + 'data-original' => 'src' + ); + + private function getDomain() { + $domain = $this->getInput('domain'); + if (empty($domain)) + $domain = self::DEFAULT_DOMAIN; + if (strpos($domain, '://') === false) + $domain = 'https://' . $domain; + return $domain; + } + + public function getURI() + { + return self::URI; + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()) or returnServerError('Could not request ' . $this->getURI()); + + // Since GQ don't want simple class scrapping, let's do it the hard way and ... discover content ! + $main = $html->find('div[id=root]', 0); + foreach ($main->find('h2') as $release) { + $item = array(); + $item['author'] = self::AUTHOR; + $release_text = $release->innertext; + if (preg_match('/(.+) \((.*)\)/', $release_text, $matches)) { + $item['title'] = $matches[1]; + // And now, build the date from the date text + $item['timestamp'] = strtotime($matches[2]); + } + $item['uri'] = $this->getURI(); + $item['content'] = $release->next_sibling (); + $this->items[] = $item; + } + } +} diff --git a/bridges/ReadComicsBridge.php b/bridges/ReadComicsBridge.php deleted file mode 100644 index 739e6ccb..00000000 --- a/bridges/ReadComicsBridge.php +++ /dev/null @@ -1,44 +0,0 @@ - array( - 'name' => 'keywords, separated by semicolons', - 'exampleValue' => 'first list;second list;...', - 'required' => true - ), - )); - - public function collectData(){ - - function parseDateTimestamp($element){ - $guessedDate = $element->find('span', 0)->plaintext; - $guessedDate = strptime($guessedDate, '%m/%d/%Y'); - $timestamp = mktime(0, 0, 0, $guessedDate['tm_mon'] + 1, $guessedDate['tm_mday'], date('Y')); - - return $timestamp; - } - - $keywordsList = explode(';', $this->getInput('q')); - foreach($keywordsList as $keywords) { - $html = $this->getSimpleHTMLDOM(self::URI . 'comic/' . rawurlencode($keywords)) - or $this->returnServerError('Could not request readcomics.tv.'); - - foreach($html->find('li') as $element) { - $item = array(); - $item['uri'] = $element->find('a.ch-name', 0)->href; - $item['id'] = $item['uri']; - $item['timestamp'] = parseDateTimestamp($element); - $item['title'] = $element->find('a.ch-name', 0)->plaintext; - if(isset($item['title'])) - $this->items[] = $item; - } - } - } -} diff --git a/bridges/RedditBridge.php b/bridges/RedditBridge.php new file mode 100644 index 00000000..8de499f9 --- /dev/null +++ b/bridges/RedditBridge.php @@ -0,0 +1,40 @@ + array( + 'r' => array( + 'name' => 'SubReddit', + 'required' => true, + 'exampleValue' => 'selfhosted', + 'title' => 'SubReddit name' + ) + ), + 'multi' => array( + 'rs' => array( + 'name' => 'SubReddits', + 'required' => true, + 'exampleValue' => 'selfhosted, php', + 'title' => 'SubReddit names, separated by commas' + ) + ) + ); + + public function collectData(){ + + switch($this->queriedContext) { + case 'single': $subreddits[] = $this->getInput('r'); break; + case 'multi': $subreddits = explode(',', $this->getInput('rs')); break; + } + + foreach ($subreddits as $subreddit) { + $name = trim($subreddit); + $this->collectExpandableDatas("https://www.reddit.com/r/$name/.rss"); + } + } +} diff --git a/bridges/Releases3DSBridge.php b/bridges/Releases3DSBridge.php index 6c159d12..fe2df8ec 100644 --- a/bridges/Releases3DSBridge.php +++ b/bridges/Releases3DSBridge.php @@ -9,22 +9,6 @@ class Releases3DSBridge extends BridgeAbstract { public function collectData(){ - function typeToString($type){ - switch($type) { - case 1: return '3DS Game'; - case 4: return 'eShop'; - default: return '??? (' . $type . ')'; - } - } - - function cardToString($card){ - switch($card) { - case 1: return 'Regular (CARD1)'; - case 2: return 'NAND (CARD2)'; - default: return '??? (' . $card . ')'; - } - } - $dataUrl = self::URI . 'xml.php'; $xml = getContents($dataUrl) or returnServerError('Could not request 3dsdb: ' . $dataUrl); @@ -95,8 +79,8 @@ class Releases3DSBridge extends BridgeAbstract { . '
Release Name: ' . $releasename . '
Trimmed size: ' . intval(intval($trimmedsize) / 1048576) . 'MB
Firmware: ' . $firmware - . '
Type: ' . typeToString($type) - . '
Card: ' . cardToString($card) + . '
Type: ' . $this->typeToString($type) + . '
Card: ' . $this->cardToString($card) . '
'; //Build search links section to facilitate release search using search engines @@ -124,4 +108,20 @@ class Releases3DSBridge extends BridgeAbstract { $limit++; } } + + private function typeToString($type){ + switch($type) { + case 1: return '3DS Game'; + case 4: return 'eShop'; + default: return '??? (' . $type . ')'; + } + } + + private function cardToString($card){ + switch($card) { + case 1: return 'Regular (CARD1)'; + case 2: return 'NAND (CARD2)'; + default: return '??? (' . $card . ')'; + } + } } diff --git a/bridges/ReporterreBridge.php b/bridges/ReporterreBridge.php index 438c55be..41f0f70f 100644 --- a/bridges/ReporterreBridge.php +++ b/bridges/ReporterreBridge.php @@ -3,7 +3,7 @@ class ReporterreBridge extends BridgeAbstract { const MAINTAINER = 'nyutag'; const NAME = 'Reporterre Bridge'; - const URI = 'http://www.reporterre.net/'; + const URI = 'https://www.reporterre.net/'; const DESCRIPTION = 'Returns the newest articles.'; private function extractContent($url){ diff --git a/bridges/RoadAndTrackBridge.php b/bridges/RoadAndTrackBridge.php index b3f0acc0..22ec8b52 100644 --- a/bridges/RoadAndTrackBridge.php +++ b/bridges/RoadAndTrackBridge.php @@ -25,7 +25,7 @@ class RoadAndTrackBridge extends BridgeAbstract { private function fixImages($content) { - $enclosures = []; + $enclosures = array(); foreach($content->find('img') as $image) { $image->src = explode('?', $image->getAttribute('data-src'))[0]; $enclosures[] = $image->src; diff --git a/bridges/Rule34Bridge.php b/bridges/Rule34Bridge.php index b46ec00a..71f48c61 100644 --- a/bridges/Rule34Bridge.php +++ b/bridges/Rule34Bridge.php @@ -5,7 +5,7 @@ class Rule34Bridge extends GelbooruBridge { const MAINTAINER = 'mitsukarenai'; const NAME = 'Rule34'; - const URI = 'http://rule34.xxx/'; + const URI = 'https://rule34.xxx/'; const DESCRIPTION = 'Returns images from given page'; const PIDBYPAGE = 50; diff --git a/bridges/Rule34pahealBridge.php b/bridges/Rule34pahealBridge.php index 1a746162..0e13ed03 100644 --- a/bridges/Rule34pahealBridge.php +++ b/bridges/Rule34pahealBridge.php @@ -5,6 +5,23 @@ class Rule34pahealBridge extends Shimmie2Bridge { const MAINTAINER = 'mitsukarenai'; const NAME = 'Rule34paheal'; - const URI = 'http://rule34.paheal.net/'; + const URI = 'https://rule34.paheal.net/'; const DESCRIPTION = 'Returns images from given page'; + + protected function getItemFromElement($element){ + $item = array(); + $item['uri'] = $this->getURI() . $element->href; + $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE)); + $item['timestamp'] = time(); + $thumbnailUri = $element->find('img', 0)->src; + $item['tags'] = $element->getAttribute('data-tags'); + $item['title'] = $this->getName() . ' | ' . $item['id']; + $item['content'] = '

Tags: ' + . $item['tags']; + return $item; + } } diff --git a/bridges/SafebooruBridge.php b/bridges/SafebooruBridge.php index d95e5572..98da6925 100644 --- a/bridges/SafebooruBridge.php +++ b/bridges/SafebooruBridge.php @@ -5,7 +5,7 @@ class SafebooruBridge extends GelbooruBridge { const MAINTAINER = 'mitsukarenai'; const NAME = 'Safebooru'; - const URI = 'http://safebooru.org/'; + const URI = 'https://safebooru.org/'; const DESCRIPTION = 'Returns images from given page'; const PIDBYPAGE = 40; diff --git a/bridges/SakugabooruBridge.php b/bridges/SakugabooruBridge.php deleted file mode 100644 index 1d6cee0a..00000000 --- a/bridges/SakugabooruBridge.php +++ /dev/null @@ -1,11 +0,0 @@ - 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(){ - // First we need to find the URI to the latest season from the - // 'seasons' page searching for 'Season Anime List' - $html = getSimpleHTMLDOM($this->getURI() . '/seasons'); - if(!$html) - returnServerError('Could not load \'seasons\' page!'); - $season = $html->find('div.follows_menu/a', 1); - if(!$season) - returnServerError('Could not find \'Season Anime List\'!'); + $html = getSimpleHTMLDOM(self::URI . '/seasons') + or returnServerError('Could not load \'seasons\' page!'); - $html = getSimpleHTMLDOM($this->getURI() . $season->href); - if(!$html) - returnServerError( + $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) + or returnServerError( 'Could not load \'Season Anime List\' from \'' . $season->innertext . '\'!' ); + $this->uri = $season->href; + + $html = defaultLinkTo($html, $season->href); + return $html; + } // Extracts the anime title private function extractAnimeTitle($anime){ - $title = $anime->find('a', 0); - if(!$title) - returnServerError('Could not find anime title!'); + $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); - if(!$uri) - returnServerError('Could not find anime URI!'); - return $this->getURI() . $uri->href; + $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) + + 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; // Sometimes the studio is unknown, so leave empty + + 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); - if(!$episode) - returnServerError('Could not find anime episode information!'); - return preg_replace('/\r|\n/', ' ', $episode->plaintext); + $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 @@ -73,15 +155,16 @@ class ShanaprojectBridge extends BridgeAbstract { // 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)) + 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 $this->getURI() + return self::URI . '/search/?title=' . urlencode($this->extractAnimeTitle($anime)) . '&subber='; @@ -102,22 +185,4 @@ class ShanaprojectBridge extends BridgeAbstract { . $this->buildAnimeSearchUri($anime) . '">Search episodes

'; } - - public function collectData(){ - $html = $this->loadSeasonAnimeList(); - - $animes = $html->find('div.header_display_box_info'); - if(!$animes) - returnServerError('Could not find anime headers!'); - - foreach($animes as $anime) { - $item = array(); - $item['title'] = $this->extractAnimeTitle($anime); - $item['author'] = $this->extractAnimeAuthor($anime); - $item['uri'] = $this->extractAnimeUri($anime); - $item['timestamp'] = $this->extractAnimeTimestamp($anime); - $item['content'] = $this->buildAnimeContent($anime); - $this->items[] = $item; - } - } } diff --git a/bridges/Shimmie2Bridge.php b/bridges/Shimmie2Bridge.php index 99235147..fdc97f4f 100644 --- a/bridges/Shimmie2Bridge.php +++ b/bridges/Shimmie2Bridge.php @@ -4,7 +4,7 @@ require_once('DanbooruBridge.php'); class Shimmie2Bridge extends DanbooruBridge { const NAME = 'Shimmie v2'; - const URI = 'http://shimmie.shishnet.org/v2/'; + const URI = 'https://shimmie.shishnet.org/v2/'; const DESCRIPTION = 'Returns images from given page'; const PATHTODATA = '.shm-thumb-link'; diff --git a/bridges/SoundcloudBridge.php b/bridges/SoundcloudBridge.php index 8938ff96..9607d33d 100644 --- a/bridges/SoundcloudBridge.php +++ b/bridges/SoundcloudBridge.php @@ -1,7 +1,7 @@ getInput('u')) - . '&client_id=' - . self::CLIENT_ID + $res = $this->apiGet('resolve', array( + 'url' => 'http://www.soundcloud.com/' . $this->getInput('u') )) or returnServerError('No results for this query'); $this->feedIcon = $res->avatar_url; - $tracks = json_decode(getContents( - 'https://api.soundcloud.com/users/' - . urlencode($res->id) - . '/tracks?client_id=' - . self::CLIENT_ID - )) or returnServerError('No results for this user'); + $tracks = $this->apiGet('users/' . urlencode($res->id) . '/tracks') + or returnServerError('No results for this user'); $numTracks = min(count($tracks), 10); for($i = 0; $i < $numTracks; $i++) { @@ -45,7 +36,7 @@ class SoundCloudBridge extends BridgeAbstract { $item['content'] = $tracks[$i]->description; $item['enclosures'] = array($tracks[$i]->uri . '/stream?client_id=' - . self::CLIENT_ID); + . $this->getClientID()); $item['id'] = self::URI . urlencode($this->getInput('u')) @@ -75,4 +66,68 @@ class SoundCloudBridge extends BridgeAbstract { return parent::getName(); } + + private function initClientIDCache(){ + if($this->clientIDCache !== null) + return; + + $cacheFac = new CacheFactory(); + $cacheFac->setWorkingDir(PATH_LIB_CACHES); + $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(); + + // Without url=http, this returns a 404 + $playerHTML = getContents('https://w.soundcloud.com/player/?url=http') + or returnServerError('Unable to get player page.'); + $regex = '/widget-.+?\.js/'; + if(preg_match($regex, $playerHTML, $matches) == false) + returnServerError('Unable to find widget JS URL.'); + $widgetURL = 'https://widget.sndcdn.com/' . $matches[0]; + + $widgetJS = getContents($widgetURL) + or returnServerError('Unable to get widget JS page.'); + $regex = '/client_id.*?"(.+?)"/'; + if(preg_match($regex, $widgetJS, $matches) == false) + returnServerError('Unable to find client ID.'); + $clientID = $matches[1]; + + $this->clientIDCache->saveData($clientID); + return $clientID; + } + + private function buildAPIURL($endpoint, $parameters){ + return 'https://api.soundcloud.com/' + . $endpoint + . '?' + . http_build_query($parameters); + } + + private function apiGet($endpoint, $parameters = array()){ + $parameters['client_id'] = $this->getClientID(); + + try { + return json_decode(getContents($this->buildAPIURL($endpoint, $parameters))); + } catch (Exception $e) { + // Retry once with refreshed client ID + $parameters['client_id'] = $this->refreshClientID(); + return json_decode(getContents($this->buildAPIURL($endpoint, $parameters))); + } + } } diff --git a/bridges/SplCenterBridge.php b/bridges/SplCenterBridge.php new file mode 100644 index 00000000..7a690908 --- /dev/null +++ b/bridges/SplCenterBridge.php @@ -0,0 +1,64 @@ + 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']) + or returnServerError('Could not request: ' . $item['uri']); + + foreach ($articleHtml->find('.file') as $index => $media) { + $articleHtml->find('div.file', $index)->outertext = '' . $media->outertext . ''; + } + + $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/SteamCommunityBridge.php b/bridges/SteamCommunityBridge.php index 56ea257c..9919a4b5 100644 --- a/bridges/SteamCommunityBridge.php +++ b/bridges/SteamCommunityBridge.php @@ -20,7 +20,8 @@ class SteamCommunityBridge extends BridgeAbstract { 'values' => array( 'Artwork' => 'images', 'Screenshots' => 'screenshots', - 'Videos' => 'videos' + 'Videos' => 'videos', + 'Workshop' => 'workshop' ) ) ) @@ -32,7 +33,7 @@ class SteamCommunityBridge extends BridgeAbstract { protected function getMainPage() { $category = $this->getInput('category'); - $html = getSimpleHTMLDOM($this->getURI() . '/?p=1&browsefilter=mostrecent') + $html = getSimpleHTMLDOM($this->getURI()) or returnServerError('Could not fetch Steam data.'); return $html; @@ -56,12 +57,17 @@ class SteamCommunityBridge extends BridgeAbstract { } 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'); + . $this->getInput('category') + . '/?p=1&browsefilter=mostrecent'; } - public function collectData() { + private function collectMedia() { $category = $this->getInput('category'); $html = $this->getMainPage(); $cards = $html->find('div.apphub_Card'); @@ -124,4 +130,62 @@ class SteamCommunityBridge extends BridgeAbstract { 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'] = '

' + . $previewImage . '

' . $fileRating + . '

' . $description . '

'; + + $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/StoriesIGBridge.php b/bridges/StoriesIGBridge.php new file mode 100644 index 00000000..9b5f7cbf --- /dev/null +++ b/bridges/StoriesIGBridge.php @@ -0,0 +1,57 @@ + array( + 'name' => 'Instagram username', + 'type' => 'text', + 'required' => true, + 'title' => 'Insert the username here' + ), + ) + ); + + public function collectData(){ + $html = getSimpleHTMLDOM($this->getURI()) + or returnServerError('Failed to receive ' . $this->getURI()); + + $results = $html->find('article'); + + foreach($results as $result) { + + $item = array(); + + $item['title'] = $this->getInput('username') . ' story'; + $item['uri'] = $result->find('div.download', 0)->find('a', 0)->href; + $item['author'] = $this->getInput('username'); + $item['timestamp'] = strtotime($result->find('time', 0)->datetime); + $item['uid'] = $result->find('time', 0)->datetime; + + $item['content'] = $result; + + $this->items[] = $item; + } + } + + public function getURI(){ + $uri = self::URI . '/stories/'; + $uri .= urlencode($this->getInput('username')); + return $uri; + + return parent::getURI(); + } + + public function getName() { + + if (!is_null($this->getInput('username'))) { + return $this->getInput('username') . ' - ' . self::NAME; + } + + return parent::getName(); + } +} diff --git a/bridges/SuperbWallpapersBridge.php b/bridges/SuperbWallpapersBridge.php deleted file mode 100644 index 610dd323..00000000 --- a/bridges/SuperbWallpapersBridge.php +++ /dev/null @@ -1,70 +0,0 @@ - array( - 'name' => 'category', - 'required' => true - ), - 'm' => array( - 'name' => 'Max number of wallpapers', - 'type' => 'number' - ), - 'r' => array( - 'name' => 'resolution', - 'exampleValue' => '1920x1200, 1680x1050,…', - 'defaultValue' => '1920x1200' - ) - )); - - public function collectData(){ - $category = $this->getInput('c'); - $resolution = $this->getInput('r'); // Wide wallpaper default - - $num = 0; - $max = $this->getInput('m') ?: 36; - $lastpage = 1; - - // Get last page number - $link = self::URI . '/' . $category . '/9999.html'; - $html = getSimpleHTMLDOM($link) - or returnServerError('Could not load ' . $link); - - $lastpage = min($html->find('.paging .cpage', 0)->innertext(), ceil($max / 36)); - - for($page = 1; $page <= $lastpage; $page++) { - $link = self::URI . '/' . $category . '/' . $page . '.html'; - $html = getSimpleHTMLDOM($link) - or returnServerError('No results for this query.'); - - foreach($html->find('.wpl .i a') as $element) { - $thumbnail = $element->find('img', 0); - - $item = array(); - $item['uri'] = str_replace('200x125', $this->resolution, $thumbnail->src); - $item['timestamp'] = time(); - $item['title'] = $element->title; - $item['content'] = $item['title'] . '
' . $thumbnail . ''; - $this->items[] = $item; - - $num++; - if ($num >= $max) - break 2; - } - } - } - - public function getName(){ - if(!is_null($this->getInput('c')) && !is_null($this->getInput('r'))) { - return self::NAME . '- ' . $this->getInput('c') . ' [' . $this->getInput('r') . ']'; - } - - return parent::getName(); - } -} diff --git a/bridges/TbibBridge.php b/bridges/TbibBridge.php index edb761ee..819d61ee 100644 --- a/bridges/TbibBridge.php +++ b/bridges/TbibBridge.php @@ -5,7 +5,7 @@ class TbibBridge extends GelbooruBridge { const MAINTAINER = 'mitsukarenai'; const NAME = 'Tbib'; - const URI = 'http://tbib.org/'; + const URI = 'https://tbib.org/'; const DESCRIPTION = 'Returns images from given page'; const PIDBYPAGE = 50; diff --git a/bridges/TelegramBridge.php b/bridges/TelegramBridge.php new file mode 100644 index 00000000..3afc2831 --- /dev/null +++ b/bridges/TelegramBridge.php @@ -0,0 +1,301 @@ + array( + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'exampleValue' => '@telegram', + ) + ) + ); + + const CACHE_TIMEOUT = 900; // 15 mins + + private $feedName = ''; + private $enclosures = array(); + private $itemTitle = ''; + + private $backgroundImageRegex = "/background-image:url\('(.*)'\)/"; + + public function collectData() { + + $html = getSimpleHTMLDOM($this->getURI()) + or returnServerError('Could not request: ' . $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'] = html_entity_decode($this->processContent($messageDiv), ENT_QUOTES); + $item['title'] = html_entity_decode($this->itemTitle, ENT_QUOTES); + $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 processContent($messageDiv) { + $message = ''; + + if ($messageDiv->find('div.tgme_widget_message_forwarded_from', 0)) { + $message = $messageDiv->find('div.tgme_widget_message_forwarded_from', 0)->innertext . '

'; + } + + 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('a.tgme_widget_message_link_preview', 0)) { + $message .= $this->processLinkPreview($messageDiv); + } + + return $message; + } + + private function processReply($messageDiv) { + + $reply = $messageDiv->find('a.tgme_widget_message_reply', 0); + + return <<{$reply->find('span.tgme_widget_message_author_name', 0)->plaintext}
+{$reply->find('div.tgme_widget_message_text', 0)->innertext} +{$reply->href}
+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); + + preg_match($this->backgroundImageRegex, $stickerDiv->find('i', 0)->style, $sticker); + + $this->enclosures[] = $sticker[1]; + + return << +EOD; + } + + private function processPoll($messageDiv) { + + $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; + + if (empty($this->itemTitle)) { + $this->itemTitle = $title; + } + + $pollOptions = '
    '; + + foreach ($poll->find('div.tgme_widget_message_poll_option') as $option) { + $pollOptions .= '
  • ' . $option->children(0)->plaintext . ' - ' . + $option->find('div.tgme_widget_message_poll_option_text', 0)->plaintext . '
  • '; + } + $pollOptions .= '
'; + + return <<$type
{$pollOptions} +EOD; + } + + private function processLinkPreview($messageDiv) { + + $image = ''; + $title = ''; + $site = ''; + $description = ''; + + $preview = $messageDiv->find('a.tgme_widget_message_link_preview', 0); + + if (trim($preview->innertext) === '') { + return ''; + } + + if($preview->find('i', 0) && + preg_match($this->backgroundImageRegex, $preview->find('i', 0)->style, $photo)) { + + $image = ''; + $this->enclosures[] = $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_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; + } + + return <<$image
+{$title} - {$site}
{$description} +EOD; + } + + 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); + } + + $this->enclosures[] = $photo[1]; + + return << + + +EOD; + } + + private function processPhoto($messageDiv) { + + if (empty($this->itemTitle)) { + $this->itemTitle = '@' . $this->processUsername() . ' posted a photo'; + } + + $photos = ''; + + foreach ($messageDiv->find('a.tgme_widget_message_photo_wrap') as $photoWrap) { + preg_match($this->backgroundImageRegex, $photoWrap->style, $photo); + + $this->enclosures[] = $photo[1]; + + $photos .= <<
+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); + } + + $this->enclosures[] = $photo[1]; + + return << +{$messageDiv->find('div.message_media_not_supported_label', 0)->innertext}

+{$messageDiv->find('span.message_media_view_in_telegram', 0)->innertext}

+ +EOD; + } + + private function processDate($messageDiv) { + + $messageMeta = $messageDiv->find('span.tgme_widget_message_meta', 0); + return $messageMeta->find('time', 0)->datetime; + + } + + private function ellipsisTitle($text) { + + $length = 100; + + if (strlen($text) > $length) { + $text = explode('
', wordwrap($text, $length, '
')); + return $text[0] . '...'; + } + return $text; + } +} diff --git a/bridges/TheCodingLoveBridge.php b/bridges/TheCodingLoveBridge.php index 2a639e33..8060c947 100644 --- a/bridges/TheCodingLoveBridge.php +++ b/bridges/TheCodingLoveBridge.php @@ -3,7 +3,7 @@ class TheCodingLoveBridge extends BridgeAbstract { const MAINTAINER = 'superbaillot.net'; const NAME = 'The Coding Love'; - const URI = 'http://thecodinglove.com/'; + const URI = 'https://thecodinglove.com/'; const CACHE_TIMEOUT = 7200; // 2h const DESCRIPTION = 'The Coding Love'; diff --git a/bridges/TheGuardianBridge.php b/bridges/TheGuardianBridge.php new file mode 100644 index 00000000..e655f0ef --- /dev/null +++ b/bridges/TheGuardianBridge.php @@ -0,0 +1,96 @@ + 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; + } +} diff --git a/bridges/ThePirateBayBridge.php b/bridges/ThePirateBayBridge.php index 9aefcbbe..4b45daf3 100644 --- a/bridges/ThePirateBayBridge.php +++ b/bridges/ThePirateBayBridge.php @@ -3,7 +3,7 @@ class ThePirateBayBridge extends BridgeAbstract { const MAINTAINER = 'mitsukarenai'; const NAME = 'The Pirate Bay'; - const URI = 'https://thepiratebay.wf/'; + 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 @@ -40,60 +40,6 @@ class ThePirateBayBridge extends BridgeAbstract { public function collectData(){ - function parseDateTimestamp($element){ - $guessedDate = $element->find('font', 0)->plaintext; - $guessedDate = explode('Uploaded ', $guessedDate)[1]; - $guessedDate = explode(',', $guessedDate)[0]; - - if(count(explode(':', $guessedDate)) == 1) { - $guessedDate = strptime($guessedDate, '%m-%d %Y'); - $timestamp = mktime( - 0, - 0, - 0, - $guessedDate['tm_mon'] + 1, - $guessedDate['tm_mday'], - 1900 + $guessedDate['tm_year'] - ); - } elseif(explode(' ', $guessedDate)[0] == 'Today') { - $guessedDate = strptime( - explode(' ', $guessedDate)[1], '%H:%M' - ); - - $timestamp = mktime( - $guessedDate['tm_hour'], - $guessedDate['tm_min'], - 0, - date('m'), - date('d'), - date('Y') - ); - } elseif(explode(' ', $guessedDate)[0] == 'Y-day') { - $guessedDate = strptime( - explode(' ', $guessedDate)[1], '%H:%M' - ); - - $timestamp = mktime( - $guessedDate['tm_hour'], - $guessedDate['tm_min'], - 0, - date('m', time() - 24 * 60 * 60), - date('d', time() - 24 * 60 * 60), - date('Y', time() - 24 * 60 * 60) - ); - } else { - $guessedDate = strptime($guessedDate, '%m-%d %H:%M'); - $timestamp = mktime( - $guessedDate['tm_hour'], - $guessedDate['tm_min'], - 0, - $guessedDate['tm_mon'] + 1, - $guessedDate['tm_mday'], - date('Y')); - } - return $timestamp; - } - $catBool = $this->getInput('catCheck'); if($catBool) { $catNum = $this->getInput('cat'); @@ -149,11 +95,12 @@ class ThePirateBayBridge extends BridgeAbstract { || !is_null($element->find('img[alt=VIP]', 0)) || !is_null($element->find('img[alt=Trusted]', 0))) { $item = array(); - $item['uri'] = $element->find('a', 3)->href; + $item['uri'] = self::URI . $element->find('a.detLink', 0)->href; $item['id'] = self::URI . $element->find('a.detLink', 0)->href; - $item['timestamp'] = parseDateTimestamp($element); + $item['timestamp'] = $this->parseDateTimestamp($element); $item['author'] = $element->find('a.detDesc', 0)->plaintext; $item['title'] = $element->find('a.detLink', 0)->plaintext; + $item['magnet'] = $element->find('a', 3)->href; $item['seeders'] = (int)$element->find('td', 2)->plaintext; $item['leechers'] = (int)$element->find('td', 3)->plaintext; $item['content'] = $element->find('font', 0)->plaintext @@ -163,7 +110,9 @@ class ThePirateBayBridge extends BridgeAbstract { . $item['leechers'] . '
info page'; + . '">info page
magnet link'; if(isset($item['title'])) $this->items[] = $item; @@ -171,4 +120,58 @@ class ThePirateBayBridge extends BridgeAbstract { } } } + + private function parseDateTimestamp($element){ + $guessedDate = $element->find('font', 0)->plaintext; + $guessedDate = explode('Uploaded ', $guessedDate)[1]; + $guessedDate = explode(',', $guessedDate)[0]; + + if(count(explode(':', $guessedDate)) == 1) { + $guessedDate = strptime($guessedDate, '%m-%d %Y'); + $timestamp = mktime( + 0, + 0, + 0, + $guessedDate['tm_mon'] + 1, + $guessedDate['tm_mday'], + 1900 + $guessedDate['tm_year'] + ); + } elseif(explode(' ', $guessedDate)[0] == 'Today') { + $guessedDate = strptime( + explode(' ', $guessedDate)[1], '%H:%M' + ); + + $timestamp = mktime( + $guessedDate['tm_hour'], + $guessedDate['tm_min'], + 0, + date('m'), + date('d'), + date('Y') + ); + } elseif(explode(' ', $guessedDate)[0] == 'Y-day') { + $guessedDate = strptime( + explode(' ', $guessedDate)[1], '%H:%M' + ); + + $timestamp = mktime( + $guessedDate['tm_hour'], + $guessedDate['tm_min'], + 0, + date('m', time() - 24 * 60 * 60), + date('d', time() - 24 * 60 * 60), + date('Y', time() - 24 * 60 * 60) + ); + } else { + $guessedDate = strptime($guessedDate, '%m-%d %H:%M'); + $timestamp = mktime( + $guessedDate['tm_hour'], + $guessedDate['tm_min'], + 0, + $guessedDate['tm_mon'] + 1, + $guessedDate['tm_mday'], + date('Y')); + } + return $timestamp; + } } diff --git a/bridges/TheWhiteboardBridge.php b/bridges/TheWhiteboardBridge.php new file mode 100644 index 00000000..051d15e4 --- /dev/null +++ b/bridges/TheWhiteboardBridge.php @@ -0,0 +1,22 @@ +find('center', 1)->find('img', 0); + $image->src = self::URI . '/' . $image->src; + + $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/TorrentGalaxyBridge.php b/bridges/TorrentGalaxyBridge.php new file mode 100644 index 00000000..b71d2a5e --- /dev/null +++ b/bridges/TorrentGalaxyBridge.php @@ -0,0 +1,120 @@ + array( + 'name' => 'search', + 'required' => true, + '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) + or returnServerError("Error querying the server at $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); + + $item = array(); + $item['uri'] = self::URI . $identity->href; + $item['title'] = $identity->plaintext; + $item['timestamp'] = DateTime::createFromFormat('d/m/y H:i', $creadate)->format('U'); + $item['author'] = $authorid->plaintext; + $item['content'] = <<{$identity->plaintext} +

Links

+

magnet

+

torrent

+

Infos

+

Size: {$result->find('div.tgxtablecell', 7)->plaintext}

+

Added by: {$authorid->plaintext}

+

Upload time: {$creadate}

+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; + } + } + + 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 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(); + } +} diff --git a/bridges/TwitchBridge.php b/bridges/TwitchBridge.php new file mode 100644 index 00000000..39b46010 --- /dev/null +++ b/bridges/TwitchBridge.php @@ -0,0 +1,202 @@ + array( + 'name' => 'Channel', + 'type' => 'text', + 'required' => true, + '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' + ), + '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'; + + public function collectData(){ + // get channel user + $query_data = array( + 'login' => $this->getInput('channel') + ); + $users = $this->apiGet('users', $query_data)->users; + if(count($users) === 0) + returnClientError('User "' + . $this->getInput('channel') + . '" could not be found'); + $user = $users[0]; + + // get video list + $query_endpoint = 'channels/' . $user->_id . '/videos'; + $query_data = array( + 'broadcast_type' => $this->getInput('type'), + 'limit' => 10 + ); + $videos = $this->apiGet($query_endpoint, $query_data)->videos; + + foreach($videos as $video) { + $item = array( + 'uri' => $video->url, + 'title' => $video->title, + 'timestamp' => $video->published_at, + 'author' => $video->channel->display_name, + ); + + // Add categories for tags and played game + $item['categories'] = array_filter(explode(' ', $video->tag_list)); + if(!empty($video->game)) + $item['categories'][] = $video->game; + + // Add enclosures for thumbnails from a few points in the video + $item['enclosures'] = array(); + foreach($video->thumbnails->large as $thumbnail) + $item['enclosures'][] = $thumbnail->url; + + /* + * 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'] = '

' + . $video->description_html + . '

Duration: ' + . $this->formatTimestampTime($video->length) + . '
Views: ' + . $video->views + . '

'; + + // Add played games list to content + $video_id = trim($video->_id, 'v'); // _id gives 'v1234' but API wants '1234' + $markers = $this->apiGet('videos/' . $video_id . '/markers')->markers; + $item['content'] .= '

Played games:

  • 00:00:00 - ' + . $video->game + . '
  • '; + if(isset($markers->game_changes)) { + usort($markers->game_changes, function($a, $b) { + return $a->time - $b->time; + }); + foreach($markers->game_changes as $game_change) { + $item['categories'][] = $game_change->label; + $item['content'] .= '
  • ' + . $this->formatTimestampTime($game_change->time) + . ' - ' + . $game_change->label + . '
  • '; + } + } + $item['content'] .= '

'; + + $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. 01h53m27s + private function formatQueryTime($seconds) { + return sprintf('%02dh%02dm%02ds', + floor($seconds / 3600), + ($seconds / 60) % 60, + $seconds % 60); + } + + /* + * Ideally the new 'helix' API should be used as v5/'kraken' is deprecated. + * The new API however still misses many features (markers, played game..) of + * the old one, so let's use the old one for as long as it's available. + */ + private function apiGet($endpoint, $query_data = array()) { + $query_data['api_version'] = 5; + $url = 'https://api.twitch.tv/kraken/' + . $endpoint + . '?' + . http_build_query($query_data); + $header = array( + 'Client-ID: ' . self::CLIENT_ID + ); + + $data = json_decode(getContents($url, $header)) + or returnServerError('API request to "' . $url . '" failed.'); + + return $data; + } + + public function getName(){ + if(!is_null($this->getInput('channel'))) { + return $this->getInput('channel') . ' twitch videos'; + } + + return parent::getName(); + } + + public function getURI(){ + if(!is_null($this->getInput('channel'))) { + return self::URI . $this->getInput('channel'); + } + + return parent::getURI(); + } + + public function detectParameters($url){ + $params = array(); + + // 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; + } + + return null; + } +} diff --git a/bridges/TwitterBridge.php b/bridges/TwitterBridge.php index b3b7bed4..2f5565b1 100644 --- a/bridges/TwitterBridge.php +++ b/bridges/TwitterBridge.php @@ -28,7 +28,31 @@ class TwitterBridge extends BridgeAbstract { 'name' => 'Keyword or #hashtag', 'required' => true, 'exampleValue' => 'rss-bridge, #rss-bridge', - 'title' => 'Insert a keyword or hashtag' + 'title' => << array( @@ -146,8 +170,15 @@ class TwitterBridge extends BridgeAbstract { public function collectData(){ $html = ''; + $page = $this->getURI(); + + if(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo'))) { + $cookies = $this->getCookies($page); + $html = getSimpleHTMLDOM($page, array("Cookie: $cookies")); + } else { + $html = getSimpleHTMLDOM($page, array(), array(CURLOPT_COOKIEFILE => '')); + } - $html = getSimpleHTMLDOM($this->getURI()); if(!$html) { switch($this->queriedContext) { case 'By keyword or hashtag': @@ -165,7 +196,7 @@ class TwitterBridge extends BridgeAbstract { // Skip retweets? if($this->getInput('noretweet') - && strcasecmp($tweet->getAttribute('data-screen-name'), $this->getInput('u'))) { + && $tweet->find('div.context span.js-retweet-text a', 0)) { continue; } @@ -189,8 +220,8 @@ class TwitterBridge extends BridgeAbstract { $item['fullname'] = htmlspecialchars_decode($tweet->getAttribute('data-name'), ENT_QUOTES); // get author $item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')'; - if(strcasecmp($tweet->getAttribute('data-screen-name'), $this->getInput('u'))) { - $item['author'] .= ' RT: @' . $this->getInput('u'); + if($rt = $tweet->find('div.context span.js-retweet-text a', 0)) { + $item['author'] .= ' RT: @' . $rt->plaintext; } // get avatar link $item['avatar'] = $tweet->find('img', 0)->src; @@ -245,22 +276,26 @@ EOD; // Add embeded image to content $image_html = ''; - $image = $this->getImageURI($tweet); - if(!$this->getInput('noimg') && !is_null($image)) { - // Set image scaling - $image_orig = $this->getInput('noimgscaling') ? $image : $image . ':orig'; - $image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb'; + $images = $this->getImageURI($tweet); + if(!$this->getInput('noimg') && !is_null($images)) { - // add enclosures - $item['enclosures'] = array($image_orig); + foreach ($images as $image) { - $image_html = <<getInput('noimgscaling') ? $image : $image . ':orig'; + $image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb'; + + // add enclosures + $item['enclosures'][] = $image_orig; + + $image_html .= << EOD; + } } // add content @@ -291,22 +326,27 @@ EOD; // Add embeded image to content $quotedImage_html = ''; - $quotedImage = $this->getQuotedImageURI($tweet); - if(!$this->getInput('noimg') && !is_null($quotedImage)) { - // Set image scaling - $quotedImage_orig = $this->getInput('noimgscaling') ? $quotedImage : $quotedImage . ':orig'; - $quotedImage_thumb = $this->getInput('noimgscaling') ? $quotedImage : $quotedImage . ':thumb'; + $quotedImages = $this->getQuotedImageURI($tweet); - // add enclosures - $item['enclosures'] = array($quotedImage_orig); + if(!$this->getInput('noimg') && !is_null($quotedImages)) { - $quotedImage_html = << + foreach ($quotedImages as $image) { + + // Set image scaling + $image_orig = $this->getInput('noimgscaling') ? $image : $image . ':orig'; + $image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb'; + + // add enclosures + $item['enclosures'][] = $image_orig; + + $quotedImage_html .= << + src="{$image_thumb}" /> EOD; + } } $item['content'] = <<find('div.AdaptiveMedia-container', 0); + if($container && $container->find('img', 0)) { - return $container->find('img', 0)->src; + foreach ($container->find('img') as $img) { + $images[] = $img->src; + } + } + + if (!empty($images)) { + return $images; } return null; @@ -370,11 +419,43 @@ EOD; private function getQuotedImageURI($tweet){ // Find media in tweet + $images = array(); + $container = $tweet->find('div.QuoteMedia-container', 0); + if($container && $container->find('img', 0)) { - return $container->find('img', 0)->src; + foreach ($container->find('img') as $img) { + $images[] = $img->src; + } + } + + if (!empty($images)) { + return $images; } return null; } + + private function getCookies($pageURL){ + + $ctx = stream_context_create(array( + 'http' => array( + 'follow_location' => false + ) + ) + ); + $a = file_get_contents($pageURL, 0, $ctx); + + //First request to get the cookie + $cookies = ''; + foreach($http_response_header as $hdr) { + if(stripos($hdr, 'Set-Cookie') !== false) { + $cLine = explode(':', $hdr)[1]; + $cLine = explode(';', $cLine)[0]; + $cookies .= ';' . $cLine; + } + } + + return substr($cookies, 2); + } } diff --git a/bridges/UnsplashBridge.php b/bridges/UnsplashBridge.php index ae767346..dad0efc2 100644 --- a/bridges/UnsplashBridge.php +++ b/bridges/UnsplashBridge.php @@ -3,7 +3,7 @@ class UnsplashBridge extends BridgeAbstract { const MAINTAINER = 'nel50n'; const NAME = 'Unsplash Bridge'; - const URI = 'http://unsplash.com/'; + const URI = 'https://unsplash.com/'; const CACHE_TIMEOUT = 43200; // 12h const DESCRIPTION = 'Returns the latests photos from Unsplash'; @@ -27,51 +27,42 @@ class UnsplashBridge extends BridgeAbstract { public function collectData(){ $width = $this->getInput('w'); - $num = 0; $max = $this->getInput('m'); $quality = $this->getInput('q'); - $lastpage = 1; - for($page = 1; $page <= $lastpage; $page++) { - $link = self::URI . '/grid?page=' . $page; - $html = getSimpleHTMLDOM($link) - or returnServerError('No results for this query.'); + $api_response = getContents('https://unsplash.com/napi/photos?page=1&per_page=' . $max) + or returnServerError('Could not request Unsplash API.'); + $json = json_decode($api_response, true); - if($page === 1) { - preg_match( - '/=(\d+)$/', - $html->find('.pagination > a[!class]', -1)->href, - $matches - ); + foreach ($json as $json_item) { + $item = array(); - $lastpage = min($matches[1], ceil($max / 40)); + // Get image URI + $uri = $json_item['urls']['regular'] . '.jpg'; // '.jpg' only for format hint + $uri = str_replace('q=80', 'q=' . $quality, $uri); + $uri = str_replace('w=1080', 'w=' . $width, $uri); + $item['uri'] = $uri; + + // Get title from description + if (is_null($json_item['alt_description'])) { + if (is_null($json_item['description'])) { + $item['title'] = 'Unsplash picture from ' . $json_item['user']['name']; + } else { + $item['title'] = $json_item['description']; + } + } else { + $item['title'] = $json_item['alt_description']; } - foreach($html->find('.photo') as $element) { - $thumbnail = $element->find('img', 0); - $thumbnail->src = str_replace('https://', 'http://', $thumbnail->src); - - $item = array(); - $item['uri'] = str_replace( - array('q=75', 'w=400'), - array("q=$quality", "w=$width"), - $thumbnail->src) . '.jpg'; // '.jpg' only for format hint - - $item['timestamp'] = time(); - $item['title'] = $thumbnail->alt; - $item['content'] = $item['title'] + $item['timestamp'] = time(); + $item['content'] = $item['title'] . '
'; - $this->items[] = $item; - - $num++; - if ($num >= $max) - break 2; - } + $this->items[] = $item; } } } diff --git a/bridges/VarietyBridge.php b/bridges/VarietyBridge.php new file mode 100644 index 00000000..a2e61700 --- /dev/null +++ b/bridges/VarietyBridge.php @@ -0,0 +1,30 @@ +collectExpandableDatas('http://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); + + // 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; + + return $item; + } +} diff --git a/bridges/ViceBridge.php b/bridges/ViceBridge.php new file mode 100644 index 00000000..4dccb8ef --- /dev/null +++ b/bridges/ViceBridge.php @@ -0,0 +1,38 @@ + 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); + } + + 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 new file mode 100644 index 00000000..12247980 --- /dev/null +++ b/bridges/VieDeMerdeBridge.php @@ -0,0 +1,56 @@ + 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()) + or returnServerError('Could not request VieDeMerde.'); + + $quotes = $html->find('article.article-panel'); + if(sizeof($quotes) === 0) { + return; + } + + foreach($quotes as $quote) { + $item = array(); + $item['uri'] = self::URI . $quote->find('.article-contents a', 0)->href; + $titleContent = $quote->find('.article-contents a h2.classic-title', 0); + + if($titleContent) { + $item['title'] = html_entity_decode($titleContent->plaintext, ENT_QUOTES); + } else { + continue; + } + + $quote->find('.article-contents a h2.classic-title', 0)->outertext = ''; + $item['content'] = $quote->find('.article-contents a', 0)->innertext; + $item['author'] = $quote->find('.article-topbar', 0)->innertext; + $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 new file mode 100644 index 00000000..d318e30e --- /dev/null +++ b/bridges/VimeoBridge.php @@ -0,0 +1,175 @@ + array( + 'name' => 'Search Query', + 'type' => 'text', + '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) + or returnServerError('Could not request ' . $this->getURI()); + + $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'] = ""; + + $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); + + $item['enclosures'] = array( + end($element->ondemand->pictures->sizes)->link + ); + + $item['content'] = ""; + + $this->items[] = $item; + } + + private function addPeople($element) { + $item = array(); + + $item['uri'] = $element->people->link; + $item['title'] = $element->people->name; + + $item['enclosures'] = array( + end($element->people->pictures->sizes)->link + ); + + $item['content'] = ""; + + $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'] = ""; + + $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'] = ""; + + $this->items[] = $item; + } +} diff --git a/bridges/VkBridge.php b/bridges/VkBridge.php index f9aaa66a..713b86f3 100644 --- a/bridges/VkBridge.php +++ b/bridges/VkBridge.php @@ -3,7 +3,9 @@ class VkBridge extends BridgeAbstract { - const MAINTAINER = 'ahiles3005'; + const MAINTAINER = 'em92'; + // const MAINTAINER = 'pmaziere'; + // const MAINTAINER = 'ahiles3005'; const NAME = 'VK.com'; const URI = 'https://vk.com/'; const CACHE_TIMEOUT = 300; // 5min @@ -26,7 +28,7 @@ class VkBridge extends BridgeAbstract protected function getAccessToken() { - return 'c8071613517c155c6cfbd2a059b2718e9c37b89094c4766834969dda75f657a2c1cbb49bab4c5e649f1db'; + return 'e69b2db9f6cd4a97c0716893232587165c18be85bc1af1834560125c1d3c8ec281eb407a78cca0ae16776'; } public function getURI() @@ -165,7 +167,7 @@ class VkBridge extends BridgeAbstract } // get all photos - foreach($post->find('div.wall_text > a.page_post_thumb_wrap') as $a) { + foreach($post->find('div.wall_text a.page_post_thumb_wrap') as $a) { $result = $this->getPhoto($a); if ($result == null) continue; $a->outertext = ''; @@ -237,6 +239,41 @@ class VkBridge extends BridgeAbstract $a->outertext = ''; } + // 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'] + )); + } + } + if (is_object($post->find('div.copy_quote', 0))) { if ($this->getInput('hide_reposts') === true) { continue; @@ -250,21 +287,9 @@ class VkBridge extends BridgeAbstract } $item = array(); - $item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '
'); + $item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '
'); $item['content'] .= $content_suffix; - $item['categories'] = array(); - - // get post hashtags - foreach($post->find('a') as $a) { - $href = $a->getAttribute('href'); - $prefix = '/feed?section=search&q=%23'; - $innertext = $a->innertext; - if ($href && substr($href, 0, strlen($prefix)) === $prefix) { - $item['categories'][] = urldecode(substr($href, strlen($prefix))); - } else if (substr($innertext, 0, 1) == '#') { - $item['categories'][] = $innertext; - } - } + $item['categories'] = $hashtags; // get post link $post_link = $post->find('a.post_link', 0)->getAttribute('href'); @@ -354,6 +379,8 @@ class VkBridge extends BridgeAbstract } $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']); diff --git a/bridges/WiredBridge.php b/bridges/WiredBridge.php new file mode 100644 index 00000000..8da93d0c --- /dev/null +++ b/bridges/WiredBridge.php @@ -0,0 +1,102 @@ + 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 + ) + ) + )); + + 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'; + + $this->collectExpandableDatas($feed_url); + } + + protected function parseItem($newsItem){ + $item = parent::parseItem($newsItem); + $article = getSimpleHTMLDOMCached($item['uri']) + or returnServerError('Could not request WIRED: ' . $item['uri']); + $item['content'] = $this->extractArticleContent($article); + + $headline = strval($newsItem->description); + if(!empty($headline)) { + $item['content'] = '

' . $headline . '

' . $item['content']; + } + + $item_image = $article->find('meta[property="og:image"]', 0); + if(!empty($item_image)) { + $item['enclosures'] = array($item_image->content); + $item['content'] = '

' . $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( + '
find('div[class~="p-body"]', 0)) { $this->version = self::XENFORO_VERSION_2; } else { returnServerError('This forum is currently not supported!'); @@ -127,7 +127,7 @@ class XenForoBridge extends BridgeAbstract { switch($this->version) { case self::XENFORO_VERSION_1: - $titleBar = $mainContent->find('div.titleBar h1', 0) + $titleBar = $mainContent->find('div.titleBar > h1', 0) or returnServerError('Error finding title bar!'); $this->title = $titleBar->plaintext; @@ -140,7 +140,7 @@ class XenForoBridge extends BridgeAbstract { case self::XENFORO_VERSION_2: - $titleBar = $mainContent->find('div[class="p-title"] h1', 0) + $titleBar = $mainContent->find('div[class~="p-title"] h1', 0) or returnServerError('Error finding title bar!'); $this->title = $titleBar->plaintext; @@ -166,7 +166,7 @@ class XenForoBridge extends BridgeAbstract { $lang = $html->find('html', 0)->lang; // Posts are contained in an "ol" - $messageList = $html->find('#messageList li') + $messageList = $html->find('#messageList > li') or returnServerError('Error finding message list!'); foreach($messageList as $post) { @@ -179,7 +179,7 @@ class XenForoBridge extends BridgeAbstract { $item['uri'] = $url . '#' . $post->getAttribute('id'); - $content = $post->find('.messageContent article', 0); + $content = $post->find('.messageContent > article', 0); // Add some style to quotes foreach($content->find('.bbCodeQuote') as $quote) { @@ -255,7 +255,7 @@ class XenForoBridge extends BridgeAbstract { $lang = $html->find('html', 0)->lang; - $messageList = $html->find('div[class="block-body"] article') + $messageList = $html->find('div[class~="block-body"] article') or returnServerError('Error finding message list!'); foreach($messageList as $post) { @@ -268,13 +268,17 @@ class XenForoBridge extends BridgeAbstract { $item['uri'] = $url . '#' . $post->getAttribute('id'); - $title = $post->find('div[class="message-content"] article', 0)->plaintext; + $title = $post->find('div[class~="message-content"] article', 0)->plaintext; $end = strpos($title, ' ', 70); $item['title'] = substr($title, 0, $end); - $item['timestamp'] = $this->fixDate($post->find('time', 0)->title, $lang); + 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); + $item['content'] = $post->find('div[class~="message-content"] article', 0); // Bridge specific properties $item['id'] = $post->getAttribute('id'); @@ -305,7 +309,7 @@ class XenForoBridge extends BridgeAbstract { // Load at least the last page do { - $pageurl = $hosturl . str_replace($sentinel, $lastpage, $baseurl); + $pageurl = str_replace($sentinel, $lastpage, $baseurl); // We can optimize performance by caching all but the last page if($page != $lastpage) { @@ -339,7 +343,7 @@ class XenForoBridge extends BridgeAbstract { } // Manually extract baseurl and inject sentinel - $baseurl = $pageNav->find('li a', -1)->href; + $baseurl = $pageNav->find('li > a', -1)->href; $baseurl = str_replace('page-' . $lastpage, 'page-{{sentinel}}', $baseurl); $sentinel = '{{sentinel}}'; @@ -353,7 +357,7 @@ class XenForoBridge extends BridgeAbstract { // Load at least the last page do { - $pageurl = $hosturl . str_replace($sentinel, $lastpage, $baseurl); + $pageurl = str_replace($sentinel, $lastpage, $baseurl); // We can optimize performance by caching all but the last page if($page != $lastpage) { @@ -364,9 +368,9 @@ class XenForoBridge extends BridgeAbstract { or returnServerError('Error loading contents from ' . $pageurl . '!'); } - $html = defaultLinkTo($html, $this->hosturl); + $html = defaultLinkTo($html, $hosturl); - $this->extractThreadPostsV2($html, $this->pageurl); + $this->extractThreadPostsV2($html, $pageurl); $page--; @@ -391,7 +395,7 @@ class XenForoBridge extends BridgeAbstract { */ private function fixDate($date, $lang = 'en-US') { - $mnamesen = [ + $mnamesen = array( 'January', 'Feburary', 'March', @@ -404,7 +408,7 @@ class XenForoBridge extends BridgeAbstract { 'October', 'November', 'December' - ]; + ); switch($lang) { case 'en-US': // example: Jun 9, 2018 at 11:46 PM @@ -414,7 +418,7 @@ class XenForoBridge extends BridgeAbstract { case 'de-DE': // example: 19 Juli 2018 um 19:27 Uhr - $mnamesde = [ + $mnamesde = array( 'Januar', 'Februar', 'März', @@ -427,9 +431,9 @@ class XenForoBridge extends BridgeAbstract { 'Oktober', 'November', 'Dezember' - ]; + ); - $mnamesdeshort = [ + $mnamesdeshort = array( 'Jan.', 'Feb.', 'Mär.', @@ -442,7 +446,7 @@ class XenForoBridge extends BridgeAbstract { 'Okt.', 'Nov.', 'Dez.' - ]; + ); $date = str_ireplace($mnamesde, $mnamesen, $date); $date = str_ireplace($mnamesdeshort, $mnamesen, $date); diff --git a/bridges/YahtzeeDevDiaryBridge.php b/bridges/YahtzeeDevDiaryBridge.php new file mode 100644 index 00000000..3e3b2b07 --- /dev/null +++ b/bridges/YahtzeeDevDiaryBridge.php @@ -0,0 +1,21 @@ +getURI()) + or returnServerError('Could not load content'); + + foreach($html->find('blockquote.wp-embedded-content a') as $element) { + $item = array(); + + $item['title'] = $element->innertext; + $item['uri'] = $element->href; + + $this->items[] = $item; + } + } +} diff --git a/bridges/YoutubeBridge.php b/bridges/YoutubeBridge.php index d2a45128..90ee0499 100644 --- a/bridges/YoutubeBridge.php +++ b/bridges/YoutubeBridge.php @@ -65,7 +65,7 @@ class YoutubeBridge extends BridgeAbstract { private $feedName = ''; private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time){ - $html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid"); + $html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid", true); // Skip unavailable videos if(!strpos($html->innertext, 'IS_UNAVAILABLE_PAGE')) { @@ -127,7 +127,6 @@ class YoutubeBridge extends BridgeAbstract { } private function ytBridgeParseHtmlListing($html, $element_selector, $title_selector, $add_parsed_items = true) { - $limit = $add_parsed_items ? 10 : INF; $count = 0; $duration_min = $this->getInput('duration_min') ?: -1; @@ -141,40 +140,38 @@ class YoutubeBridge extends BridgeAbstract { } foreach($html->find($element_selector) as $element) { - if($count < $limit) { - $author = ''; - $desc = ''; - $time = 0; - $vid = str_replace('/watch?v=', '', $element->find('a', 0)->href); - $vid = substr($vid, 0, strpos($vid, '&') ?: strlen($vid)); - $title = trim($this->ytBridgeFixTitle($element->find($title_selector, 0)->plaintext)); + $author = ''; + $desc = ''; + $time = 0; + $vid = str_replace('/watch?v=', '', $element->find('a', 0)->href); + $vid = substr($vid, 0, strpos($vid, '&') ?: strlen($vid)); + $title = trim($this->ytBridgeFixTitle($element->find($title_selector, 0)->plaintext)); - if (strpos($vid, 'googleads') !== false - || $title == '[Private video]' - || $title == '[Deleted video]' - ) { - continue; - } - - // The duration comes in one of the formats: - // hh:mm:ss / mm:ss / m:ss - // 01:03:30 / 15:06 / 1:24 - $durationText = trim($element->find('div.timestamp span', 0)->plaintext); - $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; - } - - if ($add_parsed_items) { - $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time); - $this->ytBridgeAddItem($vid, $title, $author, $desc, $time); - } - $count++; + if (strpos($vid, 'googleads') !== false + || $title == '[Private video]' + || $title == '[Deleted video]' + ) { + continue; } + + // The duration comes in one of the formats: + // hh:mm:ss / mm:ss / m:ss + // 01:03:30 / 15:06 / 1:24 + $durationText = trim($element->find('div.timestamp span', 0)->plaintext); + $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; + } + + if ($add_parsed_items) { + $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time); + $this->ytBridgeAddItem($vid, $title, $author, $desc, $time); + } + $count++; } return $count; } @@ -184,18 +181,38 @@ class YoutubeBridge extends BridgeAbstract { return html_entity_decode($title, ENT_QUOTES, 'UTF-8'); } - private function ytGetSimpleHTMLDOM($url){ + 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 = 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); + $header, + $opts, + $lowercase, + $forceTagsClosed, + $target_charset, + $stripRN, + $defaultBRText, + $defaultSpanText); } public function collectData(){ @@ -229,7 +246,7 @@ class YoutubeBridge extends BridgeAbstract { $url_listing = self::URI . 'playlist?list=' . urlencode($this->request); $html = $this->ytGetSimpleHTMLDOM($url_listing) or returnServerError("Could not request YouTube. Tried:\n - $url_listing"); - $item_count = $this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a', true); + $item_count = $this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a', false); if ($item_count <= 15 && !$this->skipFeeds() && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) { $this->ytBridgeParseXmlFeed($xml); } else { diff --git a/bridges/ZoneTelechargementBridge.php b/bridges/ZoneTelechargementBridge.php index 44cdfceb..ab7b947f 100644 --- a/bridges/ZoneTelechargementBridge.php +++ b/bridges/ZoneTelechargementBridge.php @@ -7,9 +7,9 @@ class ZoneTelechargementBridge extends BridgeAbstract { * name of the bridge. This permits to keep the same RSS Feed URL. */ - const NAME = 'Annuaire Telechargement'; - const URI = 'https://www.annuaire-telechargement.com/'; - const DESCRIPTION = 'Suivi de série sur Annuaire Telechargement'; + const NAME = 'Zone Telechargement'; + const URI = 'https://www.zone-telechargement.net/'; + const DESCRIPTION = 'Suivi de série sur Zone Telechargement'; const MAINTAINER = 'sysadminstory'; const PARAMETERS = array( 'Suivre la publication des épisodes d\'une série en cours de diffusion' => array( @@ -17,14 +17,14 @@ class ZoneTelechargementBridge extends BridgeAbstract { 'name' => 'URL de la série', 'type' => 'text', 'required' => true, - 'title' => 'URL d\'une série sans le https://www.annuaire-telechargement.com/', + 'title' => 'URL d\'une série sans le https://wwv.zone-telechargement.net/', 'exampleValue' => 'telecharger-series/31079-halt-and-catch-fire-saison-4-french-hd720p.html' ) ) ); public function getIcon() { - return 'https://www.annuaire-telechargement.com/templates/Default/images/favicon.ico'; + return self::URI . '/templates/Default/images/favicon.ico'; } public function collectData(){ diff --git a/cache/pages/.gitkeep b/cache/pages/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/cache/server/.gitkeep b/cache/server/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/composer.json b/composer.json index 8748cb39..3c03eeb0 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,39 @@ { + "name": "rss-bridge/rss-bridge", + "description": "RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one. It can be used on webservers or as a stand-alone application in CLI mode.", + "keywords": [ + "php", + "rss", + "bridge", + "rss-bridge", + "atom", + "html", + "json", + "feed", + "cli" + ], + "homepage": "https://github.com/rss-bridge/rss-bridge/", + "license": "UNLICENSE", + "support": { + "issues": "https://github.com/rss-bridge/rss-bridge/issues/", + "wiki": "https://github.com/rss-bridge/rss-bridge/wiki/", + "source": "https://github.com/rss-bridge/rss-bridge/", + "rss": "https://github.com/RSS-Bridge/rss-bridge/commits/master.atom" + }, "require": { - "php": ">=5.6", - "ext-mbstring": "*", - "ext-sqlite3": "*", - "ext-curl": "*", - "ext-openssl": "*", - "ext-libxml": "*", - "ext-simplexml": "*", - "ext-json": "*" + "php": ">=5.6", + "ext-mbstring": "*", + "ext-curl": "*", + "ext-openssl": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "ext-json": "*" + }, + "require-dev": { + "phpunit/phpunit": "^6 || ^7" + }, + "suggest": { + "ext-memcached": "Allows to use memcached as cache type", + "ext-sqlite3": "Allows to use an SQLite database for caching" } } diff --git a/composer.lock b/composer.lock index 3d8d9f2f..918c4304 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,1479 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ef341ee18f28c7bd5832e188fe157734", + "content-hash": "b60dc7dd86ffc8be27b94fc71894cad0", "packages": [], - "packages-dev": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "a2c590166b2133a4633738648b6b064edae0814a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", + "reference": "a2c590166b2133a4633738648b6b064edae0814a", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2019-03-17T17:37:11+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.9.3", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea", + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2019-08-09T12:45:53+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2018-07-08T19:23:20+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2018-07-08T19:19:57+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2018-08-07T13:53:10+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "4.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e", + "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", + "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "doctrine/instantiator": "^1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2019-09-12T14:27:41+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", + "shasum": "" + }, + "require": { + "php": "^7.1", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "^7.1", + "mockery/mockery": "~1", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2019-08-22T18:11:29+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203", + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", + "sebastian/comparator": "^1.1|^2.0|^3.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5|^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2019-10-03T11:07:50+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "6.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.1", + "phpunit/php-file-iterator": "^2.0", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.1 || ^4.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "ext-xdebug": "^2.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2018-10-31T16:06:48+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "050bedf145a257b1ff02746c31894800e5122946" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", + "reference": "050bedf145a257b1ff02746c31894800e5122946", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2018-09-13T20:33:42+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2019-06-07T04:22:29+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2019-09-17T06:23:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "7.5.17", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "4c92a15296e58191a4cd74cff3b34fc8e374174a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4c92a15296e58191a4cd74cff3b34fc8e374174a", + "reference": "4c92a15296e58191a4cd74cff3b34fc8e374174a", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.1", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^6.0.7", + "phpunit/php-file-iterator": "^2.0.1", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", + "sebastian/environment": "^4.0", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpunit/phpunit-mock-objects": "*" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2019-10-28T10:37:36+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "shasum": "" + }, + "require": { + "php": "^7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2018-07-12T15:12:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "time": "2019-02-04T06:01:07+00:00" + }, + { + "name": "sebastian/environment", + "version": "4.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404", + "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2019-05-05T09:05:15+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2019-09-14T09:02:43+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-08-03T12:35:26+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2017-03-03T06:23:57+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2018-10-04T04:07:39+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "550ebaac289296ce228a706d0867afc34687e3f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", + "reference": "550ebaac289296ce228a706d0867afc34687e3f4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.12-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2019-08-06T08:03:45+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2019-06-13T22:48:21+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4", + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2019-08-24T08:43:50+00:00" + } + ], "aliases": [], "minimum-stability": "stable", "stability-flags": [], @@ -15,7 +1485,6 @@ "platform": { "php": ">=5.6", "ext-mbstring": "*", - "ext-sqlite3": "*", "ext-curl": "*", "ext-openssl": "*", "ext-libxml": "*", diff --git a/config.default.ini.php b/config.default.ini.php index b7d4fba0..7d0bdaaa 100644 --- a/config.default.ini.php +++ b/config.default.ini.php @@ -61,6 +61,18 @@ username = "" ; Use a strong password to prevent others from guessing your login! password = "" +[error] + +; Defines how error messages are returned by RSS-Bridge +; +; "feed" = As part of the feed (default) +; "http" = As HTTP error message +; "none" = No errors are reported +output = "feed" + +; Defines how often an error must occur before it is reported to the user +report_limit = 1 + ; --- Cache specific configuration --------------------------------------------- [SQLiteCache] diff --git a/formats/AtomFormat.php b/formats/AtomFormat.php index 02d7d61a..a1ecfcf4 100644 --- a/formats/AtomFormat.php +++ b/formats/AtomFormat.php @@ -7,6 +7,8 @@ * https://validator.w3.org/feed/ */ class AtomFormat extends FormatAbstract{ + const MIME_TYPE = 'application/atom+xml'; + const LIMIT_TITLE = 140; public function stringify(){ @@ -37,7 +39,7 @@ class AtomFormat extends FormatAbstract{ $entries = ''; foreach($this->getItems() as $item) { $entryTimestamp = $item->getTimestamp(); - $entryTitle = $this->xml_encode($item->getTitle()); + $entryTitle = $item->getTitle(); $entryContent = $item->getContent(); $entryUri = $item->getURI(); $entryID = ''; @@ -147,7 +149,7 @@ EOD; public function display(){ $this - ->setContentType('application/atom+xml; charset=' . $this->getCharset()) + ->setContentType(self::MIME_TYPE . '; charset=' . $this->getCharset()) ->callContentType(); return parent::display(); diff --git a/formats/HtmlFormat.php b/formats/HtmlFormat.php index 052bedc5..49d9ca68 100644 --- a/formats/HtmlFormat.php +++ b/formats/HtmlFormat.php @@ -1,11 +1,30 @@ getExtraInfos(); $title = htmlspecialchars($extraInfos['name']); $uri = htmlspecialchars($extraInfos['uri']); - $atomquery = str_replace('format=Html', 'format=Atom', htmlentities($_SERVER['QUERY_STRING'])); - $mrssquery = str_replace('format=Html', 'format=Mrss', htmlentities($_SERVER['QUERY_STRING'])); + + // Dynamically build buttons for all formats (except HTML) + $formatFac = new FormatFactory(); + $formatFac->setWorkingDir(PATH_LIB_FORMATS); + + $buttons = ''; + $links = ''; + + foreach($formatFac->getFormatNames() as $format) { + if(strcasecmp($format, 'HTML') === 0) { + continue; + } + + $query = str_replace('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; + } $entries = ''; foreach($this->getItems() as $item) { @@ -82,18 +101,18 @@ EOD; + {$title} - - + + {$links}

{$title}

{$entries} @@ -108,9 +127,22 @@ EOD; public function display() { $this - ->setContentType('text/html; charset=' . $this->getCharset()) + ->setContentType(self::MIME_TYPE . '; charset=' . $this->getCharset()) ->callContentType(); return parent::display(); } + + private function buildButton($format, $query) { + return << +EOD; + } + + private function buildLink($format, $query, $mime) { + return << + +EOD; + } } diff --git a/formats/JsonFormat.php b/formats/JsonFormat.php index 5d091628..2c5cd073 100644 --- a/formats/JsonFormat.php +++ b/formats/JsonFormat.php @@ -8,6 +8,8 @@ * https://github.com/vigetlabs/json-feed-validator */ class JsonFormat extends FormatAbstract { + const MIME_TYPE = 'application/json'; + const VENDOR_EXCLUDES = array( 'author', 'title', @@ -119,7 +121,7 @@ class JsonFormat extends FormatAbstract { public function display(){ $this - ->setContentType('application/json; charset=' . $this->getCharset()) + ->setContentType(self::MIME_TYPE . '; charset=' . $this->getCharset()) ->callContentType(); return parent::display(); diff --git a/formats/MrssFormat.php b/formats/MrssFormat.php index 836a361a..8bf569ae 100644 --- a/formats/MrssFormat.php +++ b/formats/MrssFormat.php @@ -25,6 +25,8 @@ * 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'; + const ALLOWED_IMAGE_EXT = array( '.gif', '.jpg', '.png' ); @@ -150,7 +152,7 @@ EOD; public function display(){ $this - ->setContentType('application/rss+xml; charset=' . $this->getCharset()) + ->setContentType(self::MIME_TYPE . '; charset=' . $this->getCharset()) ->callContentType(); return parent::display(); diff --git a/formats/PlaintextFormat.php b/formats/PlaintextFormat.php index 591a4b34..5a0522cf 100644 --- a/formats/PlaintextFormat.php +++ b/formats/PlaintextFormat.php @@ -4,6 +4,8 @@ * Returns $this->items as raw php data. */ class PlaintextFormat extends FormatAbstract { + const MIME_TYPE = 'text/plain'; + public function stringify(){ $items = $this->getItems(); $data = array(); @@ -22,7 +24,7 @@ class PlaintextFormat extends FormatAbstract { public function display(){ $this - ->setContentType('text/plain; charset=' . $this->getCharset()) + ->setContentType(self::MIME_TYPE . '; charset=' . $this->getCharset()) ->callContentType(); return parent::display(); diff --git a/lib/BridgeAbstract.php b/lib/BridgeAbstract.php index 13215a46..c8ad79c5 100644 --- a/lib/BridgeAbstract.php +++ b/lib/BridgeAbstract.php @@ -165,8 +165,8 @@ abstract class BridgeAbstract implements BridgeInterface { foreach(static::PARAMETERS['global'] as $name => $properties) { if(isset($inputs[$name])) { $value = $inputs[$name]; - } elseif(isset($properties['value'])) { - $value = $properties['value']; + } elseif(isset($properties['defaultValue'])) { + $value = $properties['defaultValue']; } else { continue; } @@ -194,6 +194,11 @@ abstract class BridgeAbstract implements BridgeInterface { */ 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)) { @@ -218,8 +223,11 @@ abstract class BridgeAbstract implements BridgeInterface { ); } - // Guess the paramter context from input data - $this->queriedContext = $validator->getQueriedContext($inputs, static::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) { diff --git a/lib/BridgeCard.php b/lib/BridgeCard.php index 697433ff..4353f643 100644 --- a/lib/BridgeCard.php +++ b/lib/BridgeCard.php @@ -48,13 +48,19 @@ final class BridgeCard { * @param bool $isHttps If disabled, adds a warning to the form * @return string The form header */ - private static function getFormHeader($bridgeName, $isHttps = false) { + private static function getFormHeader($bridgeName, $isHttps = false, $parameterName = '') { $form = << EOD; + if(!empty($parameterName)) { + $form .= << +EOD; + } + if(!$isHttps) { $form .= '
Warning : This bridge is not fetching its content through a secure connection
'; @@ -80,7 +86,7 @@ This bridge is not fetching its content through a secure connection
'; $isHttps = false, $parameterName = '', $parameters = array()) { - $form = self::getFormHeader($bridgeName, $isHttps); + $form = self::getFormHeader($bridgeName, $isHttps, $parameterName); if(count($parameters) > 0) { @@ -116,6 +122,11 @@ This bridge is not fetching its content through a secure connection
'; } elseif($inputEntry['type'] === 'checkbox') { $form .= self::getCheckboxInput($inputEntry, $idArg, $id); } + + if(isset($inputEntry['title'])) + $form .= 'i'; + else + $form .= ''; } $form .= ''; @@ -146,9 +157,6 @@ This bridge is not fetching its content through a secure connection'; if(isset($entry['pattern'])) $retVal .= ' pattern="' . $entry['pattern'] . '"'; - if(isset($entry['title'])) - $retVal .= ' title="' . filter_var($entry['title'], FILTER_SANITIZE_STRING) . '"'; - return $retVal; } @@ -299,7 +307,10 @@ This bridge is not fetching its content through a secure connection'; */ static function displayBridgeCard($bridgeName, $formats, $isActive = true){ - $bridge = Bridge::create($bridgeName); + $bridgeFac = new \BridgeFactory(); + $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES); + + $bridge = $bridgeFac->create($bridgeName); if($bridge == false) return ''; diff --git a/lib/Bridge.php b/lib/BridgeFactory.php similarity index 66% rename from lib/Bridge.php rename to lib/BridgeFactory.php index 31607922..fea254f1 100644 --- a/lib/Bridge.php +++ b/lib/BridgeFactory.php @@ -35,17 +35,7 @@ * $bridge = Bridge::create('GitHubIssue'); * ``` */ -class Bridge { - - /** - * Holds a path to the working directory. - * - * Do not access this property directly! - * Use {@see Bridge::setWorkingDir()} and {@see Bridge::getWorkingDir()} instead. - * - * @var string|null - */ - protected static $workingDir = null; +class BridgeFactory extends FactoryAbstract { /** * Holds a list of whitelisted bridges. @@ -55,18 +45,7 @@ class Bridge { * * @var array */ - protected static $whitelist = array(); - - /** - * Throws an exception when trying to create a new instance of this class. - * Use {@see Bridge::create()} to instanciate a new bridge from the working - * directory. - * - * @throws \LogicException if called. - */ - public function __construct(){ - throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create bridge objects!'); - } + protected $whitelist = array(); /** * Creates a new bridge object from the working directory. @@ -77,13 +56,13 @@ class Bridge { * @param string $name Name of the bridge object. * @return object|bool The bridge object or false if the class is not instantiable. */ - public static function create($name){ - if(!self::isBridgeName($name)) { + public function create($name){ + if(!$this->isBridgeName($name)) { throw new \InvalidArgumentException('Bridge name invalid!'); } - $name = self::sanitizeBridgeName($name) . 'Bridge'; - $filePath = self::getWorkingDir() . $name . '.php'; + $name = $this->sanitizeBridgeName($name) . 'Bridge'; + $filePath = $this->getWorkingDir() . $name . '.php'; if(!file_exists($filePath)) { throw new \Exception('Bridge file ' . $filePath . ' does not exist!'); @@ -98,48 +77,6 @@ class Bridge { return false; } - /** - * Sets the working directory. - * - * @param string $dir Path to the directory containing bridges. - * @throws \LogicException if the provided path is not a valid string. - * @throws \Exception if the provided path does not exist. - * @throws \InvalidArgumentException if $dir is not a directory. - * @return void - */ - public static function setWorkingDir($dir){ - self::$workingDir = null; - - if(!is_string($dir)) { - throw new \InvalidArgumentException('Working directory is not a valid string!'); - } - - if(!file_exists($dir)) { - throw new \Exception('Working directory does not exist!'); - } - - if(!is_dir($dir)) { - throw new \InvalidArgumentException('Working directory is not a directory!'); - } - - self::$workingDir = realpath($dir) . '/'; - } - - /** - * Returns the working directory. - * The working directory must be specified with {@see Bridge::setWorkingDir()}! - * - * @throws \LogicException if the working directory is not set. - * @return string The current working directory. - */ - public static function getWorkingDir(){ - if(is_null(self::$workingDir)) { - throw new \LogicException('Working directory is not set!'); - } - - return self::$workingDir; - } - /** * Returns true if the provided name is a valid bridge name. * @@ -149,7 +86,7 @@ class Bridge { * @param string $name The bridge name. * @return bool true if the name is a valid bridge name, false otherwise. */ - public static function isBridgeName($name){ + public function isBridgeName($name){ return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1; } @@ -160,12 +97,12 @@ class Bridge { * * @return array List of bridge names */ - public static function getBridgeNames(){ + public function getBridgeNames(){ static $bridgeNames = array(); // Initialized on first call if(empty($bridgeNames)) { - $files = scandir(self::getWorkingDir()); + $files = scandir($this->getWorkingDir()); if($files !== false) { foreach($files as $file) { @@ -185,8 +122,8 @@ class Bridge { * @param string $name Name of the bridge. * @return bool True if the bridge is whitelisted. */ - public static function isWhitelisted($name){ - return in_array(self::sanitizeBridgeName($name), self::getWhitelist()); + public function isWhitelisted($name){ + return in_array($this->sanitizeBridgeName($name), $this->getWhitelist()); } /** @@ -205,7 +142,7 @@ class Bridge { * * @return array Array of whitelisted bridges */ - public static function getWhitelist() { + public function getWhitelist() { static $firstCall = true; // Initialized on first call @@ -220,17 +157,17 @@ class Bridge { } if($contents === '*') { // Whitelist all bridges - self::$whitelist = self::getBridgeNames(); + $this->whitelist = $this->getBridgeNames(); } else { - //self::$whitelist = array_map('self::sanitizeBridgeName', explode("\n", $contents)); + //$this->$whitelist = array_map('$this->sanitizeBridgeName', explode("\n", $contents)); foreach(explode("\n", $contents) as $bridgeName) { - self::$whitelist[] = self::sanitizeBridgeName($bridgeName); + $this->whitelist[] = $this->sanitizeBridgeName($bridgeName); } } } - return self::$whitelist; + return $this->whitelist; } @@ -248,8 +185,8 @@ class Bridge { * @param array $default The whitelist as array of bridge names. * @return void */ - public static function setWhitelist($default = array()) { - self::$whitelist = array_map('self::sanitizeBridgeName', $default); + public function setWhitelist($default = array()) { + $this->whitelist = array_map('$this->sanitizeBridgeName', $default); } /** @@ -269,7 +206,7 @@ class Bridge { * @return string|null The sanitized bridge name if the provided name is * valid, null otherwise. */ - protected static function sanitizeBridgeName($name) { + protected function sanitizeBridgeName($name) { if(is_string($name)) { @@ -284,15 +221,15 @@ class Bridge { } // Improve performance for correctly written bridge names - if(in_array($name, self::getBridgeNames())) { - $index = array_search($name, self::getBridgeNames()); - return self::getBridgeNames()[$index]; + 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', self::getBridgeNames()))) { - $index = array_search(strtolower($name), array_map('strtolower', self::getBridgeNames())); - return self::getBridgeNames()[$index]; + 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 . '"!'); diff --git a/lib/BridgeList.php b/lib/BridgeList.php index bcd39db1..fc92dfb7 100644 --- a/lib/BridgeList.php +++ b/lib/BridgeList.php @@ -34,6 +34,7 @@ final class BridgeList { RSS-Bridge +