From 7fe1a5ea5ff4aeecbbc2af673cbdc88fbbea18d5 Mon Sep 17 00:00:00 2001 From: Johannes 'josch' Schauer Date: Fri, 16 Jun 2017 15:18:31 +0200 Subject: New upstream version 0.12.0+dfsg1 --- .coveragerc | 20 + .gitignore | 18 + .landscape.yaml | 3 + .travis.yml | 37 + AUTHORS.rst | 69 + CHANGELOG.rst | 252 + Dockerfile | 53 + LICENSE | 661 ++ README.rst | 45 + babel.cfg | 3 + examples/basic_engine.py | 25 + manage.sh | 128 + requirements-dev.txt | 10 + requirements.txt | 10 + searx/__init__.py | 89 + searx/answerers/__init__.py | 50 + searx/answerers/random/answerer.py | 55 + searx/answerers/statistics/answerer.py | 55 + searx/autocomplete.py | 201 + searx/data/currencies.json | 7655 ++++++++++++++++++++ searx/data/engines_languages.json | 1 + searx/engines/1337x.py | 39 + searx/engines/__init__.py | 221 + searx/engines/archlinux.py | 142 + searx/engines/base.py | 121 + searx/engines/bing.py | 101 + searx/engines/bing_images.py | 108 + searx/engines/bing_news.py | 127 + searx/engines/blekko_images.py | 70 + searx/engines/btdigg.py | 92 + searx/engines/currency_convert.py | 105 + searx/engines/dailymotion.py | 97 + searx/engines/deezer.py | 67 + searx/engines/deviantart.py | 84 + searx/engines/dictzone.py | 68 + searx/engines/digbt.py | 62 + searx/engines/digg.py | 74 + searx/engines/doku.py | 84 + searx/engines/duckduckgo.py | 137 + searx/engines/duckduckgo_definitions.py | 157 + searx/engines/duckduckgo_images.py | 91 + searx/engines/dummy.py | 16 + searx/engines/faroo.py | 116 + searx/engines/fdroid.py | 51 + searx/engines/filecrop.py | 88 + searx/engines/flickr.py | 90 + searx/engines/flickr_noapi.py | 117 + searx/engines/framalibre.py | 69 + searx/engines/frinkiac.py | 44 + searx/engines/generalfile.py | 62 + searx/engines/gigablast.py | 106 + searx/engines/github.py | 60 + searx/engines/google.py | 388 + searx/engines/google_images.py | 95 + searx/engines/google_news.py | 84 + searx/engines/ina.py | 87 + searx/engines/json_engine.py | 118 + searx/engines/kickass.py | 92 + searx/engines/mediawiki.py | 90 + searx/engines/mixcloud.py | 61 + searx/engines/nyaa.py | 117 + searx/engines/openstreetmap.py | 95 + searx/engines/pdbe.py | 109 + searx/engines/photon.py | 131 + searx/engines/piratebay.py | 96 + searx/engines/qwant.py | 140 + searx/engines/reddit.py | 76 + searx/engines/scanr_structures.py | 76 + searx/engines/searchcode_code.py | 69 + searx/engines/searchcode_doc.py | 49 + searx/engines/searx_engine.py | 57 + searx/engines/seedpeer.py | 75 + searx/engines/soundcloud.py | 104 + searx/engines/spotify.py | 62 + searx/engines/stackoverflow.py | 57 + searx/engines/startpage.py | 123 + searx/engines/subtitleseeker.py | 86 + searx/engines/swisscows.py | 126 + searx/engines/tokyotoshokan.py | 100 + searx/engines/torrentz.py | 92 + searx/engines/translated.py | 68 + searx/engines/twitter.py | 87 + searx/engines/vimeo.py | 67 + searx/engines/wikidata.py | 488 ++ searx/engines/wikipedia.py | 135 + searx/engines/wolframalpha_api.py | 129 + searx/engines/wolframalpha_noapi.py | 120 + searx/engines/www1x.py | 81 + searx/engines/www500px.py | 73 + searx/engines/xpath.py | 122 + searx/engines/yacy.py | 99 + searx/engines/yahoo.py | 153 + searx/engines/yahoo_news.py | 107 + searx/engines/yandex.py | 64 + searx/engines/youtube_api.py | 83 + searx/engines/youtube_noapi.py | 89 + searx/exceptions.py | 32 + searx/languages.py | 77 + searx/plugins/__init__.py | 88 + searx/plugins/doai_rewrite.py | 31 + searx/plugins/https_rewrite.py | 232 + searx/plugins/https_rules/00README | 17 + searx/plugins/https_rules/Bing.xml | 56 + searx/plugins/https_rules/Dailymotion.xml | 69 + searx/plugins/https_rules/Deviantart.xml | 53 + searx/plugins/https_rules/DuckDuckGo.xml | 38 + searx/plugins/https_rules/Flickr.xml | 44 + searx/plugins/https_rules/Github-Pages.xml | 11 + searx/plugins/https_rules/Github.xml | 94 + searx/plugins/https_rules/Google-mismatches.xml | 26 + searx/plugins/https_rules/Google.org.xml | 14 + searx/plugins/https_rules/GoogleAPIs.xml | 143 + searx/plugins/https_rules/GoogleCanada.xml | 6 + searx/plugins/https_rules/GoogleImages.xml | 65 + searx/plugins/https_rules/GoogleMainSearch.xml | 78 + searx/plugins/https_rules/GoogleMaps.xml | 67 + searx/plugins/https_rules/GoogleMelange.xml | 6 + searx/plugins/https_rules/GoogleSearch.xml | 135 + searx/plugins/https_rules/GoogleServices.xml | 345 + searx/plugins/https_rules/GoogleShopping.xml | 28 + searx/plugins/https_rules/GoogleSorry.xml | 7 + searx/plugins/https_rules/GoogleTranslate.xml | 8 + searx/plugins/https_rules/GoogleVideos.xml | 83 + searx/plugins/https_rules/GoogleWatchBlog.xml | 17 + searx/plugins/https_rules/Google_App_Engine.xml | 21 + searx/plugins/https_rules/Googleplex.com.xml | 16 + searx/plugins/https_rules/OpenStreetMap.xml | 15 + searx/plugins/https_rules/Rawgithub.com.xml | 14 + searx/plugins/https_rules/Soundcloud.xml | 101 + searx/plugins/https_rules/ThePirateBay.xml | 36 + searx/plugins/https_rules/Torproject.xml | 18 + searx/plugins/https_rules/Twitter.xml | 169 + searx/plugins/https_rules/Vimeo.xml | 75 + searx/plugins/https_rules/WikiLeaks.xml | 13 + searx/plugins/https_rules/Wikimedia.xml | 107 + searx/plugins/https_rules/Yahoo.xml | 2450 +++++++ searx/plugins/https_rules/YouTube.xml | 46 + searx/plugins/infinite_scroll.py | 8 + searx/plugins/open_results_on_new_tab.py | 24 + searx/plugins/search_on_category_select.py | 23 + searx/plugins/self_info.py | 46 + searx/plugins/tracker_url_remover.py | 44 + searx/plugins/vim_hotkeys.py | 10 + searx/poolrequests.py | 112 + searx/preferences.py | 304 + searx/query.py | 162 + searx/results.py | 306 + searx/search.py | 432 ++ searx/settings.yml | 686 ++ searx/settings_robot.yml | 41 + searx/static/plugins/css/infinite_scroll.css | 16 + searx/static/plugins/css/vim_hotkeys.css | 26 + searx/static/plugins/js/infinite_scroll.js | 18 + searx/static/plugins/js/open_results_on_new_tab.js | 3 + .../static/plugins/js/search_on_category_select.js | 24 + searx/static/plugins/js/vim_hotkeys.js | 336 + searx/static/themes/courgette/img/favicon.png | Bin 0 -> 2039 bytes .../themes/courgette/img/preference-icon.png | Bin 0 -> 1315 bytes searx/static/themes/courgette/img/search-icon.png | Bin 0 -> 3270 bytes searx/static/themes/courgette/img/searx-mobile.png | Bin 0 -> 9415 bytes searx/static/themes/courgette/img/searx.png | Bin 0 -> 3902 bytes searx/static/themes/courgette/img/searx_logo.svg | 203 + searx/static/themes/courgette/js/searx.js | 45 + searx/static/themes/courgette/less/style-rtl.less | 42 + searx/static/themes/courgette/less/style.less | 691 ++ searx/static/themes/legacy/img/favicon.png | Bin 0 -> 2039 bytes searx/static/themes/legacy/img/preference-icon.png | Bin 0 -> 532 bytes searx/static/themes/legacy/img/search-icon.png | Bin 0 -> 2329 bytes searx/static/themes/legacy/img/searx.png | Bin 0 -> 3902 bytes searx/static/themes/legacy/img/searx_logo.svg | 203 + searx/static/themes/legacy/js/searx.js | 49 + searx/static/themes/legacy/less/autocompleter.less | 61 + searx/static/themes/legacy/less/code.less | 83 + searx/static/themes/legacy/less/definitions.less | 119 + searx/static/themes/legacy/less/mixins.less | 27 + searx/static/themes/legacy/less/search.less | 68 + searx/static/themes/legacy/less/style-rtl.less | 11 + searx/static/themes/legacy/less/style.less | 739 ++ searx/static/themes/oscar/.gitignore | 1 + searx/static/themes/oscar/README.rst | 17 + searx/static/themes/oscar/gruntfile.js | 90 + searx/static/themes/oscar/img/favicon.png | Bin 0 -> 1853 bytes searx/static/themes/oscar/img/icons/README.md | 2 + searx/static/themes/oscar/img/loader.gif | Bin 0 -> 8314 bytes searx/static/themes/oscar/img/logo_searx_a.png | Bin 0 -> 9557 bytes searx/static/themes/oscar/img/logo_searx_a_n.png | Bin 0 -> 9704 bytes searx/static/themes/oscar/img/map/layers-2x.png | Bin 0 -> 1763 bytes searx/static/themes/oscar/img/map/layers.png | Bin 0 -> 1142 bytes .../themes/oscar/img/map/marker-icon-2x-green.png | Bin 0 -> 3753 bytes .../themes/oscar/img/map/marker-icon-2x-orange.png | Bin 0 -> 3691 bytes .../themes/oscar/img/map/marker-icon-2x-red.png | Bin 0 -> 3692 bytes .../static/themes/oscar/img/map/marker-icon-2x.png | Bin 0 -> 4033 bytes .../themes/oscar/img/map/marker-icon-green.png | Bin 0 -> 1696 bytes .../themes/oscar/img/map/marker-icon-orange.png | Bin 0 -> 1714 bytes .../themes/oscar/img/map/marker-icon-red.png | Bin 0 -> 1690 bytes searx/static/themes/oscar/img/map/marker-icon.png | Bin 0 -> 1747 bytes .../static/themes/oscar/img/map/marker-shadow.png | Bin 0 -> 797 bytes searx/static/themes/oscar/img/searx_logo.png | Bin 0 -> 10611 bytes .../oscar/js/searx_src/00_requirejs_config.js | 23 + .../themes/oscar/js/searx_src/autocompleter.js | 37 + .../themes/oscar/js/searx_src/element_modifiers.js | 99 + .../themes/oscar/js/searx_src/leaflet_map.js | 167 + .../themes/oscar/less/logicodev/advanced.less | 49 + .../themes/oscar/less/logicodev/checkbox.less | 9 + searx/static/themes/oscar/less/logicodev/code.less | 103 + .../static/themes/oscar/less/logicodev/cursor.less | 8 + .../static/themes/oscar/less/logicodev/footer.less | 30 + .../themes/oscar/less/logicodev/infobox.less | 37 + .../static/themes/oscar/less/logicodev/navbar.less | 31 + .../static/themes/oscar/less/logicodev/onoff.less | 57 + .../static/themes/oscar/less/logicodev/oscar.less | 21 + .../themes/oscar/less/logicodev/results.less | 168 + .../static/themes/oscar/less/logicodev/search.less | 79 + .../themes/oscar/less/logicodev/variables.less | 13 + .../static/themes/oscar/less/pointhi/advanced.less | 49 + .../static/themes/oscar/less/pointhi/checkbox.less | 9 + searx/static/themes/oscar/less/pointhi/code.less | 79 + searx/static/themes/oscar/less/pointhi/cursor.less | 8 + searx/static/themes/oscar/less/pointhi/footer.less | 19 + .../static/themes/oscar/less/pointhi/infobox.less | 11 + searx/static/themes/oscar/less/pointhi/navbar.less | 20 + searx/static/themes/oscar/less/pointhi/onoff.less | 57 + searx/static/themes/oscar/less/pointhi/oscar.less | 19 + .../static/themes/oscar/less/pointhi/results.less | 101 + searx/static/themes/oscar/less/pointhi/search.less | 32 + searx/static/themes/oscar/package.json | 16 + searx/static/themes/pix-art/img/favicon.png | Bin 0 -> 2039 bytes .../themes/pix-art/img/preference-icon-pixel.png | Bin 0 -> 242 bytes .../themes/pix-art/img/search-icon-pixel.png | Bin 0 -> 204 bytes .../themes/pix-art/img/searx-pixel-small.png | Bin 0 -> 236 bytes searx/static/themes/pix-art/img/searx-pixel.png | Bin 0 -> 435 bytes searx/static/themes/pix-art/js/searx.js | 141 + searx/static/themes/pix-art/less/definitions.less | 119 + searx/static/themes/pix-art/less/mixins.less | 27 + searx/static/themes/pix-art/less/search.less | 57 + searx/static/themes/pix-art/less/style.less | 451 ++ searx/templates/__common__/about.html | 62 + searx/templates/__common__/opensearch.xml | 28 + .../__common__/opensearch_response_rss.xml | 29 + searx/templates/courgette/404.html | 9 + searx/templates/courgette/about.html | 5 + searx/templates/courgette/base.html | 43 + searx/templates/courgette/categories.html | 9 + searx/templates/courgette/color.css | 34 + searx/templates/courgette/github_ribbon.html | 3 + searx/templates/courgette/index.html | 17 + searx/templates/courgette/preferences.html | 132 + .../templates/courgette/result_templates/code.html | 11 + .../courgette/result_templates/default.html | 13 + .../courgette/result_templates/images.html | 6 + .../templates/courgette/result_templates/map.html | 13 + .../courgette/result_templates/torrent.html | 13 + .../courgette/result_templates/videos.html | 10 + searx/templates/courgette/results.html | 87 + searx/templates/courgette/search.html | 7 + searx/templates/courgette/stats.html | 22 + searx/templates/legacy/404.html | 9 + searx/templates/legacy/about.html | 5 + searx/templates/legacy/base.html | 38 + searx/templates/legacy/categories.html | 10 + searx/templates/legacy/github_ribbon.html | 3 + searx/templates/legacy/index.html | 18 + searx/templates/legacy/infobox.html | 51 + searx/templates/legacy/preferences.html | 129 + searx/templates/legacy/result_templates/code.html | 11 + .../templates/legacy/result_templates/default.html | 6 + .../templates/legacy/result_templates/images.html | 6 + searx/templates/legacy/result_templates/map.html | 13 + .../templates/legacy/result_templates/torrent.html | 13 + .../templates/legacy/result_templates/videos.html | 6 + searx/templates/legacy/results.html | 100 + searx/templates/legacy/search.html | 8 + searx/templates/legacy/stats.html | 22 + searx/templates/oscar/404.html | 9 + searx/templates/oscar/about.html | 5 + searx/templates/oscar/advanced.html | 16 + searx/templates/oscar/base.html | 107 + searx/templates/oscar/categories.html | 13 + searx/templates/oscar/index.html | 22 + searx/templates/oscar/infobox.html | 35 + searx/templates/oscar/languages.html | 12 + searx/templates/oscar/macros.html | 88 + searx/templates/oscar/messages/first_time.html | 8 + searx/templates/oscar/messages/no_cookies.html | 5 + .../oscar/messages/no_data_available.html | 5 + searx/templates/oscar/messages/no_results.html | 9 + .../oscar/messages/save_settings_successfull.html | 9 + searx/templates/oscar/messages/unknow_error.html | 9 + searx/templates/oscar/navbar.html | 9 + searx/templates/oscar/preferences.html | 292 + searx/templates/oscar/result_templates/code.html | 18 + .../templates/oscar/result_templates/default.html | 31 + searx/templates/oscar/result_templates/images.html | 39 + searx/templates/oscar/result_templates/map.html | 72 + .../templates/oscar/result_templates/torrent.html | 25 + searx/templates/oscar/result_templates/videos.html | 27 + searx/templates/oscar/results.html | 145 + searx/templates/oscar/search.html | 24 + searx/templates/oscar/search_full.html | 18 + searx/templates/oscar/stats.html | 33 + searx/templates/oscar/time-range.html | 17 + searx/templates/pix-art/404.html | 9 + searx/templates/pix-art/about.html | 4 + searx/templates/pix-art/base.html | 35 + searx/templates/pix-art/index.html | 12 + searx/templates/pix-art/preferences.html | 82 + .../pix-art/result_templates/default.html | 7 + .../templates/pix-art/result_templates/images.html | 6 + searx/templates/pix-art/results.html | 32 + searx/templates/pix-art/search.html | 9 + searx/templates/pix-art/stats.html | 22 + searx/testing.py | 98 + searx/translations/bg/LC_MESSAGES/messages.po | 843 +++ searx/translations/cs/LC_MESSAGES/messages.po | 842 +++ searx/translations/de/LC_MESSAGES/messages.po | 852 +++ searx/translations/de_DE/LC_MESSAGES/messages.po | 844 +++ searx/translations/el_GR/LC_MESSAGES/messages.po | 842 +++ searx/translations/en/LC_MESSAGES/messages.po | 695 ++ searx/translations/eo/LC_MESSAGES/messages.po | 844 +++ searx/translations/es/LC_MESSAGES/messages.po | 850 +++ searx/translations/fi/LC_MESSAGES/messages.po | 842 +++ searx/translations/fr/LC_MESSAGES/messages.po | 849 +++ searx/translations/he/LC_MESSAGES/messages.po | 846 +++ searx/translations/hu/LC_MESSAGES/messages.po | 845 +++ searx/translations/it/LC_MESSAGES/messages.po | 846 +++ searx/translations/ja/LC_MESSAGES/messages.po | 848 +++ searx/translations/nl/LC_MESSAGES/messages.po | 844 +++ searx/translations/pt/LC_MESSAGES/messages.po | 842 +++ searx/translations/pt_BR/LC_MESSAGES/messages.po | 844 +++ searx/translations/ro/LC_MESSAGES/messages.po | 842 +++ searx/translations/ru/LC_MESSAGES/messages.po | 845 +++ searx/translations/sk/LC_MESSAGES/messages.po | 842 +++ searx/translations/sv/LC_MESSAGES/messages.po | 842 +++ searx/translations/tr/LC_MESSAGES/messages.po | 843 +++ searx/translations/uk/LC_MESSAGES/messages.po | 843 +++ searx/translations/zh_CN/LC_MESSAGES/messages.po | 843 +++ searx/url_utils.py | 28 + searx/utils.py | 314 + searx/version.py | 26 + searx/webapp.py | 886 +++ setup.py | 74 + tests/__init__.py | 0 tests/robot/__init__.py | 75 + tests/test_robot.py | 23 + tests/unit/__init__.py | 0 tests/unit/engines/__init__.py | 0 tests/unit/engines/seedpeer_fixture.html | 110 + tests/unit/engines/test_archlinux.py | 106 + tests/unit/engines/test_bing.py | 120 + tests/unit/engines/test_bing_images.py | 84 + tests/unit/engines/test_bing_news.py | 146 + tests/unit/engines/test_blekko_images.py | 71 + tests/unit/engines/test_btdigg.py | 384 + tests/unit/engines/test_currency_convert.py | 43 + tests/unit/engines/test_dailymotion.py | 111 + tests/unit/engines/test_deezer.py | 57 + tests/unit/engines/test_deviantart.py | 95 + tests/unit/engines/test_digbt.py | 61 + tests/unit/engines/test_digg.py | 101 + tests/unit/engines/test_doku.py | 79 + tests/unit/engines/test_duckduckgo.py | 100 + tests/unit/engines/test_duckduckgo_definitions.py | 254 + tests/unit/engines/test_duckduckgo_images.py | 72 + tests/unit/engines/test_dummy.py | 26 + tests/unit/engines/test_faroo.py | 116 + tests/unit/engines/test_fdroid.py | 49 + tests/unit/engines/test_flickr.py | 142 + tests/unit/engines/test_flickr_noapi.py | 329 + tests/unit/engines/test_framalibre.py | 103 + tests/unit/engines/test_frinkiac.py | 50 + tests/unit/engines/test_gigablast.py | 119 + tests/unit/engines/test_github.py | 61 + tests/unit/engines/test_google.py | 236 + tests/unit/engines/test_google_images.py | 42 + tests/unit/engines/test_google_news.py | 50 + tests/unit/engines/test_ina.py | 64 + tests/unit/engines/test_kickass.py | 397 + tests/unit/engines/test_mediawiki.py | 130 + tests/unit/engines/test_mixcloud.py | 67 + tests/unit/engines/test_nyaa.py | 66 + tests/unit/engines/test_openstreetmap.py | 199 + tests/unit/engines/test_pdbe.py | 109 + tests/unit/engines/test_photon.py | 166 + tests/unit/engines/test_piratebay.py | 166 + tests/unit/engines/test_qwant.py | 338 + tests/unit/engines/test_reddit.py | 71 + tests/unit/engines/test_scanr_structures.py | 175 + tests/unit/engines/test_searchcode_code.py | 75 + tests/unit/engines/test_searchcode_doc.py | 70 + tests/unit/engines/test_seedpeer.py | 51 + tests/unit/engines/test_soundcloud.py | 192 + tests/unit/engines/test_spotify.py | 124 + tests/unit/engines/test_stackoverflow.py | 106 + tests/unit/engines/test_startpage.py | 140 + tests/unit/engines/test_subtitleseeker.py | 174 + tests/unit/engines/test_swisscows.py | 155 + tests/unit/engines/test_tokyotoshokan.py | 110 + tests/unit/engines/test_torrentz.py | 91 + tests/unit/engines/test_twitter.py | 502 ++ tests/unit/engines/test_vimeo.py | 36 + tests/unit/engines/test_wikidata.py | 503 ++ tests/unit/engines/test_wikipedia.py | 259 + tests/unit/engines/test_wolframalpha_api.py | 166 + tests/unit/engines/test_wolframalpha_noapi.py | 224 + tests/unit/engines/test_www1x.py | 57 + tests/unit/engines/test_www500px.py | 34 + tests/unit/engines/test_yacy.py | 96 + tests/unit/engines/test_yahoo.py | 179 + tests/unit/engines/test_yahoo_news.py | 149 + tests/unit/engines/test_youtube_api.py | 111 + tests/unit/engines/test_youtube_noapi.py | 174 + tests/unit/test_answerers.py | 16 + tests/unit/test_plugins.py | 85 + tests/unit/test_preferences.py | 124 + tests/unit/test_results.py | 41 + tests/unit/test_search.py | 10 + tests/unit/test_utils.py | 105 + tests/unit/test_webapp.py | 158 + tox.ini | 2 + utils/fabfile.py | 117 + utils/fetch_currencies.py | 161 + utils/fetch_languages.py | 189 + utils/google_search.py | 35 + utils/standalone_searx.py | 101 + utils/update-translations.sh | 15 + 425 files changed, 63460 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .landscape.yaml create mode 100644 .travis.yml create mode 100644 AUTHORS.rst create mode 100644 CHANGELOG.rst create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 babel.cfg create mode 100644 examples/basic_engine.py create mode 100755 manage.sh create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 searx/__init__.py create mode 100644 searx/answerers/__init__.py create mode 100644 searx/answerers/random/answerer.py create mode 100644 searx/answerers/statistics/answerer.py create mode 100644 searx/autocomplete.py create mode 100644 searx/data/currencies.json create mode 100644 searx/data/engines_languages.json create mode 100644 searx/engines/1337x.py create mode 100644 searx/engines/__init__.py create mode 100644 searx/engines/archlinux.py create mode 100755 searx/engines/base.py create mode 100644 searx/engines/bing.py create mode 100644 searx/engines/bing_images.py create mode 100644 searx/engines/bing_news.py create mode 100644 searx/engines/blekko_images.py create mode 100644 searx/engines/btdigg.py create mode 100644 searx/engines/currency_convert.py create mode 100644 searx/engines/dailymotion.py create mode 100644 searx/engines/deezer.py create mode 100644 searx/engines/deviantart.py create mode 100644 searx/engines/dictzone.py create mode 100644 searx/engines/digbt.py create mode 100644 searx/engines/digg.py create mode 100644 searx/engines/doku.py create mode 100644 searx/engines/duckduckgo.py create mode 100644 searx/engines/duckduckgo_definitions.py create mode 100644 searx/engines/duckduckgo_images.py create mode 100644 searx/engines/dummy.py create mode 100644 searx/engines/faroo.py create mode 100644 searx/engines/fdroid.py create mode 100644 searx/engines/filecrop.py create mode 100644 searx/engines/flickr.py create mode 100644 searx/engines/flickr_noapi.py create mode 100644 searx/engines/framalibre.py create mode 100644 searx/engines/frinkiac.py create mode 100644 searx/engines/generalfile.py create mode 100644 searx/engines/gigablast.py create mode 100644 searx/engines/github.py create mode 100644 searx/engines/google.py create mode 100644 searx/engines/google_images.py create mode 100644 searx/engines/google_news.py create mode 100644 searx/engines/ina.py create mode 100644 searx/engines/json_engine.py create mode 100644 searx/engines/kickass.py create mode 100644 searx/engines/mediawiki.py create mode 100644 searx/engines/mixcloud.py create mode 100644 searx/engines/nyaa.py create mode 100644 searx/engines/openstreetmap.py create mode 100644 searx/engines/pdbe.py create mode 100644 searx/engines/photon.py create mode 100644 searx/engines/piratebay.py create mode 100644 searx/engines/qwant.py create mode 100644 searx/engines/reddit.py create mode 100644 searx/engines/scanr_structures.py create mode 100644 searx/engines/searchcode_code.py create mode 100644 searx/engines/searchcode_doc.py create mode 100644 searx/engines/searx_engine.py create mode 100644 searx/engines/seedpeer.py create mode 100644 searx/engines/soundcloud.py create mode 100644 searx/engines/spotify.py create mode 100644 searx/engines/stackoverflow.py create mode 100644 searx/engines/startpage.py create mode 100644 searx/engines/subtitleseeker.py create mode 100644 searx/engines/swisscows.py create mode 100644 searx/engines/tokyotoshokan.py create mode 100644 searx/engines/torrentz.py create mode 100644 searx/engines/translated.py create mode 100644 searx/engines/twitter.py create mode 100644 searx/engines/vimeo.py create mode 100644 searx/engines/wikidata.py create mode 100644 searx/engines/wikipedia.py create mode 100644 searx/engines/wolframalpha_api.py create mode 100644 searx/engines/wolframalpha_noapi.py create mode 100644 searx/engines/www1x.py create mode 100644 searx/engines/www500px.py create mode 100644 searx/engines/xpath.py create mode 100644 searx/engines/yacy.py create mode 100644 searx/engines/yahoo.py create mode 100644 searx/engines/yahoo_news.py create mode 100644 searx/engines/yandex.py create mode 100644 searx/engines/youtube_api.py create mode 100644 searx/engines/youtube_noapi.py create mode 100644 searx/exceptions.py create mode 100644 searx/languages.py create mode 100644 searx/plugins/__init__.py create mode 100644 searx/plugins/doai_rewrite.py create mode 100644 searx/plugins/https_rewrite.py create mode 100644 searx/plugins/https_rules/00README create mode 100644 searx/plugins/https_rules/Bing.xml create mode 100644 searx/plugins/https_rules/Dailymotion.xml create mode 100644 searx/plugins/https_rules/Deviantart.xml create mode 100644 searx/plugins/https_rules/DuckDuckGo.xml create mode 100644 searx/plugins/https_rules/Flickr.xml create mode 100644 searx/plugins/https_rules/Github-Pages.xml create mode 100644 searx/plugins/https_rules/Github.xml create mode 100644 searx/plugins/https_rules/Google-mismatches.xml create mode 100644 searx/plugins/https_rules/Google.org.xml create mode 100644 searx/plugins/https_rules/GoogleAPIs.xml create mode 100644 searx/plugins/https_rules/GoogleCanada.xml create mode 100644 searx/plugins/https_rules/GoogleImages.xml create mode 100644 searx/plugins/https_rules/GoogleMainSearch.xml create mode 100644 searx/plugins/https_rules/GoogleMaps.xml create mode 100644 searx/plugins/https_rules/GoogleMelange.xml create mode 100644 searx/plugins/https_rules/GoogleSearch.xml create mode 100644 searx/plugins/https_rules/GoogleServices.xml create mode 100644 searx/plugins/https_rules/GoogleShopping.xml create mode 100644 searx/plugins/https_rules/GoogleSorry.xml create mode 100644 searx/plugins/https_rules/GoogleTranslate.xml create mode 100644 searx/plugins/https_rules/GoogleVideos.xml create mode 100644 searx/plugins/https_rules/GoogleWatchBlog.xml create mode 100644 searx/plugins/https_rules/Google_App_Engine.xml create mode 100644 searx/plugins/https_rules/Googleplex.com.xml create mode 100644 searx/plugins/https_rules/OpenStreetMap.xml create mode 100644 searx/plugins/https_rules/Rawgithub.com.xml create mode 100644 searx/plugins/https_rules/Soundcloud.xml create mode 100644 searx/plugins/https_rules/ThePirateBay.xml create mode 100644 searx/plugins/https_rules/Torproject.xml create mode 100644 searx/plugins/https_rules/Twitter.xml create mode 100644 searx/plugins/https_rules/Vimeo.xml create mode 100644 searx/plugins/https_rules/WikiLeaks.xml create mode 100644 searx/plugins/https_rules/Wikimedia.xml create mode 100644 searx/plugins/https_rules/Yahoo.xml create mode 100644 searx/plugins/https_rules/YouTube.xml create mode 100644 searx/plugins/infinite_scroll.py create mode 100644 searx/plugins/open_results_on_new_tab.py create mode 100644 searx/plugins/search_on_category_select.py create mode 100644 searx/plugins/self_info.py create mode 100644 searx/plugins/tracker_url_remover.py create mode 100644 searx/plugins/vim_hotkeys.py create mode 100644 searx/poolrequests.py create mode 100644 searx/preferences.py create mode 100644 searx/query.py create mode 100644 searx/results.py create mode 100644 searx/search.py create mode 100644 searx/settings.yml create mode 100644 searx/settings_robot.yml create mode 100644 searx/static/plugins/css/infinite_scroll.css create mode 100644 searx/static/plugins/css/vim_hotkeys.css create mode 100644 searx/static/plugins/js/infinite_scroll.js create mode 100644 searx/static/plugins/js/open_results_on_new_tab.js create mode 100644 searx/static/plugins/js/search_on_category_select.js create mode 100644 searx/static/plugins/js/vim_hotkeys.js create mode 100644 searx/static/themes/courgette/img/favicon.png create mode 100644 searx/static/themes/courgette/img/preference-icon.png create mode 100644 searx/static/themes/courgette/img/search-icon.png create mode 100644 searx/static/themes/courgette/img/searx-mobile.png create mode 100644 searx/static/themes/courgette/img/searx.png create mode 100644 searx/static/themes/courgette/img/searx_logo.svg create mode 100644 searx/static/themes/courgette/js/searx.js create mode 100644 searx/static/themes/courgette/less/style-rtl.less create mode 100644 searx/static/themes/courgette/less/style.less create mode 100644 searx/static/themes/legacy/img/favicon.png create mode 100644 searx/static/themes/legacy/img/preference-icon.png create mode 100644 searx/static/themes/legacy/img/search-icon.png create mode 100644 searx/static/themes/legacy/img/searx.png create mode 100644 searx/static/themes/legacy/img/searx_logo.svg create mode 100644 searx/static/themes/legacy/js/searx.js create mode 100644 searx/static/themes/legacy/less/autocompleter.less create mode 100644 searx/static/themes/legacy/less/code.less create mode 100644 searx/static/themes/legacy/less/definitions.less create mode 100644 searx/static/themes/legacy/less/mixins.less create mode 100644 searx/static/themes/legacy/less/search.less create mode 100644 searx/static/themes/legacy/less/style-rtl.less create mode 100644 searx/static/themes/legacy/less/style.less create mode 100644 searx/static/themes/oscar/.gitignore create mode 100644 searx/static/themes/oscar/README.rst create mode 100644 searx/static/themes/oscar/gruntfile.js create mode 100644 searx/static/themes/oscar/img/favicon.png create mode 100644 searx/static/themes/oscar/img/icons/README.md create mode 100644 searx/static/themes/oscar/img/loader.gif create mode 100644 searx/static/themes/oscar/img/logo_searx_a.png create mode 100644 searx/static/themes/oscar/img/logo_searx_a_n.png create mode 100644 searx/static/themes/oscar/img/map/layers-2x.png create mode 100644 searx/static/themes/oscar/img/map/layers.png create mode 100644 searx/static/themes/oscar/img/map/marker-icon-2x-green.png create mode 100644 searx/static/themes/oscar/img/map/marker-icon-2x-orange.png create mode 100644 searx/static/themes/oscar/img/map/marker-icon-2x-red.png create mode 100644 searx/static/themes/oscar/img/map/marker-icon-2x.png create mode 100644 searx/static/themes/oscar/img/map/marker-icon-green.png create mode 100644 searx/static/themes/oscar/img/map/marker-icon-orange.png create mode 100644 searx/static/themes/oscar/img/map/marker-icon-red.png create mode 100644 searx/static/themes/oscar/img/map/marker-icon.png create mode 100644 searx/static/themes/oscar/img/map/marker-shadow.png create mode 100644 searx/static/themes/oscar/img/searx_logo.png create mode 100644 searx/static/themes/oscar/js/searx_src/00_requirejs_config.js create mode 100644 searx/static/themes/oscar/js/searx_src/autocompleter.js create mode 100644 searx/static/themes/oscar/js/searx_src/element_modifiers.js create mode 100644 searx/static/themes/oscar/js/searx_src/leaflet_map.js create mode 100644 searx/static/themes/oscar/less/logicodev/advanced.less create mode 100644 searx/static/themes/oscar/less/logicodev/checkbox.less create mode 100644 searx/static/themes/oscar/less/logicodev/code.less create mode 100644 searx/static/themes/oscar/less/logicodev/cursor.less create mode 100644 searx/static/themes/oscar/less/logicodev/footer.less create mode 100644 searx/static/themes/oscar/less/logicodev/infobox.less create mode 100644 searx/static/themes/oscar/less/logicodev/navbar.less create mode 100644 searx/static/themes/oscar/less/logicodev/onoff.less create mode 100644 searx/static/themes/oscar/less/logicodev/oscar.less create mode 100644 searx/static/themes/oscar/less/logicodev/results.less create mode 100644 searx/static/themes/oscar/less/logicodev/search.less create mode 100644 searx/static/themes/oscar/less/logicodev/variables.less create mode 100644 searx/static/themes/oscar/less/pointhi/advanced.less create mode 100644 searx/static/themes/oscar/less/pointhi/checkbox.less create mode 100644 searx/static/themes/oscar/less/pointhi/code.less create mode 100644 searx/static/themes/oscar/less/pointhi/cursor.less create mode 100644 searx/static/themes/oscar/less/pointhi/footer.less create mode 100644 searx/static/themes/oscar/less/pointhi/infobox.less create mode 100644 searx/static/themes/oscar/less/pointhi/navbar.less create mode 100644 searx/static/themes/oscar/less/pointhi/onoff.less create mode 100644 searx/static/themes/oscar/less/pointhi/oscar.less create mode 100644 searx/static/themes/oscar/less/pointhi/results.less create mode 100644 searx/static/themes/oscar/less/pointhi/search.less create mode 100644 searx/static/themes/oscar/package.json create mode 100644 searx/static/themes/pix-art/img/favicon.png create mode 100644 searx/static/themes/pix-art/img/preference-icon-pixel.png create mode 100644 searx/static/themes/pix-art/img/search-icon-pixel.png create mode 100644 searx/static/themes/pix-art/img/searx-pixel-small.png create mode 100644 searx/static/themes/pix-art/img/searx-pixel.png create mode 100644 searx/static/themes/pix-art/js/searx.js create mode 100644 searx/static/themes/pix-art/less/definitions.less create mode 100644 searx/static/themes/pix-art/less/mixins.less create mode 100644 searx/static/themes/pix-art/less/search.less create mode 100644 searx/static/themes/pix-art/less/style.less create mode 100644 searx/templates/__common__/about.html create mode 100644 searx/templates/__common__/opensearch.xml create mode 100644 searx/templates/__common__/opensearch_response_rss.xml create mode 100644 searx/templates/courgette/404.html create mode 100644 searx/templates/courgette/about.html create mode 100644 searx/templates/courgette/base.html create mode 100644 searx/templates/courgette/categories.html create mode 100644 searx/templates/courgette/color.css create mode 100644 searx/templates/courgette/github_ribbon.html create mode 100644 searx/templates/courgette/index.html create mode 100644 searx/templates/courgette/preferences.html create mode 100644 searx/templates/courgette/result_templates/code.html create mode 100644 searx/templates/courgette/result_templates/default.html create mode 100644 searx/templates/courgette/result_templates/images.html create mode 100644 searx/templates/courgette/result_templates/map.html create mode 100644 searx/templates/courgette/result_templates/torrent.html create mode 100644 searx/templates/courgette/result_templates/videos.html create mode 100644 searx/templates/courgette/results.html create mode 100644 searx/templates/courgette/search.html create mode 100644 searx/templates/courgette/stats.html create mode 100644 searx/templates/legacy/404.html create mode 100644 searx/templates/legacy/about.html create mode 100644 searx/templates/legacy/base.html create mode 100644 searx/templates/legacy/categories.html create mode 100644 searx/templates/legacy/github_ribbon.html create mode 100644 searx/templates/legacy/index.html create mode 100644 searx/templates/legacy/infobox.html create mode 100644 searx/templates/legacy/preferences.html create mode 100644 searx/templates/legacy/result_templates/code.html create mode 100644 searx/templates/legacy/result_templates/default.html create mode 100644 searx/templates/legacy/result_templates/images.html create mode 100644 searx/templates/legacy/result_templates/map.html create mode 100644 searx/templates/legacy/result_templates/torrent.html create mode 100644 searx/templates/legacy/result_templates/videos.html create mode 100644 searx/templates/legacy/results.html create mode 100644 searx/templates/legacy/search.html create mode 100644 searx/templates/legacy/stats.html create mode 100644 searx/templates/oscar/404.html create mode 100644 searx/templates/oscar/about.html create mode 100644 searx/templates/oscar/advanced.html create mode 100644 searx/templates/oscar/base.html create mode 100644 searx/templates/oscar/categories.html create mode 100644 searx/templates/oscar/index.html create mode 100644 searx/templates/oscar/infobox.html create mode 100644 searx/templates/oscar/languages.html create mode 100644 searx/templates/oscar/macros.html create mode 100644 searx/templates/oscar/messages/first_time.html create mode 100644 searx/templates/oscar/messages/no_cookies.html create mode 100644 searx/templates/oscar/messages/no_data_available.html create mode 100644 searx/templates/oscar/messages/no_results.html create mode 100644 searx/templates/oscar/messages/save_settings_successfull.html create mode 100644 searx/templates/oscar/messages/unknow_error.html create mode 100644 searx/templates/oscar/navbar.html create mode 100644 searx/templates/oscar/preferences.html create mode 100644 searx/templates/oscar/result_templates/code.html create mode 100644 searx/templates/oscar/result_templates/default.html create mode 100644 searx/templates/oscar/result_templates/images.html create mode 100644 searx/templates/oscar/result_templates/map.html create mode 100644 searx/templates/oscar/result_templates/torrent.html create mode 100644 searx/templates/oscar/result_templates/videos.html create mode 100644 searx/templates/oscar/results.html create mode 100644 searx/templates/oscar/search.html create mode 100644 searx/templates/oscar/search_full.html create mode 100644 searx/templates/oscar/stats.html create mode 100644 searx/templates/oscar/time-range.html create mode 100644 searx/templates/pix-art/404.html create mode 100644 searx/templates/pix-art/about.html create mode 100644 searx/templates/pix-art/base.html create mode 100644 searx/templates/pix-art/index.html create mode 100644 searx/templates/pix-art/preferences.html create mode 100644 searx/templates/pix-art/result_templates/default.html create mode 100644 searx/templates/pix-art/result_templates/images.html create mode 100644 searx/templates/pix-art/results.html create mode 100644 searx/templates/pix-art/search.html create mode 100644 searx/templates/pix-art/stats.html create mode 100644 searx/testing.py create mode 100644 searx/translations/bg/LC_MESSAGES/messages.po create mode 100644 searx/translations/cs/LC_MESSAGES/messages.po create mode 100644 searx/translations/de/LC_MESSAGES/messages.po create mode 100644 searx/translations/de_DE/LC_MESSAGES/messages.po create mode 100644 searx/translations/el_GR/LC_MESSAGES/messages.po create mode 100644 searx/translations/en/LC_MESSAGES/messages.po create mode 100644 searx/translations/eo/LC_MESSAGES/messages.po create mode 100644 searx/translations/es/LC_MESSAGES/messages.po create mode 100644 searx/translations/fi/LC_MESSAGES/messages.po create mode 100644 searx/translations/fr/LC_MESSAGES/messages.po create mode 100644 searx/translations/he/LC_MESSAGES/messages.po create mode 100644 searx/translations/hu/LC_MESSAGES/messages.po create mode 100644 searx/translations/it/LC_MESSAGES/messages.po create mode 100644 searx/translations/ja/LC_MESSAGES/messages.po create mode 100644 searx/translations/nl/LC_MESSAGES/messages.po create mode 100644 searx/translations/pt/LC_MESSAGES/messages.po create mode 100644 searx/translations/pt_BR/LC_MESSAGES/messages.po create mode 100644 searx/translations/ro/LC_MESSAGES/messages.po create mode 100644 searx/translations/ru/LC_MESSAGES/messages.po create mode 100644 searx/translations/sk/LC_MESSAGES/messages.po create mode 100644 searx/translations/sv/LC_MESSAGES/messages.po create mode 100644 searx/translations/tr/LC_MESSAGES/messages.po create mode 100644 searx/translations/uk/LC_MESSAGES/messages.po create mode 100644 searx/translations/zh_CN/LC_MESSAGES/messages.po create mode 100644 searx/url_utils.py create mode 100644 searx/utils.py create mode 100644 searx/version.py create mode 100644 searx/webapp.py create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/robot/__init__.py create mode 100644 tests/test_robot.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/engines/__init__.py create mode 100644 tests/unit/engines/seedpeer_fixture.html create mode 100644 tests/unit/engines/test_archlinux.py create mode 100644 tests/unit/engines/test_bing.py create mode 100644 tests/unit/engines/test_bing_images.py create mode 100644 tests/unit/engines/test_bing_news.py create mode 100644 tests/unit/engines/test_blekko_images.py create mode 100644 tests/unit/engines/test_btdigg.py create mode 100644 tests/unit/engines/test_currency_convert.py create mode 100644 tests/unit/engines/test_dailymotion.py create mode 100644 tests/unit/engines/test_deezer.py create mode 100644 tests/unit/engines/test_deviantart.py create mode 100644 tests/unit/engines/test_digbt.py create mode 100644 tests/unit/engines/test_digg.py create mode 100644 tests/unit/engines/test_doku.py create mode 100644 tests/unit/engines/test_duckduckgo.py create mode 100644 tests/unit/engines/test_duckduckgo_definitions.py create mode 100644 tests/unit/engines/test_duckduckgo_images.py create mode 100644 tests/unit/engines/test_dummy.py create mode 100644 tests/unit/engines/test_faroo.py create mode 100644 tests/unit/engines/test_fdroid.py create mode 100644 tests/unit/engines/test_flickr.py create mode 100644 tests/unit/engines/test_flickr_noapi.py create mode 100644 tests/unit/engines/test_framalibre.py create mode 100644 tests/unit/engines/test_frinkiac.py create mode 100644 tests/unit/engines/test_gigablast.py create mode 100644 tests/unit/engines/test_github.py create mode 100644 tests/unit/engines/test_google.py create mode 100644 tests/unit/engines/test_google_images.py create mode 100644 tests/unit/engines/test_google_news.py create mode 100644 tests/unit/engines/test_ina.py create mode 100644 tests/unit/engines/test_kickass.py create mode 100644 tests/unit/engines/test_mediawiki.py create mode 100644 tests/unit/engines/test_mixcloud.py create mode 100644 tests/unit/engines/test_nyaa.py create mode 100644 tests/unit/engines/test_openstreetmap.py create mode 100644 tests/unit/engines/test_pdbe.py create mode 100644 tests/unit/engines/test_photon.py create mode 100644 tests/unit/engines/test_piratebay.py create mode 100644 tests/unit/engines/test_qwant.py create mode 100644 tests/unit/engines/test_reddit.py create mode 100644 tests/unit/engines/test_scanr_structures.py create mode 100644 tests/unit/engines/test_searchcode_code.py create mode 100644 tests/unit/engines/test_searchcode_doc.py create mode 100644 tests/unit/engines/test_seedpeer.py create mode 100644 tests/unit/engines/test_soundcloud.py create mode 100644 tests/unit/engines/test_spotify.py create mode 100644 tests/unit/engines/test_stackoverflow.py create mode 100644 tests/unit/engines/test_startpage.py create mode 100644 tests/unit/engines/test_subtitleseeker.py create mode 100644 tests/unit/engines/test_swisscows.py create mode 100644 tests/unit/engines/test_tokyotoshokan.py create mode 100644 tests/unit/engines/test_torrentz.py create mode 100644 tests/unit/engines/test_twitter.py create mode 100644 tests/unit/engines/test_vimeo.py create mode 100644 tests/unit/engines/test_wikidata.py create mode 100644 tests/unit/engines/test_wikipedia.py create mode 100644 tests/unit/engines/test_wolframalpha_api.py create mode 100644 tests/unit/engines/test_wolframalpha_noapi.py create mode 100644 tests/unit/engines/test_www1x.py create mode 100644 tests/unit/engines/test_www500px.py create mode 100644 tests/unit/engines/test_yacy.py create mode 100644 tests/unit/engines/test_yahoo.py create mode 100644 tests/unit/engines/test_yahoo_news.py create mode 100644 tests/unit/engines/test_youtube_api.py create mode 100644 tests/unit/engines/test_youtube_noapi.py create mode 100644 tests/unit/test_answerers.py create mode 100644 tests/unit/test_plugins.py create mode 100644 tests/unit/test_preferences.py create mode 100644 tests/unit/test_results.py create mode 100644 tests/unit/test_search.py create mode 100644 tests/unit/test_utils.py create mode 100644 tests/unit/test_webapp.py create mode 100644 tox.ini create mode 100644 utils/fabfile.py create mode 100644 utils/fetch_currencies.py create mode 100644 utils/fetch_languages.py create mode 100644 utils/google_search.py create mode 100755 utils/standalone_searx.py create mode 100755 utils/update-translations.sh diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..4f50efc --- /dev/null +++ b/.coveragerc @@ -0,0 +1,20 @@ +[run] +branch = True +source = + searx/engines + searx/__init__.py + searx/autocomplete.py + searx/https_rewrite.py + searx/languages.py + searx/search.py + searx/testing.py + searx/utils.py + searx/webapp.py + +[report] +show_missing = True +exclude_lines = + if __name__ == .__main__.: + +[html] +directory = coverage diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd6d7e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.coverage +coverage/ +.installed.cfg +engines.cfg +env +robot_log.html +robot_output.xml +robot_report.html +test_basic/ +setup.cfg + +*.pyc +*/*.pyc +*~ + +node_modules/ + +.tx/ diff --git a/.landscape.yaml b/.landscape.yaml new file mode 100644 index 0000000..1bb3977 --- /dev/null +++ b/.landscape.yaml @@ -0,0 +1,3 @@ +strictness: high +ignore-paths: + - bootstrap.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b6017cd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,37 @@ +sudo: false +cache: + - pip + - npm + - directories: + - $HOME/.cache/pip +addons: + firefox: "latest" +language: python +python: + - "2.7" + - "3.6" +before_install: + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" + - npm install less grunt-cli + - ( cd searx/static/themes/oscar;npm install; cd - ) + - mkdir -p ~/drivers; export PATH=~/drivers:$PATH; + - GECKODRIVER_URL="https://github.com/mozilla/geckodriver/releases/download/v0.11.1/geckodriver-v0.11.1-linux64.tar.gz"; + - FILE=`mktemp`; wget "$GECKODRIVER_URL" -qO $FILE && tar xz -C ~/drivers -f $FILE geckodriver; rm $FILE; chmod 777 ~/drivers/geckodriver; +install: + - ./manage.sh update_dev_packages + - pip install coveralls +script: + - ./manage.sh styles + - ./manage.sh grunt_build + - ./manage.sh tests +after_success: + - ./manage.sh py_test_coverage + - coveralls +notifications: + irc: + channels: + - "irc.freenode.org#searx" + template: + - "%{repository}/#%{build_number}/%{branch} (%{author}): %{message} %{build_url}" + on_success: change diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..9d27dd8 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,69 @@ +Searx was created by Adam Tauber and is maintained by Adam Tauber, Alexandre Flament and Noémi Ványi. + +Major contributing authors: + +- Adam Tauber `@asciimoo `_ +- Matej Cotman +- Thomas Pointhuber +- Alexandre Flament `@dalf `_ +- @Cqoicebordel +- Noémi Ványi +- Marc Abonce Seguin @a01200356 + +People who have submitted patches/translates, reported bugs, consulted features or +generally made searx better: + +- Laszlo Hammerl +- Stefan Marsiske +- Gabor Nagy +- @pw3t +- @rhapsodhy +- András Veres-Szentkirályi +- Benjamin Sonntag +- @HLFH +- @TheRadialActive +- @Okhin +- André Koot +- Alejandro León Aznar +- rike +- dp +- Martin Zimmermann +- @courgette +- @kernc +- @Reventl0v +- Caner Başaran +- Benjamin Sonntag +- @opi +- @dimqua +- Giorgos Logiotatidis +- Luc Didry +- Niklas Haas +- @underr +- Emmanuel Benazera +- @GreenLunar +- Kang-min Liu +- Kirill Isakov +- Guilhem Bonnefille +- @jibe-b +- Christian Pietsch @pietsch +- @Maxqia +- Ashutosh Das @pyprism +- YuLun Shih @imZack +- Dmitry Mikhirev @mikhirev +- David A Roberts `@davidar `_ +- Jan Verbeek @blyxxyz +- Ammar Najjar @ammarnajjar +- @stepshal +- François Revol @mmuman +- Harry Wood @harry-wood +- Thomas Renard @threnard +- Pydo ``_ +- Athemis ``_ +- Stefan Antoni `` +- @firebovine +- Lorenzo J. Lucchini @luccoj +- @eig8phei +- Joachim Cherqui +- @maxigas +- Jannik Winkel @kiney +- @juanitobananas diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..7cfa62f --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,252 @@ +0.12.0 2017.06.04 +================= + +- Python3 compatibility +- New engines + + - 1337x.to (files, music, video) + - Semantic Scholar (science) + - Library Genesis (general) + - Framalibre (IT) + - Free Software Directory (IT) +- More compact result UI (oscar theme) +- Configurable static content and template path +- Spelling suggestions +- Multiple engine fixes (duckduckgo, bing, swisscows, yahoo news, bing news, twitter, bing images) +- Reduced static image size +- Docker updates +- Translation updates + + +Special thanks to `NLNet `__ for sponsoring multiple features of this release. + + +0.11.0 2017.01.10 +================= + +- New engines + + - Protein Data Bank Europe (science) + - Voat.co (general, social media) + - Online Etimology Dictionary (science) + - CCC tv (video, it) + - Searx (all categories - can rotate multiple other instances) +- Answerer functionality (see answerer section on /preferences) +- Local answerers + + - Statistical functions + - Random value generator +- Result proxy support (with `morty `__) +- Extended time range filter +- Improved search language support +- Multiple engine fixes (digbt, 500px, google news, ixquick, bing, kickass, google play movies, habrahabr, yandex) +- Minor UI improvements +- Suggestion support for JSON engine +- Result and query escaping fixes +- Configurable HTTP server version +- More robust search error handling +- Faster webapp initialization in debug mode +- Search module refactor +- Translation updates + + +0.10.0 2016.09.06 +================= + +- New engines + + - Archive.is (general) + - INA (videos) + - Scanr (science) + - Google Scholar (science) + - Crossref (science) + - Openrepos (files) + - Microsoft Academic Search Engine (science) + - Hoogle (it) + - Diggbt (files) + - Dictzone (general - dictionary) + - Translated (general - translation) +- New Plugins + + - Infinite scroll on results page + - DOAI rewrite +- Full theme redesign +- Display the number of results +- Filter searches by date range +- Instance config API endpoint +- Dependency version updates +- Socks proxy support for outgoing requests +- 404 page + + +News +~~~~ + +@kvch joined the maintainer team + + +0.9.0 2016.05.24 +================ + +- New search category: science +- New engines + + - Wolframalpha (science) + - Frinkiac (images) + - Arch Linux (it) + - BASE - Bielefeld Academic Search Engine (science) + - Dokuwiki (general) + - Nyaa.se (files, images, music, video) + - Reddit (general, images, news, social media) + - Torrentz.eu (files, music, video) + - Tokyo Toshokan (files, music, video) + - F-Droid (files) + - Erowid (general) + - Bitbucket (it) + - GitLab (it) + - Geektimes (it) + - Habrahabr (it) +- New plugins + + - Open links in new tab + - Vim hotkeys for better navigation +- Wikipedia/Mediawiki engine improvements +- Configurable instance name +- Configurable connection pool size +- Fixed broken google engine +- Better docker image +- Images in standard results +- Fixed and refactored user settings (Warning: backward incompatibility - you have to reset your custom engine preferences) +- Suspending engines on errors +- Simplified development/deployment tooling +- Translation updates +- Multilingual autocompleter +- Qwant autocompleter backend + + +0.8.1 2015.12.22 +================ + +- More efficient result parsing +- Rewritten google engine to prevent app crashes +- Other engine fixes/tweaks + + - Bing news + - Btdigg + - Gigablast + - Google images + - Startpage + + +News +~~~~ + +New documentation page is available: https://asciimoo.github.io/searx + + +0.8.0 2015.09.08 +================ + +- New engines + + - Blekko (image) + - Gigablast (general) + - Spotify (music) + - Swisscows (general, images) + - Qwant (general, images, news, social media) +- Plugin system +- New plugins + + - HTTPS rewrite + - Search on cagetory select + - User information + - Tracker url part remover +- Multiple outgoing IP and HTTP/HTTPS proxy support +- New autocompleter: startpage +- New theme: pix-art +- Settings file structure change +- Fabfile, docker deployment +- Optional safesearch result filter +- Force HTTPS in engines if possible +- Disabled HTTP referrer on outgoing links +- Display cookie information +- Prettier search URLs +- Right-to-left text handling in themes +- Translation updates (New locales: Chinese, Hebrew, Portuguese, Romanian) + + +New dependencies +~~~~~~~~~~~~~~~~ + +- pyopenssl +- ndg-httpsclient +- pyasn1 +- pyasn1-modules +- certifi + + +News +~~~~ + +@dalf joined the maintainer "team" + + +0.7.0 2015.02.03 +================ + +- New engines + + - Digg + - Google Play Store + - Deezer + - Btdigg + - Mixcloud + - 1px +- Image proxy +- Search speed improvements +- Autocompletition of engines, shortcuts and supported languages +- Translation updates (New locales: Turkish, Russian) +- Default theme changed to oscar +- Settings option to disable engines by default +- UI code cleanup and restructure +- Engine tests +- Multiple engine bug fixes and tweaks +- Config option to set default interface locale +- Flexible result template handling +- Application logging and sophisticated engine exception tracebacks +- Kickass torrent size display (oscar theme) + + +New dependencies +~~~~~~~~~~~~~~~~ + +- pygments - http://pygments.org/ + + +0.6.0 - 2014.12.25 +================== + +- Changelog added +- New engines + + - Flickr (api) + - Subtitleseeker + - photon + - 500px + - Searchcode + - Searchcode doc + - Kickass torrent +- Precise search request timeout handling +- Better favicon support +- Stricter config parsing +- Translation updates +- Multiple ui fixes +- Flickr (noapi) engine fix +- Pep8 fixes + + +News +~~~~ + +Health status of searx instances and engines: http://stats.searx.oe5tpo.com +(source: https://github.com/pointhi/searx_stats) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fc2767a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +FROM alpine:3.5 +MAINTAINER searx +LABEL description "A privacy-respecting, hackable metasearch engine." + +ENV BASE_URL=False IMAGE_PROXY=False +EXPOSE 8888 +WORKDIR /usr/local/searx +CMD ["/sbin/tini","--","/usr/local/searx/run.sh"] + +RUN adduser -D -h /usr/local/searx -s /bin/sh searx searx \ + && echo '#!/bin/sh' >> run.sh \ + && echo 'sed -i "s|base_url : False|base_url : $BASE_URL|g" searx/settings.yml' >> run.sh \ + && echo 'sed -i "s/image_proxy : False/image_proxy : $IMAGE_PROXY/g" searx/settings.yml' >> run.sh \ + && echo 'sed -i "s/ultrasecretkey/`openssl rand -hex 16`/g" searx/settings.yml' >> run.sh \ + && echo 'python searx/webapp.py' >> run.sh \ + && chmod +x run.sh + +COPY requirements.txt ./requirements.txt + +RUN echo "@commuedge http://nl.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ + && apk -U add \ + build-base \ + python \ + python-dev \ + py-pip \ + libxml2 \ + libxml2-dev \ + libxslt \ + libxslt-dev \ + libffi-dev \ + openssl \ + openssl-dev \ + ca-certificates \ + tini@commuedge \ + && pip install --no-cache -r requirements.txt \ + && apk del \ + build-base \ + python-dev \ + libffi-dev \ + openssl-dev \ + libxslt-dev \ + libxml2-dev \ + openssl-dev \ + ca-certificates \ + && rm -f /var/cache/apk/* + +COPY . . + +RUN chown -R searx:searx * + +USER searx + +RUN sed -i "s/127.0.0.1/0.0.0.0/g" searx/settings.yml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dba13ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..a0bb12f --- /dev/null +++ b/README.rst @@ -0,0 +1,45 @@ +searx +===== + +A privacy-respecting, hackable `metasearch +engine `__. + +List of `running +instances `__. + +See the `documentation `__ and the `wiki `__ for more information. + +|Flattr searx| + +Installation +~~~~~~~~~~~~ + +- clone source: + ``git clone https://github.com/asciimoo/searx.git && cd searx`` +- install dependencies: ``./manage.sh update_packages`` +- edit your + `settings.yml `__ + (set your ``secret_key``!) +- run ``python searx/webapp.py`` to start the application + +For all the details, follow this `step by step +installation `__ + +Bugs +~~~~ + +Bugs or suggestions? Visit the `issue +tracker `__. + +`License `__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +More about searx +~~~~~~~~~~~~~~~~ + +- `ohloh `__ +- `twitter `__ +- IRC: #searx @ freenode + +.. |Flattr searx| image:: http://api.flattr.com/button/flattr-badge-large.png + :target: https://flattr.com/submit/auto?user_id=asciimoo&url=https://github.com/asciimoo/searx&title=searx&language=&tags=github&category=software diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..f0234b3 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **/templates/**.html] +extensions=jinja2.ext.autoescape,jinja2.ext.with_ diff --git a/examples/basic_engine.py b/examples/basic_engine.py new file mode 100644 index 0000000..c7d02af --- /dev/null +++ b/examples/basic_engine.py @@ -0,0 +1,25 @@ + +categories = ['general'] # optional + + +def request(query, params): + '''pre-request callback + params: + method : POST/GET + headers : {} + data : {} # if method == POST + url : '' + category: 'search category' + pageno : 1 # number of the requested page + ''' + + params['url'] = 'https://host/%s' % query + + return params + + +def response(resp): + '''post-response callback + resp: requests response object + ''' + return [{'url': '', 'title': '', 'content': ''}] diff --git a/manage.sh b/manage.sh new file mode 100755 index 0000000..2466f25 --- /dev/null +++ b/manage.sh @@ -0,0 +1,128 @@ +#!/bin/sh + +BASE_DIR=$(dirname "`readlink -f "$0"`") +PYTHONPATH=$BASE_DIR +SEARX_DIR="$BASE_DIR/searx" +ACTION=$1 + +update_packages() { + pip install -r "$BASE_DIR/requirements.txt" +} + +update_dev_packages() { + update_packages + pip install -r "$BASE_DIR/requirements-dev.txt" +} + +check_geckodriver() { + echo '[!] Checking geckodriver' + set -e + geckodriver -V 2>1 > /dev/null || NOTFOUND=1 + set +e + if [ -z $NOTFOUND ]; then + return + fi + GECKODRIVER_VERSION="v0.11.1" + PLATFORM=`python -c "import platform; print platform.system().lower(), platform.architecture()[0]"` + case $PLATFORM in + "linux 32bit" | "linux2 32bit") ARCH="linux32";; + "linux 64bit" | "linux2 64bit") ARCH="linux64";; + "windows 32 bit") ARCH="win32";; + "windows 64 bit") ARCH="win64";; + "mac 64bit") ARCH="macos";; + esac + GECKODRIVER_URL="https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/geckodriver-$GECKODRIVER_VERSION-$ARCH.tar.gz"; + if [ -z "$VIRTUAL_ENV" ]; then + echo "geckodriver can't be installed because VIRTUAL_ENV is not set, you should download it from\n $GECKODRIVER_URL" + exit + else + echo "Installing $VIRTUAL_ENV from\n $GECKODRIVER_URL" + FILE=`mktemp` + wget "$GECKODRIVER_URL" -qO $FILE && tar xz -C $VIRTUAL_ENV/bin/ -f $FILE geckodriver + rm $FILE + chmod 777 $VIRTUAL_ENV/bin/geckodriver + fi +} + +pep8_check() { + echo '[!] Running pep8 check' + # ignored rules: + # E402 module level import not at top of file + # W503 line break before binary operator + pep8 --max-line-length=120 --ignore "E402,W503" "$SEARX_DIR" "$BASE_DIR/tests" +} + +unit_tests() { + echo '[!] Running unit tests' + python -m nose2 -s "$BASE_DIR/tests/unit" +} + +py_test_coverage() { + echo '[!] Running python test coverage' + PYTHONPATH=`pwd` python -m nose2 -C --coverage "$SEARX_DIR" -s "$BASE_DIR/tests/unit" + coverage report + coverage html +} + +robot_tests() { + echo '[!] Running robot tests' + PYTHONPATH=`pwd` python "$SEARX_DIR/testing.py" robot +} + +tests() { + set -e + pep8_check + unit_tests + check_geckodriver + robot_tests + set +e +} + +build_style() { + lessc -x "$BASE_DIR/searx/static/$1" "$BASE_DIR/searx/static/$2" +} + +styles() { + echo '[!] Building styles' + build_style themes/legacy/less/style.less themes/legacy/css/style.css + build_style themes/legacy/less/style-rtl.less themes/legacy/css/style-rtl.css + build_style themes/courgette/less/style.less themes/courgette/css/style.css + build_style themes/courgette/less/style-rtl.less themes/courgette/css/style-rtl.css + build_style less/bootstrap/bootstrap.less css/bootstrap.min.css + build_style themes/oscar/less/pointhi/oscar.less themes/oscar/css/pointhi.min.css + build_style themes/oscar/less/logicodev/oscar.less themes/oscar/css/logicodev.min.css + build_style themes/pix-art/less/style.less themes/pix-art/css/style.css +} + +grunt_build() { + grunt --gruntfile "$SEARX_DIR/static/themes/oscar/gruntfile.js" +} + +locales() { + pybabel compile -d "$SEARX_DIR/translations" +} + +help() { + [ -z "$1" ] || printf "Error: $1\n" + echo "Searx manage.sh help + +Commands +======== + grunt_build - Build js files + help - This text + locales - Compile locales + pep8_check - Pep8 validation + py_test_coverage - Unit test coverage + robot_tests - Run selenium tests + styles - Build less files + tests - Run all python tests (pep8, unit, robot) + unit_tests - Run unit tests + update_dev_packages - Check & update development and production dependency changes + update_packages - Check & update dependency changes + check_geckodriver - Check & download geckodriver (required for robot_tests) +" +} + +[ "$(command -V "$ACTION" | grep ' function$')" = "" ] \ + && help "action not found" \ + || $ACTION diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..691a1e7 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,10 @@ +babel==2.3.4 +mock==2.0.0 +nose2[coverage-plugin] +pep8==1.7.0 +plone.testing==5.0.0 +splinter==0.7.5 +transifex-client==0.12.2 +unittest2==1.1.0 +zope.testrunner==4.5.1 +selenium==3.0.1 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..292b6af --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +certifi==2017.1.23 +flask==0.12 +flask-babel==0.11.1 +lxml==3.7.3 +idna==2.5 +pygments==2.1.3 +pyopenssl==16.2.0 +python-dateutil==2.6.0 +pyyaml==3.12 +requests[socks]==2.13.0 diff --git a/searx/__init__.py b/searx/__init__.py new file mode 100644 index 0000000..a57588c --- /dev/null +++ b/searx/__init__.py @@ -0,0 +1,89 @@ +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2013- by Adam Tauber, +''' + +import certifi +import logging +from os import environ +from os.path import realpath, dirname, join, abspath, isfile +from ssl import OPENSSL_VERSION_INFO, OPENSSL_VERSION +try: + from yaml import load +except: + from sys import exit, stderr + stderr.write('[E] install pyyaml\n') + exit(2) + +searx_dir = abspath(dirname(__file__)) +engine_dir = dirname(realpath(__file__)) + + +def check_settings_yml(file_name): + if isfile(file_name): + return file_name + else: + return None + +# find location of settings.yml +if 'SEARX_SETTINGS_PATH' in environ: + # if possible set path to settings using the + # enviroment variable SEARX_SETTINGS_PATH + settings_path = check_settings_yml(environ['SEARX_SETTINGS_PATH']) +else: + # if not, get it from searx code base or last solution from /etc/searx + settings_path = check_settings_yml(join(searx_dir, 'settings.yml')) or check_settings_yml('/etc/searx/settings.yml') + +if not settings_path: + raise Exception('settings.yml not found') + +# load settings +with open(settings_path) as settings_yaml: + settings = load(settings_yaml) + +''' +enable debug if +the environnement variable SEARX_DEBUG is 1 or true +(whatever the value in settings.yml) +or general.debug=True in settings.yml + +disable debug if +the environnement variable SEARX_DEBUG is 0 or false +(whatever the value in settings.yml) +or general.debug=False in settings.yml +''' +searx_debug_env = environ.get('SEARX_DEBUG', '').lower() +if searx_debug_env == 'true' or searx_debug_env == '1': + searx_debug = True +elif searx_debug_env == 'false' or searx_debug_env == '0': + searx_debug = False +else: + searx_debug = settings.get('general', {}).get('debug') + +if searx_debug: + logging.basicConfig(level=logging.DEBUG) +else: + logging.basicConfig(level=logging.WARNING) + +logger = logging.getLogger('searx') +logger.debug('read configuration from %s', settings_path) +# Workaround for openssl versions <1.0.2 +# https://github.com/certifi/python-certifi/issues/26 +if OPENSSL_VERSION_INFO[0:3] < (1, 0, 2): + if hasattr(certifi, 'old_where'): + environ['REQUESTS_CA_BUNDLE'] = certifi.old_where() + logger.warning('You are using an old openssl version({0}), please upgrade above 1.0.2!'.format(OPENSSL_VERSION)) + +logger.info('Initialisation done') diff --git a/searx/answerers/__init__.py b/searx/answerers/__init__.py new file mode 100644 index 0000000..444316f --- /dev/null +++ b/searx/answerers/__init__.py @@ -0,0 +1,50 @@ +from os import listdir +from os.path import realpath, dirname, join, isdir +from sys import version_info +from searx.utils import load_module +from collections import defaultdict + +if version_info[0] == 3: + unicode = str + + +answerers_dir = dirname(realpath(__file__)) + + +def load_answerers(): + answerers = [] + for filename in listdir(answerers_dir): + if not isdir(join(answerers_dir, filename)) or filename.startswith('_'): + continue + module = load_module('answerer.py', join(answerers_dir, filename)) + if not hasattr(module, 'keywords') or not isinstance(module.keywords, tuple) or not len(module.keywords): + exit(2) + answerers.append(module) + return answerers + + +def get_answerers_by_keywords(answerers): + by_keyword = defaultdict(list) + for answerer in answerers: + for keyword in answerer.keywords: + for keyword in answerer.keywords: + by_keyword[keyword].append(answerer.answer) + return by_keyword + + +def ask(query): + results = [] + query_parts = list(filter(None, query.query.split())) + + if query_parts[0].decode('utf-8') not in answerers_by_keywords: + return results + + for answerer in answerers_by_keywords[query_parts[0].decode('utf-8')]: + result = answerer(query) + if result: + results.append(result) + return results + + +answerers = load_answerers() +answerers_by_keywords = get_answerers_by_keywords(answerers) diff --git a/searx/answerers/random/answerer.py b/searx/answerers/random/answerer.py new file mode 100644 index 0000000..f2b8bf3 --- /dev/null +++ b/searx/answerers/random/answerer.py @@ -0,0 +1,55 @@ +import random +import string +import sys +from flask_babel import gettext + +# required answerer attribute +# specifies which search query keywords triggers this answerer +keywords = ('random',) + +random_int_max = 2**31 + +if sys.version_info[0] == 2: + random_string_letters = string.lowercase + string.digits + string.uppercase +else: + unicode = str + random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase + + +def random_string(): + return u''.join(random.choice(random_string_letters) + for _ in range(random.randint(8, 32))) + + +def random_float(): + return unicode(random.random()) + + +def random_int(): + return unicode(random.randint(-random_int_max, random_int_max)) + + +random_types = {b'string': random_string, + b'int': random_int, + b'float': random_float} + + +# required answerer function +# can return a list of results (any result type) for a given query +def answer(query): + parts = query.query.split() + if len(parts) != 2: + return [] + + if parts[1] not in random_types: + return [] + + return [{'answer': random_types[parts[1]]()}] + + +# required answerer function +# returns information about the answerer +def self_info(): + return {'name': gettext('Random value generator'), + 'description': gettext('Generate different random values'), + 'examples': [u'random {}'.format(x) for x in random_types]} diff --git a/searx/answerers/statistics/answerer.py b/searx/answerers/statistics/answerer.py new file mode 100644 index 0000000..73dd25c --- /dev/null +++ b/searx/answerers/statistics/answerer.py @@ -0,0 +1,55 @@ +from sys import version_info +from functools import reduce +from operator import mul + +from flask_babel import gettext + +if version_info[0] == 3: + unicode = str + +keywords = ('min', + 'max', + 'avg', + 'sum', + 'prod') + + +# required answerer function +# can return a list of results (any result type) for a given query +def answer(query): + parts = query.query.split() + + if len(parts) < 2: + return [] + + try: + args = list(map(float, parts[1:])) + except: + return [] + + func = parts[0] + answer = None + + if func == b'min': + answer = min(args) + elif func == b'max': + answer = max(args) + elif func == b'avg': + answer = sum(args) / len(args) + elif func == b'sum': + answer = sum(args) + elif func == b'prod': + answer = reduce(mul, args, 1) + + if answer is None: + return [] + + return [{'answer': unicode(answer)}] + + +# required answerer function +# returns information about the answerer +def self_info(): + return {'name': gettext('Statistics functions'), + 'description': gettext('Compute {functions} of the arguments').format(functions='/'.join(keywords)), + 'examples': ['avg 123 548 2.04 24.2']} diff --git a/searx/autocomplete.py b/searx/autocomplete.py new file mode 100644 index 0000000..de0623a --- /dev/null +++ b/searx/autocomplete.py @@ -0,0 +1,201 @@ +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2013- by Adam Tauber, +''' + + +from lxml import etree +from json import loads +from searx import settings +from searx.languages import language_codes +from searx.engines import ( + categories, engines, engine_shortcuts +) +from searx.poolrequests import get as http_get + +try: + from urllib import urlencode +except: + from urllib.parse import urlencode + + +def get(*args, **kwargs): + if 'timeout' not in kwargs: + kwargs['timeout'] = settings['outgoing']['request_timeout'] + + return http_get(*args, **kwargs) + + +def searx_bang(full_query): + '''check if the searchQuery contain a bang, and create fitting autocompleter results''' + # check if there is a query which can be parsed + if len(full_query.getSearchQuery()) == 0: + return [] + + results = [] + + # check if current query stats with !bang + first_char = full_query.getSearchQuery()[0] + if first_char == '!' or first_char == '?': + if len(full_query.getSearchQuery()) == 1: + # show some example queries + # TODO, check if engine is not avaliable + results.append(first_char + "images") + results.append(first_char + "wikipedia") + results.append(first_char + "osm") + else: + engine_query = full_query.getSearchQuery()[1:] + + # check if query starts with categorie name + for categorie in categories: + if categorie.startswith(engine_query): + results.append(first_char + '{categorie}'.format(categorie=categorie)) + + # check if query starts with engine name + for engine in engines: + if engine.startswith(engine_query.replace('_', ' ')): + results.append(first_char + '{engine}'.format(engine=engine.replace(' ', '_'))) + + # check if query starts with engine shortcut + for engine_shortcut in engine_shortcuts: + if engine_shortcut.startswith(engine_query): + results.append(first_char + '{engine_shortcut}'.format(engine_shortcut=engine_shortcut)) + + # check if current query stats with :bang + elif first_char == ':': + if len(full_query.getSearchQuery()) == 1: + # show some example queries + results.append(":en") + results.append(":en_us") + results.append(":english") + results.append(":united_kingdom") + else: + engine_query = full_query.getSearchQuery()[1:] + + for lc in language_codes: + lang_id, lang_name, country, english_name = map(unicode.lower, lc) + + # check if query starts with language-id + if lang_id.startswith(engine_query): + if len(engine_query) <= 2: + results.append(u':{lang_id}'.format(lang_id=lang_id.split('-')[0])) + else: + results.append(u':{lang_id}'.format(lang_id=lang_id)) + + # check if query starts with language name + if lang_name.startswith(engine_query) or english_name.startswith(engine_query): + results.append(u':{lang_name}'.format(lang_name=lang_name)) + + # check if query starts with country + if country.startswith(engine_query.replace('_', ' ')): + results.append(u':{country}'.format(country=country.replace(' ', '_'))) + + # remove duplicates + result_set = set(results) + + # remove results which are already contained in the query + for query_part in full_query.query_parts: + if query_part in result_set: + result_set.remove(query_part) + + # convert result_set back to list + return list(result_set) + + +def dbpedia(query, lang): + # dbpedia autocompleter, no HTTPS + autocomplete_url = 'http://lookup.dbpedia.org/api/search.asmx/KeywordSearch?' + + response = get(autocomplete_url + urlencode(dict(QueryString=query))) + + results = [] + + if response.ok: + dom = etree.fromstring(response.content) + results = dom.xpath('//a:Result/a:Label//text()', + namespaces={'a': 'http://lookup.dbpedia.org/'}) + + return results + + +def duckduckgo(query, lang): + # duckduckgo autocompleter + url = 'https://ac.duckduckgo.com/ac/?{0}&type=list' + + resp = loads(get(url.format(urlencode(dict(q=query)))).text) + if len(resp) > 1: + return resp[1] + return [] + + +def google(query, lang): + # google autocompleter + autocomplete_url = 'https://suggestqueries.google.com/complete/search?client=toolbar&' + + response = get(autocomplete_url + urlencode(dict(hl=lang, q=query))) + + results = [] + + if response.ok: + dom = etree.fromstring(response.text) + results = dom.xpath('//suggestion/@data') + + return results + + +def startpage(query, lang): + # startpage autocompleter + url = 'https://startpage.com/do/suggest?{query}' + + resp = get(url.format(query=urlencode({'query': query}))).text.split('\n') + if len(resp) > 1: + return resp + return [] + + +def qwant(query, lang): + # qwant autocompleter (additional parameter : lang=en_en&count=xxx ) + url = 'https://api.qwant.com/api/suggest?{query}' + + resp = get(url.format(query=urlencode({'q': query, 'lang': lang}))) + + results = [] + + if resp.ok: + data = loads(resp.text) + if data['status'] == 'success': + for item in data['data']['items']: + results.append(item['value']) + + return results + + +def wikipedia(query, lang): + # wikipedia autocompleter + url = 'https://' + lang + '.wikipedia.org/w/api.php?action=opensearch&{0}&limit=10&namespace=0&format=json' + + resp = loads(get(url.format(urlencode(dict(search=query)))).text) + if len(resp) > 1: + return resp[1] + return [] + + +backends = {'dbpedia': dbpedia, + 'duckduckgo': duckduckgo, + 'google': google, + 'startpage': startpage, + 'qwant': qwant, + 'wikipedia': wikipedia + } diff --git a/searx/data/currencies.json b/searx/data/currencies.json new file mode 100644 index 0000000..bfde5a7 --- /dev/null +++ b/searx/data/currencies.json @@ -0,0 +1,7655 @@ +{ + "names": { + "francos franceses": [ + "FRF" + ], + "bulgarischer lew": [ + "BGN" + ], + "o\u0308rme\u0301ny dram": [ + "AMD" + ], + "oekrai\u0308ense hryvnja": [ + "UAH" + ], + "guatemalan quetzal": [ + "GTQ" + ], + "ghana cedi": [ + "GHS" + ], + "livre de sainte helene": [ + "SHP" + ], + "papua new guinean kina": [ + "PGK" + ], + "aud": [ + "AUD" + ], + "\u20ab": [ + "VND" + ], + "olasz li\u0301ra": [ + "ITL" + ], + "aserbaidschan manat": [ + "AZN" + ], + "ethiopian dollar": [ + "ETB" + ], + "norwegische krone": [ + "NOK" + ], + "papoea nieuw guinese kina": [ + "PGK" + ], + "som uzbeko": [ + "UZS" + ], + "yuan chino": [ + "CNY" + ], + "nuevo dolar de taiwan": [ + "TWD" + ], + "zweedse kronen": [ + "SEK" + ], + "dollar des i\u0302les cai\u0308mans": [ + "KYD" + ], + "do\u0301lar de singapur": [ + "SGD" + ], + "gru\u0301z lari": [ + "GEL" + ], + "escudo mozambiquen\u0303o": [ + "MZE" + ], + "peso filipino": [ + "PHP" + ], + "grivnia ucraniana": [ + "UAH" + ], + "salamon szigeteki dolla\u0301r": [ + "SBD" + ], + "barbados dollar": [ + "BBD" + ], + "fuang": [ + "THB" + ], + "dirham marroqui": [ + "MAD" + ], + "sri lankan rupees": [ + "LKR" + ], + "qindarka": [ + "ALL" + ], + "dinaro macedone": [ + "MKD" + ], + "togrog": [ + "MNT" + ], + "q\u0259pik": [ + "AZN" + ], + "special drawing rights": [ + "XDR" + ], + "gibralta\u0301ri font": [ + "GIP" + ], + "dinar bahraini": [ + "BHD" + ], + "marokkaanse dirham": [ + "MAD" + ], + "rouble sovie\u0301tique": [ + "SUR" + ], + "tanzanian shilling": [ + "TZS" + ], + "libra de siria": [ + "SYP" + ], + "rand sudafricano": [ + "ZAR" + ], + "seychelse roepie": [ + "SCR" + ], + "seychelse roepia": [ + "SCR" + ], + "forint": [ + "HUF" + ], + "dinar algerino": [ + "DZD" + ], + "roupie du sri lanka": [ + "LKR" + ], + "katar riyal": [ + "QAR" + ], + "schekalim": [ + "ILS" + ], + "corona checoslovaca": [ + "CSK" + ], + "baht tailande\u0301s": [ + "THB" + ], + "nuevo peso": [ + "ARS", + "UYU" + ], + "cuna croata": [ + "HRK" + ], + "nieuwe israe\u0308lische shekel": [ + "ILS" + ], + "nieuwe israelische shekel": [ + "ILS" + ], + "dollar des i\u0302les fidji": [ + "FJD" + ], + "nieuw zeelandse dollar": [ + "NZD" + ], + "tanza\u0301niai shilling": [ + "TZS" + ], + "gold franc": [ + "XFO" + ], + "tongan pa`anga": [ + "TOP" + ], + "francia frank": [ + "FRF" + ], + "brl": [ + "BRL" + ], + "isla\u0308ndische wa\u0308hrung": [ + "ISK" + ], + "guyana dollar": [ + "GYD" + ], + "dollaro australiano": [ + "AUD" + ], + "nakfa e\u0301rythre\u0301en": [ + "ERN" + ], + "kap verde escudo": [ + "CVE" + ], + "dinar iraqui\u0301": [ + "IQD" + ], + "vietnamese dong": [ + "VND" + ], + "neuer sol": [ + "PEN" + ], + "peso de argentina": [ + "ARS" + ], + "ddr mark": [ + "DDM" + ], + "br$": [ + "BND" + ], + "e\u0301szak koreai von": [ + "KPW" + ], + "japanse yen": [ + "JPY" + ], + "franco svizzero": [ + "CHF" + ], + "afghani afgano": [ + "AFN" + ], + "lira siriana": [ + "SYP" + ], + "boliviano": [ + "BOB" + ], + "vanuatui vatu": [ + "VUV" + ], + "tnd": [ + "TND" + ], + "manat turkmene": [ + "TMT" + ], + "namibia dollar": [ + "NAD" + ], + "ern": [ + "ERN" + ], + "manat turkmeno": [ + "TMT" + ], + "bds$": [ + "BBD" + ], + "bhutaanse ngultrum": [ + "BTN" + ], + "peso chilien": [ + "CLP" + ], + "dolar jamaicano": [ + "JMD" + ], + "bahamas dollar": [ + "BSD" + ], + "eritrese nakfa": [ + "ERN" + ], + "czk": [ + "CZK" + ], + "engels pond": [ + "GBP" + ], + "dolar de bermudas": [ + "BMD" + ], + "taka bangladeshi\u0301": [ + "BDT" + ], + "riyal del qatar": [ + "QAR" + ], + "kuwaiti dinar": [ + "KWD" + ], + "seychellen rupie": [ + "SCR" + ], + "boli\u0301var ve\u0301ne\u0301zue\u0301lien": [ + "VEF" + ], + "sri lanka rupie": [ + "LKR" + ], + "do\u0301lar de brunei": [ + "BND" + ], + "do\u0301lar de las islas salomo\u0301n": [ + "SBD" + ], + "lak": [ + "LAK" + ], + "sum uzbeko": [ + "UZS" + ], + "kurus\u0327": [ + "TRY" + ], + "iraqi dinar": [ + "IQD" + ], + "livre sterling": [ + "GBP" + ], + "angolai kwanza": [ + "AOA" + ], + "lat": [ + "LVL" + ], + "lek albanais": [ + "ALL" + ], + "brunei dolla\u0301r": [ + "BND" + ], + "tetri": [ + "GEL" + ], + "sterlina sudsudanese": [ + "SSP" + ], + "mo\u0308ngo\u0308": [ + "MNT" + ], + "mexican un peso coinage": [ + "MXN" + ], + "djiboutische frank": [ + "DJF" + ], + "seychelle i ru\u0301pia": [ + "SCR" + ], + "litauischer litas": [ + "LTL" + ], + "zambiaanse kwacha": [ + "ZMW" + ], + "maldi\u0301v szigeteki ru\u0301fia": [ + "MVR" + ], + "dinar bahreini\u0301": [ + "BHD" + ], + "barbadiaanse dollar": [ + "BBD" + ], + "drachme": [ + "GRD" + ], + "emalangeni": [ + "SZL" + ], + "canadese dollar": [ + "CAD" + ], + "mx$": [ + "MXN" + ], + "chinesischer renminbi": [ + "CNY" + ], + "macedonian denar": [ + "MKD" + ], + "uic franc": [ + "XFU" + ], + "won surcoreano": [ + "KRW" + ], + "nuevo shekel": [ + "ILS" + ], + "arubaanse florin": [ + "AWG" + ], + "ruanda franc": [ + "RWF" + ], + "franco burundes": [ + "BIF" + ], + "makao\u0301i pataca": [ + "MOP" + ], + "koruna c\u030ceska\u0301": [ + "CZK" + ], + "dirham des e\u0301mirats": [ + "AED" + ], + "mozambiki metical": [ + "MZN" + ], + "panamanian balboa": [ + "PAB" + ], + "syrian pound": [ + "SYP" + ], + "ki\u0301nai ju\u0308an": [ + "CNY" + ], + "mxn": [ + "MXN" + ], + "dolar fijiano": [ + "FJD" + ], + "a\u0308thiopischer birr": [ + "ETB" + ], + "kirgiz szom": [ + "KGS" + ], + "dinar du ye\u0301men du sud": [ + "YER" + ], + "peso moneda nacional": [ + "ARS" + ], + "scellino somalo": [ + "SOS" + ], + "cseh korona": [ + "CZK" + ], + "uruguayan peso": [ + "UYU" + ], + "cl$": [ + "CLP" + ], + "convertibele peso": [ + "CUC" + ], + "britisches pfund": [ + "GBP" + ], + "tonga dollar": [ + "TOP" + ], + "peso cubain convertible": [ + "CUC" + ], + "argentin peso": [ + "ARS" + ], + "lira maltesa": [ + "MTL" + ], + "bosnische konvertibilna marka": [ + "BAM" + ], + "francs suisse": [ + "CHF" + ], + "gourde haitienne": [ + "HTG" + ], + "shilling somali": [ + "SOS" + ], + "nt$": [ + "TWD" + ], + "rp": [ + "IDR" + ], + "dolar de las bahamas": [ + "BSD" + ], + "rs": [ + "BRL" + ], + "monnaie danoise": [ + "DKK" + ], + "swaziland lilangeni": [ + "SZL" + ], + "tugrig": [ + "MNT" + ], + "gersh": [ + "ETB" + ], + "rouble russe": [ + "RUB" + ], + "tugrik": [ + "MNT" + ], + "kip": [ + "LAK" + ], + "dirham de los emiratos arabes unidos": [ + "AED" + ], + "escudo capverdien": [ + "CVE" + ], + "jemeni ria\u0301l": [ + "YER" + ], + "fcfp": [ + "XPF" + ], + "rwanda franc": [ + "RWF" + ], + "bolivar venezolano": [ + "VEF" + ], + "zambiai kwacha": [ + "ZMW" + ], + "lev bulgaro": [ + "BGN" + ], + "lev bulgare": [ + "BGN" + ], + "nakfa eritreo": [ + "ERN" + ], + "danish krone": [ + "DKK" + ], + "monnaie britannique": [ + "GBP" + ], + "r$": [ + "BRL" + ], + "di\u0301rham marroqui\u0301": [ + "MAD" + ], + "institut d'e\u0301mission d'outre mer": [ + "XPF" + ], + "manat azerbaiyano": [ + "AZN" + ], + "vietnami \u0111o\u0302\u0300ng": [ + "VND" + ], + "riyal iraniano": [ + "IRR" + ], + "franco guineano": [ + "GNF" + ], + "fiorino arubano": [ + "AWG" + ], + "rand": [ + "ZAR" + ], + "schekel": [ + "ILS" + ], + "sterlina di sant'elena": [ + "SHP" + ], + "pound sterling": [ + "GBP" + ], + "nacfa eritreo": [ + "ERN" + ], + "dolar taiwanes": [ + "TWD" + ], + "koruna c\u030ceska": [ + "CZK" + ], + "\u20ad": [ + "LAK" + ], + "kro\u0301na": [ + "ISK" + ], + "denar macedonio": [ + "MKD" + ], + "libra libanesa": [ + "LBP" + ], + "dolar belicen\u0303o": [ + "BZD" + ], + "lesotho\u0301i loti": [ + "LSL" + ], + "colombian peso": [ + "COP" + ], + "do\u0301lar de brune\u0301i": [ + "BND" + ], + "szingapu\u0301ri dolla\u0301r": [ + "SGD" + ], + "franc poincare": [ + "XFO" + ], + "metical mozambiqueno": [ + "MZN" + ], + "filipijnse peso": [ + "PHP" + ], + "somoni": [ + "TJS" + ], + "lempire hondurien": [ + "HNL" + ], + "angolan kwanza": [ + "AOA" + ], + "schwedenkrone": [ + "SEK" + ], + "lire maltaise": [ + "MTL" + ], + "balboa paname\u0301en": [ + "PAB" + ], + "israelische lire": [ + "ILS" + ], + "nzd": [ + "NZD" + ], + "hryvnia ucraina": [ + "UAH" + ], + "pataca": [ + "MOP" + ], + "coronas suecas": [ + "SEK" + ], + "cape verde escudo": [ + "CVE" + ], + "rupia pakistani": [ + "PKR" + ], + "hungarian forint": [ + "HUF" + ], + "rupia pakistana": [ + "PKR" + ], + "bs.": [ + "BOB" + ], + "kopeken": [ + "RUB" + ], + "dolar bahameno": [ + "BSD" + ], + "de\u0301l koreai von": [ + "KRW" + ], + "zo\u0308ld foki ko\u0308zta\u0301rsasa\u0301gi escudo": [ + "CVE" + ], + "romanian leu": [ + "RON" + ], + "etio\u0301p birr": [ + "ETB" + ], + "indiai ru\u0301pia": [ + "INR" + ], + "livre de gibraltar": [ + "GIP" + ], + "pa\u2019anga": [ + "TOP" + ], + "mexiko\u0301i pezo\u0301": [ + "MXN" + ], + "tansania schilling": [ + "TZS" + ], + "omanitische rial": [ + "OMR" + ], + "rial omani\u0301": [ + "OMR" + ], + "milandor": [ + "RSD" + ], + "canadischer dollar": [ + "CAD" + ], + "dollaro delle isole salomone": [ + "SBD" + ], + "noorse kronen": [ + "NOK" + ], + "samoan ta\u0304la\u0304": [ + "WST" + ], + "aruban florin": [ + "AWG" + ], + "iraakse dinar": [ + "IQD" + ], + "malaysian ringgit": [ + "MYR" + ], + "som usbeco": [ + "UZS" + ], + "ariary": [ + "MGA" + ], + "kyat": [ + "MMK" + ], + "austral (moneda de argentina)": [ + "ARA" + ], + "bruneise dollar": [ + "BND" + ], + "leu romeno": [ + "RON" + ], + "kolumbiai peso": [ + "COP" + ], + "oezbeekse sum": [ + "UZS" + ], + "burundese frank": [ + "BIF" + ], + "austral (argentina)": [ + "ARA" + ], + "riyal qatarien": [ + "QAR" + ], + "quetzal guatemalteque": [ + "GTQ" + ], + "taiwan dollar": [ + "TWD" + ], + "dolar de namibia": [ + "NAD" + ], + "indiase rupee": [ + "INR" + ], + "dollaro delle figi": [ + "FJD" + ], + "cfa franc beac": [ + "XAF" + ], + "hrk": [ + "HRK" + ], + "peso dominicano": [ + "DOP" + ], + "sh.so.": [ + "SOS" + ], + "kwacha zambiano": [ + "ZMW" + ], + "\u0441\u043e\u043c": [ + "KGS" + ], + "roupie nepalaise": [ + "NPR" + ], + "bermudian dollar": [ + "BMD" + ], + "cookinseln dollar": [ + "NZD" + ], + "bam": [ + "BAM" + ], + "kwanza angolen\u0303o": [ + "AOA" + ], + "dinaro libico": [ + "LYD" + ], + "malagasy ariary": [ + "MGA" + ], + "dolar liberiano": [ + "LRD" + ], + "falklandeilands pond": [ + "FKP" + ], + "nouvelle livre turque": [ + "TRY" + ], + "szovjet rubel": [ + "SUR" + ], + "magyar forint": [ + "HUF" + ], + "peso cubain": [ + "CUP" + ], + "xfo": [ + "XFO" + ], + "pakiszta\u0301ni ru\u0301pia": [ + "PKR" + ], + "georgische lari": [ + "GEL" + ], + "loti": [ + "LSL" + ], + "moldawischer leu": [ + "MDL" + ], + "dollaro statunitense": [ + "USD" + ], + "do\u0301lar bahame\u0301s": [ + "BSD" + ], + "lat leto\u0301n": [ + "LVL" + ], + "peso cileno": [ + "CLP" + ], + "dinar libio": [ + "LYD" + ], + "salomon dollar": [ + "SBD" + ], + "rupia nepali": [ + "NPR" + ], + "cop": [ + "COP" + ], + "sri\u0301 lanka i ru\u0301pia": [ + "LKR" + ], + "maloti": [ + "LSL" + ], + "drtrigonbot:exchange rate data:hrk": [ + "HRK" + ], + "franco ruandese": [ + "RWF" + ], + "ils": [ + "ILS" + ], + "bangladeshi taka": [ + "BDT" + ], + "konvertierbarer peso": [ + "CUC" + ], + "she\u0301kel": [ + "ILS" + ], + "kwd": [ + "KWD" + ], + "bahrain dinar": [ + "BHD" + ], + "sfr.": [ + "CHF" + ], + "livre syrienne": [ + "SYP" + ], + "macao pataca": [ + "MOP" + ], + "dolar de las islas caiman": [ + "KYD" + ], + "dollaro del brunei": [ + "BND" + ], + "chil$": [ + "CLP" + ], + "honduranischer lempira": [ + "HNL" + ], + "frf": [ + "FRF" + ], + "nakfa": [ + "ERN" + ], + "ghanese cedi": [ + "GHS" + ], + "dalasi gambien": [ + "GMD" + ], + "braziliaanse real": [ + "BRL" + ], + "frw": [ + "RWF" + ], + "libra falkland": [ + "FKP" + ], + "algerijnse dinar": [ + "DZD" + ], + "dinar du bahrei\u0308n": [ + "BHD" + ], + "dinar serbio": [ + "RSD" + ], + "rupias indias": [ + "INR" + ], + "lesotho loti": [ + "LSL" + ], + "do\u0301lar guyane\u0301s": [ + "GYD" + ], + "mauritanian ouguiya": [ + "MRO" + ], + "greek drachma": [ + "GRD" + ], + "east caribbean dollar": [ + "XCD" + ], + "dirham de emiratos a\u0301rabes unidos": [ + "AED" + ], + "scellino tanzaniano": [ + "TZS" + ], + "aurar": [ + "ISK" + ], + "oost duitse mark": [ + "DDM" + ], + "franc guine\u0301en": [ + "GNF" + ], + "dollaro hongkonghese": [ + "HKD" + ], + "sll": [ + "SLL" + ], + "\u09f3": [ + "BDT" + ], + "rial saudita": [ + "SAR" + ], + "tzs": [ + "TZS" + ], + "real bresilien": [ + "BRL" + ], + "irakischer dinar": [ + "IQD" + ], + "rupias": [ + "INR" + ], + "sistema u\u0301nico de compensacio\u0301n regional": [ + "XSU" + ], + "franco frances": [ + "FRF" + ], + "costaricaanse colo\u0301n": [ + "CRC" + ], + "lita lituano": [ + "LTL" + ], + "\u20ae": [ + "MNT" + ], + "\u0434\u043e\u043b\u0430\u0440": [ + "MKD" + ], + "peso filippino": [ + "PHP" + ], + "dolar neocelandes": [ + "NZD" + ], + "to\u0308gro\u0308g": [ + "MNT" + ], + "rupiah": [ + "IDR" + ], + "zersto\u0308\u00dft sterling": [ + "GBP" + ], + "can$": [ + "CAD" + ], + "pgk": [ + "PGK" + ], + "rupia india": [ + "INR" + ], + "do\u0301lar belicen\u0303o": [ + "BZD" + ], + "nis": [ + "ILS" + ], + "letse lats": [ + "LVL" + ], + "slrs": [ + "LKR" + ], + "dominikanischer peso": [ + "DOP" + ], + "emira\u0301tusi dirham": [ + "AED" + ], + "litas lituano": [ + "LTL" + ], + "litas lituana": [ + "LTL" + ], + "rupia nepali\u0301": [ + "NPR" + ], + "thb": [ + "THB" + ], + "swazi lilangeni": [ + "SZL" + ], + "yen": [ + "JPY" + ], + "flori\u0301n hungaro": [ + "HUF" + ], + "gourde hai\u0308tienne": [ + "HTG" + ], + "yer": [ + "YER" + ], + "argentinischer austral": [ + "ARA" + ], + "bolivar ve\u0301ne\u0301zue\u0301lien": [ + "VEF" + ], + "bolivianischer boliviano": [ + "BOB" + ], + "sheqalim": [ + "ILS" + ], + "\u20af": [ + "GRD" + ], + "brits pond": [ + "GBP" + ], + "sterlina sudanese": [ + "SDG" + ], + "koruna cesko slovenska": [ + "CSK" + ], + "argent chinois": [ + "CNY" + ], + "libische dinar": [ + "LYD" + ], + "libanese pond": [ + "LBP" + ], + "burundian franc": [ + "BIF" + ], + "nuevo sol peruano": [ + "PEN" + ], + "singapore dollar": [ + "SGD" + ], + "dollar hai\u0308tien": [ + "HTG" + ], + "balboa panameen": [ + "PAB" + ], + "aserbaidschanischer manat": [ + "AZN" + ], + "co\u0301rdoba": [ + "NIO" + ], + "sterlina delle falkland": [ + "FKP" + ], + "peso messicano": [ + "MXN" + ], + "millime": [ + "TND" + ], + "omaanse rial": [ + "OMR" + ], + "sjekel": [ + "ILS" + ], + "vatu": [ + "VUV" + ], + "colo\u0301n": [ + "CRC" + ], + "bosnisch herzegowinische konvertible mark": [ + "BAM" + ], + "dollar guyanien": [ + "GYD" + ], + "cedi": [ + "GHS" + ], + "rupia de mauricio": [ + "MUR" + ], + "cny": [ + "CNY" + ], + "pataca di macao": [ + "MOP" + ], + "argentine austral": [ + "ARA" + ], + "austral (wa\u0308hrung)": [ + "ARA" + ], + "nuevo do\u0301lar taiwane\u0301s": [ + "TWD" + ], + "dinar bahraini\u0301": [ + "BHD" + ], + "som kirghizo": [ + "KGS" + ], + "baht": [ + "THB" + ], + "sri lanka rupee": [ + "LKR" + ], + "zsenminpi": [ + "CNY" + ], + "ryal saoudien": [ + "SAR" + ], + "djibouti franc": [ + "DJF" + ], + "pula del botswana": [ + "BWP" + ], + "mauritiaanse rupee": [ + "MUR" + ], + "syrische lira": [ + "SYP" + ], + "centas": [ + "LTL" + ], + "dollars": [ + "USD" + ], + "guineai frank": [ + "GNF" + ], + "panamai balboa": [ + "PAB" + ], + "dollaro": [ + "BBD", + "BZD" + ], + "us dollar": [ + "USD" + ], + "ta\u0304la\u0304": [ + "WST" + ], + "peso dominicain": [ + "DOP" + ], + "\u4eba\u6c11\u5e01": [ + "CNY" + ], + "libra de gibraltar": [ + "GIP" + ], + "mark est allemand": [ + "DDM" + ], + "libra jamaicana": [ + "JMD" + ], + "eritrean nakfa": [ + "ERN" + ], + "som": [ + "KGS", + "UZS" + ], + "sol": [ + "PEN" + ], + "bath": [ + "THB" + ], + "do\u0301lar surinames": [ + "SRD" + ], + "srilankaanse roepie": [ + "LKR" + ], + "oma\u0301ni ria\u0301l": [ + "OMR" + ], + "dolar guyane\u0301s": [ + "GYD" + ], + "dollaro delle bahamas": [ + "BSD" + ], + "georgischer lari": [ + "GEL" + ], + "sa\u0303o tome\u0301 e\u0301s pri\u0301ncipe i dobra": [ + "STD" + ], + "namibische dollar": [ + "NAD" + ], + "lira turca": [ + "TRY" + ], + "austral": [ + "ARA" + ], + "tune\u0301ziai dina\u0301r": [ + "TND" + ], + "pyg": [ + "PYG" + ], + "\u060b": [ + "AFN" + ], + "tajikistani somoni": [ + "TJS" + ], + "fiji dollar": [ + "FJD" + ], + "south african rand": [ + "ZAR" + ], + "seychelse rupee": [ + "SCR" + ], + "anciens francs": [ + "FRF" + ], + "nuevo sheqel": [ + "ILS" + ], + "amerikai dolla\u0301r": [ + "USD" + ], + "do\u0301lar canadiense": [ + "CAD" + ], + "turkmenistan manat": [ + "TMT" + ], + "litas": [ + "LTL" + ], + "peso cubano": [ + "CUP" + ], + "maltese lira": [ + "MTL" + ], + "azn": [ + "AZN" + ], + "maltese lire": [ + "MTL" + ], + "sve\u0301d korona": [ + "SEK" + ], + "konvertibilna marka": [ + "BAM" + ], + "peso": [ + "COP", + "DOP", + "MXN" + ], + "franse frank": [ + "FRF" + ], + "mexiko\u0301i peso": [ + "MXN" + ], + "pesos": [ + "MXN" + ], + "shilling kenyan": [ + "KES" + ], + "iqd": [ + "IQD" + ], + "colo\u0301n costaricain": [ + "CRC" + ], + "dinar soudanais": [ + "SDG" + ], + "peso colombien": [ + "COP" + ], + "renminbi cinese": [ + "CNY" + ], + "mark der deutschen notenbank": [ + "DDM" + ], + "re\u0301al": [ + "BRL" + ], + "mauritius rupie": [ + "MUR" + ], + "tongai pa'anga": [ + "TOP" + ], + "deutsche mark der deutschen notenbank": [ + "DDM" + ], + "ddr geld": [ + "DDM" + ], + "usbekistan som": [ + "UZS" + ], + "trinidad und tobago dollar": [ + "TTD" + ], + "guyanai dolla\u0301r": [ + "GYD" + ], + "do\u0301lar de bermuda": [ + "BMD" + ], + "peso convertible": [ + "CUC" + ], + "livre britannique": [ + "GBP" + ], + "italienische lira": [ + "ITL" + ], + "italienische lire": [ + "ITL" + ], + "kenyai shilling": [ + "KES" + ], + "li\u0301biai dina\u0301r": [ + "LYD" + ], + "nuevo dolar taiwanes": [ + "TWD" + ], + "fijan dollar": [ + "FJD" + ], + "uruguayischer peso": [ + "UYU" + ], + "neuseela\u0308ndischer dollar": [ + "NZD" + ], + "csendes o\u0301cea\u0301ni valutako\u0308zo\u0308sse\u0301gi frank": [ + "XPF" + ], + "poisha": [ + "BDT" + ], + "lat letton": [ + "LVL" + ], + "n$": [ + "NAD" + ], + "rwandan franc": [ + "RWF" + ], + "lempira": [ + "HNL" + ], + "speciale trekkingsrechten": [ + "XDR" + ], + "maldivian rufiyaa": [ + "MVR" + ], + "rwandan frank": [ + "RWF" + ], + "israe\u0308lische lire": [ + "ILS" + ], + "afgani": [ + "AFN" + ], + "rol": [ + "RON" + ], + "u+20bd": [ + "RUB" + ], + "s\u0192": [ + "SRD" + ], + "dinar bahreini": [ + "BHD" + ], + "tongai pa\u02bbanga": [ + "TOP" + ], + "franco suizo": [ + "CHF" + ], + "marco convertible": [ + "BAM" + ], + "forint hongrois": [ + "HUF" + ], + "som kirguis": [ + "KGS" + ], + "israeli new shekel": [ + "ILS" + ], + "som kirguiz": [ + "KGS" + ], + "albanischer lek": [ + "ALL" + ], + "vef": [ + "VEF" + ], + "kongo franc": [ + "CDF" + ], + "mexicaanse peso": [ + "MXN" + ], + "argentine peso": [ + "ARS" + ], + "guatemaltekischer quetzal": [ + "GTQ" + ], + "novo kwanza": [ + "AOA" + ], + "zuid soedanese pond": [ + "SSP" + ], + "horva\u0301t kuna": [ + "HRK" + ], + "dolar neozelandes": [ + "NZD" + ], + "tu\u0308rkme\u0301n manat": [ + "TMT" + ], + "lilangeni sign": [ + "SZL" + ], + "new taiwan dollar": [ + "TWD" + ], + "swazische lilangeni": [ + "SZL" + ], + "stotinki": [ + "BGN" + ], + "\u0111o\u0302\u0300ng vietnamita": [ + "VND" + ], + "franco burunde\u0301s": [ + "BIF" + ], + "stotinka": [ + "BGN" + ], + "cordoba nicaragu\u0308ense": [ + "NIO" + ], + "lebanese pound": [ + "LBP" + ], + "flori\u0301n aruben\u0303o": [ + "AWG" + ], + "algerian dinar": [ + "DZD" + ], + "dinar jordano": [ + "JOD" + ], + "rial saoudien": [ + "SAR" + ], + "litva\u0301n litas": [ + "LTL" + ], + "scellino ugandese": [ + "UGX" + ], + "zai\u0308re": [ + "CDF" + ], + "florin d\u2019aruba": [ + "AWG" + ], + "grivnia ucraina": [ + "UAH" + ], + "sambischer kwacha": [ + "ZMW" + ], + "filler": [ + "HUF" + ], + "ringgit": [ + "MYR" + ], + "rupia del pakistan": [ + "PKR" + ], + "nieuwe turkse lira": [ + "TRY" + ], + "chilei peso": [ + "CLP" + ], + "iranian rial": [ + "IRR" + ], + "tadzjiekse somoni": [ + "TJS" + ], + "metical mozambiquen\u0303o": [ + "MZN" + ], + "sterlina inglese": [ + "GBP" + ], + "mauritian rupee": [ + "MUR" + ], + "dinaro del bahrain": [ + "BHD" + ], + "venezuelai boli\u0301var": [ + "VEF" + ], + "ruma\u0308nischer ban": [ + "RON" + ], + "dolar de las islas salomo\u0301n": [ + "SBD" + ], + "roepiah": [ + "IDR" + ], + "dinaro serbo": [ + "RSD" + ], + "riyal catari\u0301": [ + "QAR" + ], + "dollaro surinamese": [ + "SRD" + ], + "libra sursudanesa": [ + "SSP" + ], + "south sudanese pound": [ + "SSP" + ], + "boli\u0301var venezuelano": [ + "VEF" + ], + "shilling ke\u0301nyan": [ + "KES" + ], + "dolar suriname\u0301s": [ + "SRD" + ], + "bolivares fuertes": [ + "VEF" + ], + "francos suizos": [ + "CHF" + ], + "botsuanischer pula": [ + "BWP" + ], + "nieuwe israe\u0308lische sjekel": [ + "ILS" + ], + "bahama dollar": [ + "BSD" + ], + "sierra leonischer leone": [ + "SLL" + ], + "bob": [ + "BOB" + ], + "botswana pula": [ + "BWP" + ], + "nepa\u0301li ru\u0301pia": [ + "NPR" + ], + "dollaro taiwanese": [ + "TWD" + ], + "dolar de belice": [ + "BZD" + ], + "sierra leonean leone": [ + "SLL" + ], + "franco gibutiano": [ + "DJF" + ], + "franco": [ + "RWF", + "DJF", + "CDF", + "XPF" + ], + "nouveau dollar de tai\u0308wan": [ + "TWD" + ], + "libras esterlinas": [ + "GBP" + ], + "paraguayischer guarani\u0301": [ + "PYG" + ], + "drachme (antike)": [ + "GRD" + ], + "\u20b1": [ + "PHP" + ], + "s\u20a3": [ + "CHF" + ], + "csehszlova\u0301k korona": [ + "CSK" + ], + "lithuanian litas": [ + "LTL" + ], + "malagassische ariary": [ + "MGA" + ], + "afl.": [ + "AWG" + ], + "flori\u0301n hu\u0301ngaro": [ + "HUF" + ], + "izraeli u\u0301j se\u0301kel": [ + "ILS" + ], + "nigeriaanse naira": [ + "NGN" + ], + "kazakhstani tenge": [ + "KZT" + ], + "south korean won": [ + "KRW" + ], + "dollar de hong kong": [ + "HKD" + ], + "su\u0308dkoreanischer won": [ + "KRW" + ], + "peso mejicano": [ + "MXN" + ], + "won nordcoreano": [ + "KPW" + ], + "mark der ddr": [ + "DDM" + ], + "tschechische krone": [ + "CZK" + ], + "solomon islands dollar": [ + "SBD" + ], + "boli\u0301viai boliviano": [ + "BOB" + ], + "costaricaanse colon": [ + "CRC" + ], + "jemen rial": [ + "YER" + ], + "mga": [ + "MGA" + ], + "kyd": [ + "KYD" + ], + "mauritaanse ouguiya": [ + "MRO" + ], + "gambiaanse dalasi": [ + "GMD" + ], + "gibraltar pound": [ + "GIP" + ], + "tsjechoslowaakse kroon": [ + "CSK" + ], + "gourde": [ + "HTG" + ], + "corona sueca": [ + "SEK" + ], + "colon costaricano": [ + "CRC" + ], + "franc congolais": [ + "CDF" + ], + "florin arubeno": [ + "AWG" + ], + "kaapverdische escudo": [ + "CVE" + ], + "venezolaanse bolivar": [ + "VEF" + ], + "s/": [ + "PEN" + ], + "dolar de nueva zelanda": [ + "NZD" + ], + "do\u0301lar suriname\u0301s": [ + "SRD" + ], + "francs suisses": [ + "CHF" + ], + "s$": [ + "SGD" + ], + "italiaanse lire": [ + "ITL" + ], + "italiaanse lira": [ + "ITL" + ], + "bahreinse dinar": [ + "BHD" + ], + "sr": [ + "SAR" + ], + "corona": [ + "SEK" + ], + "font sterling": [ + "GBP" + ], + "peso chileno": [ + "CLP" + ], + "tala": [ + "WST" + ], + "libra gibraltarena": [ + "GIP" + ], + "saoedi arabische riyal": [ + "SAR" + ], + "guinese frank": [ + "GNF" + ], + "dracma (moderna)": [ + "GRD" + ], + "franco de burundi": [ + "BIF" + ], + "thaise baht": [ + "THB" + ], + "koruna": [ + "CZK" + ], + "koruna ceska": [ + "CZK" + ], + "dram armeno": [ + "AMD" + ], + "st. helena pfund": [ + "SHP" + ], + "lek albanese": [ + "ALL" + ], + "trinidad en tobagodollar": [ + "TTD" + ], + "cuban peso": [ + "CUP" + ], + "gtq": [ + "GTQ" + ], + "djf": [ + "DJF" + ], + "east german mark": [ + "DDM" + ], + "yuan cinese": [ + "CNY" + ], + "jordaanse dinar": [ + "JOD" + ], + "guinean franc": [ + "GNF" + ], + "szoma\u0301liai shilling": [ + "SOS" + ], + "nok": [ + "NOK" + ], + "do\u0301lar de namibia": [ + "NAD" + ], + "shilingi": [ + "TZS" + ], + "franco yibuti": [ + "DJF" + ], + "rufiyah": [ + "MVR" + ], + "col$": [ + "COP" + ], + "rufiyaa": [ + "MVR" + ], + "tt$": [ + "TTD" + ], + "cheli\u0301n": [ + "UGX", + "TZS", + "SOS" + ], + "gryvnia": [ + "UAH" + ], + "cfp franc": [ + "XPF" + ], + "real brasiliano": [ + "BRL" + ], + "cfp frank": [ + "XPF" + ], + "taka bengalese": [ + "BDT" + ], + "ngwee": [ + "ZMW" + ], + "metical mozambicano": [ + "MZN" + ], + "lempira honduregna": [ + "HNL" + ], + "libra malvinense": [ + "FKP" + ], + "nuevo she\u0301quel": [ + "ILS" + ], + "rial omanais": [ + "OMR" + ], + "arg$": [ + "ARS" + ], + "nicaraguanischer co\u0301rdoba": [ + "NIO" + ], + "colon costaricien": [ + "CRC" + ], + "drtrigonbot:exchange rate data:dkk": [ + "DKK" + ], + "goldfranken": [ + "XFO" + ], + "roupie indienne": [ + "INR" + ], + "afghani": [ + "AFN" + ], + "franc cfp": [ + "XPF" + ], + "seychelle szigeteki ru\u0301pia": [ + "SCR" + ], + "franco ruandes": [ + "RWF" + ], + "pesification": [ + "ARS" + ], + "dirham des emirats arabes unis": [ + "AED" + ], + "$can": [ + "CAD" + ], + "franc cfa": [ + "XAF", + "XOF" + ], + "nepalese roepie": [ + "NPR" + ], + "lwei": [ + "AOA" + ], + "nuovo peso argentino": [ + "ARS" + ], + "indonesian rupiah": [ + "IDR" + ], + "guatemalai quetzal": [ + "GTQ" + ], + "dolar de singapur": [ + "SGD" + ], + "peso de me\u0301xico": [ + "MXN" + ], + "surinamese guilder": [ + "SRG" + ], + "nigerian naira": [ + "NGN" + ], + "peso philippin": [ + "PHP" + ], + "mongoolse tugrik": [ + "MNT" + ], + "franc pacifique": [ + "XPF" + ], + "haitianischer gourde": [ + "HTG" + ], + "jemenitische rial": [ + "YER" + ], + "do\u0301lar": [ + "USD", + "FJD" + ], + "kolumbianischer peso": [ + "COP" + ], + "co\u0301rdoba nicaraguense": [ + "NIO" + ], + "dollar ne\u0301oze\u0301landais": [ + "NZD" + ], + "meticais": [ + "MZN" + ], + "uqiya": [ + "MRO" + ], + "grivnia": [ + "UAH" + ], + "lakhs": [ + "BDT" + ], + "zar": [ + "ZAR" + ], + "bahamian dollar": [ + "BSD" + ], + "qa\u0308pik": [ + "AZN" + ], + "ukp": [ + "GBP" + ], + "paraguayaanse guarani\u0301": [ + "PYG" + ], + "mauritiusi ru\u0301pia": [ + "MUR" + ], + "philippinischer peso": [ + "PHP" + ], + "kambodschanischer riel": [ + "KHR" + ], + "huf": [ + "HUF" + ], + "dollar de singapour": [ + "SGD" + ], + "dom$": [ + "DOP" + ], + "dinar du kowei\u0308t": [ + "KWD" + ], + "australian dollar": [ + "AUD" + ], + "namibian dollar": [ + "NAD" + ], + "arubaanse gulden": [ + "AWG" + ], + "drachme moderne grecque": [ + "GRD" + ], + "dinar kowe\u0301itien": [ + "KWD" + ], + "nieuwe israelische sheqel": [ + "ILS" + ], + "salyn": [ + "THB" + ], + "moldova\u0301n lej": [ + "MDL" + ], + "nepalesische rupie": [ + "NPR" + ], + "marka convertible": [ + "BAM" + ], + "bulgarian lev": [ + "BGN" + ], + "tengue": [ + "KZT" + ], + "currency of somalia": [ + "SOS" + ], + "franc franc\u0327ais": [ + "FRF" + ], + "do\u0301lar bahames": [ + "BSD" + ], + "som de kirguistan": [ + "KGS" + ], + "kip laotiano": [ + "LAK" + ], + "sar": [ + "SAR" + ], + "ngultrum butane\u0301s": [ + "BTN" + ], + "birr etiope": [ + "ETB" + ], + "fening": [ + "BAM" + ], + "dominicaanse peso": [ + "DOP" + ], + "taka": [ + "BDT" + ], + "\u20b2": [ + "PYG" + ], + "do\u0301lar neozelandes": [ + "NZD" + ], + "rial ye\u0301me\u0301nite": [ + "YER" + ], + "sterlina sud sudanese": [ + "SSP" + ], + "dolar de bermuda": [ + "BMD" + ], + "dollar taiwanais": [ + "TWD" + ], + "afghanis": [ + "AFN" + ], + "uyu": [ + "UYU" + ], + "cordoba": [ + "NIO" + ], + "bahamaanse dollar": [ + "BSD" + ], + "\u0111ong": [ + "VND" + ], + "baiza": [ + "OMR" + ], + "kazachse tenge": [ + "KZT" + ], + "vietnamesischer \u0111o\u0302\u0300ng": [ + "VND" + ], + "dollar de brunei": [ + "BND" + ], + "dollar du belize": [ + "BZD" + ], + "jordanian dinar": [ + "JOD" + ], + "nuevo sol peruviano": [ + "PEN" + ], + "livre turque": [ + "TRY" + ], + "fidschi dollar": [ + "FJD" + ], + "franco cfa de africa central": [ + "XAF" + ], + "kyrgyzstani som": [ + "KGS" + ], + "dolar taiwane\u0301s": [ + "TWD" + ], + "quetzales": [ + "GTQ" + ], + "pa\u0301pua u\u0301j guineai kina": [ + "PGK" + ], + "won nord core\u0301en": [ + "KPW" + ], + "couronne danoise": [ + "DKK" + ], + "nuevo do\u0301lar de taiwa\u0301n": [ + "TWD" + ], + "uruguay peso": [ + "UYU" + ], + "boli\u0301vares fuertes": [ + "VEF" + ], + "rupia de pakistan": [ + "PKR" + ], + "lilangeni": [ + "SZL" + ], + "rupia dell'india": [ + "INR" + ], + "libra esterlina": [ + "GBP" + ], + "koruna ceska\u0301": [ + "CZK" + ], + "\u20b3": [ + "ARA" + ], + "co\u0301rdoba nicaragu\u0308ense": [ + "NIO" + ], + "hongaarse forint": [ + "HUF" + ], + "loti lesothan": [ + "LSL" + ], + "baht thailandese": [ + "THB" + ], + "real brasileno": [ + "BRL" + ], + "katari ria\u0301l": [ + "QAR" + ], + "uzbekistani som": [ + "UZS" + ], + "armenischer dram": [ + "AMD" + ], + "jorda\u0301n dina\u0301r": [ + "JOD" + ], + "bulgaarse lev": [ + "BGN" + ], + "hondurasi lempira": [ + "HNL" + ], + "do\u0302\u0300ng vietnamita": [ + "VND" + ], + "gel": [ + "GEL" + ], + "trinidad en tobago dollar": [ + "TTD" + ], + "rupia de maldivas": [ + "MVR" + ], + "do\u0301lar liberiano": [ + "LRD" + ], + "vanuatuaanse vatu": [ + "VUV" + ], + "libe\u0301riai dolla\u0301r": [ + "LRD" + ], + "colon costarricense": [ + "CRC" + ], + "dobra di sa\u0303o tome\u0301 e pri\u0301ncipe": [ + "STD" + ], + "croatian kuna": [ + "HRK" + ], + "nouveau sol": [ + "PEN" + ], + "wo\u0306n norcoreano": [ + "KPW" + ], + "de\u0301l afrikai rand": [ + "ZAR" + ], + "dolar bermuden\u0303o": [ + "BMD" + ], + "tu\u0308rkische lira": [ + "TRY" + ], + "rmb": [ + "CNY" + ], + "ringgit malese": [ + "MYR" + ], + "marco de la republica democra\u0301tica alemana": [ + "DDM" + ], + "j$": [ + "JMD" + ], + "lire turque": [ + "TRY" + ], + "tunisian dinar": [ + "TND" + ], + "falkland pfund": [ + "FKP" + ], + "pakistani rupee": [ + "PKR" + ], + "central african cfa franc": [ + "XAF" + ], + "rouble": [ + "SUR" + ], + "ytl": [ + "TRY" + ], + "trinidad e\u0301s tobago\u0301 i dolla\u0301r": [ + "TTD" + ], + "orosz rubel": [ + "RUB" + ], + "dollar de surinam": [ + "SRD" + ], + "franco delle comore": [ + "KMF" + ], + "so\u02bbm": [ + "UZS" + ], + "franse franc": [ + "FRF" + ], + "kuna croata": [ + "HRK" + ], + "droits de tirage spe\u0301ciaux": [ + "XDR" + ], + "kuna croate": [ + "HRK" + ], + "dinar de kuwait": [ + "KWD" + ], + "dschibuti franc": [ + "DJF" + ], + "guinea franc": [ + "GNF" + ], + "kwacha zambese": [ + "ZMW" + ], + "guatemalteekse quetzal": [ + "GTQ" + ], + "chelin keniano": [ + "KES" + ], + "livre libanaise": [ + "LBP" + ], + "dkk": [ + "DKK" + ], + "ouguiya della mauritana": [ + "MRO" + ], + "kaaimaneilandse dollar": [ + "KYD" + ], + "drtrigonbot:exchange rate data:ltl": [ + "LTL" + ], + "comorese frank": [ + "KMF" + ], + "us $": [ + "USD" + ], + "lats lettone": [ + "LVL" + ], + "griwna": [ + "UAH" + ], + "qatari riyal": [ + "QAR" + ], + "colon": [ + "CRC" + ], + "franc germinal": [ + "FRF", + "XFO" + ], + "roupie ne\u0301palaise": [ + "NPR" + ], + "dollar jamai\u0308cain": [ + "JMD" + ], + "mark": [ + "DDM" + ], + "indische rupie": [ + "INR" + ], + "angolese kwanza": [ + "AOA" + ], + "dollar de fidji": [ + "FJD" + ], + "khr": [ + "KHR" + ], + "krona": [ + "SEK" + ], + "dollaro di trinidad e tobago": [ + "TTD" + ], + "krone": [ + "DKK" + ], + "szoma\u0301li shilling": [ + "SOS" + ], + "rupia indiana": [ + "INR" + ], + "bolivar fuerte": [ + "VEF" + ], + "euro\u0301": [ + "EUR" + ], + "rupia de indonesia": [ + "IDR" + ], + "libra gibraltaren\u0303a": [ + "GIP" + ], + "indonesische rupiah": [ + "IDR" + ], + "panamaischer balboa": [ + "PAB" + ], + "ethiopian birr": [ + "ETB" + ], + "kubai konvertibilis peso": [ + "CUC" + ], + "clp": [ + "CLP" + ], + "florin d'aruba": [ + "AWG" + ], + "dolar bahames": [ + "BSD" + ], + "ouguiya mauritanien": [ + "MRO" + ], + "salomonen dollar": [ + "SBD" + ], + "chavito": [ + "CUC" + ], + "kanadai dolla\u0301r": [ + "CAD" + ], + "britische pfund": [ + "GBP" + ], + "singaporese dollar": [ + "SGD" + ], + "chinese renminbi": [ + "CNY" + ], + "saudische riyal": [ + "SAR" + ], + "neuer taiwan dollar": [ + "TWD" + ], + "do\u0301lar taiwanes": [ + "TWD" + ], + "keniaanse shilling": [ + "KES" + ], + "do\u0301lar de bahamas": [ + "BSD" + ], + "bhutanese ngultrum": [ + "BTN" + ], + "corona noruega": [ + "NOK" + ], + "dollaro giamaicano": [ + "JMD" + ], + "afgani afgano": [ + "AFN" + ], + "pab": [ + "PAB" + ], + "aruba florin": [ + "AWG" + ], + "tajikistani ruble": [ + "TJR" + ], + "franzo\u0308sischer franc": [ + "FRF" + ], + "lira italiana": [ + "ITL" + ], + "$ can": [ + "CAD" + ], + "marco de la rda": [ + "DDM" + ], + "ostkaribische wa\u0308hrungsunion": [ + "XCD" + ], + "naf": [ + "ANG" + ], + "drtrigonbot:exchange rate data:jpy": [ + "JPY" + ], + "afghaanse afghani": [ + "AFN" + ], + "peruviaanse sol": [ + "PEN" + ], + "livre de sainte he\u0301le\u0300ne": [ + "SHP" + ], + "sa\u0303o tome\u0301 and pri\u0301ncipe dobra": [ + "STD" + ], + "co\u0301rdoba oro": [ + "NIO" + ], + "moneda nacional": [ + "CUP" + ], + "macanese pataca": [ + "MOP" + ], + "couronne tcheque": [ + "CZK" + ], + "chelin ugande\u0301s": [ + "UGX" + ], + "peso cubano convertible": [ + "CUC" + ], + "eritreai nakfa": [ + "ERN" + ], + "ira\u0301ni ria\u0301l": [ + "IRR" + ], + "dollar canadien": [ + "CAD" + ], + "litouwse litas": [ + "LTL" + ], + "venezuelan boli\u0301var": [ + "VEF" + ], + "lib$": [ + "LRD" + ], + "cheli\u0301n keniata": [ + "KES" + ], + "riyal saoudien": [ + "SAR" + ], + "usbekistan sum": [ + "UZS" + ], + "chelin keniata": [ + "KES" + ], + "peso cubano convertibile": [ + "CUC" + ], + "euros": [ + "EUR" + ], + "dollar des bermudes": [ + "BMD" + ], + "liberianischer dollar": [ + "LRD" + ], + "peso convertibile": [ + "CUC" + ], + "grd": [ + "GRD" + ], + "tschechoslowakische krone": [ + "CSK" + ], + "tongan pa'anga": [ + "TOP" + ], + "szamoai tala": [ + "WST" + ], + "namibischer dollar": [ + "NAD" + ], + "manat azerbai\u0308djanais": [ + "AZN" + ], + "real": [ + "BRL" + ], + "tanzanian shilingi": [ + "TZS" + ], + "dollar liberien": [ + "LRD" + ], + "do\u0301lar neocelandes": [ + "NZD" + ], + "do\u0301lar taiwane\u0301s": [ + "TWD" + ], + "dinaro del bahrein": [ + "BHD" + ], + "florin hu\u0301ngaro": [ + "HUF" + ], + "zambian kwacha": [ + "ZMW" + ], + "dracma greca": [ + "GRD" + ], + "italian lira": [ + "ITL" + ], + "antilliaanse gulden": [ + "ANG" + ], + "som uzbeco": [ + "UZS" + ], + "yuan renminbi": [ + "CNY" + ], + "tenge kazajo": [ + "KZT" + ], + "dolar trinitense": [ + "TTD" + ], + "dollaro bahamense": [ + "BSD" + ], + "yeni kurus\u0327": [ + "TRY" + ], + "brunei dollar": [ + "BND" + ], + "lek albanes": [ + "ALL" + ], + "yua\u0301n chino": [ + "CNY" + ], + "\u20adn": [ + "LAK" + ], + "som kirgui\u0301s": [ + "KGS" + ], + "britse pond": [ + "GBP" + ], + "\u20b4": [ + "UAH" + ], + "nuevo dolar de taiwa\u0301n": [ + "TWD" + ], + "dominican peso": [ + "DOP" + ], + "mosambikanischer escudo": [ + "MZE" + ], + "do\u0301lar de las islas caima\u0301n": [ + "KYD" + ], + "gibraltar pfund": [ + "GIP" + ], + "lats leto\u0301n": [ + "LVL" + ], + "kanadische dollar": [ + "CAD" + ], + "srd": [ + "SRD" + ], + "sre": [ + "SCR" + ], + "comore i frank": [ + "KMF" + ], + "peso colombiano": [ + "COP" + ], + "leke\u0308": [ + "ALL" + ], + "\u0433\u0440\u0438\u0432\u043d\u044f": [ + "UAH" + ], + "alu chip": [ + "DDM" + ], + "kanadischer dollar": [ + "CAD" + ], + "suriname dollar": [ + "SRD" + ], + "corona ceca": [ + "CZK" + ], + "serbischer dinar": [ + "RSD" + ], + "dollar de brune\u0301i": [ + "BND" + ], + "denar": [ + "MKD" + ], + "dinar macedonio": [ + "MKD" + ], + "lira maltese": [ + "MTL" + ], + "frans geld": [ + "FRF" + ], + "naira nigeriana": [ + "NGN" + ], + "nuevo do\u0301lar taiwanes": [ + "TWD" + ], + "dollaro neozelandese": [ + "NZD" + ], + "dinar bahrei\u0308nien": [ + "BHD" + ], + "zweedse kroon": [ + "SEK" + ], + "swedish krona": [ + "SEK" + ], + "new israeli shekel": [ + "ILS" + ], + "leu moldave": [ + "MDL" + ], + "rupia de nepal": [ + "NPR" + ], + "leu moldavo": [ + "MDL" + ], + "fidzsi dolla\u0301r": [ + "FJD" + ], + "pula": [ + "BWP" + ], + "drachmai": [ + "GRD" + ], + "marco bosnio": [ + "BAM" + ], + "roupie seychelloise": [ + "SCR" + ], + "u\u0308zbe\u0301g szom": [ + "UZS" + ], + "tanzanian schilling": [ + "TZS" + ], + "gib\u00a3": [ + "GIP" + ], + "lett lat": [ + "LVL" + ], + "kc\u030cs": [ + "CSK" + ], + "mark der deutschen demokratischen republik": [ + "DDM" + ], + "yeni tu\u0308rk liras\u0131": [ + "TRY" + ], + "\u3012": [ + "KZT" + ], + "bosnische convertibele mark": [ + "BAM" + ], + "libra siria": [ + "SYP" + ], + "peso oro": [ + "DOP" + ], + "rupia indonesia": [ + "IDR" + ], + "pakistaanse rupee": [ + "PKR" + ], + "riel cambogiano": [ + "KHR" + ], + "haitian gourde": [ + "HTG" + ], + "tschechische wa\u0308hrung": [ + "CZK" + ], + "bosnia and herzegovina convertible mark": [ + "BAM" + ], + "francs franc\u0327ais": [ + "FRF" + ], + "griechische drachme": [ + "GRD" + ], + "nuovo sol": [ + "PEN" + ], + "swiss franc": [ + "CHF" + ], + "swiss frank": [ + "CHF" + ], + "somoni tayiko": [ + "TJS" + ], + "rial yemeni\u0301": [ + "YER" + ], + "nueva lira turca": [ + "TRY" + ], + "engelse pond": [ + "GBP" + ], + "chelin tanzano": [ + "TZS" + ], + "peso de repu\u0301blica dominicana": [ + "DOP" + ], + "dalasi gambese": [ + "GMD" + ], + "nicaraguaanse co\u0301rdoba": [ + "NIO" + ], + "lira libanese": [ + "LBP" + ], + "baht tailandes": [ + "THB" + ], + "khoum": [ + "MRO" + ], + "lek albane\u0301s": [ + "ALL" + ], + "botswanischer pula": [ + "BWP" + ], + "dinar mace\u0301donien": [ + "MKD" + ], + "dollar": [ + "USD" + ], + "dolar bahame\u0301s": [ + "BSD" + ], + "\u20ac": [ + "EUR" + ], + "dollar singapourien": [ + "SGD" + ], + "israe\u0308lische sjekel": [ + "ILS" + ], + "wo\u0306n surcoreano": [ + "KRW" + ], + "ukra\u0301n hrivnya": [ + "UAH" + ], + "dinar algerien": [ + "DZD" + ], + "cedi ghanese": [ + "GHS" + ], + "cfa franc bceao": [ + "XOF" + ], + "scr": [ + "SCR" + ], + "\u0442\u04e9\u0433\u0440\u04e9\u0433": [ + "MNT" + ], + "izlandi korona": [ + "ISK" + ], + "englisches pfund": [ + "GBP" + ], + "ws$": [ + "WST" + ], + "wikipedia:raadsel/netties20070405": [ + "GRD" + ], + "dolar neozelande\u0301s": [ + "NZD" + ], + "samoanischer tala": [ + "WST" + ], + "syrisch pond": [ + "SYP" + ], + "caymaneilandse dollar": [ + "KYD" + ], + "cordoba oro": [ + "NIO" + ], + "kina papuana": [ + "PGK" + ], + "szent ilona i font": [ + "SHP" + ], + "sudanese pound": [ + "SDG" + ], + "gourde haitiano": [ + "HTG" + ], + "dollar hongkongais": [ + "HKD" + ], + "haiti gourde": [ + "HTG" + ], + "eyrir": [ + "ISK" + ], + "australes": [ + "ARA" + ], + "livres turques": [ + "TRY" + ], + "dollar barbadien": [ + "BBD" + ], + "congolese franc": [ + "CDF" + ], + "wst": [ + "WST" + ], + "t$": [ + "TOP" + ], + "congolese frank": [ + "CDF" + ], + "nafka": [ + "ERN" + ], + "dansk krone": [ + "DKK" + ], + "jordanischer dinar": [ + "JOD" + ], + "dolar de bahamas": [ + "BSD" + ], + "brasilianischer real": [ + "BRL" + ], + "nz$": [ + "NZD" + ], + "leone sierra le\u0301onais": [ + "SLL" + ], + "tunesische dinar": [ + "TND" + ], + "do\u0301lar namibio": [ + "NAD" + ], + "$ca": [ + "CAD" + ], + "bengalese taka": [ + "BDT" + ], + "dollar fidjien": [ + "FJD" + ], + "ungarischer forint": [ + "HUF" + ], + "dinar serbe": [ + "RSD" + ], + "do\u0301lar de trinidad y tobago": [ + "TTD" + ], + "belize dollar": [ + "BZD" + ], + "sum": [ + "UZS" + ], + "franc rwandais": [ + "RWF" + ], + "dinar jordanien": [ + "JOD" + ], + "moldauischer leu": [ + "MDL" + ], + "dolar de las islas salomon": [ + "SBD" + ], + "lire italienne": [ + "ITL" + ], + "ang": [ + "ANG" + ], + "\u0e3f": [ + "THB" + ], + "sucre": [ + "XSU" + ], + "kzt": [ + "KZT" + ], + "kronor": [ + "SEK" + ], + "somalische shilling": [ + "SOS" + ], + "dollaro namibiano": [ + "NAD" + ], + "omanischer rial": [ + "OMR" + ], + "do\u0301lar bermuden\u0303o": [ + "BMD" + ], + "marka": [ + "BAM" + ], + "marco convertibile": [ + "BAM" + ], + "rublo ruso": [ + "RUB" + ], + "uae dirham": [ + "AED" + ], + "vae dirham": [ + "AED" + ], + "ngultrum del bhutan": [ + "BTN" + ], + "samoaanse tala": [ + "WST" + ], + "maltesische lira": [ + "MTL" + ], + "couronne norvegienne": [ + "NOK" + ], + "franc burundais": [ + "BIF" + ], + "flori\u0301n arubeno": [ + "AWG" + ], + "georgian kupon lari": [ + "GEL" + ], + "dollar de trinidad et tobago": [ + "TTD" + ], + "t\u0323a\u0304ka\u0304": [ + "BDT" + ], + "tonga pa\u02bbanga": [ + "TOP" + ], + "dinar kuwaiti": [ + "KWD" + ], + "kenia schilling": [ + "KES" + ], + "\u20a1": [ + "CRC" + ], + "guarani paraguayen": [ + "PYG" + ], + "lats letton": [ + "LVL" + ], + "quetzal guate\u0301malte\u0300que": [ + "GTQ" + ], + "netherlands antillean guilder": [ + "ANG" + ], + "balboa panamen\u0303o": [ + "PAB" + ], + "dolar de brune\u0301i": [ + "BND" + ], + "sheqel": [ + "ILS" + ], + "escudo capoverdiano": [ + "CVE" + ], + "boli\u0301var fuerte": [ + "VEF" + ], + "franco della guinea": [ + "GNF" + ], + "boli\u0301var": [ + "VEF" + ], + "lilangeni swazilandais": [ + "SZL" + ], + "dracma griega moderna": [ + "GRD" + ], + "tenge kazako": [ + "KZT" + ], + "tenge kazakh": [ + "KZT" + ], + "mexican centavo": [ + "MXN" + ], + "peso uruguaiano": [ + "UYU" + ], + "franco cfp": [ + "XPF" + ], + "so'm": [ + "UZS" + ], + "drtrigonbot:exchange rate data:chf": [ + "CHF" + ], + "konvertible mark": [ + "BAM" + ], + "nouveau manat aze\u0301ri": [ + "AZN" + ], + "nordjemenitischer rial": [ + "YER" + ], + "bolivares": [ + "VEF" + ], + "\u043b\u0435\u0432": [ + "BGN" + ], + "deg": [ + "XDR" + ], + "guarani paraguaiano": [ + "PYG" + ], + "scellino keniano": [ + "KES" + ], + "f$": [ + "FJD" + ], + "couronne islandaise": [ + "ISK" + ], + "dollar de la barbade": [ + "BBD" + ], + "macause pataca": [ + "MOP" + ], + "do\u0301lar bermudeno": [ + "BMD" + ], + "isk": [ + "ISK" + ], + "west african cfa franc": [ + "XOF" + ], + "armeense dram": [ + "AMD" + ], + "renminbi yuan": [ + "CNY" + ], + "aussie dollar": [ + "AUD" + ], + "franco francese": [ + "FRF" + ], + "tetradrachmon": [ + "GRD" + ], + "dinar irakien": [ + "IQD" + ], + "tongan pa\u02bbanga": [ + "TOP" + ], + "fr": [ + "FRF" + ], + "ft": [ + "HUF" + ], + "nuevo sol": [ + "PEN" + ], + "peso convertible argentino": [ + "ARS" + ], + "ff": [ + "FRF" + ], + "dollar de taiwan": [ + "TWD" + ], + "azerbaijani manat": [ + "AZN" + ], + "dirham": [ + "AED" + ], + "antillen gulden": [ + "ANG" + ], + "lari ge\u0301orgien": [ + "GEL" + ], + "fijian dollar": [ + "FJD" + ], + "mark convertible de bosnie herze\u0301govine": [ + "BAM" + ], + "nuovo siclo israeliano": [ + "ILS" + ], + "bhuta\u0301ni ngultrum": [ + "BTN" + ], + "guarani\u0301 paraguayen": [ + "PYG" + ], + "jamaican dollar": [ + "JMD" + ], + "rupia": [ + "LKR", + "SCR", + "INR", + "NPR" + ], + "dinar libyen": [ + "LYD" + ], + "dinaro giordano": [ + "JOD" + ], + "paraguayan guarani\u0301": [ + "PYG" + ], + "maldivische rufiyaa": [ + "MVR" + ], + "marokkanischer dirham": [ + "MAD" + ], + "franco pacifico": [ + "XPF" + ], + "lats": [ + "LVL" + ], + "forinto": [ + "HUF" + ], + "dollar be\u0301lizien": [ + "BZD" + ], + "forints": [ + "HUF" + ], + "do\u0301lar bahameno": [ + "BSD" + ], + "hrywen": [ + "UAH" + ], + "roupie pakistanaise": [ + "PKR" + ], + "rwf": [ + "RWF" + ], + "iraanse rial": [ + "IRR" + ], + "chetrum": [ + "BTN" + ], + "do\u0301lar de las bahamas": [ + "BSD" + ], + "lesothischer loti": [ + "LSL" + ], + "djiboutian franc": [ + "DJF" + ], + "soviet ruble": [ + "SUR" + ], + "madagascan ariary": [ + "MGA" + ], + "hryvna": [ + "UAH" + ], + "komoren franc": [ + "KMF" + ], + "sterlina britannica": [ + "GBP" + ], + "sonderziehungsrecht": [ + "XDR" + ], + "jamaicai dolla\u0301r": [ + "JMD" + ], + "sierra leone i leone": [ + "SLL" + ], + "laoszi kip": [ + "LAK" + ], + "ma\u0301ltai li\u0301ra": [ + "MTL" + ], + "dolar de fiji": [ + "FJD" + ], + "dirham de los emiratos a\u0301rabes unidos": [ + "AED" + ], + "dollaro della namibia": [ + "NAD" + ], + "vn\u0111": [ + "VND" + ], + "dollar des carai\u0308bes orientales": [ + "XCD" + ], + "kelet karibi dolla\u0301r": [ + "XCD" + ], + "dinar argelino": [ + "DZD" + ], + "dolar de barbados": [ + "BBD" + ], + "sbd": [ + "SBD" + ], + "saoedische riyal": [ + "SAR" + ], + "dinar bareini\u0301": [ + "BHD" + ], + "do\u0301lar de guyana": [ + "GYD" + ], + "won norcoreano": [ + "KPW" + ], + "dram arme\u0301nien": [ + "AMD" + ], + "peso de me\u0301jico": [ + "MXN" + ], + "kuna": [ + "HRK" + ], + "kubanischer peso": [ + "CUP" + ], + "sambia kwacha": [ + "ZMW" + ], + "sri lankaanse roepie": [ + "LKR" + ], + "neue tu\u0308rkische lira": [ + "TRY" + ], + "algerischer dinar": [ + "DZD" + ], + "hong kong dollar": [ + "HKD" + ], + "$a": [ + "ARP" + ], + "rupia nepalese": [ + "NPR" + ], + "bhat": [ + "THB" + ], + "maleisische ringgit": [ + "MYR" + ], + "rupia nepalesa": [ + "NPR" + ], + "tsjechische kroon": [ + "CZK" + ], + "dong": [ + "VND" + ], + "xof": [ + "XOF" + ], + "chilean peso": [ + "CLP" + ], + "nordkoreanischer won": [ + "KPW" + ], + "soedanese pond": [ + "SDG" + ], + "angol font": [ + "GBP" + ], + "kip laosiano": [ + "LAK" + ], + "dollaro delle barbados": [ + "BBD" + ], + "gpb": [ + "GBP" + ], + "nuovo dollaro taiwanese": [ + "TWD" + ], + "pond sterling": [ + "GBP" + ], + "nouveau shekel": [ + "ILS" + ], + "libanees pond": [ + "LBP" + ], + "kuvaiti dina\u0301r": [ + "KWD" + ], + "kenyan shilling": [ + "KES" + ], + "dolar bahamen\u0303o": [ + "BSD" + ], + "surinaamse gulden": [ + "SRG" + ], + "tschang": [ + "THB" + ], + "north korean won": [ + "KPW" + ], + "fiorino ungherese": [ + "HUF" + ], + "franco yibuti\u0301": [ + "DJF" + ], + "servische dinar": [ + "RSD" + ], + "manat turkme\u0300ne": [ + "TMT" + ], + "swiss franken": [ + "CHF" + ], + "costa rica colo\u0301n": [ + "CRC" + ], + "franco yibutiense": [ + "DJF" + ], + "venezolaanse boli\u0301var": [ + "VEF" + ], + "marco de la repu\u0301blica democratica alemana": [ + "DDM" + ], + "karod": [ + "NPR" + ], + "riyal": [ + "SAR" + ], + "birr e\u0301thiopien": [ + "ETB" + ], + "francs pacifique": [ + "XPF" + ], + "rufiyaa delle maldive": [ + "MVR" + ], + "libyan dinar": [ + "LYD" + ], + "siclo israeliano": [ + "ILS" + ], + "santomese dobra": [ + "STD" + ], + "mauritiaanse roepie": [ + "MUR" + ], + "srilankaanse rupee": [ + "LKR" + ], + "sum uzbeco": [ + "UZS" + ], + "laari": [ + "MVR" + ], + "dolar de trinidad y tobago": [ + "TTD" + ], + "austral argentino": [ + "ARA" + ], + "do\u0301lar fijiano": [ + "FJD" + ], + "bz$": [ + "BZD" + ], + "argentijnse peso": [ + "ARS" + ], + "vnd": [ + "VND" + ], + "dong vietnamien": [ + "VND" + ], + "ngultrum butanes": [ + "BTN" + ], + "do\u0301lar del caribe este": [ + "XCD" + ], + "pakistaanse roepie": [ + "PKR" + ], + "drtrigonbot:exchange rate data:usd": [ + "USD" + ], + "indone\u0301z ru\u0301pia": [ + "IDR" + ], + "riyal dell'oman": [ + "OMR" + ], + "gambiai dalasi": [ + "GMD" + ], + "dollaro delle salomone": [ + "SBD" + ], + "bermuda dollar": [ + "BMD" + ], + "km": [ + "BAM" + ], + "kr": [ + "DKK" + ], + "mozambican escudo": [ + "MZE" + ], + "samoan tala": [ + "WST" + ], + "brazil real": [ + "BRL" + ], + "dollaro della guyana": [ + "GYD" + ], + "norve\u0301g korona": [ + "NOK" + ], + "dobra di sao tome\u0301 e principe": [ + "STD" + ], + "cdf": [ + "CDF" + ], + "azerbeidzjaanse manat": [ + "AZN" + ], + "droits de tirage speciaux": [ + "XDR" + ], + "paanga": [ + "TOP" + ], + "livre des i\u0302les malouines": [ + "FKP" + ], + "ugx": [ + "UGX" + ], + "holland antilla\u0301kbeli forint": [ + "ANG" + ], + "\u20a3": [ + "FRF" + ], + "costa rican colo\u0301n": [ + "CRC" + ], + "roupie indone\u0301sienne": [ + "IDR" + ], + "rd$": [ + "DOP" + ], + "dollar australien": [ + "AUD" + ], + "russian ruble": [ + "RUB" + ], + "mianmari kjap": [ + "MMK" + ], + "nicaraguan co\u0301rdoba": [ + "NIO" + ], + "florin aruben\u0303o": [ + "AWG" + ], + "rupie indiane": [ + "INR" + ], + "florin arubain": [ + "AWG" + ], + "dinar kuwaiti\u0301": [ + "KWD" + ], + "hryvnya": [ + "UAH" + ], + "tamil rupee": [ + "LKR" + ], + "oegandese shilling": [ + "UGX" + ], + "corona cecoslovacca": [ + "CSK" + ], + "clp$": [ + "CLP" + ], + "cheli\u0301n ugandes": [ + "UGX" + ], + "kina": [ + "PGK" + ], + "noord koreaanse won": [ + "KPW" + ], + "chilenischer peso": [ + "CLP" + ], + "uganda schilling": [ + "UGX" + ], + "uruguayaanse peso": [ + "UYU" + ], + "metical": [ + "MZN" + ], + "\u0440\u0443\u0431": [ + "RUB" + ], + "marokko\u0301i dirham": [ + "MAD" + ], + "ars": [ + "ARS" + ], + "iraki dina\u0301r": [ + "IQD" + ], + "tugrik mongolo": [ + "MNT" + ], + "soedanees pond": [ + "SDG" + ], + "honduran lempira": [ + "HNL" + ], + "rial dell'oman": [ + "OMR" + ], + "sek": [ + "SEK" + ], + "franc malgache": [ + "MGA" + ], + "fille\u0301r": [ + "HUF" + ], + "piso": [ + "PHP" + ], + "cayman islands dollar": [ + "KYD" + ], + "guyaanse dollar": [ + "GYD" + ], + "won": [ + "KRW" + ], + "barbadosi dolla\u0301r": [ + "BBD" + ], + "bosnische inwisselbare mark": [ + "BAM" + ], + "\u20b8": [ + "KZT" + ], + "dollar neo zelandais": [ + "NZD" + ], + "leone sierraleonese": [ + "SLL" + ], + "franco comorano": [ + "KMF" + ], + "guineese frank": [ + "GNF" + ], + "renminbi": [ + "CNY" + ], + "alba\u0301n lek": [ + "ALL" + ], + "ethiopische birr": [ + "ETB" + ], + "sterlina di sant\u2019elena": [ + "SHP" + ], + "corona islandesa": [ + "ISK" + ], + "corona islandese": [ + "ISK" + ], + "dolar bermudeno": [ + "BMD" + ], + "surinamese dollar": [ + "SRD" + ], + "nicaraguaanse cordoba": [ + "NIO" + ], + "loti lesothiano": [ + "LSL" + ], + "australischer dollar": [ + "AUD" + ], + "canadian dollar": [ + "CAD" + ], + "yen giapponese": [ + "JPY" + ], + "mongolian to\u0308gro\u0308g": [ + "MNT" + ], + "chelin ugandes": [ + "UGX" + ], + "chinese yuan": [ + "CNY" + ], + "shilling somalien": [ + "SOS" + ], + "hongkongse dollar": [ + "HKD" + ], + "bolivar": [ + "VEF" + ], + "riyal yemenita": [ + "YER" + ], + "florin des antilles ne\u0301erlandaises": [ + "ANG" + ], + "\u20b9": [ + "INR" + ], + "xaf": [ + "XAF" + ], + "philippine peso": [ + "PHP" + ], + "afghan afghani": [ + "AFN" + ], + "dominikai peso": [ + "DOP" + ], + "zuid koreaanse won": [ + "KRW" + ], + "cubaanse peso": [ + "CUP" + ], + "nepalese rupee": [ + "NPR" + ], + "kyat birmano": [ + "MMK" + ], + "franc or": [ + "XFO" + ], + "fiorino surinamese": [ + "SRG" + ], + "czech koruna": [ + "CZK" + ], + "verenigde arabische emiraten dirham": [ + "AED" + ], + "tanzaniaanse shilling": [ + "TZS" + ], + "rupia mauriziana": [ + "MUR" + ], + "monnaie canadienne": [ + "CAD" + ], + "do\u0301lar bruneano": [ + "BND" + ], + "koruna c\u030cesko slovenska\u0301": [ + "CSK" + ], + "pound": [ + "GBP" + ], + "pounds sterling": [ + "GBP" + ], + "jpy": [ + "JPY" + ], + "bs$": [ + "BSD" + ], + "pula botswanais": [ + "BWP" + ], + "haitiaanse gourde": [ + "HTG" + ], + "dinar de bahrein": [ + "BHD" + ], + "dollar jamaicain": [ + "JMD" + ], + "peso ley": [ + "ARS" + ], + "do\u0301lares neozelandeses": [ + "NZD" + ], + "ten\u030cn\u030ce": [ + "TMT" + ], + "pondteken": [ + "GBP" + ], + "\u5143": [ + "CNY" + ], + "franc uic": [ + "XFU" + ], + "syp": [ + "SYP" + ], + "dzsibuti frank": [ + "DJF" + ], + "dollar de la jamai\u0308que": [ + "JMD" + ], + "dinaro tunisino": [ + "TND" + ], + "yuan": [ + "CNY" + ], + "sudanesisches pfund": [ + "SDG" + ], + "euro": [ + "EUR" + ], + "peruanischer nuevo sol": [ + "PEN" + ], + "falkland pound": [ + "FKP" + ], + "forint hungaro": [ + "HUF" + ], + "couronne suedoise": [ + "SEK" + ], + "peso uruguayen": [ + "UYU" + ], + "nami\u0301biai dolla\u0301r": [ + "NAD" + ], + "do\u0301lar bahamen\u0303o": [ + "BSD" + ], + "leone": [ + "SLL" + ], + "libanon pfund": [ + "LBP" + ], + "riyal saudi": [ + "SAR" + ], + "mozambican metical": [ + "MZN" + ], + "dollaro liberiano": [ + "LRD" + ], + "dolar de guyana": [ + "GYD" + ], + "brazilian real": [ + "BRL" + ], + "do\u0301lar de las islas caiman": [ + "KYD" + ], + "$": [ + "USD", + "MXN", + "ARS", + "CAD" + ], + "cup": [ + "CUP" + ], + "real brasilen\u0303o": [ + "BRL" + ], + "peso mexicain": [ + "MXN" + ], + "cuc": [ + "CUC" + ], + "\u0433\u0440\u043d": [ + "UAH" + ], + "monnaie franc\u0327aise": [ + "FRF" + ], + "guarani\u0301 de paraguay": [ + "PYG" + ], + "pa\u02bbanga": [ + "TOP" + ], + "marco": [ + "DDM" + ], + "panamese balboa": [ + "PAB" + ], + "dolar caimano": [ + "KYD" + ], + "feninga": [ + "BAM" + ], + "kazah tenge": [ + "KZT" + ], + "na\u0192": [ + "ANG" + ], + "belgian congolese franc": [ + "CDF" + ], + "jamaika dollar": [ + "JMD" + ], + "to\u0308ro\u0308k u\u0301j li\u0301ra": [ + "TRY" + ], + "nige\u0301riai naira": [ + "NGN" + ], + "oude metical": [ + "MZN" + ], + "singapur dollar": [ + "SGD" + ], + "b$": [ + "BSD" + ], + "metical del mozambico": [ + "MZN" + ], + "ariary malgascio": [ + "MGA" + ], + "bolivar venezuelano": [ + "VEF" + ], + "corona norvegese": [ + "NOK" + ], + "s/.": [ + "PEN" + ], + "franco del burundi": [ + "BIF" + ], + "yemeni rial": [ + "YER" + ], + "dirham de emiratos arabes unidos": [ + "AED" + ], + "riel": [ + "KHR" + ], + "venezolanischer boli\u0301var": [ + "VEF" + ], + "de\u0301l szuda\u0301ni font": [ + "SSP" + ], + "\u20a4": [ + "ITL" + ], + "dolar de brunei": [ + "BND" + ], + "colo\u0301n costaricano": [ + "CRC" + ], + "dinaro kuwaitiano": [ + "KWD" + ], + "re\u0301aux bre\u0301siliens": [ + "BRL" + ], + "pen": [ + "PEN" + ], + "indiase roepie": [ + "INR" + ], + "rupia delle seychelles": [ + "SCR" + ], + "lari": [ + "GEL" + ], + "dollaro di barbados": [ + "BBD" + ], + "xang": [ + "THB" + ], + "taiwanese dollar": [ + "TWD" + ], + "paraguayi guarani\u0301": [ + "PYG" + ], + "cambodian riel": [ + "KHR" + ], + "rub": [ + "RUB" + ], + "dinaro algerino": [ + "DZD" + ], + "bs": [ + "BSD", + "BOB" + ], + "syrisches pfund": [ + "SYP" + ], + "rial iranien": [ + "IRR" + ], + "dollar namibien": [ + "NAD" + ], + "couronne tche\u0301coslovaque": [ + "CSK" + ], + "couronne tchecoslovaque": [ + "CSK" + ], + "peruvian nuevo sol": [ + "PEN" + ], + "lat leton": [ + "LVL" + ], + "costa ricaanse colon": [ + "CRC" + ], + "schweizer franken": [ + "CHF" + ], + "dollar tai\u0308wanais": [ + "TWD" + ], + "japanese yen": [ + "JPY" + ], + "malediven rupie": [ + "MVR" + ], + "arubaanse florijn": [ + "AWG" + ], + "grivna": [ + "UAH" + ], + "ostkaribischer dollar": [ + "XCD" + ], + "mkd": [ + "MKD" + ], + "\u00a5": [ + "JPY" + ], + "ci$": [ + "KYD" + ], + "yuans": [ + "CNY" + ], + "xpf": [ + "XPF" + ], + "lao kip": [ + "LAK" + ], + "franco congoleno": [ + "CDF" + ], + "marco bosnioherzegovino": [ + "BAM" + ], + "sdr": [ + "XDR" + ], + "dollaro del belize": [ + "BZD" + ], + "peso argentino": [ + "ARP" + ], + "dinaro iracheno": [ + "IQD" + ], + "hongkong dollar": [ + "HKD" + ], + "guarani\u0301 paraguaiano": [ + "PYG" + ], + "flori\u0301n antillano neerlande\u0301s": [ + "ANG" + ], + "dirham marocain": [ + "MAD" + ], + "rial irani": [ + "IRR" + ], + "peso d'uruguay": [ + "UYU" + ], + "forinto hu\u0301ngaro": [ + "HUF" + ], + "escudo cap verdien": [ + "CVE" + ], + "mongol tugrik": [ + "MNT" + ], + "gha\u0301nai cedi": [ + "GHS" + ], + "do\u0301lar del caribe oriental": [ + "XCD" + ], + "riyal saudita": [ + "SAR" + ], + "omani rial": [ + "OMR" + ], + "dinar tunisien": [ + "TND" + ], + "cape verdean escudo": [ + "CVE" + ], + "peso do\u0301lar": [ + "ARS" + ], + "dolar namibio": [ + "NAD" + ], + "lyd": [ + "LYD" + ], + "sint heleens pond": [ + "SHP" + ], + "nieuwe israe\u0308lische sheqel": [ + "ILS" + ], + "laotiaanse kip": [ + "LAK" + ], + "bolivian boliviano": [ + "BOB" + ], + "kirgizische som": [ + "KGS" + ], + "denaro macedone": [ + "MKD" + ], + "swiss franco": [ + "CHF" + ], + "birr eti\u0301ope": [ + "ETB" + ], + "barbadian dollar": [ + "BBD" + ], + "dolar canadiense": [ + "CAD" + ], + "swiss francs": [ + "CHF" + ], + "tonga pa`anga": [ + "TOP" + ], + "dinar de bahrei\u0308n": [ + "BHD" + ], + "dollar des iles salomon": [ + "SBD" + ], + "dobra santotomense": [ + "STD" + ], + "leu rumano": [ + "RON" + ], + "lisente": [ + "LSL" + ], + "manat turcomano": [ + "TMT" + ], + "taka bangladeshi": [ + "BDT" + ], + "dram": [ + "AMD" + ], + "macedonische denar": [ + "MKD" + ], + "israelische sjekel": [ + "ILS" + ], + "dop": [ + "DOP" + ], + "vanuatu vatu": [ + "VUV" + ], + "dollar des i\u0302les salomon": [ + "SBD" + ], + "franzo\u0308sischer franken": [ + "FRF" + ], + "guarani": [ + "PYG" + ], + "su\u0308dsudan pfund": [ + "SSP" + ], + "roemeense leu": [ + "RON" + ], + "mark convertible": [ + "BAM" + ], + "franco de djibouti": [ + "DJF" + ], + "ugandan shilling": [ + "UGX" + ], + "pazifik franc": [ + "XPF" + ], + "rublo tayiko": [ + "TJR" + ], + "argentinischer peso": [ + "ARS" + ], + "bahraini dinar": [ + "BHD" + ], + "amerikaanse dollar": [ + "USD" + ], + "franc comorien": [ + "KMF" + ], + "dolar neocelande\u0301s": [ + "NZD" + ], + "libra sudanesa": [ + "SDG" + ], + "ugandai shilling": [ + "UGX" + ], + "peso argentin": [ + "ARS" + ], + "tugrik mongol": [ + "MNT" + ], + "fiorino delle antille olandesi": [ + "ANG" + ], + "hryvnia": [ + "UAH" + ], + "ma\u0308tonya": [ + "ETB" + ], + "dalasi": [ + "GMD" + ], + "couronne tche\u0300que": [ + "CZK" + ], + "lkr": [ + "LKR" + ], + "clps": [ + "CLP" + ], + "dolar surinames": [ + "SRD" + ], + "kuwait dinar": [ + "KWD" + ], + "ruma\u0308nischer leu": [ + "RON" + ], + "do\u0301lar jamaicano": [ + "JMD" + ], + "nuevo dolar taiwane\u0301s": [ + "TWD" + ], + "venezolanischer bolivar": [ + "VEF" + ], + "qatarese rial": [ + "QAR" + ], + "do\u0301lar de surinam": [ + "SRD" + ], + "livres sterling": [ + "GBP" + ], + "g$": [ + "GYD" + ], + "ruma\u0308nischer lei": [ + "RON" + ], + "leone della sierra leone": [ + "SLL" + ], + "manat azero": [ + "AZN" + ], + "rwandese frank": [ + "RWF" + ], + "ancien franc": [ + "FRF" + ], + "naira": [ + "NGN" + ], + "koruna ceskoslovenska": [ + "CSK" + ], + "colo\u0301n costarricense": [ + "CRC" + ], + "kubai peso": [ + "CUP" + ], + "riel camboyano": [ + "KHR" + ], + "pa'anga tongano": [ + "TOP" + ], + "sri lankan rupee": [ + "LKR" + ], + "hk$": [ + "HKD" + ], + "dollar libe\u0301rien": [ + "LRD" + ], + "pa'anga di tonga": [ + "TOP" + ], + "norwegian krone": [ + "NOK" + ], + "scudo capoverdiano": [ + "CVE" + ], + "franco congolese": [ + "CDF" + ], + "birr": [ + "ETB" + ], + "schwedische krone": [ + "SEK" + ], + "boliviano bolivien": [ + "BOB" + ], + "bdt": [ + "BTN" + ], + "do\u0301lar guyanes": [ + "GYD" + ], + "lilangeni dello swaziland": [ + "SZL" + ], + "libanesisches pfund": [ + "LBP" + ], + "schottische pfund": [ + "GBP" + ], + "griekse drachme": [ + "GRD" + ], + "moldovan leu": [ + "MDL" + ], + "lek": [ + "ALL" + ], + "\u00a3": [ + "GBP" + ], + "do\u0301lar australiano": [ + "AUD" + ], + "lev": [ + "BGN" + ], + "lew": [ + "BGN" + ], + "uganda shilling": [ + "UGX" + ], + "hkd": [ + "HKD" + ], + "bd$": [ + "BMD" + ], + "re\u0301al bre\u0301silien": [ + "BRL" + ], + "tunesischer dinar": [ + "TND" + ], + "austral (monnaie)": [ + "ARA" + ], + "tongaanse pa'anga": [ + "TOP" + ], + "couronne sue\u0301doise": [ + "SEK" + ], + "franc de djibouti": [ + "DJF" + ], + "madagaszka\u0301ri ariary": [ + "MGA" + ], + "rupia mauricia": [ + "MUR" + ], + "solomon dollar": [ + "SBD" + ], + "kro\u0301nur": [ + "ISK" + ], + "khoums": [ + "MRO" + ], + "su\u0308dsudan pound": [ + "SSP" + ], + "sgd": [ + "SGD" + ], + "russischer rubel": [ + "RUB" + ], + "usd": [ + "USD" + ], + "livre des i\u0302les falkland": [ + "FKP" + ], + "comorian franc": [ + "KMF" + ], + "chf": [ + "CHF" + ], + "ush": [ + "UGX" + ], + "costa rica colon": [ + "CRC" + ], + "rial yemenita": [ + "YER" + ], + "marco bosniaco": [ + "BAM" + ], + "rial yemenite": [ + "YER" + ], + "brit font": [ + "GBP" + ], + "tercera dracma griega": [ + "GRD" + ], + "tala samoano": [ + "WST" + ], + "manat azeri": [ + "AZN" + ], + "santi\u0304ms": [ + "LVL" + ], + "ostmark": [ + "DDM" + ], + "f": [ + "ANG" + ], + "nuova lira turca": [ + "TRY" + ], + "zuid soedanees pond": [ + "SSP" + ], + "turkish lira": [ + "TRY" + ], + "rupia indonesiana": [ + "IDR" + ], + "da\u0308nische krone": [ + "DKK" + ], + "diritti speciali di prelievo": [ + "XDR" + ], + "do\u0301lar de nueva zelanda": [ + "NZD" + ], + "aluchip": [ + "DDM" + ], + "peso uruguayo": [ + "UYU" + ], + "xcd": [ + "XCD" + ], + "nuevo do\u0301lar de taiwan": [ + "TWD" + ], + "k.s.": [ + "KGS" + ], + "dinars alge\u0301rien": [ + "DZD" + ], + "russische roebel": [ + "RUB" + ], + "afn": [ + "AFN" + ], + "\u20a6": [ + "NGN" + ], + "corona danese": [ + "DKK" + ], + "corona danesa": [ + "DKK" + ], + "moneda canadiense": [ + "CAD" + ], + "ruandai frank": [ + "RWF" + ], + "libra de santa helena": [ + "SHP" + ], + "manat azeri\u0301": [ + "AZN" + ], + "do\u0301lar de hong kong": [ + "HKD" + ], + "armenian dram": [ + "AMD" + ], + "tetradrachme": [ + "GRD" + ], + "chileense peso": [ + "CLP" + ], + "franchi svizzeri": [ + "CHF" + ], + "boliviaanse boliviano": [ + "BOB" + ], + "do\u0301lar de bermudas": [ + "BMD" + ], + "colon costaricain": [ + "CRC" + ], + "dollar bahame\u0301en": [ + "BSD" + ], + "dollaro delle cayman": [ + "KYD" + ], + "do\u0301lar neozelande\u0301s": [ + "NZD" + ], + "riyal saudi\u0301": [ + "SAR" + ], + "georgian lari": [ + "GEL" + ], + "kiwi dollar": [ + "NZD" + ], + "shekkel": [ + "ILS" + ], + "si$": [ + "SBD" + ], + "dobra santome\u0301en": [ + "STD" + ], + "dolar neoze\u0301landes": [ + "NZD" + ], + "fiorino di aruba": [ + "AWG" + ], + "dobra": [ + "STD" + ], + "british pound": [ + "GBP" + ], + "to\u0308mling": [ + "THB" + ], + "afg": [ + "AFN" + ], + "thai ba\u0301t": [ + "THB" + ], + "fu\u0308lo\u0308p szigeteki peso": [ + "PHP" + ], + "noorse kroon": [ + "NOK" + ], + "dollar de trinite\u0301 et tobago": [ + "TTD" + ], + "tsh": [ + "TZS" + ], + "lm": [ + "MTL" + ], + "saudi arabische riyal": [ + "SAR" + ], + "ausztra\u0301l dolla\u0301r": [ + "AUD" + ], + "oekraiense hryvnja": [ + "UAH" + ], + "deense kroon": [ + "DKK" + ], + "eur": [ + "EUR" + ], + "uruguayi peso": [ + "UYU" + ], + "liberian dollar": [ + "LRD" + ], + "livre sud soudanaise": [ + "SSP" + ], + "do\u0301lar de fiji": [ + "FJD" + ], + "dollar de la carai\u0308be orientale": [ + "XCD" + ], + "franc poincare\u0301": [ + "XFO" + ], + "gepik": [ + "AZN" + ], + "fl\u00a3": [ + "FKP" + ], + "mexican peso": [ + "MXN" + ], + "diram": [ + "TJS" + ], + "denar mace\u0301donien": [ + "MKD" + ], + "hongkongi dolla\u0301r": [ + "HKD" + ], + "belizaanse dollar": [ + "BZD" + ], + "azeri manat": [ + "AZN" + ], + "dong vietnamita": [ + "VND" + ], + "rublo russo": [ + "RUB" + ], + "dolar beliceno": [ + "BZD" + ], + "su\u0308dsudanesisches pfund": [ + "SSP" + ], + "dolar de las islas caima\u0301n": [ + "KYD" + ], + "ec$": [ + "XCD" + ], + "dirham degli emirati arabi uniti": [ + "AED" + ], + "surinaamse dollar": [ + "SRD" + ], + "franco cfa de africa occidental": [ + "XOF" + ], + "french franc": [ + "FRF" + ], + "\u0192": [ + "ANG" + ], + "roma\u0301n lej": [ + "RON" + ], + "pa'anga": [ + "TOP" + ], + "dollaro dei caraibi orientali": [ + "XCD" + ], + "tyiyn": [ + "KGS" + ], + "cuban convertible peso": [ + "CUC" + ], + "dirham des e\u0301mirats arabes unis": [ + "AED" + ], + "japa\u0301n jen": [ + "JPY" + ], + "kroatische kuna": [ + "HRK" + ], + "sowjetischer rubel": [ + "SUR" + ], + "won sudcoreano": [ + "KRW" + ], + "chelin somali\u0301": [ + "SOS" + ], + "santims": [ + "LVL" + ], + "franc": [ + "CHF", + "FRF" + ], + "halalas": [ + "SAR" + ], + "sva\u0301jci frank": [ + "CHF" + ], + "shekel": [ + "ILS" + ], + "dinar kowei\u0308tien": [ + "KWD" + ], + "l\u00a3": [ + "LBP" + ], + "moroccan dirham": [ + "MAD" + ], + "goldfranc": [ + "XFO" + ], + "jod": [ + "JOD" + ], + "oost carai\u0308bische dollar": [ + "XCD" + ], + "ouguiya mauritana": [ + "MRO" + ], + "cambodjaanse riel": [ + "KHR" + ], + "taka bangladesi\u0301": [ + "BDT" + ], + "ltl": [ + "LTL" + ], + "lettischer lat": [ + "LVL" + ], + "santi\u0304mu": [ + "LVL" + ], + "marco de la repu\u0301blica democra\u0301tica alemana": [ + "DDM" + ], + "franco di gibuti": [ + "DJF" + ], + "santi\u0304mi": [ + "LVL" + ], + "couronne norve\u0301gienne": [ + "NOK" + ], + "libanoni font": [ + "LBP" + ], + "belize i dolla\u0301r": [ + "BZD" + ], + "da\u0301n korona": [ + "DKK" + ], + "serbian dinar": [ + "RSD" + ], + "rial omani": [ + "OMR" + ], + "mark convertible bosniaque": [ + "BAM" + ], + "dollar du be\u0301lize": [ + "BZD" + ], + "pesos argentinos": [ + "ARS" + ], + "lesothaanse loti": [ + "LSL" + ], + "tu\u0308rk liras\u0131": [ + "TRY" + ], + "kwacha zambien": [ + "ZMW" + ], + "dollar trinidadien": [ + "TTD" + ], + "moldavische leu": [ + "MDL" + ], + "tughrik": [ + "MNT" + ], + "leu roumain": [ + "RON" + ], + "szva\u0301zifo\u0308ldi lilangeni": [ + "SZL" + ], + "morocota": [ + "VEF" + ], + "haitianische gourde": [ + "HTG" + ], + "eritreischer nakfa": [ + "ERN" + ], + "mongolischer to\u0308gro\u0308g": [ + "MNT" + ], + "escudo di capo verde": [ + "CVE" + ], + "zwitserse frank": [ + "CHF" + ], + "afga\u0301n afga\u0301ni": [ + "AFN" + ], + "neet": [ + "GBP" + ], + "zwitserse franc": [ + "CHF" + ], + "roupie mauricienne": [ + "MUR" + ], + "do\u0301lar trinitense": [ + "TTD" + ], + "marco de la republica democratica alemana": [ + "DDM" + ], + "tongai pa\u2019anga": [ + "TOP" + ], + "israeli new sheqel": [ + "ILS" + ], + "bermudai dolla\u0301r": [ + "BMD" + ], + "\u20ba": [ + "TRY" + ], + "oost caribische dollar": [ + "XCD" + ], + "ugandese shilling": [ + "UGX" + ], + "derechos especiales de giro": [ + "XDR" + ], + "rupaya": [ + "INR" + ], + "suriname gulden": [ + "SRD" + ], + "tajvani u\u0301j dolla\u0301r": [ + "TWD" + ], + "costa rica i colo\u0301n": [ + "CRC" + ], + "pakistanische rupie": [ + "PKR" + ], + "irak dinar": [ + "IQD" + ], + "alge\u0301riai dina\u0301r": [ + "DZD" + ], + "perui u\u0301j sol": [ + "PEN" + ], + "do\u0301lar caribe este": [ + "XCD" + ], + "kurus": [ + "TRY" + ], + "sfr": [ + "CHF" + ], + "huard canadien": [ + "CAD" + ], + "new zealand dollar": [ + "NZD" + ], + "so\u0308m": [ + "UZS" + ], + "awg": [ + "AWG" + ], + "dollar de guyana": [ + "GYD" + ], + "bosnya\u0301k konvertibilis ma\u0301rka": [ + "BAM" + ], + "suriname i dolla\u0301r": [ + "SRD" + ], + "ukrainische hrywnja": [ + "UAH" + ], + "ngultrum": [ + "BTN" + ], + "gde.": [ + "HTG" + ], + "mexican nuevo peso": [ + "MXN" + ], + "fjd": [ + "FJD" + ], + "dolar jamaiquino": [ + "JMD" + ], + "libyscher dinar": [ + "LYD" + ], + "nuevo shequel": [ + "ILS" + ], + "cheli\u0301n keniano": [ + "KES" + ], + "dollar surinamien": [ + "SRD" + ], + "rublo sovietico": [ + "SUR" + ], + "kaiman dollar": [ + "KYD" + ], + "dollar ne\u0301o ze\u0301landais": [ + "NZD" + ], + "bolga\u0301r leva": [ + "BGN" + ], + "cub$": [ + "CUP" + ], + "szl": [ + "SZL" + ], + "aruba gulden": [ + "AWG" + ], + "mexikanischer peso": [ + "MXN" + ], + "australische dollar": [ + "AUD" + ], + "roupie indonesienne": [ + "IDR" + ], + "albanese lek": [ + "ALL" + ], + "lettische wa\u0308hrung": [ + "LVL" + ], + "dollar ame\u0301ricain": [ + "USD" + ], + "zo\u0308ld foki szigeteki escudo": [ + "CVE" + ], + "saudi riyal": [ + "SAR" + ], + "libra": [ + "GBP" + ], + "isla\u0308ndische krone": [ + "ISK" + ], + "saudi rial": [ + "SAR" + ], + "dollaro della bermuda": [ + "BMD" + ], + "macedo\u0301n de\u0301na\u0301r": [ + "MKD" + ], + "kwanza": [ + "AOA" + ], + "dollar du guyana": [ + "GYD" + ], + "nuevo peso argentino": [ + "ARS" + ], + "dollaro del suriname": [ + "SRD" + ], + "ariary malgache": [ + "MGA" + ], + "saint helena pound": [ + "SHP" + ], + "kambodzsai riel": [ + "KHR" + ], + "surinam dollar": [ + "SRD" + ], + "ouguiya": [ + "MRO" + ], + "mala\u0301j ringgit": [ + "MYR" + ], + "united states dollar": [ + "USD" + ], + "icelandic kro\u0301na": [ + "ISK" + ], + "gbp": [ + "GBP" + ], + "falkland szigeteki font": [ + "FKP" + ], + "sa\u0303o tome\u0301ischer dobra": [ + "STD" + ], + "kwanza angolano": [ + "AOA" + ], + "scellino": [ + "KES" + ], + "dollars canadiens": [ + "CAD" + ], + "guarani\u0301": [ + "PYG" + ], + "kwanza angolana": [ + "AOA" + ], + "litas lituanien": [ + "LTL" + ], + "kajma\u0301n szigeteki dolla\u0301r": [ + "KYD" + ], + "som de kirguista\u0301n": [ + "KGS" + ], + "btn": [ + "BTN" + ], + "chelin somali": [ + "SOS" + ], + "dracma griego moderno": [ + "GRD" + ], + "hai\u0308tiaanse gourde": [ + "HTG" + ], + "kc\u030c": [ + "CZK" + ], + "peso de chile": [ + "CLP" + ], + "mazedonischer denar": [ + "MKD" + ], + "sierra leoonse leone": [ + "SLL" + ], + "franco france\u0301s": [ + "FRF" + ], + "marco della germania est": [ + "DDM" + ], + "cordoba nicaraguense": [ + "NIO" + ], + "do\u0301lar jamaiquino": [ + "JMD" + ], + "cordoba nicaraguayen": [ + "NIO" + ], + "rupia de pakista\u0301n": [ + "PKR" + ], + "pfund sterling": [ + "GBP" + ], + "dollar jamai\u0308quain": [ + "JMD" + ], + "koruna c\u030ceskoslovenska\u0301": [ + "CSK" + ], + "vatu di vanuatu": [ + "VUV" + ], + "nicaraguai co\u0301rdoba": [ + "NIO" + ], + "salu\u0308ng": [ + "THB" + ], + "drachmon": [ + "GRD" + ], + "somalia schilling": [ + "SOS" + ], + "dinar iraqui": [ + "IQD" + ], + "escudo": [ + "CVE" + ], + "hrywni": [ + "UAH" + ], + "libra de santa elena": [ + "SHP" + ], + "couronnes tche\u0300ques": [ + "CZK" + ], + "dolar fiyiano": [ + "FJD" + ], + "\u20a9": [ + "KRW" + ], + "rial iraniano": [ + "IRR" + ], + "bbd": [ + "BBD" + ], + "e\u0301szak i\u0301r font": [ + "GBP" + ], + "$ ca": [ + "CAD" + ], + "quid": [ + "GBP" + ], + "ta\u0301dzsik szomoni": [ + "TJS" + ], + "dram armenio": [ + "AMD" + ], + "rupia singalese": [ + "LKR" + ], + "botswaanse pula": [ + "BWP" + ], + "co\u0301rdoba nicaraguayen": [ + "NIO" + ], + "c$": [ + "NIO", + "CAD" + ], + "oost caraibische dollar": [ + "XCD" + ], + "guyanese dollar": [ + "GYD" + ], + "indonesische roepia": [ + "IDR" + ], + "corone ceche": [ + "CZK" + ], + "franco cfa de a\u0301frica occidental": [ + "XOF" + ], + "currency of mexico": [ + "MXN" + ], + "kwanza reajustado": [ + "AOA" + ], + "botswanai pula": [ + "BWP" + ], + "reais": [ + "BRL" + ], + "cve": [ + "CVE" + ], + "flori\u0301n suriname\u0301s": [ + "SRG" + ], + "franc djibouti": [ + "DJF" + ], + "do\u0301lar beliceno": [ + "BZD" + ], + "forint hu\u0301ngaro": [ + "HUF" + ], + "iranischer rial": [ + "IRR" + ], + "tenge": [ + "KZT" + ], + "czechoslovak koruna": [ + "CSK" + ], + "grivna ucraniana": [ + "UAH" + ], + "dinar alge\u0301rien": [ + "DZD" + ], + "rupia esrilanquesa": [ + "LKR" + ], + "kyrgyz som": [ + "KGS" + ], + "turkmeense manat": [ + "TMT" + ], + "hryvnia ukrainienne": [ + "UAH" + ], + "dollaro di singapore": [ + "SGD" + ], + "dolar de belize": [ + "BZD" + ], + "boli\u0301vares": [ + "VEF" + ], + "sterlina di gibilterra": [ + "GIP" + ], + "shilling ougandais": [ + "UGX" + ], + "rupia pakistani\u0301": [ + "PKR" + ], + "united arab emirates dirham": [ + "AED" + ], + "php": [ + "PHP" + ], + "\u03b4\u03c1": [ + "GRD" + ], + "lari georgiano": [ + "GEL" + ], + "kip laotien": [ + "LAK" + ], + "uquiya": [ + "MRO" + ], + "francs or": [ + "XFO" + ], + "dinar": [ + "TND", + "DZD" + ], + "shilling tanzanien": [ + "TZS" + ], + "kongo\u0301i frank": [ + "CDF" + ], + "franco de yibuti": [ + "DJF" + ], + "dirham marroqui\u0301": [ + "MAD" + ], + "florin hungaro": [ + "HUF" + ], + "tyjyn": [ + "KGS" + ], + "di\u0301rham de los emiratos a\u0301rabes unidos": [ + "AED" + ], + "florin arubais": [ + "AWG" + ], + "la couronne danoise": [ + "DKK" + ], + "gambian dalasi": [ + "GMD" + ], + "szuda\u0301ni font": [ + "SDG" + ], + "corona svedese": [ + "SEK" + ], + "colombiaanse peso": [ + "COP" + ], + "dirham marocchino": [ + "MAD" + ], + "won sud core\u0301en": [ + "KRW" + ], + "seychellois rupee": [ + "SCR" + ], + "gibraltarees pond": [ + "GIP" + ], + "franc fort": [ + "FRF" + ], + "schweizerfranken": [ + "CHF" + ], + "livre soudanaise": [ + "SDG" + ], + "manat aze\u0301ri": [ + "AZN" + ], + "nuevo she\u0301kel": [ + "ILS" + ], + "paraguayaanse guarani": [ + "PYG" + ], + "trinidad and tobago dollar": [ + "TTD" + ], + "tiyin": [ + "UZS" + ], + "dollar de belize": [ + "BZD" + ], + "nuovo siclo": [ + "ILS" + ], + "pyas": [ + "MMK" + ], + "liberiaanse dollar": [ + "LRD" + ], + "quetzal guatemalteco": [ + "GTQ" + ], + "naira nige\u0301rian": [ + "NGN" + ], + "balboa panameno": [ + "PAB" + ], + "indian rupee": [ + "INR" + ], + "bahreini dina\u0301r": [ + "BHD" + ], + "zuid afrikaanse rand": [ + "ZAR" + ], + "roepia": [ + "IDR" + ], + "dollar bermudien": [ + "BMD" + ], + "loti del lesotho": [ + "LSL" + ], + "hondurese lempira": [ + "HNL" + ], + "tsjecho slowaakse kroon": [ + "CSK" + ], + "do\u0301lar de barbados": [ + "BBD" + ], + "su\u0308dafrikanischer rand": [ + "ZAR" + ], + "szau\u0301di ria\u0301l": [ + "SAR" + ], + "szerb dina\u0301r": [ + "RSD" + ], + "roupie du ne\u0301pal": [ + "NPR" + ], + "dinaro": [ + "BHD" + ], + "balboa": [ + "PAB" + ], + "rublo sovie\u0301tico": [ + "SUR" + ], + "dollar de la caraibe orientale": [ + "XCD" + ], + "dolar bruneano": [ + "BND" + ], + "dollaro canadese": [ + "CAD" + ], + "arubai florin": [ + "AWG" + ], + "somali shilling": [ + "SOS" + ], + "peso oro dominicano": [ + "DOP" + ], + "bangladesi taka": [ + "BDT" + ], + "lettischer lats": [ + "LVL" + ], + "marco della repubblica democratica tedesca": [ + "DDM" + ], + "ijslandse kroon": [ + "ISK" + ], + "burundi franc": [ + "BIF" + ], + "rand sud africain": [ + "ZAR" + ], + "corone norvegesi": [ + "NOK" + ], + "do\u0301lar caimano": [ + "KYD" + ], + "burundi frank": [ + "BIF" + ], + "new israeli sheqel": [ + "ILS" + ], + "metical mozambicain": [ + "MZN" + ], + "dolar de surinam": [ + "SRD" + ], + "falkland islands pound": [ + "FKP" + ], + "maurita\u0301niai ouguiya": [ + "MRO" + ], + "u\u0301j ze\u0301landi dolla\u0301r": [ + "NZD" + ], + "lats leton": [ + "LVL" + ], + "somoni tagico": [ + "TJS" + ], + "mozambikaanse metical": [ + "MZN" + ], + "dollaro delle bermuda": [ + "BMD" + ], + "dolar de hong kong": [ + "HKD" + ], + "dollaro delle bermude": [ + "BMD" + ], + "balboa panamense": [ + "PAB" + ], + "roupie srilankaise": [ + "LKR" + ], + "fya\u0308n": [ + "THB" + ], + "dolar guyanes": [ + "GYD" + ], + "franc suisse": [ + "CHF" + ], + "rial irani\u0301": [ + "IRR" + ], + "myanmarese kyat": [ + "MMK" + ], + "costa ricaanse colo\u0301n": [ + "CRC" + ], + "corona checa": [ + "CZK" + ], + "thai baht": [ + "THB" + ], + "djiboutiaanse frank": [ + "DJF" + ], + "schkalim": [ + "ILS" + ], + "\u17db": [ + "KHR" + ], + "franc djiboutien": [ + "DJF" + ], + "dinar koweitien": [ + "KWD" + ], + "loonie": [ + "CAD" + ], + "denari": [ + "MKD" + ], + "lvl": [ + "LVL" + ], + "hryvnja": [ + "UAH" + ], + "lempira hondurien": [ + "HNL" + ], + "franc guineen": [ + "GNF" + ], + "seychelles rupee": [ + "SCR" + ], + "leu rumeno": [ + "RON" + ], + "dirham emirati": [ + "AED" + ], + "co\u0301rdoba nicarague\u0301en": [ + "NIO" + ], + "neuseeland dollar": [ + "NZD" + ], + "\u20aa": [ + "ILS" + ], + "bahamai dolla\u0301r": [ + "BSD" + ], + "szi\u0301r font": [ + "SYP" + ], + "nieuwe israelische sjekel": [ + "ILS" + ], + "franc francais": [ + "FRF" + ], + "jamaicaanse dollar": [ + "JMD" + ], + "burmese kyat": [ + "MMK" + ], + "do\u0301lar de belize": [ + "BZD" + ], + "tunis dinar": [ + "TND" + ], + "hrywnja": [ + "UAH" + ], + "do\u0301lar de belice": [ + "BZD" + ], + "koeweitse dinar": [ + "KWD" + ], + "rial yemeni": [ + "YER" + ], + "quetzal": [ + "GTQ" + ], + "livres sterlings": [ + "GBP" + ], + "hnl": [ + "HNL" + ], + "franco cfa de a\u0301frica central": [ + "XAF" + ], + "scellino keniota": [ + "KES" + ] + }, + "iso4217": { + "DZD": { + "fr": "Dinar alg\u00e9rien", + "en": "Algerian dinar", + "nl": "Algerijnse dinar", + "de": "Algerischer Dinar", + "it": "Dinaro algerino", + "hu": "alg\u00e9riai din\u00e1r", + "es": "Dinar argelino" + }, + "NAD": { + "fr": "Dollar namibien", + "en": "Namibian dollar", + "nl": "Namibische dollar", + "de": "Namibia-Dollar", + "it": "Dollaro namibiano", + "hu": "Nam\u00edbiai doll\u00e1r", + "es": "D\u00f3lar namibio" + }, + "GHS": { + "fr": "Cedi", + "en": "Ghana cedi", + "nl": "Ghanese cedi", + "de": "Cedi", + "it": "Cedi ghanese", + "hu": "Gh\u00e1nai cedi", + "es": "Cedi" + }, + "BZD": { + "fr": "Dollar b\u00e9lizien", + "en": "Belize dollar", + "nl": "Belizaanse dollar", + "de": "Belize-Dollar", + "it": "Dollaro del Belize", + "hu": "Belize-i doll\u00e1r", + "es": "D\u00f3lar belice\u00f1o" + }, + "BGN": { + "fr": "Lev bulgare", + "en": "Bulgarian lev", + "nl": "Bulgaarse lev", + "de": "Lew", + "it": "Lev bulgaro", + "hu": "bolg\u00e1r leva", + "es": "Lev" + }, + "PAB": { + "fr": "Balboa", + "en": "Panamanian balboa", + "nl": "Panamese balboa", + "de": "Panamaischer Balboa", + "it": "Balboa panamense", + "hu": "Panamai balboa", + "es": "Balboa" + }, + "BOB": { + "fr": "boliviano", + "en": "boliviano", + "nl": "Boliviaanse boliviano", + "de": "Boliviano", + "it": "boliviano", + "hu": "bol\u00edviai boliviano", + "es": "boliviano" + }, + "DKK": { + "fr": "Couronne danoise", + "en": "Danish krone", + "nl": "Deense kroon", + "de": "D\u00e4nische Krone", + "it": "Corona danese", + "hu": "d\u00e1n korona", + "es": "Corona danesa" + }, + "BWP": { + "fr": "Pula", + "en": "Botswana pula", + "nl": "Botswaanse pula", + "de": "Botswanischer Pula", + "it": "Pula del Botswana", + "hu": "Botswanai pula", + "es": "Pula" + }, + "LBP": { + "fr": "livre libanaise", + "en": "Lebanese pound", + "nl": "Libanees pond", + "de": "Libanesisches Pfund", + "it": "Lira libanese", + "hu": "libanoni font", + "es": "Libra libanesa" + }, + "TZS": { + "fr": "shilling tanzanien", + "en": "Tanzanian shilling", + "nl": "Tanzaniaanse shilling", + "de": "Tansania-Schilling", + "it": "Scellino tanzaniano", + "hu": "Tanz\u00e1niai shilling", + "es": "chel\u00edn" + }, + "VND": { + "fr": "Dong", + "en": "Vietnamese dong", + "nl": "Vietnamese dong", + "de": "Vietnamesischer \u0110\u1ed3ng", + "it": "\u0110\u1ed3ng vietnamita", + "hu": "vietnami \u0111\u1ed3ng", + "es": "\u0111\u1ed3ng vietnamita" + }, + "AOA": { + "fr": "Kwanza", + "en": "Angolan kwanza", + "nl": "Angolese kwanza", + "de": "Kwanza", + "it": "Kwanza angolano", + "hu": "angolai kwanza", + "es": "Kwanza angole\u00f1o" + }, + "KHR": { + "fr": "Riel", + "en": "riel", + "nl": "Cambodjaanse riel", + "de": "Kambodschanischer Riel", + "it": "Riel cambogiano", + "hu": "kambodzsai riel", + "es": "Riel camboyano" + }, + "MYR": { + "fr": "Ringgit", + "en": "Malaysian ringgit", + "nl": "Maleisische ringgit", + "de": "Ringgit", + "it": "Ringgit malese", + "hu": "mal\u00e1j ringgit", + "es": "Ringgit" + }, + "KYD": { + "fr": "Dollar des \u00eeles Ca\u00efmans", + "en": "Cayman Islands dollar", + "nl": "Kaaimaneilandse dollar", + "de": "Kaiman-Dollar", + "it": "Dollaro delle Cayman", + "hu": "Kajm\u00e1n-szigeteki doll\u00e1r", + "es": "D\u00f3lar de las Islas Caim\u00e1n" + }, + "LYD": { + "fr": "Dinar libyen", + "en": "Libyan dinar", + "nl": "Libische dinar", + "de": "Libyscher Dinar", + "it": "Dinaro libico", + "hu": "L\u00edbiai din\u00e1r", + "es": "Dinar libio" + }, + "UAH": { + "fr": "Hryvnia", + "en": "hryvnia", + "nl": "Oekra\u00efense hryvnja", + "de": "Hrywnja", + "it": "Grivnia ucraina", + "hu": "ukr\u00e1n hrivnya", + "es": "Grivna" + }, + "JOD": { + "fr": "Dinar jordanien", + "en": "Jordanian dinar", + "nl": "Jordaanse dinar", + "de": "Jordanischer Dinar", + "it": "Dinaro giordano", + "hu": "jord\u00e1n din\u00e1r", + "es": "Dinar jordano" + }, + "SUR": { + "fr": "Rouble sovi\u00e9tique", + "en": "Soviet ruble", + "de": "Sowjetischer Rubel", + "it": "Rublo sovietico", + "hu": "Szovjet rubel", + "es": "Rublo sovi\u00e9tico" + }, + "AWG": { + "fr": "Florin arubais", + "en": "Aruban florin", + "nl": "Arubaanse florin", + "de": "Aruba-Florin", + "it": "Fiorino arubano", + "hu": "Arubai florin", + "es": "Flor\u00edn arube\u00f1o" + }, + "SAR": { + "fr": "Riyal saoudien", + "en": "Saudi riyal", + "nl": "Saoedi-Arabische riyal", + "de": "Saudi-Rial", + "it": "Riyal saudita", + "hu": "sza\u00fadi ri\u00e1l", + "es": "Riyal saud\u00ed" + }, + "EUR": { + "fr": "euro", + "en": "euro", + "nl": "euro", + "de": "Euro", + "it": "euro", + "hu": "eur\u00f3", + "es": "euro" + }, + "HKD": { + "fr": "Dollar de Hong Kong", + "en": "Hong Kong dollar", + "nl": "Hongkongse dollar", + "de": "Hongkong-Dollar", + "it": "dollaro hongkonghese", + "hu": "hongkongi doll\u00e1r", + "es": "D\u00f3lar de Hong Kong" + }, + "SRG": { + "en": "Surinamese guilder", + "nl": "Surinaamse gulden", + "it": "Fiorino surinamese", + "es": "Flor\u00edn surinam\u00e9s" + }, + "CHF": { + "fr": "Franc suisse", + "en": "Swiss franc", + "nl": "Zwitserse frank", + "de": "Schweizer Franken", + "it": "franco svizzero", + "hu": "sv\u00e1jci frank", + "es": "franco suizo" + }, + "GIP": { + "fr": "Livre de Gibraltar", + "en": "Gibraltar pound", + "nl": "Gibraltarees pond", + "de": "Gibraltar-Pfund", + "it": "Sterlina di Gibilterra", + "hu": "Gibralt\u00e1ri font", + "es": "Libra gibraltare\u00f1a" + }, + "ALL": { + "fr": "Lek", + "en": "lek", + "nl": "Albanese lek", + "de": "Albanischer Lek", + "it": "Lek albanese", + "hu": "alb\u00e1n lek", + "es": "Lek alban\u00e9s" + }, + "MRO": { + "fr": "Ouguiya", + "en": "Mauritanian ouguiya", + "nl": "Mauritaanse ouguiya", + "de": "Ouguiya", + "it": "Ouguiya mauritana", + "hu": "Maurit\u00e1niai ouguiya", + "es": "Uquiya" + }, + "HRK": { + "fr": "Kuna croate", + "en": "Croatian kuna", + "nl": "Kroatische kuna", + "de": "Kroatische Kuna", + "it": "Kuna croata", + "hu": "horv\u00e1t kuna", + "es": "Kuna croata" + }, + "DJF": { + "fr": "franc Djibouti", + "en": "Djiboutian franc", + "nl": "Djiboutiaanse frank", + "de": "Dschibuti-Franc", + "it": "Franco gibutiano", + "hu": "Dzsibuti frank", + "es": "franco" + }, + "THB": { + "fr": "Baht", + "en": "Thai baht", + "nl": "Thaise baht", + "de": "Baht", + "it": "Baht thailandese", + "hu": "thai b\u00e1t", + "es": "Baht tailand\u00e9s" + }, + "XAF": { + "fr": "Franc CFA", + "en": "Central African CFA franc", + "de": "CFA-Franc BEAC", + "es": "Franco CFA de \u00c1frica Central" + }, + "BND": { + "fr": "Dollar de Brunei", + "en": "Brunei dollar", + "nl": "Bruneise dollar", + "de": "Brunei-Dollar", + "it": "Dollaro del Brunei", + "hu": "brunei doll\u00e1r", + "es": "D\u00f3lar de Brun\u00e9i" + }, + "VUV": { + "fr": "Vatu", + "en": "Vanuatu vatu", + "nl": "Vanuatuaanse vatu", + "de": "Vatu", + "it": "Vatu di Vanuatu", + "hu": "Vanuatui vatu", + "es": "Vatu" + }, + "UYU": { + "fr": "Peso uruguayen", + "en": "Uruguayan peso", + "nl": "Uruguayaanse peso", + "de": "Uruguayischer Peso", + "it": "Peso uruguaiano", + "hu": "Uruguayi peso", + "es": "peso" + }, + "NIO": { + "fr": "C\u00f3rdoba", + "en": "Nicaraguan c\u00f3rdoba", + "nl": "Nicaraguaanse c\u00f3rdoba", + "de": "C\u00f3rdoba Oro", + "it": "C\u00f3rdoba nicaraguense", + "hu": "Nicaraguai c\u00f3rdoba", + "es": "C\u00f3rdoba" + }, + "LAK": { + "fr": "Kip laotien", + "en": "Lao kip", + "nl": "Laotiaanse kip", + "de": "Kip", + "it": "Kip laotiano", + "hu": "laoszi kip", + "es": "Kip laosiano" + }, + "MZE": { + "de": "Mosambikanischer Escudo", + "en": "Mozambican escudo", + "es": "Escudo mozambique\u00f1o" + }, + "SYP": { + "fr": "Livre syrienne", + "en": "Syrian pound", + "nl": "Syrisch pond", + "de": "Syrische Lira", + "it": "Lira siriana", + "hu": "Sz\u00edr font", + "es": "Libra siria" + }, + "MAD": { + "fr": "Dirham marocain", + "en": "Moroccan dirham", + "nl": "Marokkaanse dirham", + "de": "Marokkanischer Dirham", + "it": "Dirham marocchino", + "hu": "Marokk\u00f3i dirham", + "es": "D\u00edrham marroqu\u00ed" + }, + "MZN": { + "fr": "Metical", + "en": "Mozambican metical", + "nl": "Mozambikaanse metical", + "de": "Metical", + "it": "Metical mozambicano", + "hu": "Mozambiki metical", + "es": "Metical mozambique\u00f1o" + }, + "SCR": { + "fr": "roupie seychelloise", + "en": "Seychellois rupee", + "nl": "Seychelse roepie", + "de": "Seychellen-Rupie", + "it": "Rupia delle Seychelles", + "hu": "Seychelle-i r\u00fapia", + "es": "rupia" + }, + "ZAR": { + "fr": "rand", + "en": "South African rand", + "nl": "Zuid-Afrikaanse rand", + "de": "S\u00fcdafrikanischer Rand", + "it": "Rand sudafricano", + "hu": "D\u00e9l-afrikai rand", + "es": "Rand sudafricano" + }, + "NPR": { + "fr": "Roupie n\u00e9palaise", + "en": "Nepalese rupee", + "nl": "Nepalese roepie", + "de": "Nepalesische Rupie", + "it": "Rupia nepalese", + "hu": "nep\u00e1li r\u00fapia", + "es": "Rupia nepal\u00ed" + }, + "XSU": { + "fr": "Sucre", + "en": "SUCRE", + "nl": "SUCRE", + "es": "SUCRE", + "de": "SUCRE" + }, + "NGN": { + "fr": "Naira", + "en": "Nigerian naira", + "nl": "Nigeriaanse naira", + "de": "Naira", + "it": "Naira nigeriana", + "hu": "Nig\u00e9riai naira", + "es": "Naira" + }, + "CRC": { + "fr": "col\u00f3n", + "en": "Costa Rican col\u00f3n", + "nl": "Costa Ricaanse colon", + "de": "Costa-Rica-Col\u00f3n", + "it": "Col\u00f3n costaricano", + "hu": "Costa Rica-i col\u00f3n", + "es": "Col\u00f3n" + }, + "AED": { + "fr": "Dirham des \u00c9mirats arabes unis", + "en": "United Arab Emirates dirham", + "nl": "VAE-Dirham", + "de": "VAE-Dirham", + "it": "Dirham degli Emirati Arabi Uniti", + "hu": "emir\u00e1tusi dirham", + "es": "D\u00edrham de los Emiratos \u00c1rabes Unidos" + }, + "GBP": { + "fr": "livre sterling", + "en": "pound sterling", + "nl": "pond sterling", + "de": "Pfund Sterling", + "it": "sterlina britannica", + "hu": "font sterling", + "es": "libra esterlina" + }, + "LKR": { + "fr": "roupie srilankaise", + "en": "Sri Lankan rupee", + "nl": "Sri Lankaanse roepie", + "de": "Sri-Lanka-Rupie", + "it": "Rupia singalese", + "hu": "Sr\u00ed Lanka-i r\u00fapia", + "es": "rupia" + }, + "PKR": { + "fr": "Roupie pakistanaise", + "en": "Pakistani rupee", + "nl": "Pakistaanse roepie", + "de": "Pakistanische Rupie", + "it": "Rupia pakistana", + "hu": "pakiszt\u00e1ni r\u00fapia", + "es": "Rupia pakistan\u00ed" + }, + "HUF": { + "fr": "Forint", + "en": "Hungarian forint", + "nl": "Hongaarse forint", + "de": "Forint", + "it": "Fiorino ungherese", + "hu": "magyar forint", + "es": "Forinto h\u00fangaro" + }, + "SZL": { + "fr": "Lilangeni", + "en": "Swazi lilangeni", + "nl": "Swazische lilangeni", + "de": "Lilangeni", + "it": "Lilangeni dello Swaziland", + "hu": "Szv\u00e1zif\u00f6ldi lilangeni", + "es": "lilangeni" + }, + "LSL": { + "fr": "Loti", + "en": "Lesotho loti", + "nl": "Lesothaanse loti", + "de": "Lesothischer Loti", + "it": "Loti lesothiano", + "hu": "Lesoth\u00f3i loti", + "es": "Loti" + }, + "MNT": { + "fr": "Tugrik", + "en": "Mongolian t\u00f6gr\u00f6g", + "nl": "Mongoolse tugrik", + "de": "T\u00f6gr\u00f6g", + "it": "Tugrik mongolo", + "hu": "mongol tugrik", + "es": "Tugrik mongol" + }, + "AMD": { + "fr": "Dram", + "en": "Armenian dram", + "nl": "Armeense dram", + "de": "Armenischer Dram", + "it": "Dram armeno", + "hu": "\u00f6rm\u00e9ny dram", + "es": "Dram armenio" + }, + "UGX": { + "fr": "shilling ougandais", + "en": "Ugandan shilling", + "nl": "Oegandese shilling", + "de": "Uganda-Schilling", + "it": "Scellino ugandese", + "hu": "Ugandai shilling", + "es": "chel\u00edn" + }, + "QAR": { + "fr": "Riyal qatarien", + "en": "Qatari riyal", + "nl": "Qatarese rial", + "de": "Katar-Riyal", + "it": "Riyal del Qatar", + "hu": "katari ri\u00e1l", + "es": "Riyal catar\u00ed" + }, + "XDR": { + "fr": "Droits de tirage sp\u00e9ciaux", + "en": "Special drawing rights", + "nl": "Speciale trekkingsrechten", + "de": "Sonderziehungsrecht", + "it": "Diritti speciali di prelievo", + "hu": "SDR", + "es": "Derechos Especiales de Giro" + }, + "ITL": { + "fr": "Lire italienne", + "en": "Italian lira", + "nl": "Italiaanse lire", + "de": "Italienische Lira", + "it": "lira italiana", + "hu": "Olasz l\u00edra", + "es": "Lira italiana" + }, + "JMD": { + "fr": "Dollar jama\u00efcain", + "en": "Jamaican dollar", + "nl": "Jamaicaanse dollar", + "de": "Jamaika-Dollar", + "it": "Dollaro giamaicano", + "hu": "Jamaicai doll\u00e1r", + "es": "D\u00f3lar jamaiquino" + }, + "GEL": { + "fr": "lari", + "en": "Georgian lari", + "nl": "Georgische lari", + "de": "Georgischer Lari", + "it": "Lari georgiano", + "hu": "gr\u00faz lari", + "es": "lari" + }, + "SHP": { + "fr": "Livre de Sainte-H\u00e9l\u00e8ne", + "en": "Saint Helena pound", + "nl": "Sint-Heleens pond", + "de": "St.-Helena-Pfund", + "it": "Sterlina di Sant'Elena", + "hu": "Szent Ilona-i font", + "es": "Libra de Santa Elena" + }, + "AFN": { + "fr": "Afghani", + "en": "Afghan afghani", + "nl": "Afghaanse afghani", + "de": "Afghani", + "it": "Afghani afgano", + "hu": "afg\u00e1n afg\u00e1ni", + "es": "Afgani afgano" + }, + "MMK": { + "fr": "Kyat", + "en": "kyat", + "nl": "Myanmarese kyat", + "de": "Kyat", + "it": "Kyat birmano", + "hu": "mianmari kjap", + "es": "Kyat birmano" + }, + "CSK": { + "fr": "couronne tch\u00e9coslovaque", + "en": "Czechoslovak koruna", + "nl": "Tsjecho-Slowaakse kroon", + "de": "Tschechoslowakische Krone", + "it": "Corona cecoslovacca", + "hu": "csehszlov\u00e1k korona", + "es": "Corona checoslovaca" + }, + "KPW": { + "fr": "Won nord-cor\u00e9en", + "en": "North Korean won", + "nl": "Noord-Koreaanse won", + "de": "Nordkoreanischer Won", + "it": "Won nordcoreano", + "hu": "\u00e9szak-koreai von", + "es": "W\u014fn norcoreano" + }, + "TRY": { + "fr": "Livre turque", + "en": "Turkish lira", + "nl": "Nieuwe Turkse lira", + "de": "T\u00fcrkische Lira", + "it": "Nuova lira turca", + "hu": "t\u00f6r\u00f6k \u00faj l\u00edra", + "es": "Lira turca" + }, + "BDT": { + "fr": "Taka", + "en": "taka", + "nl": "Bengalese taka", + "de": "Taka", + "it": "Taka bengalese", + "hu": "bangladesi taka", + "es": "Taka banglades\u00ed" + }, + "GRD": { + "fr": "Drachme moderne grecque", + "en": "Greek drachma", + "nl": "Drachme", + "de": "Griechische Drachme", + "it": "Dracma greca", + "es": "Dracma griega moderna" + }, + "YER": { + "fr": "rial y\u00e9m\u00e9nite", + "en": "Yemeni rial", + "nl": "Jemenitische rial", + "de": "Jemen-Rial", + "it": "riyal yemenita", + "hu": "Jemeni ri\u00e1l", + "es": "rial yemen\u00ed" + }, + "DDM": { + "fr": "Mark est-allemand", + "en": "East German mark", + "nl": "Oost-Duitse mark", + "de": "Mark", + "it": "Marco della Repubblica Democratica Tedesca", + "es": "Marco de la Rep\u00fablica Democr\u00e1tica Alemana" + }, + "HTG": { + "fr": "Gourde", + "en": "Haitian gourde", + "nl": "Ha\u00eftiaanse gourde", + "de": "Gourde", + "it": "Gourde haitiano", + "hu": "haiti gourde", + "es": "Gourde" + }, + "XOF": { + "fr": "Franc CFA", + "en": "West African CFA franc", + "de": "CFA-Franc BCEAO", + "es": "Franco CFA de \u00c1frica Occidental" + }, + "MGA": { + "fr": "ariary", + "en": "Malagasy ariary", + "nl": "Malagassische ariary", + "de": "Ariary", + "it": "Ariary malgascio", + "hu": "Madagaszk\u00e1ri ariary", + "es": "ariary" + }, + "PHP": { + "fr": "peso philippin", + "en": "Philippine peso", + "nl": "Filipijnse peso", + "de": "Philippinischer Peso", + "it": "peso filippino", + "hu": "F\u00fcl\u00f6p-szigeteki peso", + "es": "peso" + }, + "LRD": { + "fr": "Dollar lib\u00e9rien", + "en": "Liberian dollar", + "nl": "Liberiaanse dollar", + "de": "Liberianischer Dollar", + "it": "Dollaro liberiano", + "hu": "Lib\u00e9riai doll\u00e1r", + "es": "D\u00f3lar liberiano" + }, + "RWF": { + "fr": "franc rwandais", + "en": "Rwandan franc", + "nl": "Rwandese frank", + "de": "Ruanda-Franc", + "it": "Franco ruandese", + "hu": "Ruandai frank", + "es": "franco" + }, + "NOK": { + "fr": "Couronne norv\u00e9gienne", + "en": "Norwegian krone", + "nl": "Noorse kroon", + "de": "Norwegische Krone", + "it": "Corona norvegese", + "hu": "norv\u00e9g korona", + "es": "Corona noruega" + }, + "MOP": { + "fr": "Pataca", + "en": "Macanese pataca", + "nl": "Macause pataca", + "de": "Macao-Pataca", + "it": "Pataca di Macao", + "hu": "Maka\u00f3i pataca", + "es": "Pataca" + }, + "SSP": { + "fr": "Livre sud-soudanaise", + "en": "South Sudanese pound", + "nl": "Zuid-Soedanees pond", + "de": "S\u00fcdsudanesisches Pfund", + "it": "Sterlina sudsudanese", + "hu": "D\u00e9l-szud\u00e1ni font", + "es": "Libra sursudanesa" + }, + "INR": { + "fr": "Roupie indienne", + "en": "Indian rupee", + "nl": "Indiase roepie", + "de": "Indische Rupie", + "it": "rupia indiana", + "hu": "Indiai r\u00fapia", + "es": "Rupia india" + }, + "MXN": { + "fr": "peso mexicain", + "en": "Mexican peso", + "nl": "Mexicaanse peso", + "de": "Mexikanischer Peso", + "it": "Peso messicano", + "hu": "mexik\u00f3i peso", + "es": "peso" + }, + "CZK": { + "fr": "Couronne tch\u00e8que", + "en": "Czech koruna", + "nl": "Tsjechische kroon", + "de": "Tschechische Krone", + "it": "Corona ceca", + "hu": "cseh korona", + "es": "Corona checa" + }, + "TJS": { + "fr": "Somoni", + "en": "Tajikistani somoni", + "nl": "Tadzjiekse somoni", + "de": "Somoni", + "it": "Somoni tagico", + "hu": "t\u00e1dzsik szomoni", + "es": "Somoni tayiko" + }, + "TJR": { + "en": "Tajikistani ruble", + "es": "Rublo tayiko" + }, + "BTN": { + "fr": "ngultrum", + "en": "Bhutanese ngultrum", + "nl": "Bhutaanse ngultrum", + "de": "Ngultrum", + "it": "Ngultrum del Bhutan", + "hu": "bhut\u00e1ni ngultrum", + "es": "Ngultrum butan\u00e9s" + }, + "KMF": { + "fr": "Franc comorien", + "en": "Comorian franc", + "nl": "Comorese frank", + "de": "Komoren-Franc", + "it": "Franco delle Comore", + "hu": "Comore-i frank", + "es": "Franco comorano" + }, + "TMT": { + "fr": "Manat turkm\u00e8ne", + "en": "Turkmenistan manat", + "nl": "Turkmeense manat", + "de": "Turkmenistan-Manat", + "it": "Manat turkmeno", + "hu": "T\u00fcrkm\u00e9n manat", + "es": "Manat turkmeno" + }, + "MUR": { + "fr": "Roupie mauricienne", + "en": "Mauritian rupee", + "nl": "Mauritiaanse roepie", + "de": "Mauritius-Rupie", + "it": "Rupia mauriziana", + "hu": "Mauritiusi r\u00fapia", + "es": "Rupia de Mauricio" + }, + "IDR": { + "fr": "Roupie indon\u00e9sienne", + "en": "Indonesian Rupiah", + "nl": "Indonesische roepia", + "de": "Indonesische Rupiah", + "it": "Rupia indonesiana", + "hu": "indon\u00e9z r\u00fapia", + "es": "Rupia indonesia" + }, + "HNL": { + "fr": "Lempira", + "en": "Honduran lempira", + "nl": "Hondurese lempira", + "de": "Lempira", + "it": "Lempira honduregna", + "hu": "hondurasi lempira", + "es": "lempira" + }, + "ETB": { + "fr": "Birr", + "en": "Ethiopian birr", + "nl": "Ethiopische birr", + "de": "\u00c4thiopischer Birr", + "it": "Birr etiope", + "hu": "eti\u00f3p birr", + "es": "Birr et\u00edope" + }, + "FJD": { + "fr": "dollar de Fidji", + "en": "Fijian dollar", + "nl": "Fiji-dollar", + "de": "Fidschi-Dollar", + "it": "Dollaro delle Figi", + "hu": "Fidzsi doll\u00e1r", + "es": "d\u00f3lar" + }, + "ISK": { + "fr": "Couronne islandaise", + "en": "Icelandic kr\u00f3na", + "nl": "IJslandse kroon", + "de": "Isl\u00e4ndische Krone", + "it": "Corona islandese", + "hu": "izlandi korona", + "es": "corona islandesa" + }, + "PEN": { + "fr": "nouveau sol", + "en": "Peruvian nuevo sol", + "nl": "Peruviaanse sol", + "de": "Nuevo Sol", + "it": "nuevo sol peruviano", + "hu": "perui \u00faj sol", + "es": "nuevo sol" + }, + "MKD": { + "fr": "Dinar mac\u00e9donien", + "en": "Macedonian denar", + "nl": "Macedonische denar", + "de": "Mazedonischer Denar", + "it": "Denaro macedone", + "hu": "maced\u00f3n d\u00e9n\u00e1r", + "es": "Denar macedonio" + }, + "ILS": { + "fr": "Shekel", + "en": "Israeli new shekel", + "nl": "Isra\u00eblische sjekel", + "de": "Schekel", + "it": "nuovo siclo israeliano", + "hu": "izraeli \u00faj s\u00e9kel", + "es": "Nuevo sh\u00e9quel" + }, + "DOP": { + "fr": "Peso dominicain", + "en": "Dominican peso", + "nl": "Dominicaanse peso", + "de": "Dominikanischer Peso", + "it": "Peso dominicano", + "hu": "Dominikai peso", + "es": "peso" + }, + "AZN": { + "fr": "Manat azerba\u00efdjanais", + "en": "Azerbaijani manat", + "nl": "Azerbeidzjaanse manat", + "de": "Aserbaidschan-Manat", + "it": "Manat azero", + "hu": "Azeri manat", + "es": "Manat azerbaiyano" + }, + "MDL": { + "fr": "Leu moldave", + "en": "Moldovan leu", + "nl": "Moldavische leu", + "de": "Moldauischer Leu", + "it": "Leu moldavo", + "hu": "moldov\u00e1n lej", + "es": "Leu moldavo" + }, + "BSD": { + "fr": "Dollar baham\u00e9en", + "en": "Bahamian dollar", + "nl": "Bahamaanse dollar", + "de": "Bahama-Dollar", + "it": "Dollaro delle Bahamas", + "hu": "bahamai doll\u00e1r", + "es": "D\u00f3lar bahame\u00f1o" + }, + "SEK": { + "fr": "Couronne su\u00e9doise", + "en": "Swedish krona", + "nl": "Zweedse kroon", + "de": "Schwedische Krone", + "it": "Corona svedese", + "hu": "Sv\u00e9d korona", + "es": "Corona sueca" + }, + "MVR": { + "fr": "Rufiyaa", + "en": "Maldivian rufiyaa", + "nl": "Maldivische rufiyaa", + "de": "Rufiyaa", + "it": "Rufiyaa delle Maldive", + "hu": "Mald\u00edv-szigeteki r\u00fafia", + "es": "Rupia de Maldivas" + }, + "FRF": { + "fr": "Franc fran\u00e7ais", + "en": "French franc", + "nl": "Franse frank", + "de": "Franz\u00f6sischer Franc", + "it": "Franco francese", + "hu": "Francia frank", + "es": "Franco franc\u00e9s" + }, + "SRD": { + "fr": "Dollar de Surinam", + "en": "Surinamese dollar", + "nl": "Surinaamse dollar", + "de": "Suriname-Dollar", + "it": "Dollaro surinamese", + "hu": "suriname-i doll\u00e1r", + "es": "D\u00f3lar surinam\u00e9s" + }, + "CUP": { + "fr": "peso cubain", + "en": "Cuban peso", + "nl": "Cubaanse peso", + "de": "Kubanischer Peso", + "it": "peso cubano", + "hu": "kubai peso", + "es": "peso" + }, + "BBD": { + "fr": "dollar barbadien", + "en": "Barbadian dollar", + "nl": "Barbadiaanse dollar", + "de": "Barbados-Dollar", + "it": "Dollaro di Barbados", + "hu": "barbadosi doll\u00e1r", + "es": "D\u00f3lar de Barbados" + }, + "KRW": { + "fr": "Won sud-cor\u00e9en", + "en": "South Korean won", + "nl": "Zuid-Koreaanse won", + "de": "S\u00fcdkoreanischer Won", + "it": "Won sudcoreano", + "hu": "D\u00e9l-koreai von", + "es": "W\u014fn surcoreano" + }, + "GMD": { + "fr": "Dalasi", + "en": "Gambian dalasi", + "nl": "Gambiaanse dalasi", + "de": "Dalasi", + "it": "Dalasi gambese", + "hu": "Gambiai dalasi", + "es": "Dalasi" + }, + "VEF": { + "fr": "Bol\u00edvar v\u00e9n\u00e9zu\u00e9lien", + "en": "Venezuelan bol\u00edvar", + "nl": "Venezolaanse bol\u00edvar", + "de": "Venezolanischer Bol\u00edvar", + "it": "Bol\u00edvar venezuelano", + "hu": "venezuelai bol\u00edvar", + "es": "Bol\u00edvar" + }, + "GTQ": { + "fr": "Quetzal", + "en": "Guatemalan quetzal", + "nl": "Guatemalteekse quetzal", + "de": "Guatemaltekischer Quetzal", + "it": "Quetzal guatemalteco", + "hu": "Guatemalai quetzal", + "es": "Quetzal" + }, + "ANG": { + "fr": "Florin des Antilles n\u00e9erlandaises", + "en": "Netherlands Antillean guilder", + "nl": "Antilliaanse gulden", + "de": "Antillen-Gulden", + "it": "Fiorino delle Antille Olandesi", + "hu": "Holland antill\u00e1kbeli forint", + "es": "Flor\u00edn antillano neerland\u00e9s" + }, + "CUC": { + "fr": "Peso cubain convertible", + "en": "Cuban convertible peso", + "nl": "Convertibele peso", + "de": "Peso convertible", + "it": "Peso cubano convertibile", + "hu": "Kubai konvertibilis peso", + "es": "peso convertible" + }, + "CLP": { + "fr": "Peso chilien", + "en": "Chilean peso", + "nl": "Chileense peso", + "de": "Chilenischer Peso", + "it": "Peso cileno", + "hu": "chilei peso", + "es": "peso" + }, + "ZMW": { + "fr": "Kwacha zambien", + "en": "Zambian kwacha", + "nl": "Zambiaanse kwacha", + "de": "Sambischer Kwacha", + "it": "Kwacha zambiano", + "hu": "Zambiai kwacha", + "es": "Kwacha zambiano" + }, + "LTL": { + "fr": "Litas", + "en": "Lithuanian litas", + "nl": "Litouwse litas", + "de": "Litas", + "it": "Litas lituano", + "hu": "litv\u00e1n litas", + "es": "Litas lituana" + }, + "CDF": { + "fr": "Franc congolais", + "en": "Congolese franc", + "nl": "Congolese frank", + "de": "Kongo-Franc", + "it": "Franco congolese", + "hu": "Kong\u00f3i frank", + "es": "franco" + }, + "XCD": { + "fr": "dollar des Cara\u00efbes orientales", + "en": "East Caribbean dollar", + "nl": "Oost-Caribische dollar", + "de": "ostkaribischer Dollar", + "it": "dollaro dei Caraibi Orientali", + "hu": "kelet-karibi doll\u00e1r", + "es": "d\u00f3lar del Caribe Oriental" + }, + "KZT": { + "fr": "Tenge kazakh", + "en": "Kazakhstani tenge", + "nl": "Kazachse tenge", + "de": "Tenge", + "it": "Tenge kazako", + "hu": "kazah tenge", + "es": "Tenge kazajo" + }, + "XPF": { + "fr": "Franc Pacifique", + "en": "CFP Franc", + "nl": "CFP-frank", + "de": "CFP-Franc", + "it": "Franco CFP", + "hu": "Csendes-\u00f3ce\u00e1ni valutak\u00f6z\u00f6ss\u00e9gi frank", + "es": "Franco CFP" + }, + "RUB": { + "fr": "Rouble russe", + "en": "Russian ruble", + "nl": "Russische roebel", + "de": "Russischer Rubel", + "it": "Rublo russo", + "hu": "orosz rubel", + "es": "Rublo ruso" + }, + "XFU": { + "fr": "Franc UIC", + "en": "UIC franc" + }, + "TTD": { + "fr": "Dollar de Trinit\u00e9-et-Tobago", + "en": "Trinidad and Tobago dollar", + "nl": "Trinidad en Tobagodollar", + "de": "Trinidad-und-Tobago-Dollar", + "it": "Dollaro di Trinidad e Tobago", + "hu": "Trinidad \u00e9s Tobag\u00f3-i doll\u00e1r", + "es": "D\u00f3lar trinitense" + }, + "RON": { + "fr": "Leu roumain", + "en": "Romanian leu", + "nl": "Roemeense leu", + "de": "Rum\u00e4nischer Leu", + "it": "Leu romeno", + "hu": "rom\u00e1n lej", + "es": "Leu rumano" + }, + "OMR": { + "fr": "Rial omanais", + "en": "Omani rial", + "nl": "Omaanse rial", + "de": "Omanischer Rial", + "it": "Riyal dell'Oman", + "hu": "Om\u00e1ni ri\u00e1l", + "es": "Rial oman\u00ed" + }, + "BRL": { + "fr": "r\u00e9al br\u00e9silien", + "en": "Brazilian real", + "nl": "Braziliaanse real", + "de": "Brasilianischer Real", + "it": "Real brasiliano", + "hu": "brazil real", + "es": "Real brasile\u00f1o" + }, + "SBD": { + "fr": "dollar des \u00eeles Salomon", + "en": "Solomon Islands dollar", + "nl": "Salomon-dollar", + "de": "Salomonen-Dollar", + "it": "Dollaro delle Salomone", + "hu": "Salamon-szigeteki doll\u00e1r", + "es": "d\u00f3lar de las Islas Salom\u00f3n" + }, + "PYG": { + "fr": "Guaran\u00ed", + "en": "Paraguayan guaran\u00ed", + "nl": "Paraguayaanse guaran\u00ed", + "de": "Paraguayischer Guaran\u00ed", + "it": "Guaran\u00ed paraguaiano", + "hu": "Paraguayi guaran\u00ed", + "es": "Guaran\u00ed" + }, + "KES": { + "fr": "Shilling k\u00e9nyan", + "en": "Kenyan shilling", + "nl": "Keniaanse shilling", + "de": "Kenia-Schilling", + "it": "Scellino keniota", + "hu": "Kenyai shilling", + "es": "Chel\u00edn keniano" + }, + "USD": { + "fr": "dollar am\u00e9ricain", + "en": "United States dollar", + "nl": "Amerikaanse dollar", + "de": "US-Dollar", + "it": "dollaro statunitense", + "hu": "amerikai doll\u00e1r", + "es": "d\u00f3lar" + }, + "TWD": { + "fr": "Nouveau dollar de Ta\u00efwan", + "en": "New Taiwan dollar", + "nl": "Taiwanese dollar", + "de": "Neuer Taiwan-Dollar", + "it": "Dollaro taiwanese", + "hu": "Tajvani \u00faj doll\u00e1r", + "es": "Nuevo d\u00f3lar taiwan\u00e9s" + }, + "TOP": { + "fr": "pa\u2019anga", + "en": "Tongan pa\u02bbanga", + "nl": "Tongaanse pa'anga", + "de": "Pa\u02bbanga", + "it": "Pa'anga tongano", + "hu": "Tongai pa\u02bbanga", + "es": "pa\u02bbanga" + }, + "COP": { + "fr": "Peso colombien", + "en": "peso", + "nl": "Colombiaanse peso", + "de": "Kolumbianischer Peso", + "it": "Peso colombiano", + "hu": "Kolumbiai peso", + "es": "peso" + }, + "GNF": { + "fr": "Franc guin\u00e9en", + "en": "Guinean franc", + "nl": "Guineese frank", + "de": "Franc Guin\u00e9en", + "it": "Franco guineano", + "hu": "Guineai frank", + "es": "Franco guineano" + }, + "WST": { + "fr": "Tala", + "en": "Samoan t\u0101l\u0101", + "nl": "Samoaanse tala", + "de": "Samoanischer Tala", + "it": "Tala samoano", + "hu": "Szamoai tala", + "es": "tala" + }, + "IQD": { + "fr": "Dinar irakien", + "en": "Iraqi dinar", + "nl": "Iraakse dinar", + "de": "Irakischer Dinar", + "it": "Dinaro iracheno", + "hu": "iraki din\u00e1r", + "es": "Dinar iraqu\u00ed" + }, + "ERN": { + "fr": "Nakfa \u00e9rythr\u00e9en", + "en": "Eritrean nakfa", + "nl": "Eritrese nakfa", + "de": "Eritreischer Nakfa", + "it": "Nacfa eritreo", + "hu": "Eritreai nakfa", + "es": "Nakfa" + }, + "CVE": { + "fr": "Escudo cap-verdien", + "en": "Cape Verdean escudo", + "nl": "Kaapverdische escudo", + "de": "Kap-Verde-Escudo", + "it": "Escudo capoverdiano", + "hu": "Z\u00f6ld-foki k\u00f6zt\u00e1rsas\u00e1gi escudo", + "es": "escudo" + }, + "AUD": { + "fr": "dollar australien", + "en": "Australian dollar", + "nl": "Australische dollar", + "de": "Australischer Dollar", + "it": "Dollaro australiano", + "hu": "Ausztr\u00e1l doll\u00e1r", + "es": "D\u00f3lar australiano" + }, + "BAM": { + "fr": "Mark convertible de Bosnie-Herz\u00e9govine", + "en": "Bosnia and Herzegovina convertible mark", + "nl": "Bosnische inwisselbare mark", + "de": "Konvertible Mark", + "it": "Marco bosniaco", + "hu": "bosny\u00e1k konvertibilis m\u00e1rka", + "es": "Marco bosnioherzegovino" + }, + "KWD": { + "fr": "Dinar kowe\u00eftien", + "en": "Kuwaiti dinar", + "nl": "Koeweitse dinar", + "de": "Kuwait-Dinar", + "it": "Dinaro kuwaitiano", + "hu": "kuvaiti din\u00e1r", + "es": "Dinar kuwait\u00ed" + }, + "BIF": { + "fr": "Franc burundais", + "en": "Burundian franc", + "nl": "Burundese frank", + "de": "Burundi-Franc", + "it": "Franco del Burundi", + "hu": "Burundi frank", + "es": "Franco de Burundi" + }, + "PGK": { + "fr": "Kina", + "en": "Papua New Guinean kina", + "nl": "Papoea-Nieuw-Guinese kina", + "de": "Kina", + "it": "Kina papuana", + "hu": "P\u00e1pua \u00faj-guineai kina", + "es": "Kina" + }, + "SOS": { + "fr": "shilling somalien", + "en": "Somali shilling", + "nl": "Somalische shilling", + "de": "Somalia-Schilling", + "it": "Scellino somalo", + "hu": "Szom\u00e1liai shilling", + "es": "chel\u00edn" + }, + "CAD": { + "fr": "Dollar canadien", + "en": "Canadian dollar", + "nl": "Canadese dollar", + "de": "Kanadischer Dollar", + "it": "Dollaro canadese", + "hu": "kanadai doll\u00e1r", + "es": "D\u00f3lar canadiense" + }, + "SGD": { + "fr": "Dollar de Singapour", + "en": "Singapore dollar", + "nl": "Singaporese dollar", + "de": "Singapur-Dollar", + "it": "Dollaro di Singapore", + "hu": "szingap\u00fari doll\u00e1r", + "es": "D\u00f3lar de Singapur" + }, + "UZS": { + "fr": "Sum", + "en": "Uzbekistani som", + "nl": "Oezbeekse sum", + "de": "So\u02bbm", + "it": "Som uzbeco", + "hu": "\u00dczb\u00e9g szom", + "es": "som" + }, + "STD": { + "fr": "Dobra", + "en": "S\u00e3o Tom\u00e9 and Pr\u00edncipe dobra", + "nl": "Santomese dobra", + "de": "S\u00e3o-tom\u00e9ischer Dobra", + "it": "Dobra di S\u00e3o Tom\u00e9 e Pr\u00edncipe", + "hu": "S\u00e3o Tom\u00e9 \u00e9s Pr\u00edncipe-i dobra", + "es": "Dobra santotomense" + }, + "XFO": { + "fr": "Franc-or", + "en": "Gold franc", + "de": "Goldfranken" + }, + "IRR": { + "fr": "Rial iranien", + "en": "Iranian rial", + "nl": "Iraanse rial", + "de": "Iranischer Rial", + "it": "Riyal iraniano", + "hu": "ir\u00e1ni ri\u00e1l", + "es": "Rial iran\u00ed" + }, + "CNY": { + "fr": "Yuan", + "en": "renminbi", + "nl": "Chinese renminbi", + "de": "Renminbi", + "it": "Renminbi cinese", + "hu": "Renminbi", + "es": "Yuan chino" + }, + "SLL": { + "fr": "leone", + "en": "Sierra Leonean leone", + "nl": "Sierra Leoonse leone", + "de": "Sierra-leonischer Leone", + "it": "Leone sierraleonese", + "hu": "Sierra Leone-i leone", + "es": "leone" + }, + "TND": { + "fr": "Dinar tunisien", + "en": "Tunisian dinar", + "nl": "Tunesische dinar", + "de": "Tunesischer Dinar", + "it": "Dinaro tunisino", + "hu": "Tun\u00e9ziai din\u00e1r", + "es": "dinar" + }, + "GYD": { + "fr": "Dollar guyanien", + "en": "Guyanese dollar", + "nl": "Guyaanse dollar", + "de": "Guyana-Dollar", + "it": "Dollaro della Guyana", + "hu": "Guyanai doll\u00e1r", + "es": "D\u00f3lar guyan\u00e9s" + }, + "MTL": { + "fr": "Lire maltaise", + "en": "Maltese lira", + "nl": "Maltese lire", + "de": "Maltesische Lira", + "it": "Lira maltese", + "hu": "M\u00e1ltai l\u00edra", + "es": "Lira maltesa" + }, + "NZD": { + "fr": "dollar n\u00e9o-z\u00e9landais", + "en": "New Zealand dollar", + "nl": "Nieuw-Zeelandse dollar", + "de": "Neuseeland-Dollar", + "it": "Dollaro neozelandese", + "hu": "\u00faj-z\u00e9landi doll\u00e1r", + "es": "D\u00f3lar neozeland\u00e9s" + }, + "FKP": { + "fr": "Livre des \u00celes Malouines", + "en": "Falkland Islands pound", + "nl": "Falklandeilands pond", + "de": "Falkland-Pfund", + "it": "Sterlina delle Falkland", + "hu": "Falkland-szigeteki font", + "es": "Libra malvinense" + }, + "LVL": { + "fr": "Lats", + "en": "lats", + "nl": "Letse lats", + "de": "Lats", + "it": "Lats lettone", + "hu": "lett lat", + "es": "Lats let\u00f3n" + }, + "ARP": { + "fr": "peso argentino", + "en": "peso argentino", + "nl": "peso argentino", + "it": "peso argentino", + "es": "peso argentino" + }, + "KGS": { + "fr": "Som", + "en": "Kyrgyzstani som", + "nl": "Kirgizische som", + "de": "Som", + "it": "som kirghizo", + "hu": "kirgiz szom", + "es": "Som kirgu\u00eds" + }, + "ARS": { + "fr": "peso argentin", + "en": "peso", + "nl": "Argentijnse peso", + "de": "Argentinischer Peso", + "it": "peso argentino", + "hu": "argentin peso", + "es": "peso" + }, + "BMD": { + "fr": "Dollar bermudien", + "en": "Bermudian dollar", + "nl": "Bermuda-dollar", + "de": "Bermuda-Dollar", + "it": "Dollaro della Bermuda", + "hu": "bermudai doll\u00e1r", + "es": "D\u00f3lar bermude\u00f1o" + }, + "RSD": { + "fr": "Dinar serbe", + "en": "Serbian dinar", + "nl": "Servische dinar", + "de": "Serbischer Dinar", + "it": "Dinaro serbo", + "hu": "szerb din\u00e1r", + "es": "Dinar serbio" + }, + "BHD": { + "fr": "Dinar bahre\u00efnien", + "en": "Bahraini dinar", + "nl": "Bahreinse dinar", + "de": "Bahrain-Dinar", + "it": "Dinaro del Bahrain", + "hu": "bahreini din\u00e1r", + "es": "Dinar barein\u00ed" + }, + "JPY": { + "fr": "Yen", + "en": "Japanese yen", + "nl": "Japanse yen", + "de": "Yen", + "it": "Yen giapponese", + "hu": "Jap\u00e1n jen", + "es": "yen" + }, + "ARA": { + "fr": "Austral (monnaie)", + "en": "Argentine austral", + "de": "Austral (W\u00e4hrung)", + "it": "Austral argentino", + "es": "Austral" + }, + "SDG": { + "fr": "Livre soudanaise", + "en": "Sudanese pound", + "nl": "Soedanees pond", + "de": "Sudanesisches Pfund", + "it": "Sterlina sudanese", + "hu": "Szud\u00e1ni font", + "es": "Libra sudanesa" + } + } +} \ No newline at end of file diff --git a/searx/data/engines_languages.json b/searx/data/engines_languages.json new file mode 100644 index 0000000..2d97166 --- /dev/null +++ b/searx/data/engines_languages.json @@ -0,0 +1 @@ +{"google news": {"el": {"name": "Ελληνικά"}, "eo": {"name": "Esperanto"}, "en": {"name": "English"}, "af": {"name": "Afrikaans"}, "vi": {"name": "Tiếng Việt"}, "ca": {"name": "Català"}, "it": {"name": "Italiano"}, "iw": {"name": "עברית"}, "hy": {"name": "Հայերեն"}, "cs": {"name": "Čeština"}, "et": {"name": "Eesti"}, "id": {"name": "Indonesia"}, "es": {"name": "Español"}, "ru": {"name": "Русский"}, "nl": {"name": "Nederlands"}, "pt": {"name": "Português"}, "no": {"name": "Norsk"}, "tr": {"name": "Türkçe"}, "lt": {"name": "Lietuvių"}, "lv": {"name": "Latviešu"}, "tl": {"name": "Filipino"}, "zh-TW": {"name": "中文 (繁體)"}, "th": {"name": "ไทย"}, "ro": {"name": "Română"}, "is": {"name": "Íslenska"}, "pl": {"name": "Polski"}, "be": {"name": "Беларуская"}, "fr": {"name": "Français"}, "bg": {"name": "Български"}, "hr": {"name": "Hrvatski"}, "de": {"name": "Deutsch"}, "ko": {"name": "한국어"}, "da": {"name": "Dansk"}, "fa": {"name": "فارسی"}, "hi": {"name": "हिन्दी"}, "fi": {"name": "Suomi"}, "hu": {"name": "Magyar"}, "ja": {"name": "日本語"}, "sr": {"name": "Српски"}, "sw": {"name": "Kiswahili"}, "sv": {"name": "Svenska"}, "sk": {"name": "Slovenčina"}, "zh-CN": {"name": "中文 (简体)"}, "ar": {"name": "العربية"}, "uk": {"name": "Українська"}, "sl": {"name": "Slovenščina"}}, "bing news": ["sq", "de", "ar", "bg", "ca", "cs", "zh-CHS", "zh-CHT", "ko", "hr", "da", "sk", "sl", "es", "et", "fi", "fr", "el", "he", "nl", "hu", "id", "en", "is", "it", "ja", "lv", "lt", "ms", "no", "fa", "pl", "pt-BR", "pt-PT", "ro", "ru", "sr", "sv", "th", "tr", "uk", "vi"], "google": {"el": {"name": "Ελληνικά"}, "eo": {"name": "Esperanto"}, "en": {"name": "English"}, "af": {"name": "Afrikaans"}, "vi": {"name": "Tiếng Việt"}, "ca": {"name": "Català"}, "it": {"name": "Italiano"}, "iw": {"name": "עברית"}, "hy": {"name": "Հայերեն"}, "cs": {"name": "Čeština"}, "et": {"name": "Eesti"}, "id": {"name": "Indonesia"}, "es": {"name": "Español"}, "ru": {"name": "Русский"}, "nl": {"name": "Nederlands"}, "pt": {"name": "Português"}, "no": {"name": "Norsk"}, "tr": {"name": "Türkçe"}, "lt": {"name": "Lietuvių"}, "lv": {"name": "Latviešu"}, "tl": {"name": "Filipino"}, "zh-TW": {"name": "中文 (繁體)"}, "th": {"name": "ไทย"}, "ro": {"name": "Română"}, "is": {"name": "Íslenska"}, "pl": {"name": "Polski"}, "be": {"name": "Беларуская"}, "fr": {"name": "Français"}, "bg": {"name": "Български"}, "hr": {"name": "Hrvatski"}, "de": {"name": "Deutsch"}, "ko": {"name": "한국어"}, "da": {"name": "Dansk"}, "fa": {"name": "فارسی"}, "hi": {"name": "हिन्दी"}, "fi": {"name": "Suomi"}, "hu": {"name": "Magyar"}, "ja": {"name": "日本語"}, "sr": {"name": "Српски"}, "sw": {"name": "Kiswahili"}, "sv": {"name": "Svenska"}, "sk": {"name": "Slovenčina"}, "zh-CN": {"name": "中文 (简体)"}, "ar": {"name": "العربية"}, "uk": {"name": "Українська"}, "sl": {"name": "Slovenščina"}}, "duckduckgo": ["da-DK", "vi-VN", "en-SG", "sl-SL", "en-XA", "tzh-HK", "en-UK", "ro-RO", "en-MY", "el-GR", "it-CH", "hu-HU", "fr-FR", "en-PH", "tl-PH", "fr-CA", "fi-FI", "et-EE", "sv-SE", "es-XL", "th-TH", "sk-SK", "es-ES", "en-IE", "es-US", "es-PE", "nl-NL", "en-US", "de-DE", "de-AT", "wt-WT", "no-NO", "tr-TR", "ca-ES", "it-IT", "es-CO", "ru-RU", "ca-CT", "en-ZA", "en-CA", "jp-JP", "es-MX", "id-ID", "es-AR", "he-IL", "kr-KR", "en-AU", "ms-MY", "pl-PL", "lv-LV", "bg-BG", "zh-CN", "en-NZ", "lt-LT", "tzh-TW", "hr-HR", "pt-PT", "fr-BE", "de-CH", "cs-CZ", "en-IN", "nl-BE", "fr-CH", "en-ID", "ar-XA", "pt-BR", "uk-UA", "es-CL"], "bing": ["sq", "de", "ar", "bg", "ca", "cs", "zh-CHS", "zh-CHT", "ko", "hr", "da", "sk", "sl", "es", "et", "fi", "fr", "el", "he", "nl", "hu", "id", "en", "is", "it", "ja", "lv", "lt", "ms", "no", "fa", "pl", "pt-BR", "pt-PT", "ro", "ru", "sr", "sv", "th", "tr", "uk", "vi"], "wikipedia": {"gv": {"articles": 4955, "name": "Gaelg", "english_name": "Manx"}, "sco": {"articles": 42405, "name": "Scots", "english_name": "Scots"}, "scn": {"articles": 25379, "name": "Sicilianu", "english_name": "Sicilian"}, "wuu": {"articles": 5688, "name": "吴语", "english_name": "Wu"}, "tcy": {"articles": 750, "name": "ತುಳು", "english_name": "Tulu"}, "cdo": {"articles": 4239, "name": "Mìng-dĕ̤ng-ngṳ̄", "english_name": "Min Dong"}, "gu": {"articles": 26842, "name": "ગુજરાતી", "english_name": "Gujarati"}, "kbd": {"articles": 1569, "name": "Адыгэбзэ (Adighabze)", "english_name": "Kabardian Circassian"}, "gd": {"articles": 14368, "name": "Gàidhlig", "english_name": "Scottish Gaelic"}, "jbo": {"articles": 1197, "name": "Lojban", "english_name": "Lojban"}, "ga": {"articles": 39393, "name": "Gaeilge", "english_name": "Irish"}, "gn": {"articles": 3133, "name": "Avañe'ẽ", "english_name": "Guarani"}, "gl": {"articles": 136535, "name": "Galego", "english_name": "Galician"}, "als": {"articles": 22592, "name": "Alemannisch", "english_name": "Alemannic"}, "lg": {"articles": 1135, "name": "Luganda", "english_name": "Luganda"}, "hak": {"articles": 7386, "name": "Hak-kâ-fa / 客家話", "english_name": "Hakka"}, "lb": {"articles": 48141, "name": "Lëtzebuergesch", "english_name": "Luxembourgish"}, "szl": {"articles": 5491, "name": "Ślůnski", "english_name": "Silesian"}, "vep": {"articles": 5339, "name": "Vepsän", "english_name": "Vepsian"}, "la": {"articles": 126249, "name": "Latina", "english_name": "Latin"}, "ln": {"articles": 2786, "name": "Lingala", "english_name": "Lingala"}, "frp": {"articles": 2608, "name": "Arpitan", "english_name": "Franco-Provençal"}, "tt": {"articles": 70258, "name": "Tatarça / Татарча", "english_name": "Tatar"}, "tr": {"articles": 292026, "name": "Türkçe", "english_name": "Turkish"}, "cbk-zam": {"articles": 2967, "name": "Chavacano de Zamboanga", "english_name": "Zamboanga Chavacano"}, "li": {"articles": 11620, "name": "Limburgs", "english_name": "Limburgish"}, "lv": {"articles": 75872, "name": "Latviešu", "english_name": "Latvian"}, "to": {"articles": 1688, "name": "faka Tonga", "english_name": "Tongan"}, "tl": {"articles": 66203, "name": "Tagalog", "english_name": "Tagalog"}, "jam": {"articles": 1597, "name": "Jumiekan Kryuol", "english_name": "Jamaican Patois"}, "vec": {"articles": 10877, "name": "Vèneto", "english_name": "Venetian"}, "th": {"articles": 114824, "name": "ไทย", "english_name": "Thai"}, "ti": {"articles": 175, "name": "ትግርኛ", "english_name": "Tigrinya"}, "tg": {"articles": 67713, "name": "Тоҷикӣ", "english_name": "Tajik"}, "te": {"articles": 66514, "name": "తెలుగు", "english_name": "Telugu"}, "ksh": {"articles": 2825, "name": "Ripoarisch", "english_name": "Ripuarian"}, "pcd": {"articles": 3402, "name": "Picard", "english_name": "Picard"}, "ta": {"articles": 91441, "name": "தமிழ்", "english_name": "Tamil"}, "yi": {"articles": 13744, "name": "ייִדיש", "english_name": "Yiddish"}, "lrc": {"articles": 5280, "name": "لۊری شومالی", "english_name": "Northern Luri"}, "xmf": {"articles": 9318, "name": "მარგალური (Margaluri)", "english_name": "Mingrelian"}, "ceb": {"articles": 4049908, "name": "Sinugboanong Binisaya", "english_name": "Cebuano"}, "yo": {"articles": 31528, "name": "Yorùbá", "english_name": "Yoruba"}, "de": {"articles": 2035837, "name": "Deutsch", "english_name": "German"}, "da": {"articles": 223944, "name": "Dansk", "english_name": "Danish"}, "za": {"articles": 1161, "name": "Cuengh", "english_name": "Zhuang"}, "pdc": {"articles": 1790, "name": "Deitsch", "english_name": "Pennsylvania German"}, "bxr": {"articles": 1873, "name": "Буряад", "english_name": "Buryat"}, "dz": {"articles": 217, "name": "ཇོང་ཁ", "english_name": "Dzongkha"}, "hif": {"articles": 9646, "name": "Fiji Hindi", "english_name": "Fiji Hindi"}, "rm": {"articles": 3417, "name": "Rumantsch", "english_name": "Romansh"}, "dv": {"articles": 2970, "name": "ދިވެހިބަސް", "english_name": "Divehi"}, "qu": {"articles": 20037, "name": "Runa Simi", "english_name": "Quechua"}, "vls": {"articles": 5988, "name": "West-Vlams", "english_name": "West Flemish"}, "bar": {"articles": 22085, "name": "Boarisch", "english_name": "Bavarian"}, "eml": {"articles": 8670, "name": "Emiliàn e rumagnòl", "english_name": "Emilian-Romagnol"}, "kn": {"articles": 21679, "name": "ಕನ್ನಡ", "english_name": "Kannada"}, "fiu-vro": {"articles": 5449, "name": "Võro", "english_name": "Võro"}, "mo": {"articles": 394, "name": "Молдовеняскэ", "english_name": "Moldovan"}, "bpy": {"articles": 25069, "name": "ইমার ঠার/বিষ্ণুপ্রিয়া মণিপুরী", "english_name": "Bishnupriya Manipuri"}, "crh": {"articles": 5113, "name": "Qırımtatarca", "english_name": "Crimean Tatar"}, "mhr": {"articles": 9413, "name": "Олык Марий (Olyk Marij)", "english_name": "Meadow Mari"}, "diq": {"articles": 7537, "name": "Zazaki", "english_name": "Zazaki"}, "el": {"articles": 127970, "name": "Ελληνικά", "english_name": "Greek"}, "eo": {"articles": 237478, "name": "Esperanto", "english_name": "Esperanto"}, "en": {"articles": 5343454, "name": "English", "english_name": "English"}, "zh": {"articles": 927993, "name": "中文", "english_name": "Chinese"}, "pms": {"articles": 64046, "name": "Piemontèis", "english_name": "Piedmontese"}, "ee": {"articles": 334, "name": "Eʋegbe", "english_name": "Ewe"}, "tpi": {"articles": 1351, "name": "Tok Pisin", "english_name": "Tok Pisin"}, "arz": {"articles": 16354, "name": "مصرى (Maṣri)", "english_name": "Egyptian Arabic"}, "rmy": {"articles": 583, "name": "romani - रोमानी", "english_name": "Romani"}, "mdf": {"articles": 1133, "name": "Мокшень (Mokshanj Kälj)", "english_name": "Moksha"}, "kaa": {"articles": 1955, "name": "Qaraqalpaqsha", "english_name": "Karakalpak"}, "olo": {"articles": 1934, "name": "Karjalan", "english_name": "Livvi-Karelian"}, "arc": {"articles": 1617, "name": "ܐܪܡܝܐ", "english_name": "Aramaic"}, "cr": {"articles": 125, "name": "Nehiyaw", "english_name": "Cree"}, "eu": {"articles": 277712, "name": "Euskara", "english_name": "Basque"}, "et": {"articles": 154506, "name": "Eesti", "english_name": "Estonian"}, "tet": {"articles": 1390, "name": "Tetun", "english_name": "Tetum"}, "es": {"articles": 1318337, "name": "Español", "english_name": "Spanish"}, "ba": {"articles": 37407, "name": "Башҡорт", "english_name": "Bashkir"}, "gom": {"articles": 3119, "name": "गोंयची कोंकणी / Gõychi Konknni", "english_name": "Goan Konkani"}, "ru": {"articles": 1375970, "name": "Русский", "english_name": "Russian"}, "roa-tara": {"articles": 9229, "name": "Tarandíne", "english_name": "Tarantino"}, "ha": {"articles": 1410, "name": "هَوُسَ", "english_name": "Hausa"}, "ak": {"articles": 271, "name": "Akana", "english_name": "Akan"}, "lad": {"articles": 4421, "name": "Dzhudezmo", "english_name": "Ladino"}, "bm": {"articles": 411, "name": "Bamanankan", "english_name": "Bambara"}, "new": {"articles": 72123, "name": "नेपाल भाषा", "english_name": "Newar"}, "rn": {"articles": 495, "name": "Kirundi", "english_name": "Kirundi"}, "ro": {"articles": 374753, "name": "Română", "english_name": "Romanian"}, "dsb": {"articles": 3071, "name": "Dolnoserbski", "english_name": "Lower Sorbian"}, "jv": {"articles": 49700, "name": "Basa Jawa", "english_name": "Javanese"}, "hsb": {"articles": 11071, "name": "Hornjoserbsce", "english_name": "Upper Sorbian"}, "be": {"articles": 126878, "name": "Беларуская", "english_name": "Belarusian"}, "bg": {"articles": 227507, "name": "Български", "english_name": "Bulgarian"}, "myv": {"articles": 3510, "name": "Эрзянь (Erzjanj Kelj)", "english_name": "Erzya"}, "uk": {"articles": 682728, "name": "Українська", "english_name": "Ukrainian"}, "wa": {"articles": 14408, "name": "Walon", "english_name": "Walloon"}, "ast": {"articles": 48457, "name": "Asturianu", "english_name": "Asturian"}, "wo": {"articles": 1132, "name": "Wolof", "english_name": "Wolof"}, "got": {"articles": 493, "name": "𐌲𐌿𐍄𐌹𐍃𐌺", "english_name": "Gothic"}, "bn": {"articles": 48261, "name": "বাংলা", "english_name": "Bengali"}, "bo": {"articles": 5717, "name": "བོད་སྐད", "english_name": "Tibetan"}, "bh": {"articles": 8674, "name": "भोजपुरी", "english_name": "Bihari"}, "bi": {"articles": 817, "name": "Bislama", "english_name": "Bislama"}, "rue": {"articles": 5963, "name": "Русиньскый", "english_name": "Rusyn"}, "map-bms": {"articles": 13284, "name": "Basa Banyumasan", "english_name": "Banyumasan"}, "tum": {"articles": 564, "name": "chiTumbuka", "english_name": "Tumbuka"}, "br": {"articles": 61346, "name": "Brezhoneg", "english_name": "Breton"}, "bs": {"articles": 73437, "name": "Bosanski", "english_name": "Bosnian"}, "lez": {"articles": 3606, "name": "Лезги чІал (Lezgi č’al)", "english_name": "Lezgian"}, "ja": {"articles": 1050743, "name": "日本語", "english_name": "Japanese"}, "om": {"articles": 725, "name": "Oromoo", "english_name": "Oromo"}, "glk": {"articles": 6076, "name": "گیلکی", "english_name": "Gilaki"}, "ace": {"articles": 4077, "name": "Bahsa Acèh", "english_name": "Acehnese"}, "ilo": {"articles": 10461, "name": "Ilokano", "english_name": "Ilokano"}, "roa-rup": {"articles": 1206, "name": "Armãneashce", "english_name": "Aromanian"}, "oc": {"articles": 82700, "name": "Occitan", "english_name": "Occitan"}, "ltg": {"articles": 797, "name": "Latgaļu", "english_name": "Latgalian"}, "be-tarask": {"articles": 60248, "name": "Беларуская (тарашкевіца)", "english_name": "Belarusian (Taraškievica)"}, "st": {"articles": 384, "name": "Sesotho", "english_name": "Sesotho"}, "lo": {"articles": 1623, "name": "ລາວ", "english_name": "Lao"}, "krc": {"articles": 2016, "name": "Къарачай-Малкъар (Qarachay-Malqar)", "english_name": "Karachay-Balkar"}, "nds": {"articles": 25822, "name": "Plattdüütsch", "english_name": "Low Saxon"}, "os": {"articles": 10382, "name": "Иронау", "english_name": "Ossetian"}, "or": {"articles": 12252, "name": "ଓଡ଼ିଆ", "english_name": "Oriya"}, "udm": {"articles": 3875, "name": "Удмурт кыл", "english_name": "Udmurt"}, "xh": {"articles": 604, "name": "isiXhosa", "english_name": "Xhosa"}, "ch": {"articles": 419, "name": "Chamoru", "english_name": "Chamorro"}, "co": {"articles": 5424, "name": "Corsu", "english_name": "Corsican"}, "nso": {"articles": 7642, "name": "Sepedi", "english_name": "Northern Sotho"}, "simple": {"articles": 123305, "name": "Simple English", "english_name": "Simple English"}, "bjn": {"articles": 1710, "name": "Bahasa Banjar", "english_name": "Banjar"}, "ca": {"articles": 535045, "name": "Català", "english_name": "Catalan"}, "lmo": {"articles": 34788, "name": "Lumbaart", "english_name": "Lombard"}, "ce": {"articles": 160122, "name": "Нохчийн", "english_name": "Chechen"}, "ts": {"articles": 392, "name": "Xitsonga", "english_name": "Tsonga"}, "cy": {"articles": 90328, "name": "Cymraeg", "english_name": "Welsh"}, "ang": {"articles": 2857, "name": "Englisc", "english_name": "Anglo-Saxon"}, "cs": {"articles": 374940, "name": "Čeština", "english_name": "Czech"}, "ty": {"articles": 1179, "name": "Reo Mā`ohi", "english_name": "Tahitian"}, "ady": {"articles": 399, "name": "Адыгэбзэ", "english_name": "Adyghe"}, "cv": {"articles": 38132, "name": "Чăваш", "english_name": "Chuvash"}, "cu": {"articles": 582, "name": "Словѣньскъ", "english_name": "Old Church Slavonic"}, "ve": {"articles": 238, "name": "Tshivenda", "english_name": "Venda"}, "koi": {"articles": 3429, "name": "Перем Коми (Perem Komi)", "english_name": "Komi-Permyak"}, "ps": {"articles": 7991, "name": "پښتو", "english_name": "Pashto"}, "fj": {"articles": 340, "name": "Na Vosa Vakaviti", "english_name": "Fijian"}, "srn": {"articles": 1047, "name": "Sranantongo", "english_name": "Sranan"}, "pt": {"articles": 957637, "name": "Português", "english_name": "Portuguese"}, "sm": {"articles": 745, "name": "Gagana Samoa", "english_name": "Samoan"}, "ext": {"articles": 2898, "name": "Estremeñu", "english_name": "Extremaduran"}, "lt": {"articles": 181095, "name": "Lietuvių", "english_name": "Lithuanian"}, "zh-min-nan": {"articles": 203047, "name": "Bân-lâm-gú", "english_name": "Min Nan"}, "frr": {"articles": 4712, "name": "Nordfriisk", "english_name": "North Frisian"}, "chr": {"articles": 785, "name": "ᏣᎳᎩ", "english_name": "Cherokee"}, "pa": {"articles": 24776, "name": "ਪੰਜਾਬੀ", "english_name": "Punjabi"}, "xal": {"articles": 2073, "name": "Хальмг", "english_name": "Kalmyk"}, "chy": {"articles": 607, "name": "Tsetsêhestâhese", "english_name": "Cheyenne"}, "pi": {"articles": 2517, "name": "पाऴि", "english_name": "Pali"}, "war": {"articles": 1262274, "name": "Winaray", "english_name": "Waray-Waray"}, "pl": {"articles": 1209184, "name": "Polski", "english_name": "Polish"}, "tk": {"articles": 5193, "name": "تركمن / Туркмен", "english_name": "Turkmen"}, "hy": {"articles": 216349, "name": "Հայերեն", "english_name": "Armenian"}, "an": {"articles": 31888, "name": "Aragonés", "english_name": "Aragonese"}, "nrm": {"articles": 3621, "name": "Nouormand/Normaund", "english_name": "Norman"}, "hr": {"articles": 172218, "name": "Hrvatski", "english_name": "Croatian"}, "iu": {"articles": 391, "name": "ᐃᓄᒃᑎᑐᑦ", "english_name": "Inuktitut"}, "pfl": {"articles": 2067, "name": "Pälzisch", "english_name": "Palatinate German"}, "ht": {"articles": 51150, "name": "Krèyol ayisyen", "english_name": "Haitian"}, "hu": {"articles": 404333, "name": "Magyar", "english_name": "Hungarian"}, "gan": {"articles": 6367, "name": "贛語", "english_name": "Gan"}, "bat-smg": {"articles": 16015, "name": "Žemaitėška", "english_name": "Samogitian"}, "hi": {"articles": 117254, "name": "हिन्दी", "english_name": "Hindi"}, "tw": {"articles": 584, "name": "Twi", "english_name": "Twi"}, "gag": {"articles": 2744, "name": "Gagauz", "english_name": "Gagauz"}, "kg": {"articles": 1170, "name": "KiKongo", "english_name": "Kongo"}, "pnb": {"articles": 43662, "name": "شاہ مکھی پنجابی (Shāhmukhī Pañjābī)", "english_name": "Western Punjabi"}, "bug": {"articles": 14118, "name": "Basa Ugi", "english_name": "Buginese"}, "he": {"articles": 202507, "name": "עברית", "english_name": "Hebrew"}, "mg": {"articles": 83434, "name": "Malagasy", "english_name": "Malagasy"}, "fur": {"articles": 3173, "name": "Furlan", "english_name": "Friulian"}, "uz": {"articles": 128882, "name": "O‘zbek", "english_name": "Uzbek"}, "ml": {"articles": 47909, "name": "മലയാളം", "english_name": "Malayalam"}, "azb": {"articles": 14683, "name": "تۆرکجه", "english_name": "South Azerbaijani"}, "mn": {"articles": 16812, "name": "Монгол", "english_name": "Mongolian"}, "mi": {"articles": 7112, "name": "Māori", "english_name": "Maori"}, "ik": {"articles": 246, "name": "Iñupiak", "english_name": "Inupiak"}, "mk": {"articles": 88524, "name": "Македонски", "english_name": "Macedonian"}, "ur": {"articles": 115102, "name": "اردو", "english_name": "Urdu"}, "zea": {"articles": 4369, "name": "Zeêuws", "english_name": "Zeelandic"}, "mt": {"articles": 3190, "name": "Malti", "english_name": "Maltese"}, "stq": {"articles": 3754, "name": "Seeltersk", "english_name": "Saterland Frisian"}, "ms": {"articles": 287146, "name": "Bahasa Melayu", "english_name": "Malay"}, "mr": {"articles": 46054, "name": "मराठी", "english_name": "Marathi"}, "ug": {"articles": 3273, "name": "ئۇيغۇر تىلى", "english_name": "Uyghur"}, "mwl": {"articles": 2828, "name": "Mirandés", "english_name": "Mirandese"}, "my": {"articles": 34904, "name": "မြန်မာဘာသာ", "english_name": "Burmese"}, "ki": {"articles": 1339, "name": "Gĩkũyũ", "english_name": "Kikuyu"}, "pih": {"articles": 514, "name": "Norfuk", "english_name": "Norfolk"}, "sah": {"articles": 11079, "name": "Саха тыла (Saxa Tyla)", "english_name": "Sakha"}, "ss": {"articles": 421, "name": "SiSwati", "english_name": "Swati"}, "af": {"articles": 43924, "name": "Afrikaans", "english_name": "Afrikaans"}, "tn": {"articles": 620, "name": "Setswana", "english_name": "Tswana"}, "vi": {"articles": 1153964, "name": "Tiếng Việt", "english_name": "Vietnamese"}, "is": {"articles": 41946, "name": "Íslenska", "english_name": "Icelandic"}, "am": {"articles": 13359, "name": "አማርኛ", "english_name": "Amharic"}, "it": {"articles": 1337509, "name": "Italiano", "english_name": "Italian"}, "vo": {"articles": 120479, "name": "Volapük", "english_name": "Volapük"}, "ay": {"articles": 3900, "name": "Aymar", "english_name": "Aymara"}, "as": {"articles": 4462, "name": "অসমীয়া", "english_name": "Assamese"}, "ar": {"articles": 468031, "name": "العربية", "english_name": "Arabic"}, "lbe": {"articles": 1210, "name": "Лакку", "english_name": "Lak"}, "km": {"articles": 5029, "name": "ភាសាខ្មែរ", "english_name": "Khmer"}, "io": {"articles": 26917, "name": "Ido", "english_name": "Ido"}, "av": {"articles": 2302, "name": "Авар", "english_name": "Avar"}, "ia": {"articles": 19677, "name": "Interlingua", "english_name": "Interlingua"}, "haw": {"articles": 1976, "name": "Hawai`i", "english_name": "Hawaiian"}, "az": {"articles": 114754, "name": "Azərbaycanca", "english_name": "Azerbaijani"}, "ie": {"articles": 3594, "name": "Interlingue", "english_name": "Interlingue"}, "id": {"articles": 394114, "name": "Bahasa Indonesia", "english_name": "Indonesian"}, "nds-nl": {"articles": 6724, "name": "Nedersaksisch", "english_name": "Dutch Low Saxon"}, "pap": {"articles": 1769, "name": "Papiamentu", "english_name": "Papiamentu"}, "ks": {"articles": 268, "name": "कश्मीरी / كشميري", "english_name": "Kashmiri"}, "nl": {"articles": 1894647, "name": "Nederlands", "english_name": "Dutch"}, "nn": {"articles": 132461, "name": "Nynorsk", "english_name": "Norwegian (Nynorsk)"}, "no": {"articles": 462394, "name": "Norsk (Bokmål)", "english_name": "Norwegian (Bokmål)"}, "na": {"articles": 1269, "name": "dorerin Naoero", "english_name": "Nauruan"}, "nah": {"articles": 8763, "name": "Nāhuatl", "english_name": "Nahuatl"}, "ne": {"articles": 29974, "name": "नेपाली", "english_name": "Nepali"}, "lij": {"articles": 3236, "name": "Líguru", "english_name": "Ligurian"}, "csb": {"articles": 5128, "name": "Kaszëbsczi", "english_name": "Kashubian"}, "tyv": {"articles": 1366, "name": "Тыва", "english_name": "Tuvan"}, "ny": {"articles": 364, "name": "Chichewa", "english_name": "Chichewa"}, "nap": {"articles": 14434, "name": "Nnapulitano", "english_name": "Neapolitan"}, "ig": {"articles": 1308, "name": "Igbo", "english_name": "Igbo"}, "pag": {"articles": 2526, "name": "Pangasinan", "english_name": "Pangasinan"}, "zu": {"articles": 918, "name": "isiZulu", "english_name": "Zulu"}, "kw": {"articles": 3755, "name": "Kernewek/Karnuack", "english_name": "Cornish"}, "pam": {"articles": 8525, "name": "Kapampangan", "english_name": "Kapampangan"}, "nv": {"articles": 2638, "name": "Diné bizaad", "english_name": "Navajo"}, "sn": {"articles": 2678, "name": "chiShona", "english_name": "Shona"}, "kab": {"articles": 2842, "name": "Taqbaylit", "english_name": "Kabyle"}, "fr": {"articles": 1846517, "name": "Français", "english_name": "French"}, "mrj": {"articles": 10165, "name": "Кырык Мары (Kyryk Mary)", "english_name": "Hill Mari"}, "zh-yue": {"articles": 51227, "name": "粵語", "english_name": "Cantonese"}, "fy": {"articles": 37193, "name": "Frysk", "english_name": "West Frisian"}, "pnt": {"articles": 448, "name": "Ποντιακά", "english_name": "Pontic"}, "fa": {"articles": 524259, "name": "فارسی", "english_name": "Persian"}, "rw": {"articles": 1804, "name": "Ikinyarwanda", "english_name": "Kinyarwanda"}, "ff": {"articles": 213, "name": "Fulfulde", "english_name": "Fula"}, "mai": {"articles": 10392, "name": "मैथिली", "english_name": "Maithili"}, "fi": {"articles": 409282, "name": "Suomi", "english_name": "Finnish"}, "mzn": {"articles": 12372, "name": "مَزِروني", "english_name": "Mazandarani"}, "ab": {"articles": 1217, "name": "Аҧсуа", "english_name": "Abkhazian"}, "sa": {"articles": 10745, "name": "संस्कृतम्", "english_name": "Sanskrit"}, "zh-classical": {"articles": 5142, "name": "古文 / 文言文", "english_name": "Classical Chinese"}, "fo": {"articles": 12449, "name": "Føroyskt", "english_name": "Faroese"}, "bcl": {"articles": 7020, "name": "Bikol", "english_name": "Central Bicolano"}, "ka": {"articles": 113079, "name": "ქართული", "english_name": "Georgian"}, "nov": {"articles": 1645, "name": "Novial", "english_name": "Novial"}, "ckb": {"articles": 18597, "name": "Soranî / کوردی", "english_name": "Sorani"}, "kk": {"articles": 218109, "name": "Қазақша", "english_name": "Kazakh"}, "sr": {"articles": 345474, "name": "Српски / Srpski", "english_name": "Serbian"}, "sq": {"articles": 64573, "name": "Shqip", "english_name": "Albanian"}, "min": {"articles": 221972, "name": "Minangkabau", "english_name": "Minangkabau"}, "ko": {"articles": 373631, "name": "한국어", "english_name": "Korean"}, "sv": {"articles": 3783165, "name": "Svenska", "english_name": "Swedish"}, "su": {"articles": 19256, "name": "Basa Sunda", "english_name": "Sundanese"}, "kl": {"articles": 1643, "name": "Kalaallisut", "english_name": "Greenlandic"}, "sk": {"articles": 216444, "name": "Slovenčina", "english_name": "Slovak"}, "si": {"articles": 13236, "name": "සිංහල", "english_name": "Sinhalese"}, "sh": {"articles": 437610, "name": "Srpskohrvatski / Српскохрватски", "english_name": "Serbo-Croatian"}, "so": {"articles": 4363, "name": "Soomaali", "english_name": "Somali"}, "kv": {"articles": 4925, "name": "Коми", "english_name": "Komi"}, "ku": {"articles": 22705, "name": "Kurdî / كوردی", "english_name": "Kurdish"}, "sl": {"articles": 154822, "name": "Slovenščina", "english_name": "Slovenian"}, "sc": {"articles": 5447, "name": "Sardu", "english_name": "Sardinian"}, "ky": {"articles": 62712, "name": "Кыргызча", "english_name": "Kirghiz"}, "sg": {"articles": 247, "name": "Sängö", "english_name": "Sango"}, "sw": {"articles": 35324, "name": "Kiswahili", "english_name": "Swahili"}, "se": {"articles": 7278, "name": "Sámegiella", "english_name": "Northern Sami"}, "sd": {"articles": 7341, "name": "سنڌي، سندھی ، सिन्ध", "english_name": "Sindhi"}}, "dailymotion": {"gv": {"english_name": "Manx"}, "gu": {"name": "ગુજરાતી", "english_name": "Gujarati"}, "gd": {"english_name": "Gaelic, Scottish"}, "ga": {"name": "Gaeilge", "english_name": "Irish"}, "gn": {"english_name": "Guarani"}, "gl": {"name": "Galego", "english_name": "Galician"}, "lg": {"english_name": "Ganda"}, "lb": {"english_name": "Luxembourgish"}, "la": {"english_name": "Latin"}, "ln": {"english_name": "Lingala"}, "lo": {"english_name": "Lao"}, "tt": {"name": "Татарча", "english_name": "Tatar"}, "tr": {"name": "Türkçe", "english_name": "Turkish"}, "ts": {"english_name": "Tsonga"}, "li": {"english_name": "Limburgan"}, "lv": {"name": "Latviešu", "english_name": "Latvian"}, "to": {"english_name": "Tonga (Tonga Islands)"}, "lt": {"name": "Lietuvių", "english_name": "Lithuanian"}, "lu": {"english_name": "Luba-Katanga"}, "tk": {"english_name": "Turkmen"}, "th": {"name": "ไทย", "english_name": "Thai"}, "ti": {"name": "ትግርኛ", "english_name": "Tigrinya"}, "tg": {"english_name": "Tajik"}, "te": {"english_name": "Telugu"}, "ta": {"name": "தமிழ்", "english_name": "Tamil"}, "yi": {"english_name": "Yiddish"}, "yo": {"english_name": "Yoruba"}, "de": {"name": "Deutsch", "english_name": "German"}, "da": {"name": "Dansk", "english_name": "Danish"}, "dz": {"english_name": "Dzongkha"}, "st": {"english_name": "Sotho, Southern"}, "dv": {"english_name": "Dhivehi"}, "qu": {"english_name": "Quechua"}, "el": {"name": "Ελληνικά", "english_name": "Greek, Modern (1453-)"}, "eo": {"name": "Esperanto", "english_name": "Esperanto"}, "en": {"english_name": "English"}, "zh": {"name": "中文", "english_name": "Chinese"}, "ee": {"english_name": "Ewe"}, "za": {"english_name": "Zhuang"}, "mh": {"english_name": "Marshallese"}, "uk": {"name": "українська", "english_name": "Ukrainian"}, "eu": {"name": "Euskara", "english_name": "Basque"}, "et": {"name": "Eesti", "english_name": "Estonian"}, "es": {"name": "Español", "english_name": "Spanish"}, "ru": {"name": "русский", "english_name": "Russian"}, "rw": {"name": "Ikinyarwanda", "english_name": "Kinyarwanda"}, "rm": {"english_name": "Romansh"}, "rn": {"english_name": "Rundi"}, "ro": {"name": "Română", "english_name": "Romanian"}, "bn": {"name": "বাংলা", "english_name": "Bengali"}, "be": {"english_name": "Belarusian"}, "bg": {"name": "Български", "english_name": "Bulgarian"}, "ba": {"english_name": "Bashkir"}, "wa": {"name": "Walon", "english_name": "Walloon"}, "wo": {"english_name": "Wolof"}, "bm": {"english_name": "Bambara"}, "jv": {"english_name": "Javanese"}, "bo": {"english_name": "Tibetan"}, "bi": {"english_name": "Bislama"}, "br": {"name": "Brezhoneg", "english_name": "Breton"}, "bs": {"name": "Bosnian", "english_name": "Bosnian"}, "ja": {"name": "日本語", "english_name": "Japanese"}, "om": {"english_name": "Oromo"}, "oj": {"english_name": "Ojibwa"}, "ty": {"english_name": "Tahitian"}, "oc": {"name": "Occitan", "english_name": "Occitan"}, "tw": {"english_name": "Twi"}, "os": {"english_name": "Ossetian"}, "or": {"name": "Oriya", "english_name": "Oriya"}, "xh": {"name": "Xhosa", "english_name": "Xhosa"}, "ch": {"english_name": "Chamorro"}, "co": {"english_name": "Corsican"}, "ca": {"name": "Català", "english_name": "Catalan"}, "ce": {"english_name": "Chechen"}, "cy": {"name": "Cymraeg", "english_name": "Welsh"}, "cs": {"name": "čeština", "english_name": "Czech"}, "cr": {"english_name": "Cree"}, "cv": {"english_name": "Chuvash"}, "cu": {"english_name": "Slavic, Church"}, "ve": {"name": "Venda", "english_name": "Venda"}, "ps": {"name": "Pushto", "english_name": "Pushto"}, "pt": {"name": "Português", "english_name": "Portuguese"}, "tl": {"english_name": "Tagalog"}, "pa": {"name": "ਪੰਜਾਬੀ", "english_name": "Panjabi"}, "vi": {"name": "Tiếng Việt", "english_name": "Vietnamese"}, "pi": {"english_name": "Pali"}, "is": {"name": "Íslenska", "english_name": "Icelandic"}, "pl": {"name": "polski", "english_name": "Polish"}, "hz": {"english_name": "Herero"}, "hy": {"english_name": "Armenian"}, "hr": {"name": "hrvatski", "english_name": "Croatian"}, "iu": {"english_name": "Inuktitut"}, "ht": {"english_name": "Haitian"}, "hu": {"name": "magyar", "english_name": "Hungarian"}, "hi": {"name": "हिंदी", "english_name": "Hindi"}, "ho": {"english_name": "Hiri Motu"}, "ha": {"english_name": "Hausa"}, "he": {"name": "עברית", "english_name": "Hebrew"}, "mg": {"english_name": "Malagasy"}, "uz": {"english_name": "Uzbek"}, "ml": {"english_name": "Malayalam"}, "mn": {"name": "Монгол", "english_name": "Mongolian"}, "mi": {"name": "Reo Māori", "english_name": "Maori"}, "ik": {"english_name": "Inupiaq"}, "mk": {"name": "Македонски", "english_name": "Macedonian"}, "ur": {"english_name": "Urdu"}, "mt": {"name": "Malti", "english_name": "Maltese"}, "ms": {"name": "Malay", "english_name": "Malay"}, "mr": {"name": "मराठी", "english_name": "Marathi"}, "ug": {"english_name": "Uighur"}, "my": {"english_name": "Burmese"}, "sq": {"english_name": "Albanian"}, "ae": {"english_name": "Avestan"}, "ss": {"english_name": "Swati"}, "af": {"name": "Afrikaans", "english_name": "Afrikaans"}, "tn": {"english_name": "Tswana"}, "sw": {"english_name": "Swahili (macrolanguage)"}, "ak": {"english_name": "Akan"}, "am": {"name": "አማርኛ", "english_name": "Amharic"}, "it": {"name": "Italiano", "english_name": "Italian"}, "an": {"english_name": "Aragonese"}, "ii": {"english_name": "Yi, Sichuan"}, "ia": {"english_name": "Interlingua"}, "as": {"english_name": "Assamese"}, "ar": {"name": "العربية", "english_name": "Arabic"}, "su": {"english_name": "Sundanese"}, "io": {"english_name": "Ido"}, "av": {"english_name": "Avaric"}, "ay": {"english_name": "Aymara"}, "az": {"name": "Azerbaijani", "english_name": "Azerbaijani"}, "ie": {"english_name": "Interlingue"}, "id": {"name": "Indonesian", "english_name": "Indonesian"}, "ig": {"english_name": "Igbo"}, "sk": {"name": "Slovenský", "english_name": "Slovak"}, "sr": {"name": "српски", "english_name": "Serbian"}, "nl": {"name": "Nederlands", "english_name": "Dutch"}, "nn": {"name": "Norwegian Nynorsk", "english_name": "Norwegian Nynorsk"}, "no": {"english_name": "Norwegian"}, "na": {"english_name": "Nauru"}, "nb": {"name": "Norwegian Bokmål", "english_name": "Norwegian Bokmål"}, "nd": {"english_name": "Ndebele, North"}, "ne": {"english_name": "Nepali (macrolanguage)"}, "ng": {"english_name": "Ndonga"}, "ny": {"english_name": "Nyanja"}, "vo": {"english_name": "Volapük"}, "zu": {"name": "Isi-Zulu", "english_name": "Zulu"}, "so": {"english_name": "Somali"}, "nr": {"english_name": "Ndebele, South"}, "nv": {"english_name": "Navajo"}, "sn": {"english_name": "Shona"}, "fr": {"name": "français", "english_name": "French"}, "sm": {"english_name": "Samoan"}, "fy": {"english_name": "Frisian, Western"}, "sv": {"name": "Svenska", "english_name": "Swedish"}, "fa": {"name": "فارسی", "english_name": "Persian"}, "ff": {"english_name": "Fulah"}, "fi": {"name": "suomi", "english_name": "Finnish"}, "fj": {"english_name": "Fijian"}, "sa": {"english_name": "Sanskrit"}, "fo": {"english_name": "Faroese"}, "ka": {"english_name": "Georgian"}, "kg": {"english_name": "Kongo"}, "kk": {"english_name": "Kazakh"}, "kj": {"english_name": "Kuanyama"}, "ki": {"english_name": "Kikuyu"}, "ko": {"name": "한국어", "english_name": "Korean"}, "kn": {"name": "ಕನ್ನಡ", "english_name": "Kannada"}, "km": {"english_name": "Khmer, Central"}, "kl": {"english_name": "Kalaallisut"}, "ks": {"english_name": "Kashmiri"}, "kr": {"english_name": "Kanuri"}, "si": {"english_name": "Sinhala"}, "sh": {"name": "Serbo-Croatian", "english_name": "Serbo-Croatian"}, "kw": {"english_name": "Cornish"}, "kv": {"english_name": "Komi"}, "ku": {"english_name": "Kurdish"}, "sl": {"name": "slovenščina", "english_name": "Slovenian"}, "sc": {"english_name": "Sardinian"}, "ky": {"english_name": "Kirghiz"}, "sg": {"english_name": "Sango"}, "se": {"english_name": "Sami, Northern"}, "sd": {"english_name": "Sindhi"}}, "yahoo news": ["ar", "bg", "zh-chs", "zh-cht", "hr", "cs", "da", "nl", "en", "et", "fi", "fr", "de", "el", "he", "hu", "it", "ja", "ko", "lv", "lt", "no", "pl", "pt", "ro", "ru", "sk", "sl", "es", "sv", "th", "tr"], "swisscows": ["browser", "ar-SA", "es-AR", "en-AU", "de-AT", "fr-BE", "nl-BE", "pt-BR", "en-CA", "fr-CA", "es-CL", "zh-CN", "da-DK", "fi-FI", "fr-FR", "de-DE", "zh-HK", "en-IN", "en-IE", "it-IT", "ja-JP", "ko-KR", "en-MY", "es-MX", "nl-NL", "en-NZ", "no-NO", "en-PH", "pl-PL", "pt-PT", "ru-RU", "en-ZA", "es-ES", "sv-SE", "de-CH", "fr-CH", "zh-TW", "tr-TR", "en-GB", "en-US", "es-US"], "qwant images": ["el-GR", "en-GB", "en-IE", "en-CY", "en-GD", "en-US", "en-CA", "en-SG", "en-IN", "en-MY", "en-AU", "en-PH", "en-NZ", "co-FR", "vi-VN", "it-IT", "it-CH", "cy-GB", "ar-SA", "et-EE", "cs-CZ", "zh-TW", "id-ID", "es-ES", "es-AR", "es-CL", "es-CO", "es-MX", "es-PE", "ru-RU", "nl-BE", "nl-NL", "pt-BR", "pt-PT", "no-NO", "tr-TR", "th-TH", "ro-RO", "pl-PL", "fr-FR", "fr-BE", "fr-CH", "fr-CA", "bg-BG", "de-DE", "de-CH", "de-AT", "hu-HU", "fi-FI", "da-DK", "ja-JP", "he-IL", "ko-KR", "sv-SE", "gd-GB", "ms-MY"], "wikidata": {"gv": {"articles": 4955, "name": "Gaelg", "english_name": "Manx"}, "sco": {"articles": 42405, "name": "Scots", "english_name": "Scots"}, "scn": {"articles": 25379, "name": "Sicilianu", "english_name": "Sicilian"}, "wuu": {"articles": 5688, "name": "吴语", "english_name": "Wu"}, "tcy": {"articles": 750, "name": "ತುಳು", "english_name": "Tulu"}, "cdo": {"articles": 4239, "name": "Mìng-dĕ̤ng-ngṳ̄", "english_name": "Min Dong"}, "gu": {"articles": 26842, "name": "ગુજરાતી", "english_name": "Gujarati"}, "kbd": {"articles": 1569, "name": "Адыгэбзэ (Adighabze)", "english_name": "Kabardian Circassian"}, "gd": {"articles": 14368, "name": "Gàidhlig", "english_name": "Scottish Gaelic"}, "jbo": {"articles": 1197, "name": "Lojban", "english_name": "Lojban"}, "ga": {"articles": 39393, "name": "Gaeilge", "english_name": "Irish"}, "gn": {"articles": 3133, "name": "Avañe'ẽ", "english_name": "Guarani"}, "gl": {"articles": 136535, "name": "Galego", "english_name": "Galician"}, "als": {"articles": 22592, "name": "Alemannisch", "english_name": "Alemannic"}, "lg": {"articles": 1135, "name": "Luganda", "english_name": "Luganda"}, "hak": {"articles": 7386, "name": "Hak-kâ-fa / 客家話", "english_name": "Hakka"}, "lb": {"articles": 48141, "name": "Lëtzebuergesch", "english_name": "Luxembourgish"}, "szl": {"articles": 5491, "name": "Ślůnski", "english_name": "Silesian"}, "vep": {"articles": 5339, "name": "Vepsän", "english_name": "Vepsian"}, "la": {"articles": 126249, "name": "Latina", "english_name": "Latin"}, "ln": {"articles": 2786, "name": "Lingala", "english_name": "Lingala"}, "frp": {"articles": 2608, "name": "Arpitan", "english_name": "Franco-Provençal"}, "tt": {"articles": 70258, "name": "Tatarça / Татарча", "english_name": "Tatar"}, "tr": {"articles": 292026, "name": "Türkçe", "english_name": "Turkish"}, "cbk-zam": {"articles": 2967, "name": "Chavacano de Zamboanga", "english_name": "Zamboanga Chavacano"}, "li": {"articles": 11620, "name": "Limburgs", "english_name": "Limburgish"}, "lv": {"articles": 75872, "name": "Latviešu", "english_name": "Latvian"}, "to": {"articles": 1688, "name": "faka Tonga", "english_name": "Tongan"}, "tl": {"articles": 66203, "name": "Tagalog", "english_name": "Tagalog"}, "jam": {"articles": 1597, "name": "Jumiekan Kryuol", "english_name": "Jamaican Patois"}, "vec": {"articles": 10877, "name": "Vèneto", "english_name": "Venetian"}, "th": {"articles": 114824, "name": "ไทย", "english_name": "Thai"}, "ti": {"articles": 175, "name": "ትግርኛ", "english_name": "Tigrinya"}, "tg": {"articles": 67713, "name": "Тоҷикӣ", "english_name": "Tajik"}, "te": {"articles": 66514, "name": "తెలుగు", "english_name": "Telugu"}, "ksh": {"articles": 2825, "name": "Ripoarisch", "english_name": "Ripuarian"}, "pcd": {"articles": 3402, "name": "Picard", "english_name": "Picard"}, "ta": {"articles": 91441, "name": "தமிழ்", "english_name": "Tamil"}, "yi": {"articles": 13744, "name": "ייִדיש", "english_name": "Yiddish"}, "lrc": {"articles": 5280, "name": "لۊری شومالی", "english_name": "Northern Luri"}, "xmf": {"articles": 9318, "name": "მარგალური (Margaluri)", "english_name": "Mingrelian"}, "ceb": {"articles": 4049908, "name": "Sinugboanong Binisaya", "english_name": "Cebuano"}, "yo": {"articles": 31528, "name": "Yorùbá", "english_name": "Yoruba"}, "de": {"articles": 2035837, "name": "Deutsch", "english_name": "German"}, "da": {"articles": 223944, "name": "Dansk", "english_name": "Danish"}, "za": {"articles": 1161, "name": "Cuengh", "english_name": "Zhuang"}, "pdc": {"articles": 1790, "name": "Deitsch", "english_name": "Pennsylvania German"}, "bxr": {"articles": 1873, "name": "Буряад", "english_name": "Buryat"}, "dz": {"articles": 217, "name": "ཇོང་ཁ", "english_name": "Dzongkha"}, "hif": {"articles": 9646, "name": "Fiji Hindi", "english_name": "Fiji Hindi"}, "rm": {"articles": 3417, "name": "Rumantsch", "english_name": "Romansh"}, "dv": {"articles": 2970, "name": "ދިވެހިބަސް", "english_name": "Divehi"}, "qu": {"articles": 20037, "name": "Runa Simi", "english_name": "Quechua"}, "vls": {"articles": 5988, "name": "West-Vlams", "english_name": "West Flemish"}, "bar": {"articles": 22085, "name": "Boarisch", "english_name": "Bavarian"}, "eml": {"articles": 8670, "name": "Emiliàn e rumagnòl", "english_name": "Emilian-Romagnol"}, "kn": {"articles": 21679, "name": "ಕನ್ನಡ", "english_name": "Kannada"}, "fiu-vro": {"articles": 5449, "name": "Võro", "english_name": "Võro"}, "mo": {"articles": 394, "name": "Молдовеняскэ", "english_name": "Moldovan"}, "bpy": {"articles": 25069, "name": "ইমার ঠার/বিষ্ণুপ্রিয়া মণিপুরী", "english_name": "Bishnupriya Manipuri"}, "crh": {"articles": 5113, "name": "Qırımtatarca", "english_name": "Crimean Tatar"}, "mhr": {"articles": 9413, "name": "Олык Марий (Olyk Marij)", "english_name": "Meadow Mari"}, "diq": {"articles": 7537, "name": "Zazaki", "english_name": "Zazaki"}, "el": {"articles": 127970, "name": "Ελληνικά", "english_name": "Greek"}, "eo": {"articles": 237478, "name": "Esperanto", "english_name": "Esperanto"}, "en": {"articles": 5343454, "name": "English", "english_name": "English"}, "zh": {"articles": 927993, "name": "中文", "english_name": "Chinese"}, "pms": {"articles": 64046, "name": "Piemontèis", "english_name": "Piedmontese"}, "ee": {"articles": 334, "name": "Eʋegbe", "english_name": "Ewe"}, "tpi": {"articles": 1351, "name": "Tok Pisin", "english_name": "Tok Pisin"}, "arz": {"articles": 16354, "name": "مصرى (Maṣri)", "english_name": "Egyptian Arabic"}, "rmy": {"articles": 583, "name": "romani - रोमानी", "english_name": "Romani"}, "mdf": {"articles": 1133, "name": "Мокшень (Mokshanj Kälj)", "english_name": "Moksha"}, "kaa": {"articles": 1955, "name": "Qaraqalpaqsha", "english_name": "Karakalpak"}, "olo": {"articles": 1934, "name": "Karjalan", "english_name": "Livvi-Karelian"}, "arc": {"articles": 1617, "name": "ܐܪܡܝܐ", "english_name": "Aramaic"}, "cr": {"articles": 125, "name": "Nehiyaw", "english_name": "Cree"}, "eu": {"articles": 277712, "name": "Euskara", "english_name": "Basque"}, "et": {"articles": 154506, "name": "Eesti", "english_name": "Estonian"}, "tet": {"articles": 1390, "name": "Tetun", "english_name": "Tetum"}, "es": {"articles": 1318337, "name": "Español", "english_name": "Spanish"}, "ba": {"articles": 37407, "name": "Башҡорт", "english_name": "Bashkir"}, "gom": {"articles": 3119, "name": "गोंयची कोंकणी / Gõychi Konknni", "english_name": "Goan Konkani"}, "ru": {"articles": 1375970, "name": "Русский", "english_name": "Russian"}, "roa-tara": {"articles": 9229, "name": "Tarandíne", "english_name": "Tarantino"}, "ha": {"articles": 1410, "name": "هَوُسَ", "english_name": "Hausa"}, "ak": {"articles": 271, "name": "Akana", "english_name": "Akan"}, "lad": {"articles": 4421, "name": "Dzhudezmo", "english_name": "Ladino"}, "bm": {"articles": 411, "name": "Bamanankan", "english_name": "Bambara"}, "new": {"articles": 72123, "name": "नेपाल भाषा", "english_name": "Newar"}, "rn": {"articles": 495, "name": "Kirundi", "english_name": "Kirundi"}, "ro": {"articles": 374753, "name": "Română", "english_name": "Romanian"}, "dsb": {"articles": 3071, "name": "Dolnoserbski", "english_name": "Lower Sorbian"}, "jv": {"articles": 49700, "name": "Basa Jawa", "english_name": "Javanese"}, "hsb": {"articles": 11071, "name": "Hornjoserbsce", "english_name": "Upper Sorbian"}, "be": {"articles": 126878, "name": "Беларуская", "english_name": "Belarusian"}, "bg": {"articles": 227507, "name": "Български", "english_name": "Bulgarian"}, "myv": {"articles": 3510, "name": "Эрзянь (Erzjanj Kelj)", "english_name": "Erzya"}, "uk": {"articles": 682728, "name": "Українська", "english_name": "Ukrainian"}, "wa": {"articles": 14408, "name": "Walon", "english_name": "Walloon"}, "ast": {"articles": 48457, "name": "Asturianu", "english_name": "Asturian"}, "wo": {"articles": 1132, "name": "Wolof", "english_name": "Wolof"}, "got": {"articles": 493, "name": "𐌲𐌿𐍄𐌹𐍃𐌺", "english_name": "Gothic"}, "bn": {"articles": 48261, "name": "বাংলা", "english_name": "Bengali"}, "bo": {"articles": 5717, "name": "བོད་སྐད", "english_name": "Tibetan"}, "bh": {"articles": 8674, "name": "भोजपुरी", "english_name": "Bihari"}, "bi": {"articles": 817, "name": "Bislama", "english_name": "Bislama"}, "rue": {"articles": 5963, "name": "Русиньскый", "english_name": "Rusyn"}, "map-bms": {"articles": 13284, "name": "Basa Banyumasan", "english_name": "Banyumasan"}, "tum": {"articles": 564, "name": "chiTumbuka", "english_name": "Tumbuka"}, "br": {"articles": 61346, "name": "Brezhoneg", "english_name": "Breton"}, "bs": {"articles": 73437, "name": "Bosanski", "english_name": "Bosnian"}, "lez": {"articles": 3606, "name": "Лезги чІал (Lezgi č’al)", "english_name": "Lezgian"}, "ja": {"articles": 1050743, "name": "日本語", "english_name": "Japanese"}, "om": {"articles": 725, "name": "Oromoo", "english_name": "Oromo"}, "glk": {"articles": 6076, "name": "گیلکی", "english_name": "Gilaki"}, "ace": {"articles": 4077, "name": "Bahsa Acèh", "english_name": "Acehnese"}, "ilo": {"articles": 10461, "name": "Ilokano", "english_name": "Ilokano"}, "roa-rup": {"articles": 1206, "name": "Armãneashce", "english_name": "Aromanian"}, "oc": {"articles": 82700, "name": "Occitan", "english_name": "Occitan"}, "ltg": {"articles": 797, "name": "Latgaļu", "english_name": "Latgalian"}, "be-tarask": {"articles": 60248, "name": "Беларуская (тарашкевіца)", "english_name": "Belarusian (Taraškievica)"}, "st": {"articles": 384, "name": "Sesotho", "english_name": "Sesotho"}, "lo": {"articles": 1623, "name": "ລາວ", "english_name": "Lao"}, "krc": {"articles": 2016, "name": "Къарачай-Малкъар (Qarachay-Malqar)", "english_name": "Karachay-Balkar"}, "nds": {"articles": 25822, "name": "Plattdüütsch", "english_name": "Low Saxon"}, "os": {"articles": 10382, "name": "Иронау", "english_name": "Ossetian"}, "or": {"articles": 12252, "name": "ଓଡ଼ିଆ", "english_name": "Oriya"}, "udm": {"articles": 3875, "name": "Удмурт кыл", "english_name": "Udmurt"}, "xh": {"articles": 604, "name": "isiXhosa", "english_name": "Xhosa"}, "ch": {"articles": 419, "name": "Chamoru", "english_name": "Chamorro"}, "co": {"articles": 5424, "name": "Corsu", "english_name": "Corsican"}, "nso": {"articles": 7642, "name": "Sepedi", "english_name": "Northern Sotho"}, "simple": {"articles": 123305, "name": "Simple English", "english_name": "Simple English"}, "bjn": {"articles": 1710, "name": "Bahasa Banjar", "english_name": "Banjar"}, "ca": {"articles": 535045, "name": "Català", "english_name": "Catalan"}, "lmo": {"articles": 34788, "name": "Lumbaart", "english_name": "Lombard"}, "ce": {"articles": 160122, "name": "Нохчийн", "english_name": "Chechen"}, "ts": {"articles": 392, "name": "Xitsonga", "english_name": "Tsonga"}, "cy": {"articles": 90328, "name": "Cymraeg", "english_name": "Welsh"}, "ang": {"articles": 2857, "name": "Englisc", "english_name": "Anglo-Saxon"}, "cs": {"articles": 374940, "name": "Čeština", "english_name": "Czech"}, "ty": {"articles": 1179, "name": "Reo Mā`ohi", "english_name": "Tahitian"}, "ady": {"articles": 399, "name": "Адыгэбзэ", "english_name": "Adyghe"}, "cv": {"articles": 38132, "name": "Чăваш", "english_name": "Chuvash"}, "cu": {"articles": 582, "name": "Словѣньскъ", "english_name": "Old Church Slavonic"}, "ve": {"articles": 238, "name": "Tshivenda", "english_name": "Venda"}, "koi": {"articles": 3429, "name": "Перем Коми (Perem Komi)", "english_name": "Komi-Permyak"}, "ps": {"articles": 7991, "name": "پښتو", "english_name": "Pashto"}, "fj": {"articles": 340, "name": "Na Vosa Vakaviti", "english_name": "Fijian"}, "srn": {"articles": 1047, "name": "Sranantongo", "english_name": "Sranan"}, "pt": {"articles": 957637, "name": "Português", "english_name": "Portuguese"}, "sm": {"articles": 745, "name": "Gagana Samoa", "english_name": "Samoan"}, "ext": {"articles": 2898, "name": "Estremeñu", "english_name": "Extremaduran"}, "lt": {"articles": 181095, "name": "Lietuvių", "english_name": "Lithuanian"}, "zh-min-nan": {"articles": 203047, "name": "Bân-lâm-gú", "english_name": "Min Nan"}, "frr": {"articles": 4712, "name": "Nordfriisk", "english_name": "North Frisian"}, "chr": {"articles": 785, "name": "ᏣᎳᎩ", "english_name": "Cherokee"}, "pa": {"articles": 24776, "name": "ਪੰਜਾਬੀ", "english_name": "Punjabi"}, "xal": {"articles": 2073, "name": "Хальмг", "english_name": "Kalmyk"}, "chy": {"articles": 607, "name": "Tsetsêhestâhese", "english_name": "Cheyenne"}, "pi": {"articles": 2517, "name": "पाऴि", "english_name": "Pali"}, "war": {"articles": 1262274, "name": "Winaray", "english_name": "Waray-Waray"}, "pl": {"articles": 1209184, "name": "Polski", "english_name": "Polish"}, "tk": {"articles": 5193, "name": "تركمن / Туркмен", "english_name": "Turkmen"}, "hy": {"articles": 216349, "name": "Հայերեն", "english_name": "Armenian"}, "an": {"articles": 31888, "name": "Aragonés", "english_name": "Aragonese"}, "nrm": {"articles": 3621, "name": "Nouormand/Normaund", "english_name": "Norman"}, "hr": {"articles": 172218, "name": "Hrvatski", "english_name": "Croatian"}, "iu": {"articles": 391, "name": "ᐃᓄᒃᑎᑐᑦ", "english_name": "Inuktitut"}, "pfl": {"articles": 2067, "name": "Pälzisch", "english_name": "Palatinate German"}, "ht": {"articles": 51150, "name": "Krèyol ayisyen", "english_name": "Haitian"}, "hu": {"articles": 404333, "name": "Magyar", "english_name": "Hungarian"}, "gan": {"articles": 6367, "name": "贛語", "english_name": "Gan"}, "bat-smg": {"articles": 16015, "name": "Žemaitėška", "english_name": "Samogitian"}, "hi": {"articles": 117254, "name": "हिन्दी", "english_name": "Hindi"}, "tw": {"articles": 584, "name": "Twi", "english_name": "Twi"}, "gag": {"articles": 2744, "name": "Gagauz", "english_name": "Gagauz"}, "kg": {"articles": 1170, "name": "KiKongo", "english_name": "Kongo"}, "pnb": {"articles": 43662, "name": "شاہ مکھی پنجابی (Shāhmukhī Pañjābī)", "english_name": "Western Punjabi"}, "bug": {"articles": 14118, "name": "Basa Ugi", "english_name": "Buginese"}, "he": {"articles": 202507, "name": "עברית", "english_name": "Hebrew"}, "mg": {"articles": 83434, "name": "Malagasy", "english_name": "Malagasy"}, "fur": {"articles": 3173, "name": "Furlan", "english_name": "Friulian"}, "uz": {"articles": 128882, "name": "O‘zbek", "english_name": "Uzbek"}, "ml": {"articles": 47909, "name": "മലയാളം", "english_name": "Malayalam"}, "azb": {"articles": 14683, "name": "تۆرکجه", "english_name": "South Azerbaijani"}, "mn": {"articles": 16812, "name": "Монгол", "english_name": "Mongolian"}, "mi": {"articles": 7112, "name": "Māori", "english_name": "Maori"}, "ik": {"articles": 246, "name": "Iñupiak", "english_name": "Inupiak"}, "mk": {"articles": 88524, "name": "Македонски", "english_name": "Macedonian"}, "ur": {"articles": 115102, "name": "اردو", "english_name": "Urdu"}, "zea": {"articles": 4369, "name": "Zeêuws", "english_name": "Zeelandic"}, "mt": {"articles": 3190, "name": "Malti", "english_name": "Maltese"}, "stq": {"articles": 3754, "name": "Seeltersk", "english_name": "Saterland Frisian"}, "ms": {"articles": 287146, "name": "Bahasa Melayu", "english_name": "Malay"}, "mr": {"articles": 46054, "name": "मराठी", "english_name": "Marathi"}, "ug": {"articles": 3273, "name": "ئۇيغۇر تىلى", "english_name": "Uyghur"}, "mwl": {"articles": 2828, "name": "Mirandés", "english_name": "Mirandese"}, "my": {"articles": 34904, "name": "မြန်မာဘာသာ", "english_name": "Burmese"}, "ki": {"articles": 1339, "name": "Gĩkũyũ", "english_name": "Kikuyu"}, "pih": {"articles": 514, "name": "Norfuk", "english_name": "Norfolk"}, "sah": {"articles": 11079, "name": "Саха тыла (Saxa Tyla)", "english_name": "Sakha"}, "ss": {"articles": 421, "name": "SiSwati", "english_name": "Swati"}, "af": {"articles": 43924, "name": "Afrikaans", "english_name": "Afrikaans"}, "tn": {"articles": 620, "name": "Setswana", "english_name": "Tswana"}, "vi": {"articles": 1153964, "name": "Tiếng Việt", "english_name": "Vietnamese"}, "is": {"articles": 41946, "name": "Íslenska", "english_name": "Icelandic"}, "am": {"articles": 13359, "name": "አማርኛ", "english_name": "Amharic"}, "it": {"articles": 1337509, "name": "Italiano", "english_name": "Italian"}, "vo": {"articles": 120479, "name": "Volapük", "english_name": "Volapük"}, "ay": {"articles": 3900, "name": "Aymar", "english_name": "Aymara"}, "as": {"articles": 4462, "name": "অসমীয়া", "english_name": "Assamese"}, "ar": {"articles": 468031, "name": "العربية", "english_name": "Arabic"}, "lbe": {"articles": 1210, "name": "Лакку", "english_name": "Lak"}, "km": {"articles": 5029, "name": "ភាសាខ្មែរ", "english_name": "Khmer"}, "io": {"articles": 26917, "name": "Ido", "english_name": "Ido"}, "av": {"articles": 2302, "name": "Авар", "english_name": "Avar"}, "ia": {"articles": 19677, "name": "Interlingua", "english_name": "Interlingua"}, "haw": {"articles": 1976, "name": "Hawai`i", "english_name": "Hawaiian"}, "az": {"articles": 114754, "name": "Azərbaycanca", "english_name": "Azerbaijani"}, "ie": {"articles": 3594, "name": "Interlingue", "english_name": "Interlingue"}, "id": {"articles": 394114, "name": "Bahasa Indonesia", "english_name": "Indonesian"}, "nds-nl": {"articles": 6724, "name": "Nedersaksisch", "english_name": "Dutch Low Saxon"}, "pap": {"articles": 1769, "name": "Papiamentu", "english_name": "Papiamentu"}, "ks": {"articles": 268, "name": "कश्मीरी / كشميري", "english_name": "Kashmiri"}, "nl": {"articles": 1894647, "name": "Nederlands", "english_name": "Dutch"}, "nn": {"articles": 132461, "name": "Nynorsk", "english_name": "Norwegian (Nynorsk)"}, "no": {"articles": 462394, "name": "Norsk (Bokmål)", "english_name": "Norwegian (Bokmål)"}, "na": {"articles": 1269, "name": "dorerin Naoero", "english_name": "Nauruan"}, "nah": {"articles": 8763, "name": "Nāhuatl", "english_name": "Nahuatl"}, "ne": {"articles": 29974, "name": "नेपाली", "english_name": "Nepali"}, "lij": {"articles": 3236, "name": "Líguru", "english_name": "Ligurian"}, "csb": {"articles": 5128, "name": "Kaszëbsczi", "english_name": "Kashubian"}, "tyv": {"articles": 1366, "name": "Тыва", "english_name": "Tuvan"}, "ny": {"articles": 364, "name": "Chichewa", "english_name": "Chichewa"}, "nap": {"articles": 14434, "name": "Nnapulitano", "english_name": "Neapolitan"}, "ig": {"articles": 1308, "name": "Igbo", "english_name": "Igbo"}, "pag": {"articles": 2526, "name": "Pangasinan", "english_name": "Pangasinan"}, "zu": {"articles": 918, "name": "isiZulu", "english_name": "Zulu"}, "kw": {"articles": 3755, "name": "Kernewek/Karnuack", "english_name": "Cornish"}, "pam": {"articles": 8525, "name": "Kapampangan", "english_name": "Kapampangan"}, "nv": {"articles": 2638, "name": "Diné bizaad", "english_name": "Navajo"}, "sn": {"articles": 2678, "name": "chiShona", "english_name": "Shona"}, "kab": {"articles": 2842, "name": "Taqbaylit", "english_name": "Kabyle"}, "fr": {"articles": 1846517, "name": "Français", "english_name": "French"}, "mrj": {"articles": 10165, "name": "Кырык Мары (Kyryk Mary)", "english_name": "Hill Mari"}, "zh-yue": {"articles": 51227, "name": "粵語", "english_name": "Cantonese"}, "fy": {"articles": 37193, "name": "Frysk", "english_name": "West Frisian"}, "pnt": {"articles": 448, "name": "Ποντιακά", "english_name": "Pontic"}, "fa": {"articles": 524259, "name": "فارسی", "english_name": "Persian"}, "rw": {"articles": 1804, "name": "Ikinyarwanda", "english_name": "Kinyarwanda"}, "ff": {"articles": 213, "name": "Fulfulde", "english_name": "Fula"}, "mai": {"articles": 10392, "name": "मैथिली", "english_name": "Maithili"}, "fi": {"articles": 409282, "name": "Suomi", "english_name": "Finnish"}, "mzn": {"articles": 12372, "name": "مَزِروني", "english_name": "Mazandarani"}, "ab": {"articles": 1217, "name": "Аҧсуа", "english_name": "Abkhazian"}, "sa": {"articles": 10745, "name": "संस्कृतम्", "english_name": "Sanskrit"}, "zh-classical": {"articles": 5142, "name": "古文 / 文言文", "english_name": "Classical Chinese"}, "fo": {"articles": 12449, "name": "Føroyskt", "english_name": "Faroese"}, "bcl": {"articles": 7020, "name": "Bikol", "english_name": "Central Bicolano"}, "ka": {"articles": 113079, "name": "ქართული", "english_name": "Georgian"}, "nov": {"articles": 1645, "name": "Novial", "english_name": "Novial"}, "ckb": {"articles": 18597, "name": "Soranî / کوردی", "english_name": "Sorani"}, "kk": {"articles": 218109, "name": "Қазақша", "english_name": "Kazakh"}, "sr": {"articles": 345474, "name": "Српски / Srpski", "english_name": "Serbian"}, "sq": {"articles": 64573, "name": "Shqip", "english_name": "Albanian"}, "min": {"articles": 221972, "name": "Minangkabau", "english_name": "Minangkabau"}, "ko": {"articles": 373631, "name": "한국어", "english_name": "Korean"}, "sv": {"articles": 3783165, "name": "Svenska", "english_name": "Swedish"}, "su": {"articles": 19256, "name": "Basa Sunda", "english_name": "Sundanese"}, "kl": {"articles": 1643, "name": "Kalaallisut", "english_name": "Greenlandic"}, "sk": {"articles": 216444, "name": "Slovenčina", "english_name": "Slovak"}, "si": {"articles": 13236, "name": "සිංහල", "english_name": "Sinhalese"}, "sh": {"articles": 437610, "name": "Srpskohrvatski / Српскохрватски", "english_name": "Serbo-Croatian"}, "so": {"articles": 4363, "name": "Soomaali", "english_name": "Somali"}, "kv": {"articles": 4925, "name": "Коми", "english_name": "Komi"}, "ku": {"articles": 22705, "name": "Kurdî / كوردی", "english_name": "Kurdish"}, "sl": {"articles": 154822, "name": "Slovenščina", "english_name": "Slovenian"}, "sc": {"articles": 5447, "name": "Sardu", "english_name": "Sardinian"}, "ky": {"articles": 62712, "name": "Кыргызча", "english_name": "Kirghiz"}, "sg": {"articles": 247, "name": "Sängö", "english_name": "Sango"}, "sw": {"articles": 35324, "name": "Kiswahili", "english_name": "Swahili"}, "se": {"articles": 7278, "name": "Sámegiella", "english_name": "Northern Sami"}, "sd": {"articles": 7341, "name": "سنڌي، سندھی ، सिन्ध", "english_name": "Sindhi"}}, "qwant news": ["el-GR", "en-GB", "en-IE", "en-CY", "en-GD", "en-US", "en-CA", "en-SG", "en-IN", "en-MY", "en-AU", "en-PH", "en-NZ", "co-FR", "vi-VN", "it-IT", "it-CH", "cy-GB", "ar-SA", "et-EE", "cs-CZ", "zh-TW", "id-ID", "es-ES", "es-AR", "es-CL", "es-CO", "es-MX", "es-PE", "ru-RU", "nl-BE", "nl-NL", "pt-BR", "pt-PT", "no-NO", "tr-TR", "th-TH", "ro-RO", "pl-PL", "fr-FR", "fr-BE", "fr-CH", "fr-CA", "bg-BG", "de-DE", "de-CH", "de-AT", "hu-HU", "fi-FI", "da-DK", "ja-JP", "he-IL", "ko-KR", "sv-SE", "gd-GB", "ms-MY"], "ddg definitions": ["da-DK", "vi-VN", "en-SG", "sl-SL", "en-XA", "tzh-HK", "en-UK", "ro-RO", "en-MY", "el-GR", "it-CH", "hu-HU", "fr-FR", "en-PH", "tl-PH", "fr-CA", "fi-FI", "et-EE", "sv-SE", "es-XL", "th-TH", "sk-SK", "es-ES", "en-IE", "es-US", "es-PE", "nl-NL", "en-US", "de-DE", "de-AT", "wt-WT", "no-NO", "tr-TR", "ca-ES", "it-IT", "es-CO", "ru-RU", "ca-CT", "en-ZA", "en-CA", "jp-JP", "es-MX", "id-ID", "es-AR", "he-IL", "kr-KR", "en-AU", "ms-MY", "pl-PL", "lv-LV", "bg-BG", "zh-CN", "en-NZ", "lt-LT", "tzh-TW", "hr-HR", "pt-PT", "fr-BE", "de-CH", "cs-CZ", "en-IN", "nl-BE", "fr-CH", "en-ID", "ar-XA", "pt-BR", "uk-UA", "es-CL"], "bing images": ["sq", "de", "ar", "bg", "ca", "cs", "zh-CHS", "zh-CHT", "ko", "hr", "da", "sk", "sl", "es", "et", "fi", "fr", "el", "he", "nl", "hu", "id", "en", "is", "it", "ja", "lv", "lt", "ms", "no", "fa", "pl", "pt-BR", "pt-PT", "ro", "ru", "sr", "sv", "th", "tr", "uk", "vi"], "qwant social": ["el-GR", "en-GB", "en-IE", "en-CY", "en-GD", "en-US", "en-CA", "en-SG", "en-IN", "en-MY", "en-AU", "en-PH", "en-NZ", "co-FR", "vi-VN", "it-IT", "it-CH", "cy-GB", "ar-SA", "et-EE", "cs-CZ", "zh-TW", "id-ID", "es-ES", "es-AR", "es-CL", "es-CO", "es-MX", "es-PE", "ru-RU", "nl-BE", "nl-NL", "pt-BR", "pt-PT", "no-NO", "tr-TR", "th-TH", "ro-RO", "pl-PL", "fr-FR", "fr-BE", "fr-CH", "fr-CA", "bg-BG", "de-DE", "de-CH", "de-AT", "hu-HU", "fi-FI", "da-DK", "ja-JP", "he-IL", "ko-KR", "sv-SE", "gd-GB", "ms-MY"], "qwant": ["el-GR", "en-GB", "en-IE", "en-CY", "en-GD", "en-US", "en-CA", "en-SG", "en-IN", "en-MY", "en-AU", "en-PH", "en-NZ", "co-FR", "vi-VN", "it-IT", "it-CH", "cy-GB", "ar-SA", "et-EE", "cs-CZ", "zh-TW", "id-ID", "es-ES", "es-AR", "es-CL", "es-CO", "es-MX", "es-PE", "ru-RU", "nl-BE", "nl-NL", "pt-BR", "pt-PT", "no-NO", "tr-TR", "th-TH", "ro-RO", "pl-PL", "fr-FR", "fr-BE", "fr-CH", "fr-CA", "bg-BG", "de-DE", "de-CH", "de-AT", "hu-HU", "fi-FI", "da-DK", "ja-JP", "he-IL", "ko-KR", "sv-SE", "gd-GB", "ms-MY"], "yahoo": ["ar", "bg", "zh-chs", "zh-cht", "hr", "cs", "da", "nl", "en", "et", "fi", "fr", "de", "el", "he", "hu", "it", "ja", "ko", "lv", "lt", "no", "pl", "pt", "ro", "ru", "sk", "sl", "es", "sv", "th", "tr"], "gigablast": []} \ No newline at end of file diff --git a/searx/engines/1337x.py b/searx/engines/1337x.py new file mode 100644 index 0000000..0de04bd --- /dev/null +++ b/searx/engines/1337x.py @@ -0,0 +1,39 @@ +from lxml import html +from searx.engines.xpath import extract_text +from searx.utils import get_torrent_size +from searx.url_utils import quote, urljoin + +url = 'https://1337x.to/' +search_url = url + 'search/{search_term}/{pageno}/' +categories = ['videos'] +paging = True + + +def request(query, params): + params['url'] = search_url.format(search_term=quote(query), pageno=params['pageno']) + + return params + + +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + for result in dom.xpath('//table[contains(@class, "table-list")]/tbody//tr'): + href = urljoin(url, result.xpath('./td[contains(@class, "name")]/a[2]/@href')[0]) + title = extract_text(result.xpath('./td[contains(@class, "name")]/a[2]')) + seed = extract_text(result.xpath('.//td[contains(@class, "seeds")]')) + leech = extract_text(result.xpath('.//td[contains(@class, "leeches")]')) + filesize_info = extract_text(result.xpath('.//td[contains(@class, "size")]/text()')) + filesize, filesize_multiplier = filesize_info.split() + filesize = get_torrent_size(filesize, filesize_multiplier) + + results.append({'url': href, + 'title': title, + 'seed': seed, + 'leech': leech, + 'filesize': filesize, + 'template': 'torrent.html'}) + + return results diff --git a/searx/engines/__init__.py b/searx/engines/__init__.py new file mode 100644 index 0000000..023ec40 --- /dev/null +++ b/searx/engines/__init__.py @@ -0,0 +1,221 @@ + +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2013- by Adam Tauber, +''' + +from os.path import realpath, dirname +import sys +from flask_babel import gettext +from operator import itemgetter +from json import loads +from requests import get +from searx import settings +from searx import logger +from searx.utils import load_module + + +logger = logger.getChild('engines') + +engine_dir = dirname(realpath(__file__)) + +engines = {} + +categories = {'general': []} + +languages = loads(open(engine_dir + '/../data/engines_languages.json').read()) + +engine_shortcuts = {} +engine_default_args = {'paging': False, + 'categories': ['general'], + 'language_support': True, + 'supported_languages': [], + 'safesearch': False, + 'timeout': settings['outgoing']['request_timeout'], + 'shortcut': '-', + 'disabled': False, + 'suspend_end_time': 0, + 'continuous_errors': 0, + 'time_range_support': False} + + +def load_engine(engine_data): + + if '_' in engine_data['name']: + logger.error('Engine name conains underscore: "{}"'.format(engine_data['name'])) + sys.exit(1) + + engine_module = engine_data['engine'] + + try: + engine = load_module(engine_module + '.py', engine_dir) + except: + logger.exception('Cannot load engine "{}"'.format(engine_module)) + return None + + for param_name in engine_data: + if param_name == 'engine': + continue + if param_name == 'categories': + if engine_data['categories'] == 'none': + engine.categories = [] + else: + engine.categories = list(map(str.strip, engine_data['categories'].split(','))) + continue + setattr(engine, param_name, engine_data[param_name]) + + for arg_name, arg_value in engine_default_args.items(): + if not hasattr(engine, arg_name): + setattr(engine, arg_name, arg_value) + + # checking required variables + for engine_attr in dir(engine): + if engine_attr.startswith('_'): + continue + if getattr(engine, engine_attr) is None: + logger.error('Missing engine config attribute: "{0}.{1}"' + .format(engine.name, engine_attr)) + sys.exit(1) + + # assign supported languages from json file + if engine_data['name'] in languages: + setattr(engine, 'supported_languages', languages[engine_data['name']]) + + # assign language fetching method if auxiliary method exists + if hasattr(engine, '_fetch_supported_languages'): + setattr(engine, 'fetch_supported_languages', + lambda: engine._fetch_supported_languages(get(engine.supported_languages_url))) + + engine.stats = { + 'result_count': 0, + 'search_count': 0, + 'page_load_time': 0, + 'page_load_count': 0, + 'engine_time': 0, + 'engine_time_count': 0, + 'score_count': 0, + 'errors': 0 + } + + for category_name in engine.categories: + categories.setdefault(category_name, []).append(engine) + + if engine.shortcut in engine_shortcuts: + logger.error('Engine config error: ambigious shortcut: {0}'.format(engine.shortcut)) + sys.exit(1) + + engine_shortcuts[engine.shortcut] = engine.name + + return engine + + +def to_percentage(stats, maxvalue): + for engine_stat in stats: + if maxvalue: + engine_stat['percentage'] = int(engine_stat['avg'] / maxvalue * 100) + else: + engine_stat['percentage'] = 0 + return stats + + +def get_engines_stats(): + # TODO refactor + pageloads = [] + engine_times = [] + results = [] + scores = [] + errors = [] + scores_per_result = [] + + max_pageload = max_engine_times = max_results = max_score = max_errors = max_score_per_result = 0 # noqa + for engine in engines.values(): + if engine.stats['search_count'] == 0: + continue + results_num = \ + engine.stats['result_count'] / float(engine.stats['search_count']) + + if engine.stats['page_load_count'] != 0: + load_times = engine.stats['page_load_time'] / float(engine.stats['page_load_count']) # noqa + else: + load_times = 0 + + if engine.stats['engine_time_count'] != 0: + this_engine_time = engine.stats['engine_time'] / float(engine.stats['engine_time_count']) # noqa + else: + this_engine_time = 0 + + if results_num: + score = engine.stats['score_count'] / float(engine.stats['search_count']) # noqa + score_per_result = score / results_num + else: + score = score_per_result = 0.0 + + max_pageload = max(load_times, max_pageload) + max_engine_times = max(this_engine_time, max_engine_times) + max_results = max(results_num, max_results) + max_score = max(score, max_score) + max_score_per_result = max(score_per_result, max_score_per_result) + max_errors = max(max_errors, engine.stats['errors']) + + pageloads.append({'avg': load_times, 'name': engine.name}) + engine_times.append({'avg': this_engine_time, 'name': engine.name}) + results.append({'avg': results_num, 'name': engine.name}) + scores.append({'avg': score, 'name': engine.name}) + errors.append({'avg': engine.stats['errors'], 'name': engine.name}) + scores_per_result.append({ + 'avg': score_per_result, + 'name': engine.name + }) + + pageloads = to_percentage(pageloads, max_pageload) + engine_times = to_percentage(engine_times, max_engine_times) + results = to_percentage(results, max_results) + scores = to_percentage(scores, max_score) + scores_per_result = to_percentage(scores_per_result, max_score_per_result) + erros = to_percentage(errors, max_errors) + + return [ + ( + gettext('Engine time (sec)'), + sorted(engine_times, key=itemgetter('avg')) + ), + ( + gettext('Page loads (sec)'), + sorted(pageloads, key=itemgetter('avg')) + ), + ( + gettext('Number of results'), + sorted(results, key=itemgetter('avg'), reverse=True) + ), + ( + gettext('Scores'), + sorted(scores, key=itemgetter('avg'), reverse=True) + ), + ( + gettext('Scores per result'), + sorted(scores_per_result, key=itemgetter('avg'), reverse=True) + ), + ( + gettext('Errors'), + sorted(errors, key=itemgetter('avg'), reverse=True) + ), + ] + + +def initialize_engines(engine_list): + for engine_data in engine_list: + engine = load_engine(engine_data) + if engine is not None: + engines[engine.name] = engine diff --git a/searx/engines/archlinux.py b/searx/engines/archlinux.py new file mode 100644 index 0000000..cad06f8 --- /dev/null +++ b/searx/engines/archlinux.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- + +""" + Arch Linux Wiki + + @website https://wiki.archlinux.org + @provide-api no (Mediawiki provides API, but Arch Wiki blocks access to it + @using-api no + @results HTML + @stable no (HTML can change) + @parse url, title +""" + +from lxml import html +from searx.engines.xpath import extract_text +from searx.url_utils import urlencode, urljoin + +# engine dependent config +categories = ['it'] +language_support = True +paging = True +base_url = 'https://wiki.archlinux.org' + +# xpath queries +xpath_results = '//ul[@class="mw-search-results"]/li' +xpath_link = './/div[@class="mw-search-result-heading"]/a' + + +# cut 'en' from 'en_US', 'de' from 'de_CH', and so on +def locale_to_lang_code(locale): + if locale.find('-') >= 0: + locale = locale.split('-')[0] + return locale + + +# wikis for some languages were moved off from the main site, we need to make +# requests to correct URLs to be able to get results in those languages +lang_urls = { + 'all': { + 'base': 'https://wiki.archlinux.org', + 'search': '/index.php?title=Special:Search&offset={offset}&{query}' + }, + 'de': { + 'base': 'https://wiki.archlinux.de', + 'search': '/index.php?title=Spezial:Suche&offset={offset}&{query}' + }, + 'fr': { + 'base': 'https://wiki.archlinux.fr', + 'search': '/index.php?title=Spécial:Recherche&offset={offset}&{query}' + }, + 'ja': { + 'base': 'https://wiki.archlinuxjp.org', + 'search': '/index.php?title=特別:検索&offset={offset}&{query}' + }, + 'ro': { + 'base': 'http://wiki.archlinux.ro', + 'search': '/index.php?title=Special:Căutare&offset={offset}&{query}' + }, + 'tr': { + 'base': 'http://archtr.org/wiki', + 'search': '/index.php?title=Özel:Ara&offset={offset}&{query}' + } +} + + +# get base & search URLs for selected language +def get_lang_urls(language): + if language in lang_urls: + return lang_urls[language] + return lang_urls['all'] + + +# Language names to build search requests for +# those languages which are hosted on the main site. +main_langs = { + 'ar': 'العربية', + 'bg': 'Български', + 'cs': 'Česky', + 'da': 'Dansk', + 'el': 'Ελληνικά', + 'es': 'Español', + 'he': 'עברית', + 'hr': 'Hrvatski', + 'hu': 'Magyar', + 'it': 'Italiano', + 'ko': '한국어', + 'lt': 'Lietuviškai', + 'nl': 'Nederlands', + 'pl': 'Polski', + 'pt': 'Português', + 'ru': 'Русский', + 'sl': 'Slovenský', + 'th': 'ไทย', + 'uk': 'Українська', + 'zh': '简体中文' +} +supported_languages = dict(lang_urls, **main_langs) + + +# do search-request +def request(query, params): + # translate the locale (e.g. 'en_US') to language code ('en') + language = locale_to_lang_code(params['language']) + + # if our language is hosted on the main site, we need to add its name + # to the query in order to narrow the results to that language + if language in main_langs: + query += '(' + main_langs[language] + ')' + + # prepare the request parameters + query = urlencode({'search': query}) + offset = (params['pageno'] - 1) * 20 + + # get request URLs for our language of choice + urls = get_lang_urls(language) + search_url = urls['base'] + urls['search'] + + params['url'] = search_url.format(query=query, offset=offset) + + return params + + +# get response from search-request +def response(resp): + # get the base URL for the language in which request was made + language = locale_to_lang_code(resp.search_params['language']) + base_url = get_lang_urls(language)['base'] + + results = [] + + dom = html.fromstring(resp.text) + + # parse results + for result in dom.xpath(xpath_results): + link = result.xpath(xpath_link)[0] + href = urljoin(base_url, link.attrib.get('href')) + title = extract_text(link) + + results.append({'url': href, + 'title': title}) + + return results diff --git a/searx/engines/base.py b/searx/engines/base.py new file mode 100755 index 0000000..ff006a3 --- /dev/null +++ b/searx/engines/base.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python + +""" + BASE (Scholar publications) + + @website https://base-search.net + @provide-api yes with authorization (https://api.base-search.net/) + + @using-api yes + @results XML + @stable ? + @parse url, title, publishedDate, content + More info on api: http://base-search.net/about/download/base_interface.pdf +""" + +from lxml import etree +from datetime import datetime +import re +from searx.url_utils import urlencode +from searx.utils import searx_useragent + + +categories = ['science'] + +base_url = 'https://api.base-search.net/cgi-bin/BaseHttpSearchInterface.fcgi'\ + + '?func=PerformSearch&{query}&boost=oa&hits={hits}&offset={offset}' + +# engine dependent config +paging = True +number_of_results = 10 + +# shortcuts for advanced search +shorcut_dict = { + # user-friendly keywords + 'format:': 'dcformat:', + 'author:': 'dccreator:', + 'collection:': 'dccollection:', + 'hdate:': 'dchdate:', + 'contributor:': 'dccontributor:', + 'coverage:': 'dccoverage:', + 'date:': 'dcdate:', + 'abstract:': 'dcdescription:', + 'urls:': 'dcidentifier:', + 'language:': 'dclanguage:', + 'publisher:': 'dcpublisher:', + 'relation:': 'dcrelation:', + 'rights:': 'dcrights:', + 'source:': 'dcsource:', + 'subject:': 'dcsubject:', + 'title:': 'dctitle:', + 'type:': 'dcdctype:' +} + + +def request(query, params): + # replace shortcuts with API advanced search keywords + for key in shorcut_dict.keys(): + query = re.sub(str(key), str(shorcut_dict[key]), query) + + # basic search + offset = (params['pageno'] - 1) * number_of_results + + string_args = dict(query=urlencode({'query': query}), + offset=offset, + hits=number_of_results) + + params['url'] = base_url.format(**string_args) + + params['headers']['User-Agent'] = searx_useragent() + return params + + +def response(resp): + results = [] + + search_results = etree.XML(resp.text) + + for entry in search_results.xpath('./result/doc'): + content = "No description available" + + date = datetime.now() # needed in case no dcdate is available for an item + for item in entry: + if item.attrib["name"] == "dchdate": + harvestDate = item.text + + elif item.attrib["name"] == "dcdate": + date = item.text + + elif item.attrib["name"] == "dctitle": + title = item.text + + elif item.attrib["name"] == "dclink": + url = item.text + + elif item.attrib["name"] == "dcdescription": + content = item.text[:300] + if len(item.text) > 300: + content += "..." + +# dates returned by the BASE API are not several formats + publishedDate = None + for date_format in ['%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%d', '%Y-%m', '%Y']: + try: + publishedDate = datetime.strptime(date, date_format) + break + except: + pass + + if publishedDate is not None: + res_dict = {'url': url, + 'title': title, + 'publishedDate': publishedDate, + 'content': content} + else: + res_dict = {'url': url, + 'title': title, + 'content': content} + + results.append(res_dict) + + return results diff --git a/searx/engines/bing.py b/searx/engines/bing.py new file mode 100644 index 0000000..052d567 --- /dev/null +++ b/searx/engines/bing.py @@ -0,0 +1,101 @@ +""" + Bing (Web) + + @website https://www.bing.com + @provide-api yes (http://datamarket.azure.com/dataset/bing/search), + max. 5000 query/month + + @using-api no (because of query limit) + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content + + @todo publishedDate +""" + +from lxml import html +from searx.engines.xpath import extract_text +from searx.url_utils import urlencode + +# engine dependent config +categories = ['general'] +paging = True +language_support = True +supported_languages_url = 'https://www.bing.com/account/general' + +# search-url +base_url = 'https://www.bing.com/' +search_string = 'search?{query}&first={offset}' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 10 + 1 + + if params['language'] != 'all': + lang = params['language'].split('-')[0].upper() + else: + lang = 'EN' + + query = u'language:{} {}'.format(lang, query.decode('utf-8')).encode('utf-8') + + search_path = search_string.format( + query=urlencode({'q': query}), + offset=offset) + + params['url'] = base_url + search_path + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + try: + results.append({'number_of_results': int(dom.xpath('//span[@class="sb_count"]/text()')[0] + .split()[0].replace(',', ''))}) + except: + pass + + # parse results + for result in dom.xpath('//div[@class="sa_cc"]'): + link = result.xpath('.//h3/a')[0] + url = link.attrib.get('href') + title = extract_text(link) + content = extract_text(result.xpath('.//p')) + + # append result + results.append({'url': url, + 'title': title, + 'content': content}) + + # parse results again if nothing is found yet + for result in dom.xpath('//li[@class="b_algo"]'): + link = result.xpath('.//h2/a')[0] + url = link.attrib.get('href') + title = extract_text(link) + content = extract_text(result.xpath('.//p')) + + # append result + results.append({'url': url, + 'title': title, + 'content': content}) + + # return results + return results + + +# get supported languages from their site +def _fetch_supported_languages(resp): + supported_languages = [] + dom = html.fromstring(resp.text) + options = dom.xpath('//div[@id="limit-languages"]//input') + for option in options: + code = option.xpath('./@id')[0].replace('_', '-') + if code == 'nb': + code = 'no' + supported_languages.append(code) + + return supported_languages diff --git a/searx/engines/bing_images.py b/searx/engines/bing_images.py new file mode 100644 index 0000000..6300c94 --- /dev/null +++ b/searx/engines/bing_images.py @@ -0,0 +1,108 @@ +""" + Bing (Images) + + @website https://www.bing.com/images + @provide-api yes (http://datamarket.azure.com/dataset/bing/search), + max. 5000 query/month + + @using-api no (because of query limit) + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, img_src + + @todo currently there are up to 35 images receive per page, + because bing does not parse count=10. + limited response to 10 images +""" + +from lxml import html +from json import loads +import re +from searx.engines.bing import _fetch_supported_languages, supported_languages_url +from searx.url_utils import urlencode + +# engine dependent config +categories = ['images'] +paging = True +safesearch = True +time_range_support = True + +# search-url +base_url = 'https://www.bing.com/' +search_string = 'images/search?{query}&count=10&first={offset}' +time_range_string = '&qft=+filterui:age-lt{interval}' +time_range_dict = {'day': '1440', + 'week': '10080', + 'month': '43200', + 'year': '525600'} + +# safesearch definitions +safesearch_types = {2: 'STRICT', + 1: 'DEMOTE', + 0: 'OFF'} + + +_quote_keys_regex = re.compile('({|,)([a-z][a-z0-9]*):(")', re.I | re.U) + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 10 + 1 + + # required for cookie + if params['language'] == 'all': + language = 'en-US' + else: + language = params['language'] + + search_path = search_string.format( + query=urlencode({'q': query}), + offset=offset) + + params['cookies']['SRCHHPGUSR'] = \ + 'NEWWND=0&NRSLT=-1&SRCHLANG=' + language.split('-')[0] +\ + '&ADLT=' + safesearch_types.get(params['safesearch'], 'DEMOTE') + + params['url'] = base_url + search_path + if params['time_range'] in time_range_dict: + params['url'] += time_range_string.format(interval=time_range_dict[params['time_range']]) + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + # parse results + for result in dom.xpath('//div[@id="mmComponent_images_1"]/ul/li/div/div[@class="imgpt"]'): + link = result.xpath('./a')[0] + + # TODO find actual title + title = link.xpath('.//img/@alt')[0] + + # parse json-data (it is required to add a space, to make it parsable) + json_data = loads(_quote_keys_regex.sub(r'\1"\2": \3', link.attrib.get('m'))) + + url = json_data.get('purl') + img_src = json_data.get('murl') + + thumb_json_data = loads(_quote_keys_regex.sub(r'\1"\2": \3', link.attrib.get('mad'))) + thumbnail = thumb_json_data.get('turl') + + # append result + results.append({'template': 'images.html', + 'url': url, + 'title': title, + 'content': '', + 'thumbnail_src': thumbnail, + 'img_src': img_src}) + + # TODO stop parsing if 10 images are found + # if len(results) >= 10: + # break + + # return results + return results diff --git a/searx/engines/bing_news.py b/searx/engines/bing_news.py new file mode 100644 index 0000000..b999b2a --- /dev/null +++ b/searx/engines/bing_news.py @@ -0,0 +1,127 @@ +""" + Bing (News) + + @website https://www.bing.com/news + @provide-api yes (http://datamarket.azure.com/dataset/bing/search), + max. 5000 query/month + + @using-api no (because of query limit) + @results RSS (using search portal) + @stable yes (except perhaps for the images) + @parse url, title, content, publishedDate, thumbnail +""" + +from datetime import datetime +from dateutil import parser +from lxml import etree +from searx.utils import list_get +from searx.engines.bing import _fetch_supported_languages, supported_languages_url +from searx.url_utils import urlencode, urlparse, parse_qsl + +# engine dependent config +categories = ['news'] +paging = True +language_support = True +time_range_support = True + +# search-url +base_url = 'https://www.bing.com/' +search_string = 'news/search?{query}&first={offset}&format=RSS' +search_string_with_time = 'news/search?{query}&first={offset}&qft=interval%3d"{interval}"&format=RSS' +time_range_dict = {'day': '7', + 'week': '8', + 'month': '9'} + + +# remove click +def url_cleanup(url_string): + parsed_url = urlparse(url_string) + if parsed_url.netloc == 'www.bing.com' and parsed_url.path == '/news/apiclick.aspx': + query = dict(parse_qsl(parsed_url.query)) + return query.get('url', None) + return url_string + + +# replace the http://*bing4.com/th?id=... by https://www.bing.com/th?id=... +def image_url_cleanup(url_string): + parsed_url = urlparse(url_string) + if parsed_url.netloc.endswith('bing4.com') and parsed_url.path == '/th': + query = dict(parse_qsl(parsed_url.query)) + return "https://www.bing.com/th?id=" + query.get('id') + return url_string + + +def _get_url(query, language, offset, time_range): + if time_range in time_range_dict: + search_path = search_string_with_time.format( + query=urlencode({'q': query, 'setmkt': language}), + offset=offset, + interval=time_range_dict[time_range]) + else: + search_path = search_string.format( + query=urlencode({'q': query, 'setmkt': language}), + offset=offset) + return base_url + search_path + + +# do search-request +def request(query, params): + if params['time_range'] and params['time_range'] not in time_range_dict: + return params + + offset = (params['pageno'] - 1) * 10 + 1 + + if params['language'] == 'all': + language = 'en-US' + else: + language = params['language'] + + params['url'] = _get_url(query, language, offset, params['time_range']) + + return params + + +# get response from search-request +def response(resp): + results = [] + + rss = etree.fromstring(resp.content) + + ns = rss.nsmap + + # parse results + for item in rss.xpath('./channel/item'): + # url / title / content + url = url_cleanup(item.xpath('./link/text()')[0]) + title = list_get(item.xpath('./title/text()'), 0, url) + content = list_get(item.xpath('./description/text()'), 0, '') + + # publishedDate + publishedDate = list_get(item.xpath('./pubDate/text()'), 0) + try: + publishedDate = parser.parse(publishedDate, dayfirst=False) + except TypeError: + publishedDate = datetime.now() + except ValueError: + publishedDate = datetime.now() + + # thumbnail + thumbnail = list_get(item.xpath('./News:Image/text()', namespaces=ns), 0) + if thumbnail is not None: + thumbnail = image_url_cleanup(thumbnail) + + # append result + if thumbnail is not None: + results.append({'url': url, + 'title': title, + 'publishedDate': publishedDate, + 'content': content, + 'img_src': thumbnail}) + else: + results.append({'url': url, + 'title': title, + 'publishedDate': publishedDate, + 'content': content}) + + # return results + return results diff --git a/searx/engines/blekko_images.py b/searx/engines/blekko_images.py new file mode 100644 index 0000000..f716456 --- /dev/null +++ b/searx/engines/blekko_images.py @@ -0,0 +1,70 @@ +""" + Blekko (Images) + + @website https://blekko.com + @provide-api yes (inofficial) + + @using-api yes + @results JSON + @stable yes + @parse url, title, img_src +""" + +from json import loads +from searx.url_utils import urlencode + +# engine dependent config +categories = ['images'] +paging = True +safesearch = True + +# search-url +base_url = 'https://blekko.com' +search_url = '/api/images?{query}&c={c}' + +# safesearch definitions +safesearch_types = {2: '1', + 1: '', + 0: '0'} + + +# do search-request +def request(query, params): + c = (params['pageno'] - 1) * 48 + + params['url'] = base_url +\ + search_url.format(query=urlencode({'q': query}), + c=c) + + if params['pageno'] != 1: + params['url'] += '&page={pageno}'.format(pageno=(params['pageno'] - 1)) + + # let Blekko know we wan't have profiling + params['cookies']['tag_lesslogging'] = '1' + + # parse safesearch argument + params['cookies']['safesearch'] = safesearch_types.get(params['safesearch'], '') + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_results = loads(resp.text) + + # return empty array if there are no results + if not search_results: + return [] + + for result in search_results: + # append result + results.append({'url': result['page_url'], + 'title': result['title'], + 'content': '', + 'img_src': result['url'], + 'template': 'images.html'}) + + # return results + return results diff --git a/searx/engines/btdigg.py b/searx/engines/btdigg.py new file mode 100644 index 0000000..4043867 --- /dev/null +++ b/searx/engines/btdigg.py @@ -0,0 +1,92 @@ +""" + BTDigg (Videos, Music, Files) + + @website https://btdigg.org + @provide-api yes (on demand) + + @using-api no + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content, seed, leech, magnetlink +""" + +from lxml import html +from operator import itemgetter +from searx.engines.xpath import extract_text +from searx.url_utils import quote, urljoin +from searx.utils import get_torrent_size + +# engine dependent config +categories = ['videos', 'music', 'files'] +paging = True + +# search-url +url = 'https://btdigg.org' +search_url = url + '/search?q={search_term}&p={pageno}' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(search_term=quote(query), + pageno=params['pageno'] - 1) + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + search_res = dom.xpath('//div[@id="search_res"]/table/tr') + + # return empty array if nothing is found + if not search_res: + return [] + + # parse results + for result in search_res: + link = result.xpath('.//td[@class="torrent_name"]//a')[0] + href = urljoin(url, link.attrib.get('href')) + title = extract_text(link) + content = extract_text(result.xpath('.//pre[@class="snippet"]')[0]) + content = "
".join(content.split("\n")) + + filesize = result.xpath('.//span[@class="attr_val"]/text()')[0].split()[0] + filesize_multiplier = result.xpath('.//span[@class="attr_val"]/text()')[0].split()[1] + files = result.xpath('.//span[@class="attr_val"]/text()')[1] + seed = result.xpath('.//span[@class="attr_val"]/text()')[2] + + # convert seed to int if possible + if seed.isdigit(): + seed = int(seed) + else: + seed = 0 + + leech = 0 + + # convert filesize to byte if possible + filesize = get_torrent_size(filesize, filesize_multiplier) + + # convert files to int if possible + if files.isdigit(): + files = int(files) + else: + files = None + + magnetlink = result.xpath('.//td[@class="ttth"]//a')[0].attrib['href'] + + # append result + results.append({'url': href, + 'title': title, + 'content': content, + 'seed': seed, + 'leech': leech, + 'filesize': filesize, + 'files': files, + 'magnetlink': magnetlink, + 'template': 'torrent.html'}) + + # return results sorted by seeder + return sorted(results, key=itemgetter('seed'), reverse=True) diff --git a/searx/engines/currency_convert.py b/searx/engines/currency_convert.py new file mode 100644 index 0000000..1218d48 --- /dev/null +++ b/searx/engines/currency_convert.py @@ -0,0 +1,105 @@ +import json +import re +import os +import sys +import unicodedata + +from datetime import datetime + +if sys.version_info[0] == 3: + unicode = str + +categories = [] +url = 'https://download.finance.yahoo.com/d/quotes.csv?e=.csv&f=sl1d1t1&s={query}=X' +weight = 100 + +parser_re = re.compile(b'.*?(\\d+(?:\\.\\d+)?) ([^.0-9]+) (?:in|to) ([^.0-9]+)', re.I) + +db = 1 + + +def normalize_name(name): + name = name.decode('utf-8').lower().replace('-', ' ').rstrip('s') + name = re.sub(' +', ' ', name) + return unicodedata.normalize('NFKD', name).lower() + + +def name_to_iso4217(name): + global db + + name = normalize_name(name) + currencies = db['names'].get(name, [name]) + return currencies[0] + + +def iso4217_to_name(iso4217, language): + global db + + return db['iso4217'].get(iso4217, {}).get(language, iso4217) + + +def request(query, params): + m = parser_re.match(query) + if not m: + # wrong query + return params + + ammount, from_currency, to_currency = m.groups() + ammount = float(ammount) + from_currency = name_to_iso4217(from_currency.strip()) + to_currency = name_to_iso4217(to_currency.strip()) + + q = (from_currency + to_currency).upper() + + params['url'] = url.format(query=q) + params['ammount'] = ammount + params['from'] = from_currency + params['to'] = to_currency + params['from_name'] = iso4217_to_name(from_currency, 'en') + params['to_name'] = iso4217_to_name(to_currency, 'en') + + return params + + +def response(resp): + results = [] + try: + _, conversion_rate, _ = resp.text.split(',', 2) + conversion_rate = float(conversion_rate) + except: + return results + + answer = '{0} {1} = {2} {3}, 1 {1} ({5}) = {4} {3} ({6})'.format( + resp.search_params['ammount'], + resp.search_params['from'], + resp.search_params['ammount'] * conversion_rate, + resp.search_params['to'], + conversion_rate, + resp.search_params['from_name'], + resp.search_params['to_name'], + ) + + now_date = datetime.now().strftime('%Y%m%d') + url = 'https://finance.yahoo.com/currency/converter-results/{0}/{1}-{2}-to-{3}.html' # noqa + url = url.format( + now_date, + resp.search_params['ammount'], + resp.search_params['from'].lower(), + resp.search_params['to'].lower() + ) + + results.append({'answer': answer, 'url': url}) + + return results + + +def load(): + global db + + current_dir = os.path.dirname(os.path.realpath(__file__)) + json_data = open(current_dir + "/../data/currencies.json").read() + + db = json.loads(json_data) + + +load() diff --git a/searx/engines/dailymotion.py b/searx/engines/dailymotion.py new file mode 100644 index 0000000..fad7e59 --- /dev/null +++ b/searx/engines/dailymotion.py @@ -0,0 +1,97 @@ +""" + Dailymotion (Videos) + + @website https://www.dailymotion.com + @provide-api yes (http://www.dailymotion.com/developer) + + @using-api yes + @results JSON + @stable yes + @parse url, title, thumbnail, publishedDate, embedded + + @todo set content-parameter with correct data +""" + +from json import loads +from datetime import datetime +from searx.url_utils import urlencode + +# engine dependent config +categories = ['videos'] +paging = True +language_support = True + +# search-url +# see http://www.dailymotion.com/doc/api/obj-video.html +search_url = 'https://api.dailymotion.com/videos?fields=created_time,title,description,duration,url,thumbnail_360_url,id&sort=relevance&limit=5&page={pageno}&{query}' # noqa +embedded_url = '' + +supported_languages_url = 'https://api.dailymotion.com/languages' + + +# do search-request +def request(query, params): + if params['language'] == 'all': + locale = 'en-US' + else: + locale = params['language'] + + params['url'] = search_url.format( + query=urlencode({'search': query, 'localization': locale}), + pageno=params['pageno']) + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_res = loads(resp.text) + + # return empty array if there are no results + if 'list' not in search_res: + return [] + + # parse results + for res in search_res['list']: + title = res['title'] + url = res['url'] + content = res['description'] + thumbnail = res['thumbnail_360_url'] + publishedDate = datetime.fromtimestamp(res['created_time'], None) + embedded = embedded_url.format(videoid=res['id']) + + # http to https + thumbnail = thumbnail.replace("http://", "https://") + + results.append({'template': 'videos.html', + 'url': url, + 'title': title, + 'content': content, + 'publishedDate': publishedDate, + 'embedded': embedded, + 'thumbnail': thumbnail}) + + # return results + return results + + +# get supported languages from their site +def _fetch_supported_languages(resp): + supported_languages = {} + + response_json = loads(resp.text) + + for language in response_json['list']: + supported_languages[language['code']] = {} + + name = language['native_name'] + if name: + supported_languages[language['code']]['name'] = name + english_name = language['name'] + if english_name: + supported_languages[language['code']]['english_name'] = english_name + + return supported_languages diff --git a/searx/engines/deezer.py b/searx/engines/deezer.py new file mode 100644 index 0000000..af63478 --- /dev/null +++ b/searx/engines/deezer.py @@ -0,0 +1,67 @@ +""" + Deezer (Music) + + @website https://deezer.com + @provide-api yes (http://developers.deezer.com/api/) + + @using-api yes + @results JSON + @stable yes + @parse url, title, content, embedded +""" + +from json import loads +from searx.url_utils import urlencode + +# engine dependent config +categories = ['music'] +paging = True + +# search-url +url = 'https://api.deezer.com/' +search_url = url + 'search?{query}&index={offset}' + +embedded_url = '' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 25 + + params['url'] = search_url.format(query=urlencode({'q': query}), offset=offset) + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_res = loads(resp.text) + + # parse results + for result in search_res.get('data', []): + if result['type'] == 'track': + title = result['title'] + url = result['link'] + + if url.startswith('http://'): + url = 'https' + url[4:] + + content = u'{} - {} - {}'.format( + result['artist']['name'], + result['album']['title'], + result['title']) + + embedded = embedded_url.format(audioid=result['id']) + + # append result + results.append({'url': url, + 'title': title, + 'embedded': embedded, + 'content': content}) + + # return results + return results diff --git a/searx/engines/deviantart.py b/searx/engines/deviantart.py new file mode 100644 index 0000000..bb85c6d --- /dev/null +++ b/searx/engines/deviantart.py @@ -0,0 +1,84 @@ +""" + Deviantart (Images) + + @website https://www.deviantart.com/ + @provide-api yes (https://www.deviantart.com/developers/) (RSS) + + @using-api no (TODO, rewrite to api) + @results HTML + @stable no (HTML can change) + @parse url, title, thumbnail_src, img_src + + @todo rewrite to api +""" + +from lxml import html +import re +from searx.engines.xpath import extract_text +from searx.url_utils import urlencode + +# engine dependent config +categories = ['images'] +paging = True +time_range_support = True + +# search-url +base_url = 'https://www.deviantart.com/' +search_url = base_url + 'browse/all/?offset={offset}&{query}' +time_range_url = '&order={range}' + +time_range_dict = {'day': 11, + 'week': 14, + 'month': 15} + + +# do search-request +def request(query, params): + if params['time_range'] and params['time_range'] not in time_range_dict: + return params + + offset = (params['pageno'] - 1) * 24 + + params['url'] = search_url.format(offset=offset, + query=urlencode({'q': query})) + if params['time_range'] in time_range_dict: + params['url'] += time_range_url.format(range=time_range_dict[params['time_range']]) + + return params + + +# get response from search-request +def response(resp): + results = [] + + # return empty array if a redirection code is returned + if resp.status_code == 302: + return [] + + dom = html.fromstring(resp.text) + + regex = re.compile(r'\/200H\/') + + # parse results + for result in dom.xpath('.//span[@class="thumb wide"]'): + link = result.xpath('.//a[@class="torpedo-thumb-link"]')[0] + url = link.attrib.get('href') + title = extract_text(result.xpath('.//span[@class="title"]')) + thumbnail_src = link.xpath('.//img')[0].attrib.get('src') + img_src = regex.sub('/', thumbnail_src) + + # http to https, remove domain sharding + thumbnail_src = re.sub(r"https?://(th|fc)\d+.", "https://th01.", thumbnail_src) + thumbnail_src = re.sub(r"http://", "https://", thumbnail_src) + + url = re.sub(r"http://(.*)\.deviantart\.com/", "https://\\1.deviantart.com/", url) + + # append result + results.append({'url': url, + 'title': title, + 'img_src': img_src, + 'thumbnail_src': thumbnail_src, + 'template': 'images.html'}) + + # return results + return results diff --git a/searx/engines/dictzone.py b/searx/engines/dictzone.py new file mode 100644 index 0000000..7c34786 --- /dev/null +++ b/searx/engines/dictzone.py @@ -0,0 +1,68 @@ +""" + Dictzone + + @website https://dictzone.com/ + @provide-api no + @using-api no + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content +""" + +import re +from lxml import html +from searx.utils import is_valid_lang +from searx.url_utils import urljoin + +categories = ['general'] +url = u'http://dictzone.com/{from_lang}-{to_lang}-dictionary/{query}' +weight = 100 + +parser_re = re.compile(b'.*?([a-z]+)-([a-z]+) ([^ ]+)$', re.I) +results_xpath = './/table[@id="r"]/tr' + + +def request(query, params): + m = parser_re.match(query) + if not m: + return params + + from_lang, to_lang, query = m.groups() + + from_lang = is_valid_lang(from_lang) + to_lang = is_valid_lang(to_lang) + + if not from_lang or not to_lang: + return params + + params['url'] = url.format(from_lang=from_lang[2], + to_lang=to_lang[2], + query=query) + + return params + + +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + for k, result in enumerate(dom.xpath(results_xpath)[1:]): + try: + from_result, to_results_raw = result.xpath('./td') + except: + continue + + to_results = [] + for to_result in to_results_raw.xpath('./p/a'): + t = to_result.text_content() + if t.strip(): + to_results.append(to_result.text_content()) + + results.append({ + 'url': urljoin(resp.url, '?%d' % k), + 'title': from_result.text_content(), + 'content': '; '.join(to_results) + }) + + return results diff --git a/searx/engines/digbt.py b/searx/engines/digbt.py new file mode 100644 index 0000000..ff2f945 --- /dev/null +++ b/searx/engines/digbt.py @@ -0,0 +1,62 @@ +""" + DigBT (Videos, Music, Files) + + @website https://digbt.org + @provide-api no + + @using-api no + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content, magnetlink +""" + +from sys import version_info +from lxml import html +from searx.engines.xpath import extract_text +from searx.utils import get_torrent_size +from searx.url_utils import urljoin + +if version_info[0] == 3: + unicode = str + +categories = ['videos', 'music', 'files'] +paging = True + +URL = 'https://digbt.org' +SEARCH_URL = URL + '/search/{query}-time-{pageno}' +FILESIZE = 3 +FILESIZE_MULTIPLIER = 4 + + +def request(query, params): + params['url'] = SEARCH_URL.format(query=query, pageno=params['pageno']) + + return params + + +def response(resp): + dom = html.fromstring(resp.text) + search_res = dom.xpath('.//td[@class="x-item"]') + + if not search_res: + return list() + + results = list() + for result in search_res: + url = urljoin(URL, result.xpath('.//a[@title]/@href')[0]) + title = extract_text(result.xpath('.//a[@title]')) + content = extract_text(result.xpath('.//div[@class="files"]')) + files_data = extract_text(result.xpath('.//div[@class="tail"]')).split() + filesize = get_torrent_size(files_data[FILESIZE], files_data[FILESIZE_MULTIPLIER]) + magnetlink = result.xpath('.//div[@class="tail"]//a[@class="title"]/@href')[0] + + results.append({'url': url, + 'title': title, + 'content': content, + 'filesize': filesize, + 'magnetlink': magnetlink, + 'seed': 'N/A', + 'leech': 'N/A', + 'template': 'torrent.html'}) + + return results diff --git a/searx/engines/digg.py b/searx/engines/digg.py new file mode 100644 index 0000000..606747a --- /dev/null +++ b/searx/engines/digg.py @@ -0,0 +1,74 @@ +""" + Digg (News, Social media) + + @website https://digg.com/ + @provide-api no + + @using-api no + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content, publishedDate, thumbnail +""" + +from dateutil import parser +from json import loads +from lxml import html +from searx.url_utils import quote_plus + +# engine dependent config +categories = ['news', 'social media'] +paging = True + +# search-url +base_url = 'https://digg.com/' +search_url = base_url + 'api/search/{query}.json?position={position}&format=html' + +# specific xpath variables +results_xpath = '//article' +link_xpath = './/small[@class="time"]//a' +title_xpath = './/h2//a//text()' +content_xpath = './/p//text()' +pubdate_xpath = './/time' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 10 + params['url'] = search_url.format(position=offset, + query=quote_plus(query)) + return params + + +# get response from search-request +def response(resp): + results = [] + + search_result = loads(resp.text) + + if 'html' not in search_result or search_result['html'] == '': + return results + + dom = html.fromstring(search_result['html']) + + # parse results + for result in dom.xpath(results_xpath): + url = result.attrib.get('data-contenturl') + thumbnail = result.xpath('.//img')[0].attrib.get('src') + title = ''.join(result.xpath(title_xpath)) + content = ''.join(result.xpath(content_xpath)) + pubdate = result.xpath(pubdate_xpath)[0].attrib.get('datetime') + publishedDate = parser.parse(pubdate) + + # http to https + thumbnail = thumbnail.replace("http://static.digg.com", "https://static.digg.com") + + # append result + results.append({'url': url, + 'title': title, + 'content': content, + 'template': 'videos.html', + 'publishedDate': publishedDate, + 'thumbnail': thumbnail}) + + # return results + return results diff --git a/searx/engines/doku.py b/searx/engines/doku.py new file mode 100644 index 0000000..a391be4 --- /dev/null +++ b/searx/engines/doku.py @@ -0,0 +1,84 @@ +# Doku Wiki +# +# @website https://www.dokuwiki.org/ +# @provide-api yes +# (https://www.dokuwiki.org/devel:xmlrpc) +# +# @using-api no +# @results HTML +# @stable yes +# @parse (general) url, title, content + +from lxml.html import fromstring +from searx.engines.xpath import extract_text +from searx.url_utils import urlencode + +# engine dependent config +categories = ['general'] # TODO , 'images', 'music', 'videos', 'files' +paging = False +language_support = False +number_of_results = 5 + +# search-url +# Doku is OpenSearch compatible +base_url = 'http://localhost:8090' +search_url = '/?do=search'\ + '&{query}' +# TODO '&startRecord={offset}'\ +# TODO '&maximumRecords={limit}'\ + + +# do search-request +def request(query, params): + + params['url'] = base_url +\ + search_url.format(query=urlencode({'id': query})) + + return params + + +# get response from search-request +def response(resp): + results = [] + + doc = fromstring(resp.text) + + # parse results + # Quickhits + for r in doc.xpath('//div[@class="search_quickresult"]/ul/li'): + try: + res_url = r.xpath('.//a[@class="wikilink1"]/@href')[-1] + except: + continue + + if not res_url: + continue + + title = extract_text(r.xpath('.//a[@class="wikilink1"]/@title')) + + # append result + results.append({'title': title, + 'content': "", + 'url': base_url + res_url}) + + # Search results + for r in doc.xpath('//dl[@class="search_results"]/*'): + try: + if r.tag == "dt": + res_url = r.xpath('.//a[@class="wikilink1"]/@href')[-1] + title = extract_text(r.xpath('.//a[@class="wikilink1"]/@title')) + elif r.tag == "dd": + content = extract_text(r.xpath('.')) + + # append result + results.append({'title': title, + 'content': content, + 'url': base_url + res_url}) + except: + continue + + if not res_url: + continue + + # return results + return results diff --git a/searx/engines/duckduckgo.py b/searx/engines/duckduckgo.py new file mode 100644 index 0000000..8b6411c --- /dev/null +++ b/searx/engines/duckduckgo.py @@ -0,0 +1,137 @@ +""" + DuckDuckGo (Web) + + @website https://duckduckgo.com/ + @provide-api yes (https://duckduckgo.com/api), + but not all results from search-site + + @using-api no + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content + + @todo rewrite to api +""" + +from lxml.html import fromstring +from requests import get +from json import loads +from searx.engines.xpath import extract_text +from searx.url_utils import urlencode + +# engine dependent config +categories = ['general'] +paging = True +language_support = True +supported_languages_url = 'https://duckduckgo.com/d2030.js' +time_range_support = True + +# search-url +url = 'https://duckduckgo.com/html?{query}&s={offset}&api=/d.js&o=json&dc={dc_param}' +time_range_url = '&df={range}' + +time_range_dict = {'day': 'd', + 'week': 'w', + 'month': 'm'} + +# specific xpath variables +result_xpath = '//div[@class="result results_links results_links_deep web-result "]' # noqa +url_xpath = './/a[@class="result__a"]/@href' +title_xpath = './/a[@class="result__a"]' +content_xpath = './/a[@class="result__snippet"]' + + +# match query's language to a region code that duckduckgo will accept +def get_region_code(lang): + # custom fixes for languages + if lang == 'all': + region_code = None + elif lang[:2] == 'ja': + region_code = 'jp-jp' + elif lang[:2] == 'sl': + region_code = 'sl-sl' + elif lang == 'zh-TW': + region_code = 'tw-tzh' + elif lang == 'zh-HK': + region_code = 'hk-tzh' + elif lang[-2:] == 'SA': + region_code = 'xa-' + lang.split('-')[0] + elif lang[-2:] == 'GB': + region_code = 'uk-' + lang.split('-')[0] + else: + region_code = lang.split('-') + if len(region_code) == 2: + # country code goes first + region_code = region_code[1].lower() + '-' + region_code[0].lower() + else: + # tries to get a country code from language + region_code = region_code[0].lower() + for lc in supported_languages: + lc = lc.split('-') + if region_code == lc[0]: + region_code = lc[1].lower() + '-' + lc[0].lower() + break + return region_code + + +# do search-request +def request(query, params): + if params['time_range'] and params['time_range'] not in time_range_dict: + return params + + offset = (params['pageno'] - 1) * 30 + + region_code = get_region_code(params['language']) + if region_code: + params['url'] = url.format( + query=urlencode({'q': query, 'kl': region_code}), offset=offset, dc_param=offset) + else: + params['url'] = url.format( + query=urlencode({'q': query}), offset=offset, dc_param=offset) + + if params['time_range'] in time_range_dict: + params['url'] += time_range_url.format(range=time_range_dict[params['time_range']]) + + return params + + +# get response from search-request +def response(resp): + results = [] + + doc = fromstring(resp.text) + + # parse results + for r in doc.xpath(result_xpath): + try: + res_url = r.xpath(url_xpath)[-1] + except: + continue + + if not res_url: + continue + + title = extract_text(r.xpath(title_xpath)) + content = extract_text(r.xpath(content_xpath)) + + # append result + results.append({'title': title, + 'content': content, + 'url': res_url}) + + # return results + return results + + +# get supported languages from their site +def _fetch_supported_languages(resp): + + # response is a js file with regions as an embedded object + response_page = resp.text + response_page = response_page[response_page.find('regions:{') + 8:] + response_page = response_page[:response_page.find('}') + 1] + + regions_json = loads(response_page) + supported_languages = map((lambda x: x[3:] + '-' + x[:2].upper()), regions_json.keys()) + + return supported_languages diff --git a/searx/engines/duckduckgo_definitions.py b/searx/engines/duckduckgo_definitions.py new file mode 100644 index 0000000..21c6a65 --- /dev/null +++ b/searx/engines/duckduckgo_definitions.py @@ -0,0 +1,157 @@ +import json +from lxml import html +from re import compile +from searx.engines.xpath import extract_text +from searx.engines.duckduckgo import _fetch_supported_languages, supported_languages_url +from searx.url_utils import urlencode +from searx.utils import html_to_text + +url = 'https://api.duckduckgo.com/'\ + + '?{query}&format=json&pretty=0&no_redirect=1&d=1' + +http_regex = compile(r'^http:') + + +def result_to_text(url, text, htmlResult): + # TODO : remove result ending with "Meaning" or "Category" + dom = html.fromstring(htmlResult) + a = dom.xpath('//a') + if len(a) >= 1: + return extract_text(a[0]) + else: + return text + + +def request(query, params): + params['url'] = url.format(query=urlencode({'q': query})) + params['headers']['Accept-Language'] = params['language'].split('-')[0] + return params + + +def response(resp): + results = [] + + search_res = json.loads(resp.text) + + content = '' + heading = search_res.get('Heading', '') + attributes = [] + urls = [] + infobox_id = None + relatedTopics = [] + + # add answer if there is one + answer = search_res.get('Answer', '') + if answer != '': + results.append({'answer': html_to_text(answer)}) + + # add infobox + if 'Definition' in search_res: + content = content + search_res.get('Definition', '') + + if 'Abstract' in search_res: + content = content + search_res.get('Abstract', '') + + # image + image = search_res.get('Image', '') + image = None if image == '' else image + + # attributes + if 'Infobox' in search_res: + infobox = search_res.get('Infobox', None) + if 'content' in infobox: + for info in infobox.get('content'): + attributes.append({'label': info.get('label'), + 'value': info.get('value')}) + + # urls + for ddg_result in search_res.get('Results', []): + if 'FirstURL' in ddg_result: + firstURL = ddg_result.get('FirstURL', '') + text = ddg_result.get('Text', '') + urls.append({'title': text, 'url': firstURL}) + results.append({'title': heading, 'url': firstURL}) + + # related topics + for ddg_result in search_res.get('RelatedTopics', []): + if 'FirstURL' in ddg_result: + suggestion = result_to_text(ddg_result.get('FirstURL', None), + ddg_result.get('Text', None), + ddg_result.get('Result', None)) + if suggestion != heading: + results.append({'suggestion': suggestion}) + elif 'Topics' in ddg_result: + suggestions = [] + relatedTopics.append({'name': ddg_result.get('Name', ''), + 'suggestions': suggestions}) + for topic_result in ddg_result.get('Topics', []): + suggestion = result_to_text(topic_result.get('FirstURL', None), + topic_result.get('Text', None), + topic_result.get('Result', None)) + if suggestion != heading: + suggestions.append(suggestion) + + # abstract + abstractURL = search_res.get('AbstractURL', '') + if abstractURL != '': + # add as result ? problem always in english + infobox_id = abstractURL + urls.append({'title': search_res.get('AbstractSource'), + 'url': abstractURL}) + + # definition + definitionURL = search_res.get('DefinitionURL', '') + if definitionURL != '': + # add as result ? as answer ? problem always in english + infobox_id = definitionURL + urls.append({'title': search_res.get('DefinitionSource'), + 'url': definitionURL}) + + # to merge with wikidata's infobox + if infobox_id: + infobox_id = http_regex.sub('https:', infobox_id) + + # entity + entity = search_res.get('Entity', None) + # TODO continent / country / department / location / waterfall / + # mountain range : + # link to map search, get weather, near by locations + # TODO musician : link to music search + # TODO concert tour : ?? + # TODO film / actor / television / media franchise : + # links to IMDB / rottentomatoes (or scrap result) + # TODO music : link tu musicbrainz / last.fm + # TODO book : ?? + # TODO artist / playwright : ?? + # TODO compagny : ?? + # TODO software / os : ?? + # TODO software engineer : ?? + # TODO prepared food : ?? + # TODO website : ?? + # TODO performing art : ?? + # TODO prepared food : ?? + # TODO programming language : ?? + # TODO file format : ?? + + if len(heading) > 0: + # TODO get infobox.meta.value where .label='article_title' + if image is None and len(attributes) == 0 and len(urls) == 1 and\ + len(relatedTopics) == 0 and len(content) == 0: + results.append({ + 'url': urls[0]['url'], + 'title': heading, + 'content': content + }) + else: + results.append({ + 'infobox': heading, + 'id': infobox_id, + 'entity': entity, + 'content': content, + 'img_src': image, + 'attributes': attributes, + 'urls': urls, + 'relatedTopics': relatedTopics + }) + + return results diff --git a/searx/engines/duckduckgo_images.py b/searx/engines/duckduckgo_images.py new file mode 100644 index 0000000..f355523 --- /dev/null +++ b/searx/engines/duckduckgo_images.py @@ -0,0 +1,91 @@ +""" + DuckDuckGo (Images) + + @website https://duckduckgo.com/ + @provide-api yes (https://duckduckgo.com/api), + but images are not supported + + @using-api no + @results JSON (site requires js to get images) + @stable no (JSON can change) + @parse url, title, img_src + + @todo avoid extra request +""" + +from requests import get +from json import loads +from searx.engines.xpath import extract_text +from searx.engines.duckduckgo import _fetch_supported_languages, supported_languages_url, get_region_code +from searx.url_utils import urlencode + +# engine dependent config +categories = ['images'] +paging = True +language_support = True +safesearch = True + +# search-url +images_url = 'https://duckduckgo.com/i.js?{query}&s={offset}&p={safesearch}&o=json&vqd={vqd}' +site_url = 'https://duckduckgo.com/?{query}&iar=images&iax=1&ia=images' + + +# run query in site to get vqd number needed for requesting images +# TODO: find a way to get this number without an extra request (is it a hash of the query?) +def get_vqd(query): + res = get(site_url.format(query=urlencode({'q': query}))) + content = res.text + vqd = content[content.find('vqd=\'') + 5:] + vqd = vqd[:vqd.find('\'')] + return vqd + + +# do search-request +def request(query, params): + # to avoid running actual external requests when testing + if 'is_test' not in params: + vqd = get_vqd(query) + else: + vqd = '12345' + + offset = (params['pageno'] - 1) * 50 + + safesearch = params['safesearch'] - 1 + + region_code = get_region_code(params['language']) + if region_code: + params['url'] = images_url.format( + query=urlencode({'q': query, 'l': region_code}), offset=offset, safesearch=safesearch, vqd=vqd) + else: + params['url'] = images_url.format( + query=urlencode({'q': query}), offset=offset, safesearch=safesearch, vqd=vqd) + + return params + + +# get response from search-request +def response(resp): + results = [] + + content = resp.text + try: + res_json = loads(content) + except: + return [] + + # parse results + for result in res_json['results']: + title = result['title'] + url = result['url'] + thumbnail = result['thumbnail'] + image = result['image'] + + # append result + results.append({'template': 'images.html', + 'title': title, + 'content': '', + 'thumbnail_src': thumbnail, + 'img_src': image, + 'url': url}) + + return results diff --git a/searx/engines/dummy.py b/searx/engines/dummy.py new file mode 100644 index 0000000..50b56ef --- /dev/null +++ b/searx/engines/dummy.py @@ -0,0 +1,16 @@ +""" + Dummy + + @results empty array + @stable yes +""" + + +# do search-request +def request(query, params): + return params + + +# get response from search-request +def response(resp): + return [] diff --git a/searx/engines/faroo.py b/searx/engines/faroo.py new file mode 100644 index 0000000..e24d1b7 --- /dev/null +++ b/searx/engines/faroo.py @@ -0,0 +1,116 @@ +""" + Faroo (Web, News) + + @website http://www.faroo.com + @provide-api yes (http://www.faroo.com/hp/api/api.html), require API-key + + @using-api yes + @results JSON + @stable yes + @parse url, title, content, publishedDate, img_src +""" + +from json import loads +import datetime +from searx.utils import searx_useragent +from searx.url_utils import urlencode + +# engine dependent config +categories = ['general', 'news'] +paging = True +language_support = True +number_of_results = 10 +api_key = None + +# search-url +url = 'http://www.faroo.com/' +search_url = url + 'api?{query}'\ + '&start={offset}'\ + '&length={number_of_results}'\ + '&l={language}'\ + '&src={categorie}'\ + '&i=false'\ + '&f=json'\ + '&key={api_key}' # noqa + +search_category = {'general': 'web', + 'news': 'news'} + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * number_of_results + 1 + categorie = search_category.get(params['category'], 'web') + + if params['language'] == 'all': + language = 'en' + else: + language = params['language'].split('_')[0] + + # if language is not supported, put it in english + if language != 'en' and\ + language != 'de' and\ + language != 'zh': + language = 'en' + + params['url'] = search_url.format(offset=offset, + number_of_results=number_of_results, + query=urlencode({'q': query}), + language=language, + categorie=categorie, + api_key=api_key) + + # using searx User-Agent + params['headers']['User-Agent'] = searx_useragent() + + return params + + +# get response from search-request +def response(resp): + # HTTP-Code 401: api-key is not valide + if resp.status_code == 401: + raise Exception("API key is not valide") + + # HTTP-Code 429: rate limit exceeded + if resp.status_code == 429: + raise Exception("rate limit has been exceeded!") + + results = [] + + search_res = loads(resp.text) + + # return empty array if there are no results + if not search_res.get('results', {}): + return [] + + # parse results + for result in search_res['results']: + if result['news']: + # timestamp (milliseconds since 1970) + publishedDate = datetime.datetime.fromtimestamp(result['date'] / 1000.0) # noqa + + # append news result + results.append({'url': result['url'], + 'title': result['title'], + 'publishedDate': publishedDate, + 'content': result['kwic']}) + + else: + # append general result + # TODO, publishedDate correct? + results.append({'url': result['url'], + 'title': result['title'], + 'content': result['kwic']}) + + # append image result if image url is set + # TODO, show results with an image like in faroo + if result['iurl']: + results.append({'template': 'images.html', + 'url': result['url'], + 'title': result['title'], + 'content': result['kwic'], + 'img_src': result['iurl']}) + + # return results + return results diff --git a/searx/engines/fdroid.py b/searx/engines/fdroid.py new file mode 100644 index 0000000..a6b01a8 --- /dev/null +++ b/searx/engines/fdroid.py @@ -0,0 +1,51 @@ +""" + F-Droid (a repository of FOSS applications for Android) + + @website https://f-droid.org/ + @provide-api no + @using-api no + @results HTML + @stable no (HTML can change) + @parse url, title, content +""" + +from lxml import html +from searx.engines.xpath import extract_text +from searx.url_utils import urlencode + +# engine dependent config +categories = ['files'] +paging = True + +# search-url +base_url = 'https://f-droid.org/' +search_url = base_url + 'repository/browse/?{query}' + + +# do search-request +def request(query, params): + query = urlencode({'fdfilter': query, 'fdpage': params['pageno']}) + params['url'] = search_url.format(query=query) + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + for app in dom.xpath('//div[@id="appheader"]'): + url = app.xpath('./ancestor::a/@href')[0] + title = app.xpath('./p/span/text()')[0] + img_src = app.xpath('.//img/@src')[0] + + content = extract_text(app.xpath('./p')[0]) + content = content.replace(title, '', 1).strip() + + results.append({'url': url, + 'title': title, + 'content': content, + 'img_src': img_src}) + + return results diff --git a/searx/engines/filecrop.py b/searx/engines/filecrop.py new file mode 100644 index 0000000..ed57a6b --- /dev/null +++ b/searx/engines/filecrop.py @@ -0,0 +1,88 @@ +from searx.url_utils import urlencode + +try: + from HTMLParser import HTMLParser +except: + from html.parser import HTMLParser + +url = 'http://www.filecrop.com/' +search_url = url + '/search.php?{query}&size_i=0&size_f=100000000&engine_r=1&engine_d=1&engine_e=1&engine_4=1&engine_m=1&pos={index}' # noqa + +paging = True + + +class FilecropResultParser(HTMLParser): + + def __init__(self): + HTMLParser.__init__(self) + self.__start_processing = False + + self.results = [] + self.result = {} + + self.tr_counter = 0 + self.data_counter = 0 + + def handle_starttag(self, tag, attrs): + + if tag == 'tr': + if ('bgcolor', '#edeff5') in attrs or\ + ('bgcolor', '#ffffff') in attrs: + self.__start_processing = True + + if not self.__start_processing: + return + + if tag == 'label': + self.result['title'] = [attr[1] for attr in attrs + if attr[0] == 'title'][0] + elif tag == 'a' and ('rel', 'nofollow') in attrs\ + and ('class', 'sourcelink') in attrs: + if 'content' in self.result: + self.result['content'] += [attr[1] for attr in attrs + if attr[0] == 'title'][0] + else: + self.result['content'] = [attr[1] for attr in attrs + if attr[0] == 'title'][0] + self.result['content'] += ' ' + elif tag == 'a': + self.result['url'] = url + [attr[1] for attr in attrs + if attr[0] == 'href'][0] + + def handle_endtag(self, tag): + if self.__start_processing is False: + return + + if tag == 'tr': + self.tr_counter += 1 + + if self.tr_counter == 2: + self.__start_processing = False + self.tr_counter = 0 + self.data_counter = 0 + self.results.append(self.result) + self.result = {} + + def handle_data(self, data): + if not self.__start_processing: + return + + if 'content' in self.result: + self.result['content'] += data + ' ' + else: + self.result['content'] = data + ' ' + + self.data_counter += 1 + + +def request(query, params): + index = 1 + (params['pageno'] - 1) * 30 + params['url'] = search_url.format(query=urlencode({'w': query}), index=index) + return params + + +def response(resp): + parser = FilecropResultParser() + parser.feed(resp.text) + + return parser.results diff --git a/searx/engines/flickr.py b/searx/engines/flickr.py new file mode 100644 index 0000000..de17693 --- /dev/null +++ b/searx/engines/flickr.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python + +""" + Flickr (Images) + + @website https://www.flickr.com + @provide-api yes (https://secure.flickr.com/services/api/flickr.photos.search.html) + + @using-api yes + @results JSON + @stable yes + @parse url, title, thumbnail, img_src + More info on api-key : https://www.flickr.com/services/apps/create/ +""" + +from json import loads +from searx.url_utils import urlencode + +categories = ['images'] + +nb_per_page = 15 +paging = True +api_key = None + + +url = 'https://api.flickr.com/services/rest/?method=flickr.photos.search' +\ + '&api_key={api_key}&{text}&sort=relevance' +\ + '&extras=description%2C+owner_name%2C+url_o%2C+url_n%2C+url_z' +\ + '&per_page={nb_per_page}&format=json&nojsoncallback=1&page={page}' +photo_url = 'https://www.flickr.com/photos/{userid}/{photoid}' + +paging = True + + +def build_flickr_url(user_id, photo_id): + return photo_url.format(userid=user_id, photoid=photo_id) + + +def request(query, params): + params['url'] = url.format(text=urlencode({'text': query}), + api_key=api_key, + nb_per_page=nb_per_page, + page=params['pageno']) + return params + + +def response(resp): + results = [] + + search_results = loads(resp.text) + + # return empty array if there are no results + if 'photos' not in search_results: + return [] + + if 'photo' not in search_results['photos']: + return [] + + photos = search_results['photos']['photo'] + + # parse results + for photo in photos: + if 'url_o' in photo: + img_src = photo['url_o'] + elif 'url_z' in photo: + img_src = photo['url_z'] + else: + continue + +# For a bigger thumbnail, keep only the url_z, not the url_n + if 'url_n' in photo: + thumbnail_src = photo['url_n'] + elif 'url_z' in photo: + thumbnail_src = photo['url_z'] + else: + thumbnail_src = img_src + + url = build_flickr_url(photo['owner'], photo['id']) + + # append result + results.append({'url': url, + 'title': photo['title'], + 'img_src': img_src, + 'thumbnail_src': thumbnail_src, + 'content': photo['description']['_content'], + 'author': photo['ownername'], + 'template': 'images.html'}) + + # return results + return results diff --git a/searx/engines/flickr_noapi.py b/searx/engines/flickr_noapi.py new file mode 100644 index 0000000..08f07f7 --- /dev/null +++ b/searx/engines/flickr_noapi.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +""" + Flickr (Images) + + @website https://www.flickr.com + @provide-api yes (https://secure.flickr.com/services/api/flickr.photos.search.html) + + @using-api no + @results HTML + @stable no + @parse url, title, thumbnail, img_src +""" + +from json import loads +from time import time +import re +from searx.engines import logger +from searx.url_utils import urlencode + + +logger = logger.getChild('flickr-noapi') + +categories = ['images'] + +url = 'https://www.flickr.com/' +search_url = url + 'search?{query}&page={page}' +time_range_url = '&min_upload_date={start}&max_upload_date={end}' +photo_url = 'https://www.flickr.com/photos/{userid}/{photoid}' +regex = re.compile(r"\"search-photos-lite-models\",\"photos\":(.*}),\"totalItems\":", re.DOTALL) +image_sizes = ('o', 'k', 'h', 'b', 'c', 'z', 'n', 'm', 't', 'q', 's') + +paging = True +time_range_support = True +time_range_dict = {'day': 60 * 60 * 24, + 'week': 60 * 60 * 24 * 7, + 'month': 60 * 60 * 24 * 7 * 4, + 'year': 60 * 60 * 24 * 7 * 52} + + +def build_flickr_url(user_id, photo_id): + return photo_url.format(userid=user_id, photoid=photo_id) + + +def _get_time_range_url(time_range): + if time_range in time_range_dict: + return time_range_url.format(start=time(), end=str(int(time()) - time_range_dict[time_range])) + return '' + + +def request(query, params): + params['url'] = (search_url.format(query=urlencode({'text': query}), page=params['pageno']) + + _get_time_range_url(params['time_range'])) + return params + + +def response(resp): + results = [] + + matches = regex.search(resp.text) + + if matches is None: + return results + + match = matches.group(1) + search_results = loads(match) + + if '_data' not in search_results: + return [] + + photos = search_results['_data'] + + for photo in photos: + + # In paged configuration, the first pages' photos + # are represented by a None object + if photo is None: + continue + + img_src = None + # From the biggest to the lowest format + for image_size in image_sizes: + if image_size in photo['sizes']: + img_src = photo['sizes'][image_size]['url'] + break + + if not img_src: + logger.debug('cannot find valid image size: {0}'.format(repr(photo))) + continue + + if 'ownerNsid' not in photo: + continue + + # For a bigger thumbnail, keep only the url_z, not the url_n + if 'n' in photo['sizes']: + thumbnail_src = photo['sizes']['n']['url'] + elif 'z' in photo['sizes']: + thumbnail_src = photo['sizes']['z']['url'] + else: + thumbnail_src = img_src + + url = build_flickr_url(photo['ownerNsid'], photo['id']) + + title = photo.get('title', '') + + author = photo['username'] + + # append result + results.append({'url': url, + 'title': title, + 'img_src': img_src, + 'thumbnail_src': thumbnail_src, + 'content': '', + 'author': author, + 'template': 'images.html'}) + + return results diff --git a/searx/engines/framalibre.py b/searx/engines/framalibre.py new file mode 100644 index 0000000..146cdae --- /dev/null +++ b/searx/engines/framalibre.py @@ -0,0 +1,69 @@ +""" + FramaLibre (It) + + @website https://framalibre.org/ + @provide-api no + + @using-api no + @results HTML + @stable no (HTML can change) + @parse url, title, content, thumbnail, img_src +""" + +from cgi import escape +from lxml import html +from searx.engines.xpath import extract_text +from searx.url_utils import urljoin, urlencode + +# engine dependent config +categories = ['it'] +paging = True + +# search-url +base_url = 'https://framalibre.org/' +search_url = base_url + 'recherche-par-crit-res?{query}&page={offset}' + +# specific xpath variables +results_xpath = '//div[@class="nodes-list-row"]/div[contains(@typeof,"sioc:Item")]' +link_xpath = './/h3[@class="node-title"]/a[@href]' +thumbnail_xpath = './/img[@class="media-object img-responsive"]/@src' +content_xpath = './/div[@class="content"]//p' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) + params['url'] = search_url.format(query=urlencode({'keys': query}), + offset=offset) + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + # parse results + for result in dom.xpath(results_xpath): + link = result.xpath(link_xpath)[0] + href = urljoin(base_url, link.attrib.get('href')) + # there's also a span (class="rdf-meta element-hidden" property="dc:title")'s content property for this... + title = escape(extract_text(link)) + thumbnail_tags = result.xpath(thumbnail_xpath) + thumbnail = None + if len(thumbnail_tags) > 0: + thumbnail = extract_text(thumbnail_tags[0]) + if thumbnail[0] == '/': + thumbnail = base_url + thumbnail + content = escape(extract_text(result.xpath(content_xpath))) + + # append result + results.append({'url': href, + 'title': title, + 'img_src': thumbnail, + 'content': content}) + + # return results + return results diff --git a/searx/engines/frinkiac.py b/searx/engines/frinkiac.py new file mode 100644 index 0000000..a67b42d --- /dev/null +++ b/searx/engines/frinkiac.py @@ -0,0 +1,44 @@ +""" +Frinkiac (Images) + +@website https://www.frinkiac.com +@provide-api no +@using-api no +@results JSON +@stable no +@parse url, title, img_src +""" + +from json import loads +from searx.url_utils import urlencode + +categories = ['images'] + +BASE = 'https://frinkiac.com/' +SEARCH_URL = '{base}api/search?{query}' +RESULT_URL = '{base}?{query}' +THUMB_URL = '{base}img/{episode}/{timestamp}/medium.jpg' +IMAGE_URL = '{base}img/{episode}/{timestamp}.jpg' + + +def request(query, params): + params['url'] = SEARCH_URL.format(base=BASE, query=urlencode({'q': query})) + return params + + +def response(resp): + results = [] + response_data = loads(resp.text) + for result in response_data: + episode = result['Episode'] + timestamp = result['Timestamp'] + + results.append({'template': 'images.html', + 'url': RESULT_URL.format(base=BASE, + query=urlencode({'p': 'caption', 'e': episode, 't': timestamp})), + 'title': episode, + 'content': '', + 'thumbnail_src': THUMB_URL.format(base=BASE, episode=episode, timestamp=timestamp), + 'img_src': IMAGE_URL.format(base=BASE, episode=episode, timestamp=timestamp)}) + + return results diff --git a/searx/engines/generalfile.py b/searx/engines/generalfile.py new file mode 100644 index 0000000..3bb2744 --- /dev/null +++ b/searx/engines/generalfile.py @@ -0,0 +1,62 @@ +""" + General Files (Files) + + @website http://www.general-files.org + @provide-api no (nothing found) + + @using-api no (because nothing found) + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content + + @todo detect torrents? +""" + +from lxml import html + +# engine dependent config +categories = ['files'] +paging = True + +# search-url +base_url = 'http://www.general-file.com' +search_url = base_url + '/files-{letter}/{query}/{pageno}' + +# specific xpath variables +result_xpath = '//table[@class="block-file"]' +title_xpath = './/h2/a//text()' +url_xpath = './/h2/a/@href' +content_xpath = './/p//text()' + + +# do search-request +def request(query, params): + + params['url'] = search_url.format(query=query, + letter=query[0], + pageno=params['pageno']) + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + # parse results + for result in dom.xpath(result_xpath): + url = result.xpath(url_xpath)[0] + + # skip fast download links + if not url.startswith('/'): + continue + + # append result + results.append({'url': base_url + url, + 'title': ''.join(result.xpath(title_xpath)), + 'content': ''.join(result.xpath(content_xpath))}) + + # return results + return results diff --git a/searx/engines/gigablast.py b/searx/engines/gigablast.py new file mode 100644 index 0000000..37933c6 --- /dev/null +++ b/searx/engines/gigablast.py @@ -0,0 +1,106 @@ +""" + Gigablast (Web) + + @website https://gigablast.com + @provide-api yes (https://gigablast.com/api.html) + + @using-api yes + @results XML + @stable yes + @parse url, title, content +""" + +from json import loads +from time import time +from lxml.html import fromstring +from searx.url_utils import urlencode + +# engine dependent config +categories = ['general'] +paging = True +number_of_results = 10 +language_support = True +safesearch = True + +# search-url +base_url = 'https://gigablast.com/' +search_string = 'search?{query}'\ + '&n={number_of_results}'\ + '&c=main'\ + '&s={offset}'\ + '&format=json'\ + '&qh=0'\ + '&qlang={lang}'\ + '&ff={safesearch}'\ + '&rxikd={rxikd}' # random number - 9 digits + +# specific xpath variables +results_xpath = '//response//result' +url_xpath = './/url' +title_xpath = './/title' +content_xpath = './/sum' + +supported_languages_url = 'https://gigablast.com/search?&rxikd=1' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * number_of_results + + if params['language'] == 'all': + language = 'xx' + else: + language = params['language'].replace('-', '_').lower() + if language.split('-')[0] != 'zh': + language = language.split('-')[0] + + if params['safesearch'] >= 1: + safesearch = 1 + else: + safesearch = 0 + + search_path = search_string.format(query=urlencode({'q': query}), + offset=offset, + number_of_results=number_of_results, + rxikd=str(time())[:9], + lang=language, + safesearch=safesearch) + + params['url'] = base_url + search_path + + return params + + +# get response from search-request +def response(resp): + results = [] + + # parse results + response_json = loads(resp.text) + + for result in response_json['results']: + # append result + results.append({'url': result['url'], + 'title': result['title'], + 'content': result['sum']}) + + # return results + return results + + +# get supported languages from their site +def _fetch_supported_languages(resp): + supported_languages = [] + dom = fromstring(resp.text) + links = dom.xpath('//span[@id="menu2"]/a') + for link in links: + href = link.xpath('./@href')[0].split('lang%3A') + if len(href) == 2: + code = href[1].split('_') + if len(code) == 2: + code = code[0] + '-' + code[1].upper() + else: + code = code[0] + supported_languages.append(code) + + return supported_languages diff --git a/searx/engines/github.py b/searx/engines/github.py new file mode 100644 index 0000000..eaa00da --- /dev/null +++ b/searx/engines/github.py @@ -0,0 +1,60 @@ +""" + Github (It) + + @website https://github.com/ + @provide-api yes (https://developer.github.com/v3/) + + @using-api yes + @results JSON + @stable yes (using api) + @parse url, title, content +""" + +from json import loads +from searx.url_utils import urlencode + +# engine dependent config +categories = ['it'] + +# search-url +search_url = 'https://api.github.com/search/repositories?sort=stars&order=desc&{query}' # noqa + +accept_header = 'application/vnd.github.preview.text-match+json' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(query=urlencode({'q': query})) + + params['headers']['Accept'] = accept_header + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_res = loads(resp.text) + + # check if items are recieved + if 'items' not in search_res: + return [] + + # parse results + for res in search_res['items']: + title = res['name'] + url = res['html_url'] + + if res['description']: + content = res['description'][:500] + else: + content = '' + + # append result + results.append({'url': url, + 'title': title, + 'content': content}) + + # return results + return results diff --git a/searx/engines/google.py b/searx/engines/google.py new file mode 100644 index 0000000..934f5c2 --- /dev/null +++ b/searx/engines/google.py @@ -0,0 +1,388 @@ +# Google (Web) +# +# @website https://www.google.com +# @provide-api yes (https://developers.google.com/custom-search/) +# +# @using-api no +# @results HTML +# @stable no (HTML can change) +# @parse url, title, content, suggestion + +import re +from lxml import html, etree +from searx.engines.xpath import extract_text, extract_url +from searx import logger +from searx.url_utils import urlencode, urlparse, parse_qsl + +logger = logger.getChild('google engine') + + +# engine dependent config +categories = ['general'] +paging = True +language_support = True +use_locale_domain = True +time_range_support = True + +# based on https://en.wikipedia.org/wiki/List_of_Google_domains and tests +default_hostname = 'www.google.com' + +country_to_hostname = { + 'BG': 'www.google.bg', # Bulgaria + 'CZ': 'www.google.cz', # Czech Republic + 'DE': 'www.google.de', # Germany + 'DK': 'www.google.dk', # Denmark + 'AT': 'www.google.at', # Austria + 'CH': 'www.google.ch', # Switzerland + 'GR': 'www.google.gr', # Greece + 'AU': 'www.google.com.au', # Australia + 'CA': 'www.google.ca', # Canada + 'GB': 'www.google.co.uk', # United Kingdom + 'ID': 'www.google.co.id', # Indonesia + 'IE': 'www.google.ie', # Ireland + 'IN': 'www.google.co.in', # India + 'MY': 'www.google.com.my', # Malaysia + 'NZ': 'www.google.co.nz', # New Zealand + 'PH': 'www.google.com.ph', # Philippines + 'SG': 'www.google.com.sg', # Singapore + # 'US': 'www.google.us', # United States, redirect to .com + 'ZA': 'www.google.co.za', # South Africa + 'AR': 'www.google.com.ar', # Argentina + 'CL': 'www.google.cl', # Chile + 'ES': 'www.google.es', # Spain + 'MX': 'www.google.com.mx', # Mexico + 'EE': 'www.google.ee', # Estonia + 'FI': 'www.google.fi', # Finland + 'BE': 'www.google.be', # Belgium + 'FR': 'www.google.fr', # France + 'IL': 'www.google.co.il', # Israel + 'HR': 'www.google.hr', # Croatia + 'HU': 'www.google.hu', # Hungary + 'IT': 'www.google.it', # Italy + 'JP': 'www.google.co.jp', # Japan + 'KR': 'www.google.co.kr', # South Korea + 'LT': 'www.google.lt', # Lithuania + 'LV': 'www.google.lv', # Latvia + 'NO': 'www.google.no', # Norway + 'NL': 'www.google.nl', # Netherlands + 'PL': 'www.google.pl', # Poland + 'BR': 'www.google.com.br', # Brazil + 'PT': 'www.google.pt', # Portugal + 'RO': 'www.google.ro', # Romania + 'RU': 'www.google.ru', # Russia + 'SK': 'www.google.sk', # Slovakia + 'SL': 'www.google.si', # Slovenia (SL -> si) + 'SE': 'www.google.se', # Sweden + 'TH': 'www.google.co.th', # Thailand + 'TR': 'www.google.com.tr', # Turkey + 'UA': 'www.google.com.ua', # Ukraine + # 'CN': 'www.google.cn', # China, only from China ? + 'HK': 'www.google.com.hk', # Hong Kong + 'TW': 'www.google.com.tw' # Taiwan +} + +# osm +url_map = 'https://www.openstreetmap.org/'\ + + '?lat={latitude}&lon={longitude}&zoom={zoom}&layers=M' + +# search-url +search_path = '/search' +search_url = ('https://{hostname}' + + search_path + + '?{query}&start={offset}&gws_rd=cr&gbv=1&lr={lang}&ei=x') + +time_range_search = "&tbs=qdr:{range}" +time_range_dict = {'day': 'd', + 'week': 'w', + 'month': 'm', + 'year': 'y'} + +# other URLs +map_hostname_start = 'maps.google.' +maps_path = '/maps' +redirect_path = '/url' +images_path = '/images' +supported_languages_url = 'https://www.google.com/preferences?#languages' + +# specific xpath variables +results_xpath = '//div[@class="g"]' +url_xpath = './/h3/a/@href' +title_xpath = './/h3' +content_xpath = './/span[@class="st"]' +content_misc_xpath = './/div[@class="f slp"]' +suggestion_xpath = '//p[@class="_Bmc"]' +spelling_suggestion_xpath = '//a[@class="spell"]' + +# map : detail location +map_address_xpath = './/div[@class="s"]//table//td[2]/span/text()' +map_phone_xpath = './/div[@class="s"]//table//td[2]/span/span' +map_website_url_xpath = 'h3[2]/a/@href' +map_website_title_xpath = 'h3[2]' + +# map : near the location +map_near = 'table[@class="ts"]//tr' +map_near_title = './/h4' +map_near_url = './/h4/a/@href' +map_near_phone = './/span[@class="nobr"]' + +# images +images_xpath = './/div/a' +image_url_xpath = './@href' +image_img_src_xpath = './img/@src' + +# property names +# FIXME : no translation +property_address = "Address" +property_phone = "Phone number" + + +# remove google-specific tracking-url +def parse_url(url_string, google_hostname): + # sanity check + if url_string is None: + return url_string + + # normal case + parsed_url = urlparse(url_string) + if (parsed_url.netloc in [google_hostname, ''] + and parsed_url.path == redirect_path): + query = dict(parse_qsl(parsed_url.query)) + return query['q'] + else: + return url_string + + +# returns extract_text on the first result selected by the xpath or None +def extract_text_from_dom(result, xpath): + r = result.xpath(xpath) + if len(r) > 0: + return extract_text(r[0]) + return None + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 10 + + if params['language'] == 'all': + language = 'en' + country = 'US' + url_lang = '' + elif params['language'][:2] == 'jv': + language = 'jw' + country = 'ID' + url_lang = 'lang_jw' + else: + language_array = params['language'].lower().split('-') + if len(language_array) == 2: + country = language_array[1] + else: + country = 'US' + language = language_array[0] + ',' + language_array[0] + '-' + country + url_lang = 'lang_' + language_array[0] + + if use_locale_domain: + google_hostname = country_to_hostname.get(country.upper(), default_hostname) + else: + google_hostname = default_hostname + + params['url'] = search_url.format(offset=offset, + query=urlencode({'q': query}), + hostname=google_hostname, + lang=url_lang) + if params['time_range'] in time_range_dict: + params['url'] += time_range_search.format(range=time_range_dict[params['time_range']]) + + params['headers']['Accept-Language'] = language + params['headers']['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' + + params['google_hostname'] = google_hostname + + return params + + +# get response from search-request +def response(resp): + results = [] + + # detect google sorry + resp_url = urlparse(resp.url) + if resp_url.netloc == 'sorry.google.com' or resp_url.path == '/sorry/IndexRedirect': + raise RuntimeWarning('sorry.google.com') + + # which hostname ? + google_hostname = resp.search_params.get('google_hostname') + google_url = "https://" + google_hostname + + # convert the text to dom + dom = html.fromstring(resp.text) + + instant_answer = dom.xpath('//div[@id="_vBb"]//text()') + if instant_answer: + results.append({'answer': u' '.join(instant_answer)}) + try: + results_num = int(dom.xpath('//div[@id="resultStats"]//text()')[0] + .split()[1].replace(',', '')) + results.append({'number_of_results': results_num}) + except: + pass + + # parse results + for result in dom.xpath(results_xpath): + try: + title = extract_text(result.xpath(title_xpath)[0]) + url = parse_url(extract_url(result.xpath(url_xpath), google_url), google_hostname) + parsed_url = urlparse(url, google_hostname) + + # map result + if parsed_url.netloc == google_hostname: + # TODO fix inside links + continue + # if parsed_url.path.startswith(maps_path) or parsed_url.netloc.startswith(map_hostname_start): + # print "yooooo"*30 + # x = result.xpath(map_near) + # if len(x) > 0: + # # map : near the location + # results = results + parse_map_near(parsed_url, x, google_hostname) + # else: + # # map : detail about a location + # results = results + parse_map_detail(parsed_url, result, google_hostname) + # # google news + # elif parsed_url.path == search_path: + # # skipping news results + # pass + + # # images result + # elif parsed_url.path == images_path: + # # only thumbnail image provided, + # # so skipping image results + # # results = results + parse_images(result, google_hostname) + # pass + + else: + # normal result + content = extract_text_from_dom(result, content_xpath) + if content is None: + continue + content_misc = extract_text_from_dom(result, content_misc_xpath) + if content_misc is not None: + content = content_misc + "
" + content + # append result + results.append({'url': url, + 'title': title, + 'content': content + }) + except: + logger.debug('result parse error in:\n%s', etree.tostring(result, pretty_print=True)) + continue + + # parse suggestion + for suggestion in dom.xpath(suggestion_xpath): + # append suggestion + results.append({'suggestion': extract_text(suggestion)}) + + for correction in dom.xpath(spelling_suggestion_xpath): + results.append({'correction': extract_text(correction)}) + + # return results + return results + + +def parse_images(result, google_hostname): + results = [] + for image in result.xpath(images_xpath): + url = parse_url(extract_text(image.xpath(image_url_xpath)[0]), google_hostname) + img_src = extract_text(image.xpath(image_img_src_xpath)[0]) + + # append result + results.append({'url': url, + 'title': '', + 'content': '', + 'img_src': img_src, + 'template': 'images.html' + }) + + return results + + +def parse_map_near(parsed_url, x, google_hostname): + results = [] + + for result in x: + title = extract_text_from_dom(result, map_near_title) + url = parse_url(extract_text_from_dom(result, map_near_url), google_hostname) + attributes = [] + phone = extract_text_from_dom(result, map_near_phone) + add_attributes(attributes, property_phone, phone, 'tel:' + phone) + results.append({'title': title, + 'url': url, + 'content': attributes_to_html(attributes) + }) + + return results + + +def parse_map_detail(parsed_url, result, google_hostname): + results = [] + + # try to parse the geoloc + m = re.search(r'@([0-9\.]+),([0-9\.]+),([0-9]+)', parsed_url.path) + if m is None: + m = re.search(r'll\=([0-9\.]+),([0-9\.]+)\&z\=([0-9]+)', parsed_url.query) + + if m is not None: + # geoloc found (ignored) + lon = float(m.group(2)) # noqa + lat = float(m.group(1)) # noqa + zoom = int(m.group(3)) # noqa + + # attributes + attributes = [] + address = extract_text_from_dom(result, map_address_xpath) + phone = extract_text_from_dom(result, map_phone_xpath) + add_attributes(attributes, property_address, address, 'geo:' + str(lat) + ',' + str(lon)) + add_attributes(attributes, property_phone, phone, 'tel:' + phone) + + # title / content / url + website_title = extract_text_from_dom(result, map_website_title_xpath) + content = extract_text_from_dom(result, content_xpath) + website_url = parse_url(extract_text_from_dom(result, map_website_url_xpath), google_hostname) + + # add a result if there is a website + if website_url is not None: + results.append({'title': website_title, + 'content': (content + '
' if content is not None else '') + + attributes_to_html(attributes), + 'url': website_url + }) + + return results + + +def add_attributes(attributes, name, value, url): + if value is not None and len(value) > 0: + attributes.append({'label': name, 'value': value, 'url': url}) + + +def attributes_to_html(attributes): + retval = '' + for a in attributes: + value = a.get('value') + if 'url' in a: + value = '' + value + '' + retval = retval + '' + retval = retval + '
' + a.get('label') + '' + value + '
' + return retval + + +# get supported languages from their site +def _fetch_supported_languages(resp): + supported_languages = {} + dom = html.fromstring(resp.text) + options = dom.xpath('//table//td/font/label/span') + for option in options: + code = option.xpath('./@id')[0][1:] + name = option.text.title() + supported_languages[code] = {"name": name} + + return supported_languages diff --git a/searx/engines/google_images.py b/searx/engines/google_images.py new file mode 100644 index 0000000..9692f4b --- /dev/null +++ b/searx/engines/google_images.py @@ -0,0 +1,95 @@ +""" + Google (Images) + + @website https://www.google.com + @provide-api yes (https://developers.google.com/custom-search/) + + @using-api no + @results HTML chunks with JSON inside + @stable no + @parse url, title, img_src +""" + +from datetime import date, timedelta +from json import loads +from lxml import html +from searx.url_utils import urlencode + + +# engine dependent config +categories = ['images'] +paging = True +safesearch = True +time_range_support = True +number_of_results = 100 + +search_url = 'https://www.google.com/search'\ + '?{query}'\ + '&asearch=ichunk'\ + '&async=_id:rg_s,_pms:s'\ + '&tbm=isch'\ + '&yv=2'\ + '&{search_options}' +time_range_attr = "qdr:{range}" +time_range_custom_attr = "cdr:1,cd_min:{start},cd_max{end}" +time_range_dict = {'day': 'd', + 'week': 'w', + 'month': 'm'} + + +# do search-request +def request(query, params): + search_options = { + 'ijn': params['pageno'] - 1, + 'start': (params['pageno'] - 1) * number_of_results + } + + if params['time_range'] in time_range_dict: + search_options['tbs'] = time_range_attr.format(range=time_range_dict[params['time_range']]) + elif params['time_range'] == 'year': + now = date.today() + then = now - timedelta(days=365) + start = then.strftime('%m/%d/%Y') + end = now.strftime('%m/%d/%Y') + search_options['tbs'] = time_range_custom_attr.format(start=start, end=end) + + if safesearch and params['safesearch']: + search_options['safe'] = 'on' + + params['url'] = search_url.format(query=urlencode({'q': query}), + search_options=urlencode(search_options)) + + return params + + +# get response from search-request +def response(resp): + results = [] + + g_result = loads(resp.text) + + dom = html.fromstring(g_result[1][1]) + + # parse results + for result in dom.xpath('//div[@data-ved]'): + + try: + metadata = loads(''.join(result.xpath('./div[@class="rg_meta"]/text()'))) + except: + continue + + thumbnail_src = metadata['tu'] + + # http to https + thumbnail_src = thumbnail_src.replace("http://", "https://") + + # append result + results.append({'url': metadata['ru'], + 'title': metadata['pt'], + 'content': metadata['s'], + 'thumbnail_src': thumbnail_src, + 'img_src': metadata['ou'], + 'template': 'images.html'}) + + # return results + return results diff --git a/searx/engines/google_news.py b/searx/engines/google_news.py new file mode 100644 index 0000000..7344b52 --- /dev/null +++ b/searx/engines/google_news.py @@ -0,0 +1,84 @@ +""" + Google (News) + + @website https://news.google.com + @provide-api no + + @using-api no + @results HTML + @stable no + @parse url, title, content, publishedDate +""" + +from lxml import html +from searx.engines.google import _fetch_supported_languages, supported_languages_url +from searx.url_utils import urlencode + +# search-url +categories = ['news'] +paging = True +language_support = True +safesearch = True +time_range_support = True +number_of_results = 10 + +search_url = 'https://www.google.com/search'\ + '?{query}'\ + '&tbm=nws'\ + '&gws_rd=cr'\ + '&{search_options}' +time_range_attr = "qdr:{range}" +time_range_dict = {'day': 'd', + 'week': 'w', + 'month': 'm', + 'year': 'y'} + + +# do search-request +def request(query, params): + + search_options = { + 'start': (params['pageno'] - 1) * number_of_results + } + + if params['time_range'] in time_range_dict: + search_options['tbs'] = time_range_attr.format(range=time_range_dict[params['time_range']]) + + if safesearch and params['safesearch']: + search_options['safe'] = 'on' + + params['url'] = search_url.format(query=urlencode({'q': query}), + search_options=urlencode(search_options)) + + if params['language'] != 'all': + language_array = params['language'].lower().split('-') + params['url'] += '&lr=lang_' + language_array[0] + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + # parse results + for result in dom.xpath('//div[@class="g"]|//div[@class="g _cy"]'): + try: + r = { + 'url': result.xpath('.//div[@class="_cnc"]//a/@href')[0], + 'title': ''.join(result.xpath('.//div[@class="_cnc"]//h3//text()')), + 'content': ''.join(result.xpath('.//div[@class="st"]//text()')), + } + except: + continue + + imgs = result.xpath('.//img/@src') + if len(imgs) and not imgs[0].startswith('data'): + r['img_src'] = imgs[0] + + results.append(r) + + # return results + return results diff --git a/searx/engines/ina.py b/searx/engines/ina.py new file mode 100644 index 0000000..37a05f0 --- /dev/null +++ b/searx/engines/ina.py @@ -0,0 +1,87 @@ +# INA (Videos) +# +# @website https://www.ina.fr/ +# @provide-api no +# +# @using-api no +# @results HTML (using search portal) +# @stable no (HTML can change) +# @parse url, title, content, publishedDate, thumbnail +# +# @todo set content-parameter with correct data +# @todo embedded (needs some md5 from video page) + +from json import loads +from lxml import html +from dateutil import parser +from searx.engines.xpath import extract_text +from searx.url_utils import urlencode + +try: + from HTMLParser import HTMLParser +except: + from html.parser import HTMLParser + +# engine dependent config +categories = ['videos'] +paging = True +page_size = 48 + +# search-url +base_url = 'https://www.ina.fr' +search_url = base_url + '/layout/set/ajax/recherche/result?autopromote=&hf={ps}&b={start}&type=Video&r=&{query}' + +# specific xpath variables +results_xpath = '//div[contains(@class,"search-results--list")]/div[@class="media"]' +url_xpath = './/a/@href' +title_xpath = './/h3[@class="h3--title media-heading"]' +thumbnail_xpath = './/img/@src' +publishedDate_xpath = './/span[@class="broadcast"]' +content_xpath = './/p[@class="media-body__summary"]' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(ps=page_size, + start=params['pageno'] * page_size, + query=urlencode({'q': query})) + + return params + + +# get response from search-request +def response(resp): + results = [] + + # we get html in a JSON container... + response = loads(resp.text) + if "content" not in response: + return [] + dom = html.fromstring(response["content"]) + p = HTMLParser() + + # parse results + for result in dom.xpath(results_xpath): + videoid = result.xpath(url_xpath)[0] + url = base_url + videoid + title = p.unescape(extract_text(result.xpath(title_xpath))) + thumbnail = extract_text(result.xpath(thumbnail_xpath)[0]) + if thumbnail[0] == '/': + thumbnail = base_url + thumbnail + d = extract_text(result.xpath(publishedDate_xpath)[0]) + d = d.split('/') + # force ISO date to avoid wrong parsing + d = "%s-%s-%s" % (d[2], d[1], d[0]) + publishedDate = parser.parse(d) + content = extract_text(result.xpath(content_xpath)) + + # append result + results.append({'url': url, + 'title': title, + 'content': content, + 'template': 'videos.html', + 'publishedDate': publishedDate, + 'thumbnail': thumbnail}) + + # return results + return results diff --git a/searx/engines/json_engine.py b/searx/engines/json_engine.py new file mode 100644 index 0000000..67d6a5a --- /dev/null +++ b/searx/engines/json_engine.py @@ -0,0 +1,118 @@ +from collections import Iterable +from json import loads +from sys import version_info +from searx.url_utils import urlencode + +if version_info[0] == 3: + unicode = str + +search_url = None +url_query = None +content_query = None +title_query = None +paging = False +suggestion_query = '' +results_query = '' + +# parameters for engines with paging support +# +# number of results on each page +# (only needed if the site requires not a page number, but an offset) +page_size = 1 +# number of the first page (usually 0 or 1) +first_page_num = 1 + + +def iterate(iterable): + if type(iterable) == dict: + it = iterable.items() + + else: + it = enumerate(iterable) + for index, value in it: + yield str(index), value + + +def is_iterable(obj): + if type(obj) == str: + return False + if type(obj) == unicode: + return False + return isinstance(obj, Iterable) + + +def parse(query): + q = [] + for part in query.split('/'): + if part == '': + continue + else: + q.append(part) + return q + + +def do_query(data, q): + ret = [] + if not q: + return ret + + qkey = q[0] + + for key, value in iterate(data): + + if len(q) == 1: + if key == qkey: + ret.append(value) + elif is_iterable(value): + ret.extend(do_query(value, q)) + else: + if not is_iterable(value): + continue + if key == qkey: + ret.extend(do_query(value, q[1:])) + else: + ret.extend(do_query(value, q)) + return ret + + +def query(data, query_string): + q = parse(query_string) + + return do_query(data, q) + + +def request(query, params): + query = urlencode({'q': query})[2:] + + fp = {'query': query} + if paging and search_url.find('{pageno}') >= 0: + fp['pageno'] = (params['pageno'] - 1) * page_size + first_page_num + + params['url'] = search_url.format(**fp) + params['query'] = query + + return params + + +def response(resp): + results = [] + json = loads(resp.text) + if results_query: + for result in query(json, results_query)[0]: + url = query(result, url_query)[0] + title = query(result, title_query)[0] + content = query(result, content_query)[0] + results.append({'url': url, 'title': title, 'content': content}) + else: + for url, title, content in zip( + query(json, url_query), + query(json, title_query), + query(json, content_query) + ): + results.append({'url': url, 'title': title, 'content': content}) + + if not suggestion_query: + return results + for suggestion in query(json, suggestion_query): + results.append({'suggestion': suggestion}) + return results diff --git a/searx/engines/kickass.py b/searx/engines/kickass.py new file mode 100644 index 0000000..5e897c9 --- /dev/null +++ b/searx/engines/kickass.py @@ -0,0 +1,92 @@ +""" + Kickass Torrent (Videos, Music, Files) + + @website https://kickass.so + @provide-api no (nothing found) + + @using-api no + @results HTML (using search portal) + @stable yes (HTML can change) + @parse url, title, content, seed, leech, magnetlink +""" + +from lxml import html +from operator import itemgetter +from searx.engines.xpath import extract_text +from searx.utils import get_torrent_size, convert_str_to_int +from searx.url_utils import quote, urljoin + +# engine dependent config +categories = ['videos', 'music', 'files'] +paging = True + +# search-url +url = 'https://kickass.cd/' +search_url = url + 'search/{search_term}/{pageno}/' + +# specific xpath variables +magnet_xpath = './/a[@title="Torrent magnet link"]' +torrent_xpath = './/a[@title="Download torrent file"]' +content_xpath = './/span[@class="font11px lightgrey block"]' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(search_term=quote(query), + pageno=params['pageno']) + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + search_res = dom.xpath('//table[@class="data"]//tr') + + # return empty array if nothing is found + if not search_res: + return [] + + # parse results + for result in search_res[1:]: + link = result.xpath('.//a[@class="cellMainLink"]')[0] + href = urljoin(url, link.attrib['href']) + title = extract_text(link) + content = extract_text(result.xpath(content_xpath)) + seed = extract_text(result.xpath('.//td[contains(@class, "green")]')) + leech = extract_text(result.xpath('.//td[contains(@class, "red")]')) + filesize_info = extract_text(result.xpath('.//td[contains(@class, "nobr")]')) + files = extract_text(result.xpath('.//td[contains(@class, "center")][2]')) + + seed = convert_str_to_int(seed) + leech = convert_str_to_int(leech) + + filesize, filesize_multiplier = filesize_info.split() + filesize = get_torrent_size(filesize, filesize_multiplier) + if files.isdigit(): + files = int(files) + else: + files = None + + magnetlink = result.xpath(magnet_xpath)[0].attrib['href'] + + torrentfile = result.xpath(torrent_xpath)[0].attrib['href'] + torrentfileurl = quote(torrentfile, safe="%/:=&?~#+!$,;'@()*") + + # append result + results.append({'url': href, + 'title': title, + 'content': content, + 'seed': seed, + 'leech': leech, + 'filesize': filesize, + 'files': files, + 'magnetlink': magnetlink, + 'torrentfile': torrentfileurl, + 'template': 'torrent.html'}) + + # return results sorted by seeder + return sorted(results, key=itemgetter('seed'), reverse=True) diff --git a/searx/engines/mediawiki.py b/searx/engines/mediawiki.py new file mode 100644 index 0000000..0607ac9 --- /dev/null +++ b/searx/engines/mediawiki.py @@ -0,0 +1,90 @@ +""" + general mediawiki-engine (Web) + + @website websites built on mediawiki (https://www.mediawiki.org) + @provide-api yes (http://www.mediawiki.org/wiki/API:Search) + + @using-api yes + @results JSON + @stable yes + @parse url, title + + @todo content +""" + +from json import loads +from string import Formatter +from searx.url_utils import urlencode, quote + +# engine dependent config +categories = ['general'] +language_support = True +paging = True +number_of_results = 1 +search_type = 'nearmatch' # possible values: title, text, nearmatch + +# search-url +base_url = 'https://{language}.wikipedia.org/' +search_postfix = 'w/api.php?action=query'\ + '&list=search'\ + '&{query}'\ + '&format=json'\ + '&sroffset={offset}'\ + '&srlimit={limit}'\ + '&srwhat={searchtype}' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * number_of_results + + string_args = dict(query=urlencode({'srsearch': query}), + offset=offset, + limit=number_of_results, + searchtype=search_type) + + format_strings = list(Formatter().parse(base_url)) + + if params['language'] == 'all': + language = 'en' + else: + language = params['language'].split('-')[0] + + # format_string [('https://', 'language', '', None), ('.wikipedia.org/', None, None, None)] + if any(x[1] == 'language' for x in format_strings): + string_args['language'] = language + + # write search-language back to params, required in response + params['language'] = language + + search_url = base_url + search_postfix + + params['url'] = search_url.format(**string_args) + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_results = loads(resp.text) + + # return empty array if there are no results + if not search_results.get('query', {}).get('search'): + return [] + + # parse results + for result in search_results['query']['search']: + if result.get('snippet', '').startswith('#REDIRECT'): + continue + url = base_url.format(language=resp.search_params['language']) +\ + 'wiki/' + quote(result['title'].replace(' ', '_').encode('utf-8')) + + # append result + results.append({'url': url, + 'title': result['title'], + 'content': ''}) + + # return results + return results diff --git a/searx/engines/mixcloud.py b/searx/engines/mixcloud.py new file mode 100644 index 0000000..470c007 --- /dev/null +++ b/searx/engines/mixcloud.py @@ -0,0 +1,61 @@ +""" + Mixcloud (Music) + + @website https://http://www.mixcloud.com/ + @provide-api yes (http://www.mixcloud.com/developers/ + + @using-api yes + @results JSON + @stable yes + @parse url, title, content, embedded, publishedDate +""" + +from json import loads +from dateutil import parser +from searx.url_utils import urlencode + +# engine dependent config +categories = ['music'] +paging = True + +# search-url +url = 'https://api.mixcloud.com/' +search_url = url + 'search/?{query}&type=cloudcast&limit=10&offset={offset}' + +embedded_url = '' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 10 + + params['url'] = search_url.format(query=urlencode({'q': query}), + offset=offset) + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_res = loads(resp.text) + + # parse results + for result in search_res.get('data', []): + title = result['name'] + url = result['url'] + content = result['user']['name'] + embedded = embedded_url.format(url=url) + publishedDate = parser.parse(result['created_time']) + + # append result + results.append({'url': url, + 'title': title, + 'embedded': embedded, + 'publishedDate': publishedDate, + 'content': content}) + + # return results + return results diff --git a/searx/engines/nyaa.py b/searx/engines/nyaa.py new file mode 100644 index 0000000..272c712 --- /dev/null +++ b/searx/engines/nyaa.py @@ -0,0 +1,117 @@ +""" + Nyaa.se (Anime Bittorrent tracker) + + @website http://www.nyaa.se/ + @provide-api no + @using-api no + @results HTML + @stable no (HTML can change) + @parse url, title, content, seed, leech, torrentfile +""" + +from lxml import html +from searx.engines.xpath import extract_text +from searx.url_utils import urlencode + +# engine dependent config +categories = ['files', 'images', 'videos', 'music'] +paging = True + +# search-url +base_url = 'http://www.nyaa.se/' +search_url = base_url + '?page=search&{query}&offset={offset}' + +# xpath queries +xpath_results = '//table[@class="tlist"]//tr[contains(@class, "tlistrow")]' +xpath_category = './/td[@class="tlisticon"]/a' +xpath_title = './/td[@class="tlistname"]/a' +xpath_torrent_file = './/td[@class="tlistdownload"]/a' +xpath_filesize = './/td[@class="tlistsize"]/text()' +xpath_seeds = './/td[@class="tlistsn"]/text()' +xpath_leeches = './/td[@class="tlistln"]/text()' +xpath_downloads = './/td[@class="tlistdn"]/text()' + + +# convert a variable to integer or return 0 if it's not a number +def int_or_zero(num): + if isinstance(num, list): + if len(num) < 1: + return 0 + num = num[0] + if num.isdigit(): + return int(num) + return 0 + + +# get multiplier to convert torrent size to bytes +def get_filesize_mul(suffix): + return { + 'KB': 1024, + 'MB': 1024 ** 2, + 'GB': 1024 ** 3, + 'TB': 1024 ** 4, + + 'KIB': 1024, + 'MIB': 1024 ** 2, + 'GIB': 1024 ** 3, + 'TIB': 1024 ** 4 + }[str(suffix).upper()] + + +# do search-request +def request(query, params): + query = urlencode({'term': query}) + params['url'] = search_url.format(query=query, offset=params['pageno']) + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + for result in dom.xpath(xpath_results): + # category in which our torrent belongs + category = result.xpath(xpath_category)[0].attrib.get('title') + + # torrent title + page_a = result.xpath(xpath_title)[0] + title = extract_text(page_a) + + # link to the page + href = page_a.attrib.get('href') + + # link to the torrent file + torrent_link = result.xpath(xpath_torrent_file)[0].attrib.get('href') + + # torrent size + try: + file_size, suffix = result.xpath(xpath_filesize)[0].split(' ') + file_size = int(float(file_size) * get_filesize_mul(suffix)) + except: + file_size = None + + # seed count + seed = int_or_zero(result.xpath(xpath_seeds)) + + # leech count + leech = int_or_zero(result.xpath(xpath_leeches)) + + # torrent downloads count + downloads = int_or_zero(result.xpath(xpath_downloads)) + + # content string contains all information not included into template + content = 'Category: "{category}". Downloaded {downloads} times.' + content = content.format(category=category, downloads=downloads) + + results.append({'url': href, + 'title': title, + 'content': content, + 'seed': seed, + 'leech': leech, + 'filesize': file_size, + 'torrentfile': torrent_link, + 'template': 'torrent.html'}) + + return results diff --git a/searx/engines/openstreetmap.py b/searx/engines/openstreetmap.py new file mode 100644 index 0000000..733ba62 --- /dev/null +++ b/searx/engines/openstreetmap.py @@ -0,0 +1,95 @@ +""" + OpenStreetMap (Map) + + @website https://openstreetmap.org/ + @provide-api yes (http://wiki.openstreetmap.org/wiki/Nominatim) + + @using-api yes + @results JSON + @stable yes + @parse url, title +""" + +from json import loads + +# engine dependent config +categories = ['map'] +paging = False + +# search-url +base_url = 'https://nominatim.openstreetmap.org/' +search_string = 'search/{query}?format=json&polygon_geojson=1&addressdetails=1' +result_base_url = 'https://openstreetmap.org/{osm_type}/{osm_id}' + + +# do search-request +def request(query, params): + params['url'] = base_url + search_string.format(query=query) + + return params + + +# get response from search-request +def response(resp): + results = [] + json = loads(resp.text) + + # parse results + for r in json: + if 'display_name' not in r: + continue + + title = r['display_name'] or u'' + osm_type = r.get('osm_type', r.get('type')) + url = result_base_url.format(osm_type=osm_type, + osm_id=r['osm_id']) + + osm = {'type': osm_type, + 'id': r['osm_id']} + + geojson = r.get('geojson') + + # if no geojson is found and osm_type is a node, add geojson Point + if not geojson and osm_type == 'node': + geojson = {u'type': u'Point', u'coordinates': [r['lon'], r['lat']]} + + address_raw = r.get('address') + address = {} + + # get name + if r['class'] == 'amenity' or\ + r['class'] == 'shop' or\ + r['class'] == 'tourism' or\ + r['class'] == 'leisure': + if address_raw.get('address29'): + address = {'name': address_raw.get('address29')} + else: + address = {'name': address_raw.get(r['type'])} + + # add rest of adressdata, if something is already found + if address.get('name'): + address.update({'house_number': address_raw.get('house_number'), + 'road': address_raw.get('road'), + 'locality': address_raw.get('city', + address_raw.get('town', # noqa + address_raw.get('village'))), # noqa + 'postcode': address_raw.get('postcode'), + 'country': address_raw.get('country'), + 'country_code': address_raw.get('country_code')}) + else: + address = None + + # append result + results.append({'template': 'map.html', + 'title': title, + 'content': '', + 'longitude': r['lon'], + 'latitude': r['lat'], + 'boundingbox': r['boundingbox'], + 'geojson': geojson, + 'address': address, + 'osm': osm, + 'url': url}) + + # return results + return results diff --git a/searx/engines/pdbe.py b/searx/engines/pdbe.py new file mode 100644 index 0000000..f784e10 --- /dev/null +++ b/searx/engines/pdbe.py @@ -0,0 +1,109 @@ +""" + PDBe (Protein Data Bank in Europe) + + @website https://www.ebi.ac.uk/pdbe + @provide-api yes (https://www.ebi.ac.uk/pdbe/api/doc/search.html), + unlimited + @using-api yes + @results python dictionary (from json) + @stable yes + @parse url, title, content, img_src +""" + +from json import loads +from flask_babel import gettext + +categories = ['science'] + +hide_obsolete = False + +# status codes of unpublished entries +pdb_unpublished_codes = ['HPUB', 'HOLD', 'PROC', 'WAIT', 'AUTH', 'AUCO', 'REPL', 'POLC', 'REFI', 'TRSF', 'WDRN'] +# url for api query +pdbe_solr_url = 'https://www.ebi.ac.uk/pdbe/search/pdb/select?' +# base url for results +pdbe_entry_url = 'https://www.ebi.ac.uk/pdbe/entry/pdb/{pdb_id}' +# link to preview image of structure +pdbe_preview_url = 'https://www.ebi.ac.uk/pdbe/static/entry/{pdb_id}_deposited_chain_front_image-200x200.png' + + +def request(query, params): + + params['url'] = pdbe_solr_url + params['method'] = 'POST' + params['data'] = { + 'q': query, + 'wt': "json" # request response in parsable format + } + return params + + +def construct_body(result): + # set title + title = result['title'] + + # construct content body + content = """{title}
{authors} {journal} {volume} {page} ({year})""" + + # replace placeholders with actual content + try: + if result['journal']: + content = content.format( + title=result['citation_title'], + authors=result['entry_author_list'][0], journal=result['journal'], volume=result['journal_volume'], + page=result['journal_page'], year=result['citation_year']) + else: + content = content.format( + title=result['citation_title'], + authors=result['entry_author_list'][0], journal='', volume='', page='', year=result['release_year']) + img_src = pdbe_preview_url.format(pdb_id=result['pdb_id']) + except (KeyError): + content = None + img_src = None + + # construct url for preview image + try: + img_src = pdbe_preview_url.format(pdb_id=result['pdb_id']) + except (KeyError): + img_src = None + + return [title, content, img_src] + + +def response(resp): + + results = [] + json = loads(resp.text)['response']['docs'] + + # parse results + for result in json: + # catch obsolete entries and mark them accordingly + if result['status'] in pdb_unpublished_codes: + continue + if hide_obsolete: + continue + if result['status'] == 'OBS': + # expand title to add some sort of warning message + title = gettext('{title} (OBSOLETE)').format(title=result['title']) + superseded_url = pdbe_entry_url.format(pdb_id=result['superseded_by']) + + # since we can't construct a proper body from the response, we'll make up our own + msg_superseded = gettext("This entry has been superseded by") + content = '{msg_superseded} \{pdb_id}'.format( + msg_superseded=msg_superseded, + url=superseded_url, + pdb_id=result['superseded_by'], ) + + # obsoleted entries don't have preview images + img_src = None + else: + title, content, img_src = construct_body(result) + + results.append({ + 'url': pdbe_entry_url.format(pdb_id=result['pdb_id']), + 'title': title, + 'content': content, + 'img_src': img_src + }) + + return results diff --git a/searx/engines/photon.py b/searx/engines/photon.py new file mode 100644 index 0000000..15236f6 --- /dev/null +++ b/searx/engines/photon.py @@ -0,0 +1,131 @@ +""" + Photon (Map) + + @website https://photon.komoot.de + @provide-api yes (https://photon.komoot.de/) + + @using-api yes + @results JSON + @stable yes + @parse url, title +""" + +from json import loads +from searx.utils import searx_useragent +from searx.url_utils import urlencode + +# engine dependent config +categories = ['map'] +paging = False +language_support = True +number_of_results = 10 + +# search-url +base_url = 'https://photon.komoot.de/' +search_string = 'api/?{query}&limit={limit}' +result_base_url = 'https://openstreetmap.org/{osm_type}/{osm_id}' + +# list of supported languages +supported_languages = ['de', 'en', 'fr', 'it'] + + +# do search-request +def request(query, params): + params['url'] = base_url +\ + search_string.format(query=urlencode({'q': query}), + limit=number_of_results) + + if params['language'] != 'all': + language = params['language'].split('_')[0] + if language in supported_languages: + params['url'] = params['url'] + "&lang=" + language + + # using searx User-Agent + params['headers']['User-Agent'] = searx_useragent() + + return params + + +# get response from search-request +def response(resp): + results = [] + json = loads(resp.text) + + # parse results + for r in json.get('features', {}): + + properties = r.get('properties') + + if not properties: + continue + + # get title + title = properties.get('name') + + # get osm-type + if properties.get('osm_type') == 'N': + osm_type = 'node' + elif properties.get('osm_type') == 'W': + osm_type = 'way' + elif properties.get('osm_type') == 'R': + osm_type = 'relation' + else: + # continue if invalide osm-type + continue + + url = result_base_url.format(osm_type=osm_type, + osm_id=properties.get('osm_id')) + + osm = {'type': osm_type, + 'id': properties.get('osm_id')} + + geojson = r.get('geometry') + + if properties.get('extent'): + boundingbox = [properties.get('extent')[3], + properties.get('extent')[1], + properties.get('extent')[0], + properties.get('extent')[2]] + else: + # TODO: better boundingbox calculation + boundingbox = [geojson['coordinates'][1], + geojson['coordinates'][1], + geojson['coordinates'][0], + geojson['coordinates'][0]] + + # address calculation + address = {} + + # get name + if properties.get('osm_key') == 'amenity' or\ + properties.get('osm_key') == 'shop' or\ + properties.get('osm_key') == 'tourism' or\ + properties.get('osm_key') == 'leisure': + address = {'name': properties.get('name')} + + # add rest of adressdata, if something is already found + if address.get('name'): + address.update({'house_number': properties.get('housenumber'), + 'road': properties.get('street'), + 'locality': properties.get('city', + properties.get('town', # noqa + properties.get('village'))), # noqa + 'postcode': properties.get('postcode'), + 'country': properties.get('country')}) + else: + address = None + + # append result + results.append({'template': 'map.html', + 'title': title, + 'content': '', + 'longitude': geojson['coordinates'][0], + 'latitude': geojson['coordinates'][1], + 'boundingbox': boundingbox, + 'geojson': geojson, + 'address': address, + 'osm': osm, + 'url': url}) + + # return results + return results diff --git a/searx/engines/piratebay.py b/searx/engines/piratebay.py new file mode 100644 index 0000000..a5af8d8 --- /dev/null +++ b/searx/engines/piratebay.py @@ -0,0 +1,96 @@ +# Piratebay (Videos, Music, Files) +# +# @website https://thepiratebay.se +# @provide-api no (nothing found) +# +# @using-api no +# @results HTML (using search portal) +# @stable yes (HTML can change) +# @parse url, title, content, seed, leech, magnetlink + +from lxml import html +from operator import itemgetter +from searx.engines.xpath import extract_text +from searx.url_utils import quote, urljoin + +# engine dependent config +categories = ['videos', 'music', 'files'] +paging = True + +# search-url +url = 'https://thepiratebay.se/' +search_url = url + 'search/{search_term}/{pageno}/99/{search_type}' + +# piratebay specific type-definitions +search_types = {'files': '0', + 'music': '100', + 'videos': '200'} + +# specific xpath variables +magnet_xpath = './/a[@title="Download this torrent using magnet"]' +torrent_xpath = './/a[@title="Download this torrent"]' +content_xpath = './/font[@class="detDesc"]' + + +# do search-request +def request(query, params): + search_type = search_types.get(params['category'], '0') + + params['url'] = search_url.format(search_term=quote(query), + search_type=search_type, + pageno=params['pageno'] - 1) + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + search_res = dom.xpath('//table[@id="searchResult"]//tr') + + # return empty array if nothing is found + if not search_res: + return [] + + # parse results + for result in search_res[1:]: + link = result.xpath('.//div[@class="detName"]//a')[0] + href = urljoin(url, link.attrib.get('href')) + title = extract_text(link) + content = extract_text(result.xpath(content_xpath)) + seed, leech = result.xpath('.//td[@align="right"]/text()')[:2] + + # convert seed to int if possible + if seed.isdigit(): + seed = int(seed) + else: + seed = 0 + + # convert leech to int if possible + if leech.isdigit(): + leech = int(leech) + else: + leech = 0 + + magnetlink = result.xpath(magnet_xpath)[0] + torrentfile_links = result.xpath(torrent_xpath) + if torrentfile_links: + torrentfile_link = torrentfile_links[0].attrib.get('href') + else: + torrentfile_link = None + + # append result + results.append({'url': href, + 'title': title, + 'content': content, + 'seed': seed, + 'leech': leech, + 'magnetlink': magnetlink.attrib.get('href'), + 'torrentfile': torrentfile_link, + 'template': 'torrent.html'}) + + # return results sorted by seeder + return sorted(results, key=itemgetter('seed'), reverse=True) diff --git a/searx/engines/qwant.py b/searx/engines/qwant.py new file mode 100644 index 0000000..3d266e2 --- /dev/null +++ b/searx/engines/qwant.py @@ -0,0 +1,140 @@ +""" + Qwant (Web, Images, News, Social) + + @website https://qwant.com/ + @provide-api not officially (https://api.qwant.com/api/search/) + + @using-api yes + @results JSON + @stable yes + @parse url, title, content +""" + +from datetime import datetime +from json import loads +from searx.utils import html_to_text +from searx.url_utils import urlencode + +# engine dependent config +categories = None +paging = True +language_support = True +supported_languages_url = 'https://qwant.com/region' + +category_to_keyword = {'general': 'web', + 'images': 'images', + 'news': 'news', + 'social media': 'social'} + +# search-url +url = 'https://api.qwant.com/api/search/{keyword}?count=10&offset={offset}&f=&{query}' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 10 + + if categories[0] and categories[0] in category_to_keyword: + + params['url'] = url.format(keyword=category_to_keyword[categories[0]], + query=urlencode({'q': query}), + offset=offset) + else: + params['url'] = url.format(keyword='web', + query=urlencode({'q': query}), + offset=offset) + + # add language tag if specified + if params['language'] != 'all': + if params['language'] == 'no' or params['language'].startswith('no-'): + params['language'] = params['language'].replace('no', 'nb', 1) + if params['language'].find('-') < 0: + # tries to get a country code from language + for lang in supported_languages: + lc = lang.split('-') + if params['language'] == lc[0]: + params['language'] = lang + break + params['url'] += '&locale=' + params['language'].replace('-', '_').lower() + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_results = loads(resp.text) + + # return empty array if there are no results + if 'data' not in search_results: + return [] + + data = search_results.get('data', {}) + + res = data.get('result', {}) + + # parse results + for result in res.get('items', {}): + + title = html_to_text(result['title']) + res_url = result['url'] + content = html_to_text(result['desc']) + + if category_to_keyword.get(categories[0], '') == 'web': + results.append({'title': title, + 'content': content, + 'url': res_url}) + + elif category_to_keyword.get(categories[0], '') == 'images': + thumbnail_src = result['thumbnail'] + img_src = result['media'] + results.append({'template': 'images.html', + 'url': res_url, + 'title': title, + 'content': '', + 'thumbnail_src': thumbnail_src, + 'img_src': img_src}) + + elif category_to_keyword.get(categories[0], '') == 'social': + published_date = datetime.fromtimestamp(result['date'], None) + img_src = result.get('img', None) + results.append({'url': res_url, + 'title': title, + 'publishedDate': published_date, + 'content': content, + 'img_src': img_src}) + + elif category_to_keyword.get(categories[0], '') == 'news': + published_date = datetime.fromtimestamp(result['date'], None) + media = result.get('media', []) + if len(media) > 0: + img_src = media[0].get('pict', {}).get('url', None) + else: + img_src = None + results.append({'url': res_url, + 'title': title, + 'publishedDate': published_date, + 'content': content, + 'img_src': img_src}) + + return results + + +# get supported languages from their site +def _fetch_supported_languages(resp): + # list of regions is embedded in page as a js object + response_text = resp.text + response_text = response_text[response_text.find('regionalisation'):] + response_text = response_text[response_text.find('{'):response_text.find(');')] + + regions_json = loads(response_text) + + supported_languages = [] + for lang in regions_json['languages'].values(): + if lang['code'] == 'nb': + lang['code'] = 'no' + for country in lang['countries']: + supported_languages.append(lang['code'] + '-' + country) + + return supported_languages diff --git a/searx/engines/reddit.py b/searx/engines/reddit.py new file mode 100644 index 0000000..d197249 --- /dev/null +++ b/searx/engines/reddit.py @@ -0,0 +1,76 @@ +""" + Reddit + + @website https://www.reddit.com/ + @provide-api yes (https://www.reddit.com/dev/api) + + @using-api yes + @results JSON + @stable yes + @parse url, title, content, thumbnail, publishedDate +""" + +import json +from datetime import datetime +from searx.url_utils import urlencode, urljoin, urlparse + +# engine dependent config +categories = ['general', 'images', 'news', 'social media'] +page_size = 25 + +# search-url +base_url = 'https://www.reddit.com/' +search_url = base_url + 'search.json?{query}' + + +# do search-request +def request(query, params): + query = urlencode({'q': query, 'limit': page_size}) + params['url'] = search_url.format(query=query) + + return params + + +# get response from search-request +def response(resp): + img_results = [] + text_results = [] + + search_results = json.loads(resp.text) + + # return empty array if there are no results + if 'data' not in search_results: + return [] + + posts = search_results.get('data', {}).get('children', []) + + # process results + for post in posts: + data = post['data'] + + # extract post information + params = { + 'url': urljoin(base_url, data['permalink']), + 'title': data['title'] + } + + # if thumbnail field contains a valid URL, we need to change template + thumbnail = data['thumbnail'] + url_info = urlparse(thumbnail) + # netloc & path + if url_info[1] != '' and url_info[2] != '': + params['img_src'] = data['url'] + params['thumbnail_src'] = thumbnail + params['template'] = 'images.html' + img_results.append(params) + else: + created = datetime.fromtimestamp(data['created_utc']) + content = data['selftext'] + if len(content) > 500: + content = content[:500] + '...' + params['content'] = content + params['publishedDate'] = created + text_results.append(params) + + # show images first and text results second + return img_results + text_results diff --git a/searx/engines/scanr_structures.py b/searx/engines/scanr_structures.py new file mode 100644 index 0000000..72fd2b3 --- /dev/null +++ b/searx/engines/scanr_structures.py @@ -0,0 +1,76 @@ +""" + ScanR Structures (Science) + + @website https://scanr.enseignementsup-recherche.gouv.fr + @provide-api yes (https://scanr.enseignementsup-recherche.gouv.fr/api/swagger-ui.html) + + @using-api yes + @results JSON + @stable yes + @parse url, title, content, img_src +""" + +from json import loads, dumps +from searx.utils import html_to_text + +# engine dependent config +categories = ['science'] +paging = True +page_size = 20 + +# search-url +url = 'https://scanr.enseignementsup-recherche.gouv.fr/' +search_url = url + 'api/structures/search' + + +# do search-request +def request(query, params): + + params['url'] = search_url + params['method'] = 'POST' + params['headers']['Content-type'] = "application/json" + params['data'] = dumps({"query": query, + "searchField": "ALL", + "sortDirection": "ASC", + "sortOrder": "RELEVANCY", + "page": params['pageno'], + "pageSize": page_size}) + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_res = loads(resp.text) + + # return empty array if there are no results + if search_res.get('total', 0) < 1: + return [] + + # parse results + for result in search_res['results']: + if 'id' not in result: + continue + + # is it thumbnail or img_src?? + thumbnail = None + if 'logo' in result: + thumbnail = result['logo'] + if thumbnail[0] == '/': + thumbnail = url + thumbnail + + content = None + if 'highlights' in result: + content = result['highlights'][0]['value'] + + # append result + results.append({'url': url + 'structure/' + result['id'], + 'title': result['label'], + # 'thumbnail': thumbnail, + 'img_src': thumbnail, + 'content': html_to_text(content)}) + + # return results + return results diff --git a/searx/engines/searchcode_code.py b/searx/engines/searchcode_code.py new file mode 100644 index 0000000..789e8e7 --- /dev/null +++ b/searx/engines/searchcode_code.py @@ -0,0 +1,69 @@ +""" + Searchcode (It) + + @website https://searchcode.com/ + @provide-api yes (https://searchcode.com/api/) + + @using-api yes + @results JSON + @stable yes + @parse url, title, content +""" + +from json import loads +from searx.url_utils import urlencode + + +# engine dependent config +categories = ['it'] +paging = True + +# search-url +url = 'https://searchcode.com/' +search_url = url + 'api/codesearch_I/?{query}&p={pageno}' + +# special code-endings which are not recognised by the file ending +code_endings = {'cs': 'c#', + 'h': 'c', + 'hpp': 'cpp', + 'cxx': 'cpp'} + + +# do search-request +def request(query, params): + params['url'] = search_url.format(query=urlencode({'q': query}), pageno=params['pageno'] - 1) + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_results = loads(resp.text) + + # parse results + for result in search_results.get('results', []): + href = result['url'] + title = "" + result['name'] + " - " + result['filename'] + repo = result['repo'] + + lines = dict() + for line, code in result['lines'].items(): + lines[int(line)] = code + + code_language = code_endings.get( + result['filename'].split('.')[-1].lower(), + result['filename'].split('.')[-1].lower()) + + # append result + results.append({'url': href, + 'title': title, + 'content': '', + 'repository': repo, + 'codelines': sorted(lines.items()), + 'code_language': code_language, + 'template': 'code.html'}) + + # return results + return results diff --git a/searx/engines/searchcode_doc.py b/searx/engines/searchcode_doc.py new file mode 100644 index 0000000..4b8e9a8 --- /dev/null +++ b/searx/engines/searchcode_doc.py @@ -0,0 +1,49 @@ +""" + Searchcode (It) + + @website https://searchcode.com/ + @provide-api yes (https://searchcode.com/api/) + + @using-api yes + @results JSON + @stable yes + @parse url, title, content +""" + +from json import loads +from searx.url_utils import urlencode + +# engine dependent config +categories = ['it'] +paging = True + +# search-url +url = 'https://searchcode.com/' +search_url = url + 'api/search_IV/?{query}&p={pageno}' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(query=urlencode({'q': query}), pageno=params['pageno'] - 1) + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_results = loads(resp.text) + + # parse results + for result in search_results.get('results', []): + href = result['url'] + title = "[{}] {} {}".format(result['type'], result['namespace'], result['name']) + + # append result + results.append({'url': href, + 'title': title, + 'content': result['description']}) + + # return results + return results diff --git a/searx/engines/searx_engine.py b/searx/engines/searx_engine.py new file mode 100644 index 0000000..91c2644 --- /dev/null +++ b/searx/engines/searx_engine.py @@ -0,0 +1,57 @@ +""" + Searx (all) + + @website https://github.com/asciimoo/searx + @provide-api yes (https://asciimoo.ithub.io/searx/dev/search_api.html) + + @using-api yes + @results JSON + @stable yes (using api) + @parse url, title, content +""" + +from json import loads +from searx.engines import categories as searx_categories + + +categories = searx_categories.keys() + +# search-url +instance_urls = [] +instance_index = 0 + + +# do search-request +def request(query, params): + global instance_index + params['url'] = instance_urls[instance_index % len(instance_urls)] + params['method'] = 'POST' + + instance_index += 1 + + params['data'] = { + 'q': query, + 'pageno': params['pageno'], + 'language': params['language'], + 'time_range': params['time_range'], + 'category': params['category'], + 'format': 'json' + } + + return params + + +# get response from search-request +def response(resp): + + response_json = loads(resp.text) + results = response_json['results'] + + for i in ('answers', 'infoboxes'): + results.extend(response_json[i]) + + results.extend({'suggestion': s} for s in response_json['suggestions']) + + results.append({'number_of_results': response_json['number_of_results']}) + + return results diff --git a/searx/engines/seedpeer.py b/searx/engines/seedpeer.py new file mode 100644 index 0000000..3770dac --- /dev/null +++ b/searx/engines/seedpeer.py @@ -0,0 +1,75 @@ +# Seedpeer (Videos, Music, Files) +# +# @website http://seedpeer.eu +# @provide-api no (nothing found) +# +# @using-api no +# @results HTML (using search portal) +# @stable yes (HTML can change) +# @parse url, title, content, seed, leech, magnetlink + +from lxml import html +from operator import itemgetter +from searx.url_utils import quote, urljoin + + +url = 'http://www.seedpeer.eu/' +search_url = url + 'search/{search_term}/7/{page_no}.html' +# specific xpath variables +torrent_xpath = '//*[@id="body"]/center/center/table[2]/tr/td/a' +alternative_torrent_xpath = '//*[@id="body"]/center/center/table[1]/tr/td/a' +title_xpath = '//*[@id="body"]/center/center/table[2]/tr/td/a/text()' +alternative_title_xpath = '//*[@id="body"]/center/center/table/tr/td/a' +seeds_xpath = '//*[@id="body"]/center/center/table[2]/tr/td[4]/font/text()' +alternative_seeds_xpath = '//*[@id="body"]/center/center/table/tr/td[4]/font/text()' +peers_xpath = '//*[@id="body"]/center/center/table[2]/tr/td[5]/font/text()' +alternative_peers_xpath = '//*[@id="body"]/center/center/table/tr/td[5]/font/text()' +age_xpath = '//*[@id="body"]/center/center/table[2]/tr/td[2]/text()' +alternative_age_xpath = '//*[@id="body"]/center/center/table/tr/td[2]/text()' +size_xpath = '//*[@id="body"]/center/center/table[2]/tr/td[3]/text()' +alternative_size_xpath = '//*[@id="body"]/center/center/table/tr/td[3]/text()' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(search_term=quote(query), + page_no=params['pageno'] - 1) + return params + + +# get response from search-request +def response(resp): + results = [] + dom = html.fromstring(resp.text) + torrent_links = dom.xpath(torrent_xpath) + if len(torrent_links) > 0: + seeds = dom.xpath(seeds_xpath) + peers = dom.xpath(peers_xpath) + titles = dom.xpath(title_xpath) + sizes = dom.xpath(size_xpath) + ages = dom.xpath(age_xpath) + else: # under ~5 results uses a different xpath + torrent_links = dom.xpath(alternative_torrent_xpath) + seeds = dom.xpath(alternative_seeds_xpath) + peers = dom.xpath(alternative_peers_xpath) + titles = dom.xpath(alternative_title_xpath) + sizes = dom.xpath(alternative_size_xpath) + ages = dom.xpath(alternative_age_xpath) + # return empty array if nothing is found + if not torrent_links: + return [] + + # parse results + for index, result in enumerate(torrent_links): + link = result.attrib.get('href') + href = urljoin(url, link) + results.append({'url': href, + 'title': titles[index].text_content(), + 'content': '{}, {}'.format(sizes[index], ages[index]), + 'seed': seeds[index], + 'leech': peers[index], + + 'template': 'torrent.html'}) + + # return results sorted by seeder + return sorted(results, key=itemgetter('seed'), reverse=True) diff --git a/searx/engines/soundcloud.py b/searx/engines/soundcloud.py new file mode 100644 index 0000000..41b40da --- /dev/null +++ b/searx/engines/soundcloud.py @@ -0,0 +1,104 @@ +""" + Soundcloud (Music) + + @website https://soundcloud.com + @provide-api yes (https://developers.soundcloud.com/) + + @using-api yes + @results JSON + @stable yes + @parse url, title, content, publishedDate, embedded +""" + +import re +from json import loads +from lxml import html +from dateutil import parser +from searx import logger +from searx.poolrequests import get as http_get +from searx.url_utils import quote_plus, urlencode + +try: + from cStringIO import StringIO +except: + from io import StringIO + +# engine dependent config +categories = ['music'] +paging = True + +# search-url +url = 'https://api.soundcloud.com/' +search_url = url + 'search?{query}'\ + '&facet=model'\ + '&limit=20'\ + '&offset={offset}'\ + '&linked_partitioning=1'\ + '&client_id={client_id}' # noqa + +embedded_url = '' + +cid_re = re.compile(r'client_id:"([^"]*)"', re.I | re.U) + + +def get_client_id(): + response = http_get("https://soundcloud.com") + + if response.ok: + tree = html.fromstring(response.content) + script_tags = tree.xpath("//script[contains(@src, '/assets/app')]") + app_js_urls = [script_tag.get('src') for script_tag in script_tags if script_tag is not None] + + # extracts valid app_js urls from soundcloud.com content + for app_js_url in app_js_urls: + # gets app_js and searches for the clientid + response = http_get(app_js_url) + if response.ok: + cids = cid_re.search(response.text) + if cids is not None and len(cids.groups()): + return cids.groups()[0] + logger.warning("Unable to fetch guest client_id from SoundCloud, check parser!") + return "" + + +# api-key +guest_client_id = get_client_id() + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 20 + + params['url'] = search_url.format(query=urlencode({'q': query}), + offset=offset, + client_id=guest_client_id) + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_res = loads(resp.text) + + # parse results + for result in search_res.get('collection', []): + if result['kind'] in ('track', 'playlist'): + title = result['title'] + content = result['description'] + publishedDate = parser.parse(result['last_modified']) + uri = quote_plus(result['uri']) + embedded = embedded_url.format(uri=uri) + + # append result + results.append({'url': result['permalink_url'], + 'title': title, + 'publishedDate': publishedDate, + 'embedded': embedded, + 'content': content}) + + # return results + return results diff --git a/searx/engines/spotify.py b/searx/engines/spotify.py new file mode 100644 index 0000000..aed756b --- /dev/null +++ b/searx/engines/spotify.py @@ -0,0 +1,62 @@ +""" + Spotify (Music) + + @website https://spotify.com + @provide-api yes (https://developer.spotify.com/web-api/search-item/) + + @using-api yes + @results JSON + @stable yes + @parse url, title, content, embedded +""" + +from json import loads +from searx.url_utils import urlencode + +# engine dependent config +categories = ['music'] +paging = True + +# search-url +url = 'https://api.spotify.com/' +search_url = url + 'v1/search?{query}&type=track&offset={offset}' + +embedded_url = '' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 20 + + params['url'] = search_url.format(query=urlencode({'q': query}), offset=offset) + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_res = loads(resp.text) + + # parse results + for result in search_res.get('tracks', {}).get('items', {}): + if result['type'] == 'track': + title = result['name'] + url = result['external_urls']['spotify'] + content = u'{} - {} - {}'.format( + result['artists'][0]['name'], + result['album']['name'], + result['name']) + + embedded = embedded_url.format(audioid=result['id']) + + # append result + results.append({'url': url, + 'title': title, + 'embedded': embedded, + 'content': content}) + + # return results + return results diff --git a/searx/engines/stackoverflow.py b/searx/engines/stackoverflow.py new file mode 100644 index 0000000..25875aa --- /dev/null +++ b/searx/engines/stackoverflow.py @@ -0,0 +1,57 @@ +""" + Stackoverflow (It) + + @website https://stackoverflow.com/ + @provide-api not clear (https://api.stackexchange.com/docs/advanced-search) + + @using-api no + @results HTML + @stable no (HTML can change) + @parse url, title, content +""" + +from lxml import html +from searx.engines.xpath import extract_text +from searx.url_utils import urlencode, urljoin + +# engine dependent config +categories = ['it'] +paging = True + +# search-url +url = 'https://stackoverflow.com/' +search_url = url + 'search?{query}&page={pageno}' + +# specific xpath variables +results_xpath = '//div[contains(@class,"question-summary")]' +link_xpath = './/div[@class="result-link"]//a|.//div[@class="summary"]//h3//a' +content_xpath = './/div[@class="excerpt"]' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(query=urlencode({'q': query}), pageno=params['pageno']) + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + # parse results + for result in dom.xpath(results_xpath): + link = result.xpath(link_xpath)[0] + href = urljoin(url, link.attrib.get('href')) + title = extract_text(link) + content = extract_text(result.xpath(content_xpath)) + + # append result + results.append({'url': href, + 'title': title, + 'content': content}) + + # return results + return results diff --git a/searx/engines/startpage.py b/searx/engines/startpage.py new file mode 100644 index 0000000..314b7b9 --- /dev/null +++ b/searx/engines/startpage.py @@ -0,0 +1,123 @@ +# Startpage (Web) +# +# @website https://startpage.com +# @provide-api no (nothing found) +# +# @using-api no +# @results HTML +# @stable no (HTML can change) +# @parse url, title, content +# +# @todo paging + +from lxml import html +from dateutil import parser +from datetime import datetime, timedelta +import re +from searx.engines.xpath import extract_text + +# engine dependent config +categories = ['general'] +# there is a mechanism to block "bot" search +# (probably the parameter qid), require +# storing of qid's between mulitble search-calls + +# paging = False +language_support = True + +# search-url +base_url = 'https://startpage.com/' +search_url = base_url + 'do/search' + +# specific xpath variables +# ads xpath //div[@id="results"]/div[@id="sponsored"]//div[@class="result"] +# not ads: div[@class="result"] are the direct childs of div[@id="results"] +results_xpath = '//div[@class="result"]' +link_xpath = './/h3/a' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 10 + + params['url'] = search_url + params['method'] = 'POST' + params['data'] = {'query': query, + 'startat': offset} + + # set language if specified + if params['language'] != 'all': + params['data']['with_language'] = ('lang_' + params['language'].split('-')[0]) + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + # parse results + for result in dom.xpath(results_xpath): + links = result.xpath(link_xpath) + if not links: + continue + link = links[0] + url = link.attrib.get('href') + + # block google-ad url's + if re.match(r"^http(s|)://(www\.)?google\.[a-z]+/aclk.*$", url): + continue + + # block startpage search url's + if re.match(r"^http(s|)://(www\.)?startpage\.com/do/search\?.*$", url): + continue + + # block ixquick search url's + if re.match(r"^http(s|)://(www\.)?ixquick\.com/do/search\?.*$", url): + continue + + title = extract_text(link) + + if result.xpath('./p[@class="desc clk"]'): + content = extract_text(result.xpath('./p[@class="desc clk"]')) + else: + content = '' + + published_date = None + + # check if search result starts with something like: "2 Sep 2014 ... " + if re.match(r"^([1-9]|[1-2][0-9]|3[0-1]) [A-Z][a-z]{2} [0-9]{4} \.\.\. ", content): + date_pos = content.find('...') + 4 + date_string = content[0:date_pos - 5] + published_date = parser.parse(date_string, dayfirst=True) + + # fix content string + content = content[date_pos:] + + # check if search result starts with something like: "5 days ago ... " + elif re.match(r"^[0-9]+ days? ago \.\.\. ", content): + date_pos = content.find('...') + 4 + date_string = content[0:date_pos - 5] + + # calculate datetime + published_date = datetime.now() - timedelta(days=int(re.match(r'\d+', date_string).group())) + + # fix content string + content = content[date_pos:] + + if published_date: + # append result + results.append({'url': url, + 'title': title, + 'content': content, + 'publishedDate': published_date}) + else: + # append result + results.append({'url': url, + 'title': title, + 'content': content}) + + # return results + return results diff --git a/searx/engines/subtitleseeker.py b/searx/engines/subtitleseeker.py new file mode 100644 index 0000000..2cbc991 --- /dev/null +++ b/searx/engines/subtitleseeker.py @@ -0,0 +1,86 @@ +""" + Subtitleseeker (Video) + + @website http://www.subtitleseeker.com + @provide-api no + + @using-api no + @results HTML + @stable no (HTML can change) + @parse url, title, content +""" + +from lxml import html +from searx.languages import language_codes +from searx.engines.xpath import extract_text +from searx.url_utils import quote_plus + +# engine dependent config +categories = ['videos'] +paging = True +language = "" + +# search-url +url = 'http://www.subtitleseeker.com/' +search_url = url + 'search/TITLES/{query}?p={pageno}' + +# specific xpath variables +results_xpath = '//div[@class="boxRows"]' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(query=quote_plus(query), + pageno=params['pageno']) + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + search_lang = "" + + # dirty fix for languages named differenly in their site + if resp.search_params['language'][:2] == 'fa': + search_lang = 'Farsi' + elif resp.search_params['language'] == 'pt-BR': + search_lang = 'Brazilian' + elif resp.search_params['language'] != 'all': + search_lang = [lc[3] + for lc in language_codes + if lc[0].split('-')[0] == resp.search_params['language'].split('-')[0]] + search_lang = search_lang[0].split(' (')[0] + + # parse results + for result in dom.xpath(results_xpath): + link = result.xpath(".//a")[0] + href = link.attrib.get('href') + + if language is not "": + href = href + language + '/' + elif search_lang: + href = href + search_lang + '/' + + title = extract_text(link) + + content = extract_text(result.xpath('.//div[contains(@class,"red")]')) + content = content + " - " + text = extract_text(result.xpath('.//div[contains(@class,"grey-web")]')[0]) + content = content + text + + if result.xpath(".//span") != []: + content = content +\ + " - (" +\ + extract_text(result.xpath(".//span")) +\ + ")" + + # append result + results.append({'url': href, + 'title': title, + 'content': content}) + + # return results + return results diff --git a/searx/engines/swisscows.py b/searx/engines/swisscows.py new file mode 100644 index 0000000..e9c13ca --- /dev/null +++ b/searx/engines/swisscows.py @@ -0,0 +1,126 @@ +""" + Swisscows (Web, Images) + + @website https://swisscows.ch + @provide-api no + + @using-api no + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content +""" + +from json import loads +import re +from lxml.html import fromstring +from searx.url_utils import unquote, urlencode + +# engine dependent config +categories = ['general', 'images'] +paging = True +language_support = True + +# search-url +base_url = 'https://swisscows.ch/' +search_string = '?{query}&page={page}' + +supported_languages_url = base_url + +# regex +regex_json = re.compile(b'initialData: {"Request":(.|\n)*},\s*environment') +regex_json_remove_start = re.compile(b'^initialData:\s*') +regex_json_remove_end = re.compile(b',\s*environment$') +regex_img_url_remove_start = re.compile(b'^https?://i\.swisscows\.ch/\?link=') + + +# do search-request +def request(query, params): + if params['language'] == 'all': + ui_language = 'browser' + region = 'browser' + elif params['language'].split('-')[0] == 'no': + region = 'nb-NO' + else: + region = params['language'] + ui_language = params['language'].split('-')[0] + + search_path = search_string.format( + query=urlencode({'query': query, 'uiLanguage': ui_language, 'region': region}), + page=params['pageno'] + ) + + # image search query is something like 'image?{query}&page={page}' + if params['category'] == 'images': + search_path = 'image' + search_path + + params['url'] = base_url + search_path + + return params + + +# get response from search-request +def response(resp): + results = [] + + json_regex = regex_json.search(resp.text) + + # check if results are returned + if not json_regex: + return [] + + json_raw = regex_json_remove_end.sub(b'', regex_json_remove_start.sub(b'', json_regex.group())) + json = loads(json_raw.decode('utf-8')) + + # parse results + for result in json['Results'].get('items', []): + result_title = result['Title'].replace(u'\uE000', '').replace(u'\uE001', '') + + # parse image results + if result.get('ContentType', '').startswith('image'): + img_url = unquote(regex_img_url_remove_start.sub(b'', result['Url'].encode('utf-8')).decode('utf-8')) + + # append result + results.append({'url': result['SourceUrl'], + 'title': result['Title'], + 'content': '', + 'img_src': img_url, + 'template': 'images.html'}) + + # parse general results + else: + result_url = result['Url'].replace(u'\uE000', '').replace(u'\uE001', '') + result_content = result['Description'].replace(u'\uE000', '').replace(u'\uE001', '') + + # append result + results.append({'url': result_url, + 'title': result_title, + 'content': result_content}) + + # parse images + for result in json.get('Images', []): + # decode image url + img_url = unquote(regex_img_url_remove_start.sub(b'', result['Url'].encode('utf-8')).decode('utf-8')) + + # append result + results.append({'url': result['SourceUrl'], + 'title': result['Title'], + 'content': '', + 'img_src': img_url, + 'template': 'images.html'}) + + # return results + return results + + +# get supported languages from their site +def _fetch_supported_languages(resp): + supported_languages = [] + dom = fromstring(resp.text) + options = dom.xpath('//div[@id="regions-popup"]//ul/li/a') + for option in options: + code = option.xpath('./@data-val')[0] + if code.startswith('nb-'): + code = code.replace('nb', 'no', 1) + supported_languages.append(code) + + return supported_languages diff --git a/searx/engines/tokyotoshokan.py b/searx/engines/tokyotoshokan.py new file mode 100644 index 0000000..9a6b5e5 --- /dev/null +++ b/searx/engines/tokyotoshokan.py @@ -0,0 +1,100 @@ +""" + Tokyo Toshokan (A BitTorrent Library for Japanese Media) + + @website https://www.tokyotosho.info/ + @provide-api no + @using-api no + @results HTML + @stable no (HTML can change) + @parse url, title, publishedDate, seed, leech, + filesize, magnetlink, content +""" + +import re +from lxml import html +from searx.engines.xpath import extract_text +from datetime import datetime +from searx.engines.nyaa import int_or_zero, get_filesize_mul +from searx.url_utils import urlencode + +# engine dependent config +categories = ['files', 'videos', 'music'] +paging = True + +# search-url +base_url = 'https://www.tokyotosho.info/' +search_url = base_url + 'search.php?{query}' + + +# do search-request +def request(query, params): + query = urlencode({'page': params['pageno'], 'terms': query}) + params['url'] = search_url.format(query=query) + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + rows = dom.xpath('//table[@class="listing"]//tr[contains(@class, "category_0")]') + + # check if there are no results or page layout was changed so we cannot parse it + # currently there are two rows for each result, so total count must be even + if len(rows) == 0 or len(rows) % 2 != 0: + return [] + + # regular expression for parsing torrent size strings + size_re = re.compile(r'Size:\s*([\d.]+)(TB|GB|MB|B)', re.IGNORECASE) + + # processing the results, two rows at a time + for i in range(0, len(rows), 2): + # parse the first row + name_row = rows[i] + + links = name_row.xpath('./td[@class="desc-top"]/a') + params = { + 'template': 'torrent.html', + 'url': links[-1].attrib.get('href'), + 'title': extract_text(links[-1]) + } + # I have not yet seen any torrents without magnet links, but + # it's better to be prepared to stumble upon one some day + if len(links) == 2: + magnet = links[0].attrib.get('href') + if magnet.startswith('magnet'): + # okay, we have a valid magnet link, let's add it to the result + params['magnetlink'] = magnet + + # no more info in the first row, start parsing the second one + info_row = rows[i + 1] + desc = extract_text(info_row.xpath('./td[@class="desc-bot"]')[0]) + for item in desc.split('|'): + item = item.strip() + if item.startswith('Size:'): + try: + # ('1.228', 'GB') + groups = size_re.match(item).groups() + multiplier = get_filesize_mul(groups[1]) + params['filesize'] = int(multiplier * float(groups[0])) + except: + pass + elif item.startswith('Date:'): + try: + # Date: 2016-02-21 21:44 UTC + date = datetime.strptime(item, 'Date: %Y-%m-%d %H:%M UTC') + params['publishedDate'] = date + except: + pass + elif item.startswith('Comment:'): + params['content'] = item + stats = info_row.xpath('./td[@class="stats"]/span') + # has the layout not changed yet? + if len(stats) == 3: + params['seed'] = int_or_zero(extract_text(stats[0])) + params['leech'] = int_or_zero(extract_text(stats[1])) + + results.append(params) + + return results diff --git a/searx/engines/torrentz.py b/searx/engines/torrentz.py new file mode 100644 index 0000000..dda56fc --- /dev/null +++ b/searx/engines/torrentz.py @@ -0,0 +1,92 @@ +""" + Torrentz.eu (BitTorrent meta-search engine) + + @website https://torrentz.eu/ + @provide-api no + + @using-api no + @results HTML + @stable no (HTML can change, although unlikely, + see https://torrentz.eu/torrentz.btsearch) + @parse url, title, publishedDate, seed, leech, filesize, magnetlink +""" + +import re +from lxml import html +from datetime import datetime +from searx.engines.nyaa import int_or_zero, get_filesize_mul +from searx.engines.xpath import extract_text +from searx.url_utils import urlencode + +# engine dependent config +categories = ['files', 'videos', 'music'] +paging = True + +# search-url +# https://torrentz.eu/search?f=EXAMPLE&p=6 +base_url = 'https://torrentz.eu/' +search_url = base_url + 'search?{query}' + + +# do search-request +def request(query, params): + page = params['pageno'] - 1 + query = urlencode({'q': query, 'p': page}) + params['url'] = search_url.format(query=query) + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + for result in dom.xpath('//div[@class="results"]/dl'): + name_cell = result.xpath('./dt')[0] + title = extract_text(name_cell) + + # skip rows that do not contain a link to a torrent + links = name_cell.xpath('./a') + if len(links) != 1: + continue + + # extract url and remove a slash in the beginning + link = links[0].attrib.get('href').lstrip('/') + + seed = result.xpath('./dd/span[@class="u"]/text()')[0].replace(',', '') + leech = result.xpath('./dd/span[@class="d"]/text()')[0].replace(',', '') + + params = { + 'url': base_url + link, + 'title': title, + 'seed': int_or_zero(seed), + 'leech': int_or_zero(leech), + 'template': 'torrent.html' + } + + # let's try to calculate the torrent size + try: + size_str = result.xpath('./dd/span[@class="s"]/text()')[0] + size, suffix = size_str.split() + params['filesize'] = int(size) * get_filesize_mul(suffix) + except: + pass + + # does our link contain a valid SHA1 sum? + if re.compile('[0-9a-fA-F]{40}').match(link): + # add a magnet link to the result + params['magnetlink'] = 'magnet:?xt=urn:btih:' + link + + # extract and convert creation date + try: + date_str = result.xpath('./dd/span[@class="a"]/span')[0].attrib.get('title') + # Fri, 25 Mar 2016 16:29:01 + date = datetime.strptime(date_str, '%a, %d %b %Y %H:%M:%S') + params['publishedDate'] = date + except: + pass + + results.append(params) + + return results diff --git a/searx/engines/translated.py b/searx/engines/translated.py new file mode 100644 index 0000000..5c7b170 --- /dev/null +++ b/searx/engines/translated.py @@ -0,0 +1,68 @@ +""" + MyMemory Translated + + @website https://mymemory.translated.net/ + @provide-api yes (https://mymemory.translated.net/doc/spec.php) + @using-api yes + @results JSON + @stable yes + @parse url, title, content +""" +import re +from sys import version_info +from searx.utils import is_valid_lang + +if version_info[0] == 3: + unicode = str + +categories = ['general'] +url = u'http://api.mymemory.translated.net/get?q={query}&langpair={from_lang}|{to_lang}{key}' +web_url = u'http://mymemory.translated.net/en/{from_lang}/{to_lang}/{query}' +weight = 100 + +parser_re = re.compile(u'.*?([a-z]+)-([a-z]+) (.{2,})$', re.I) +api_key = '' + + +def request(query, params): + m = parser_re.match(unicode(query, 'utf8')) + if not m: + return params + + from_lang, to_lang, query = m.groups() + + from_lang = is_valid_lang(from_lang) + to_lang = is_valid_lang(to_lang) + + if not from_lang or not to_lang: + return params + + if api_key: + key_form = '&key=' + api_key + else: + key_form = '' + params['url'] = url.format(from_lang=from_lang[1], + to_lang=to_lang[1], + query=query, + key=key_form) + params['query'] = query + params['from_lang'] = from_lang + params['to_lang'] = to_lang + + return params + + +def response(resp): + results = [] + results.append({ + 'url': web_url.format( + from_lang=resp.search_params['from_lang'][2], + to_lang=resp.search_params['to_lang'][2], + query=resp.search_params['query']), + 'title': '[{0}-{1}] {2}'.format( + resp.search_params['from_lang'][1], + resp.search_params['to_lang'][1], + resp.search_params['query']), + 'content': resp.json()['responseData']['translatedText'] + }) + return results diff --git a/searx/engines/twitter.py b/searx/engines/twitter.py new file mode 100644 index 0000000..d2a8d20 --- /dev/null +++ b/searx/engines/twitter.py @@ -0,0 +1,87 @@ +""" + Twitter (Social media) + + @website https://twitter.com/ + @provide-api yes (https://dev.twitter.com/docs/using-search) + + @using-api no + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content + + @todo publishedDate +""" + +from lxml import html +from datetime import datetime +from searx.engines.xpath import extract_text +from searx.url_utils import urlencode, urljoin + +# engine dependent config +categories = ['social media'] +language_support = True + +# search-url +base_url = 'https://twitter.com/' +search_url = base_url + 'search?' + +# specific xpath variables +results_xpath = '//li[@data-item-type="tweet"]' +avatar_xpath = './/img[contains(@class, "avatar")]/@src' +link_xpath = './/small[@class="time"]//a' +title_xpath = './/span[contains(@class, "username")]' +content_xpath = './/p[contains(@class, "tweet-text")]' +timestamp_xpath = './/span[contains(@class,"_timestamp")]' + + +# do search-request +def request(query, params): + params['url'] = search_url + urlencode({'q': query}) + + # set language if specified + if params['language'] != 'all': + params['cookies']['lang'] = params['language'].split('-')[0] + else: + params['cookies']['lang'] = 'en' + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + # parse results + for tweet in dom.xpath(results_xpath): + try: + link = tweet.xpath(link_xpath)[0] + content = extract_text(tweet.xpath(content_xpath)[0]) + img_src = tweet.xpath(avatar_xpath)[0] + img_src = img_src.replace('_bigger', '_normal') + except Exception: + continue + + url = urljoin(base_url, link.attrib.get('href')) + title = extract_text(tweet.xpath(title_xpath)) + + pubdate = tweet.xpath(timestamp_xpath) + if len(pubdate) > 0: + timestamp = float(pubdate[0].attrib.get('data-time')) + publishedDate = datetime.fromtimestamp(timestamp, None) + # append result + results.append({'url': url, + 'title': title, + 'content': content, + 'img_src': img_src, + 'publishedDate': publishedDate}) + else: + # append result + results.append({'url': url, + 'title': title, + 'content': content, + 'img_src': img_src}) + + # return results + return results diff --git a/searx/engines/vimeo.py b/searx/engines/vimeo.py new file mode 100644 index 0000000..1408be8 --- /dev/null +++ b/searx/engines/vimeo.py @@ -0,0 +1,67 @@ +# Vimeo (Videos) +# +# @website https://vimeo.com/ +# @provide-api yes (http://developer.vimeo.com/api), +# they have a maximum count of queries/hour +# +# @using-api no (TODO, rewrite to api) +# @results HTML (using search portal) +# @stable no (HTML can change) +# @parse url, title, publishedDate, thumbnail, embedded +# +# @todo rewrite to api +# @todo set content-parameter with correct data + +from json import loads +from dateutil import parser +from searx.url_utils import urlencode + +# engine dependent config +categories = ['videos'] +paging = True + +# search-url +base_url = 'https://vimeo.com/' +search_url = base_url + '/search/page:{pageno}?{query}' + +embedded_url = '' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(pageno=params['pageno'], + query=urlencode({'q': query})) + + return params + + +# get response from search-request +def response(resp): + results = [] + data_start_pos = resp.text.find('{"filtered"') + data_end_pos = resp.text.find(';\n', data_start_pos + 1) + data = loads(resp.text[data_start_pos:data_end_pos]) + + # parse results + for result in data['filtered']['data']: + result = result[result['type']] + videoid = result['uri'].split('/')[-1] + url = base_url + videoid + title = result['name'] + thumbnail = result['pictures']['sizes'][-1]['link'] + publishedDate = parser.parse(result['created_time']) + embedded = embedded_url.format(videoid=videoid) + + # append result + results.append({'url': url, + 'title': title, + 'content': '', + 'template': 'videos.html', + 'publishedDate': publishedDate, + 'embedded': embedded, + 'thumbnail': thumbnail}) + + # return results + return results diff --git a/searx/engines/wikidata.py b/searx/engines/wikidata.py new file mode 100644 index 0000000..be21746 --- /dev/null +++ b/searx/engines/wikidata.py @@ -0,0 +1,488 @@ +# -*- coding: utf-8 -*- +""" + Wikidata + + @website https://wikidata.org + @provide-api yes (https://wikidata.org/w/api.php) + + @using-api partially (most things require scraping) + @results JSON, HTML + @stable no (html can change) + @parse url, infobox +""" + +from searx import logger +from searx.poolrequests import get +from searx.engines.xpath import extract_text +from searx.engines.wikipedia import _fetch_supported_languages, supported_languages_url +from searx.url_utils import urlencode + +from json import loads +from lxml.html import fromstring + +logger = logger.getChild('wikidata') +result_count = 1 + +# urls +wikidata_host = 'https://www.wikidata.org' +url_search = wikidata_host \ + + '/wiki/Special:ItemDisambiguation?{query}' + +wikidata_api = wikidata_host + '/w/api.php' +url_detail = wikidata_api\ + + '?action=parse&format=json&{query}'\ + + '&redirects=1&prop=text%7Cdisplaytitle%7Clanglinks%7Crevid'\ + + '&disableeditsection=1&disabletidy=1&preview=1§ionpreview=1&disabletoc=1&utf8=1&formatversion=2' + +url_map = 'https://www.openstreetmap.org/'\ + + '?lat={latitude}&lon={longitude}&zoom={zoom}&layers=M' +url_image = 'https://commons.wikimedia.org/wiki/Special:FilePath/{filename}?width=500&height=400' + +# xpaths +wikidata_ids_xpath = '//div/ul[@class="wikibase-disambiguation"]/li/a/@title' +title_xpath = '//*[contains(@class,"wikibase-title-label")]' +description_xpath = '//div[contains(@class,"wikibase-entitytermsview-heading-description")]' +property_xpath = '//div[@id="{propertyid}"]' +label_xpath = './/div[contains(@class,"wikibase-statementgroupview-property-label")]/a' +url_xpath = './/a[contains(@class,"external free") or contains(@class, "wb-external-id")]' +wikilink_xpath = './/ul[contains(@class,"wikibase-sitelinklistview-listview")]'\ + + '/li[contains(@data-wb-siteid,"{wikiid}")]//a/@href' +property_row_xpath = './/div[contains(@class,"wikibase-statementview")]' +preferred_rank_xpath = './/span[contains(@class,"wikibase-rankselector-preferred")]' +value_xpath = './/div[contains(@class,"wikibase-statementview-mainsnak")]'\ + + '/*/div[contains(@class,"wikibase-snakview-value")]' +language_fallback_xpath = '//sup[contains(@class,"wb-language-fallback-indicator")]' +calendar_name_xpath = './/sup[contains(@class,"wb-calendar-name")]' + + +def request(query, params): + language = params['language'].split('-')[0] + if language == 'all': + language = 'en' + + params['url'] = url_search.format( + query=urlencode({'label': query, 'language': language})) + return params + + +def response(resp): + results = [] + html = fromstring(resp.text) + wikidata_ids = html.xpath(wikidata_ids_xpath) + + language = resp.search_params['language'].split('-')[0] + if language == 'all': + language = 'en' + + # TODO: make requests asynchronous to avoid timeout when result_count > 1 + for wikidata_id in wikidata_ids[:result_count]: + url = url_detail.format(query=urlencode({'page': wikidata_id, 'uselang': language})) + htmlresponse = get(url) + jsonresponse = loads(htmlresponse.text) + results += getDetail(jsonresponse, wikidata_id, language, resp.search_params['language']) + + return results + + +def getDetail(jsonresponse, wikidata_id, language, locale): + results = [] + urls = [] + attributes = [] + + title = jsonresponse.get('parse', {}).get('displaytitle', {}) + result = jsonresponse.get('parse', {}).get('text', {}) + + if not title or not result: + return results + + title = fromstring(title) + for elem in title.xpath(language_fallback_xpath): + elem.getparent().remove(elem) + title = extract_text(title.xpath(title_xpath)) + + result = fromstring(result) + for elem in result.xpath(language_fallback_xpath): + elem.getparent().remove(elem) + + description = extract_text(result.xpath(description_xpath)) + + # URLS + + # official website + add_url(urls, result, 'P856', results=results) + + # wikipedia + wikipedia_link_count = 0 + wikipedia_link = get_wikilink(result, language + 'wiki') + if wikipedia_link: + wikipedia_link_count += 1 + urls.append({'title': 'Wikipedia (' + language + ')', + 'url': wikipedia_link}) + + if language != 'en': + wikipedia_en_link = get_wikilink(result, 'enwiki') + if wikipedia_en_link: + wikipedia_link_count += 1 + urls.append({'title': 'Wikipedia (en)', + 'url': wikipedia_en_link}) + + # TODO: get_wiki_firstlanguage + # if wikipedia_link_count == 0: + + # more wikis + add_url(urls, result, default_label='Wikivoyage (' + language + ')', link_type=language + 'wikivoyage') + add_url(urls, result, default_label='Wikiquote (' + language + ')', link_type=language + 'wikiquote') + add_url(urls, result, default_label='Wikimedia Commons', link_type='commonswiki') + + add_url(urls, result, 'P625', 'OpenStreetMap', link_type='geo') + + # musicbrainz + add_url(urls, result, 'P434', 'MusicBrainz', 'http://musicbrainz.org/artist/') + add_url(urls, result, 'P435', 'MusicBrainz', 'http://musicbrainz.org/work/') + add_url(urls, result, 'P436', 'MusicBrainz', 'http://musicbrainz.org/release-group/') + add_url(urls, result, 'P966', 'MusicBrainz', 'http://musicbrainz.org/label/') + + # IMDb + add_url(urls, result, 'P345', 'IMDb', 'https://www.imdb.com/', link_type='imdb') + # source code repository + add_url(urls, result, 'P1324') + # blog + add_url(urls, result, 'P1581') + # social media links + add_url(urls, result, 'P2397', 'YouTube', 'https://www.youtube.com/channel/') + add_url(urls, result, 'P1651', 'YouTube', 'https://www.youtube.com/watch?v=') + add_url(urls, result, 'P2002', 'Twitter', 'https://twitter.com/') + add_url(urls, result, 'P2013', 'Facebook', 'https://facebook.com/') + add_url(urls, result, 'P2003', 'Instagram', 'https://instagram.com/') + + urls.append({'title': 'Wikidata', + 'url': 'https://www.wikidata.org/wiki/' + + wikidata_id + '?uselang=' + language}) + + # INFOBOX ATTRIBUTES (ROWS) + + # DATES + # inception date + add_attribute(attributes, result, 'P571', date=True) + # dissolution date + add_attribute(attributes, result, 'P576', date=True) + # start date + add_attribute(attributes, result, 'P580', date=True) + # end date + add_attribute(attributes, result, 'P582', date=True) + # date of birth + add_attribute(attributes, result, 'P569', date=True) + # date of death + add_attribute(attributes, result, 'P570', date=True) + # date of spacecraft launch + add_attribute(attributes, result, 'P619', date=True) + # date of spacecraft landing + add_attribute(attributes, result, 'P620', date=True) + + # nationality + add_attribute(attributes, result, 'P27') + # country of origin + add_attribute(attributes, result, 'P495') + # country + add_attribute(attributes, result, 'P17') + # headquarters + add_attribute(attributes, result, 'Q180') + + # PLACES + # capital + add_attribute(attributes, result, 'P36', trim=True) + # head of state + add_attribute(attributes, result, 'P35', trim=True) + # head of government + add_attribute(attributes, result, 'P6', trim=True) + # type of government + add_attribute(attributes, result, 'P122') + # official language + add_attribute(attributes, result, 'P37') + # population + add_attribute(attributes, result, 'P1082', trim=True) + # area + add_attribute(attributes, result, 'P2046') + # currency + add_attribute(attributes, result, 'P38', trim=True) + # heigth (building) + add_attribute(attributes, result, 'P2048') + + # MEDIA + # platform (videogames) + add_attribute(attributes, result, 'P400') + # author + add_attribute(attributes, result, 'P50') + # creator + add_attribute(attributes, result, 'P170') + # director + add_attribute(attributes, result, 'P57') + # performer + add_attribute(attributes, result, 'P175') + # developer + add_attribute(attributes, result, 'P178') + # producer + add_attribute(attributes, result, 'P162') + # manufacturer + add_attribute(attributes, result, 'P176') + # screenwriter + add_attribute(attributes, result, 'P58') + # production company + add_attribute(attributes, result, 'P272') + # record label + add_attribute(attributes, result, 'P264') + # publisher + add_attribute(attributes, result, 'P123') + # original network + add_attribute(attributes, result, 'P449') + # distributor + add_attribute(attributes, result, 'P750') + # composer + add_attribute(attributes, result, 'P86') + # publication date + add_attribute(attributes, result, 'P577', date=True) + # genre + add_attribute(attributes, result, 'P136') + # original language + add_attribute(attributes, result, 'P364') + # isbn + add_attribute(attributes, result, 'Q33057') + # software license + add_attribute(attributes, result, 'P275') + # programming language + add_attribute(attributes, result, 'P277') + # version + add_attribute(attributes, result, 'P348', trim=True) + # narrative location + add_attribute(attributes, result, 'P840') + + # LANGUAGES + # number of speakers + add_attribute(attributes, result, 'P1098') + # writing system + add_attribute(attributes, result, 'P282') + # regulatory body + add_attribute(attributes, result, 'P1018') + # language code + add_attribute(attributes, result, 'P218') + + # OTHER + # ceo + add_attribute(attributes, result, 'P169', trim=True) + # founder + add_attribute(attributes, result, 'P112') + # legal form (company/organization) + add_attribute(attributes, result, 'P1454') + # operator + add_attribute(attributes, result, 'P137') + # crew members (tripulation) + add_attribute(attributes, result, 'P1029') + # taxon + add_attribute(attributes, result, 'P225') + # chemical formula + add_attribute(attributes, result, 'P274') + # winner (sports/contests) + add_attribute(attributes, result, 'P1346') + # number of deaths + add_attribute(attributes, result, 'P1120') + # currency code + add_attribute(attributes, result, 'P498') + + image = add_image(result) + + if len(attributes) == 0 and len(urls) == 2 and len(description) == 0: + results.append({ + 'url': urls[0]['url'], + 'title': title, + 'content': description + }) + else: + results.append({ + 'infobox': title, + 'id': wikipedia_link, + 'content': description, + 'img_src': image, + 'attributes': attributes, + 'urls': urls + }) + + return results + + +# only returns first match +def add_image(result): + # P15: route map, P242: locator map, P154: logo, P18: image, P242: map, P41: flag, P2716: collage, P2910: icon + property_ids = ['P15', 'P242', 'P154', 'P18', 'P242', 'P41', 'P2716', 'P2910'] + + for property_id in property_ids: + image = result.xpath(property_xpath.replace('{propertyid}', property_id)) + if image: + image_name = image[0].xpath(value_xpath) + image_src = url_image.replace('{filename}', extract_text(image_name[0])) + return image_src + + +# setting trim will only returned high ranked rows OR the first row +def add_attribute(attributes, result, property_id, default_label=None, date=False, trim=False): + attribute = result.xpath(property_xpath.replace('{propertyid}', property_id)) + if attribute: + + if default_label: + label = default_label + else: + label = extract_text(attribute[0].xpath(label_xpath)) + label = label[0].upper() + label[1:] + + if date: + trim = True + # remove calendar name + calendar_name = attribute[0].xpath(calendar_name_xpath) + for calendar in calendar_name: + calendar.getparent().remove(calendar) + + concat_values = "" + values = [] + first_value = None + for row in attribute[0].xpath(property_row_xpath): + if not first_value or not trim or row.xpath(preferred_rank_xpath): + + value = row.xpath(value_xpath) + if not value: + continue + value = extract_text(value) + + # save first value in case no ranked row is found + if trim and not first_value: + first_value = value + else: + # to avoid duplicate values + if value not in values: + concat_values += value + ", " + values.append(value) + + if trim and not values: + attributes.append({'label': label, + 'value': first_value}) + else: + attributes.append({'label': label, + 'value': concat_values[:-2]}) + + +# requires property_id unless it's a wiki link (defined in link_type) +def add_url(urls, result, property_id=None, default_label=None, url_prefix=None, results=None, link_type=None): + links = [] + + # wiki links don't have property in wikidata page + if link_type and 'wiki' in link_type: + links.append(get_wikilink(result, link_type)) + else: + dom_element = result.xpath(property_xpath.replace('{propertyid}', property_id)) + if dom_element: + dom_element = dom_element[0] + if not default_label: + label = extract_text(dom_element.xpath(label_xpath)) + label = label[0].upper() + label[1:] + + if link_type == 'geo': + links.append(get_geolink(dom_element)) + + elif link_type == 'imdb': + links.append(get_imdblink(dom_element, url_prefix)) + + else: + url_results = dom_element.xpath(url_xpath) + for link in url_results: + if link is not None: + if url_prefix: + link = url_prefix + extract_text(link) + else: + link = extract_text(link) + links.append(link) + + # append urls + for url in links: + if url is not None: + urls.append({'title': default_label or label, + 'url': url}) + if results is not None: + results.append({'title': default_label or label, + 'url': url}) + + +def get_imdblink(result, url_prefix): + imdb_id = result.xpath(value_xpath) + if imdb_id: + imdb_id = extract_text(imdb_id) + id_prefix = imdb_id[:2] + if id_prefix == 'tt': + url = url_prefix + 'title/' + imdb_id + elif id_prefix == 'nm': + url = url_prefix + 'name/' + imdb_id + elif id_prefix == 'ch': + url = url_prefix + 'character/' + imdb_id + elif id_prefix == 'co': + url = url_prefix + 'company/' + imdb_id + elif id_prefix == 'ev': + url = url_prefix + 'event/' + imdb_id + else: + url = None + return url + + +def get_geolink(result): + coordinates = result.xpath(value_xpath) + if not coordinates: + return None + coordinates = extract_text(coordinates[0]) + latitude, longitude = coordinates.split(',') + + # convert to decimal + lat = int(latitude[:latitude.find(u'°')]) + if latitude.find('\'') >= 0: + lat += int(latitude[latitude.find(u'°') + 1:latitude.find('\'')] or 0) / 60.0 + if latitude.find('"') >= 0: + lat += float(latitude[latitude.find('\'') + 1:latitude.find('"')] or 0) / 3600.0 + if latitude.find('S') >= 0: + lat *= -1 + lon = int(longitude[:longitude.find(u'°')]) + if longitude.find('\'') >= 0: + lon += int(longitude[longitude.find(u'°') + 1:longitude.find('\'')] or 0) / 60.0 + if longitude.find('"') >= 0: + lon += float(longitude[longitude.find('\'') + 1:longitude.find('"')] or 0) / 3600.0 + if longitude.find('W') >= 0: + lon *= -1 + + # TODO: get precision + precision = 0.0002 + # there is no zoom information, deduce from precision (error prone) + # samples : + # 13 --> 5 + # 1 --> 6 + # 0.016666666666667 --> 9 + # 0.00027777777777778 --> 19 + # wolframalpha : + # quadratic fit { {13, 5}, {1, 6}, {0.0166666, 9}, {0.0002777777,19}} + # 14.1186-8.8322 x+0.625447 x^2 + if precision < 0.0003: + zoom = 19 + else: + zoom = int(15 - precision * 8.8322 + precision * precision * 0.625447) + + url = url_map\ + .replace('{latitude}', str(lat))\ + .replace('{longitude}', str(lon))\ + .replace('{zoom}', str(zoom)) + + return url + + +def get_wikilink(result, wikiid): + url = result.xpath(wikilink_xpath.replace('{wikiid}', wikiid)) + if not url: + return None + url = url[0] + if url.startswith('http://'): + url = url.replace('http://', 'https://') + elif url.startswith('//'): + url = 'https:' + url + return url diff --git a/searx/engines/wikipedia.py b/searx/engines/wikipedia.py new file mode 100644 index 0000000..db2fdc0 --- /dev/null +++ b/searx/engines/wikipedia.py @@ -0,0 +1,135 @@ +""" + Wikipedia (Web) + + @website https://{language}.wikipedia.org + @provide-api yes + + @using-api yes + @results JSON + @stable yes + @parse url, infobox +""" + +from json import loads +from lxml.html import fromstring +from searx.url_utils import quote, urlencode + +# search-url +base_url = u'https://{language}.wikipedia.org/' +search_url = base_url + u'w/api.php?'\ + 'action=query'\ + '&format=json'\ + '&{query}'\ + '&prop=extracts|pageimages'\ + '&exintro'\ + '&explaintext'\ + '&pithumbsize=300'\ + '&redirects' +supported_languages_url = 'https://meta.wikimedia.org/wiki/List_of_Wikipedias' + + +# set language in base_url +def url_lang(lang): + lang = lang.split('-')[0] + if lang == 'all' or lang not in supported_languages: + language = 'en' + else: + language = lang + + return language + + +# do search-request +def request(query, params): + if query.islower(): + query = u'{0}|{1}'.format(query.decode('utf-8'), query.decode('utf-8').title()).encode('utf-8') + + params['url'] = search_url.format(query=urlencode({'titles': query}), + language=url_lang(params['language'])) + + return params + + +# get first meaningful paragraph +# this should filter out disambiguation pages and notes above first paragraph +# "magic numbers" were obtained by fine tuning +def extract_first_paragraph(content, title, image): + first_paragraph = None + + failed_attempts = 0 + for paragraph in content.split('\n'): + + starts_with_title = paragraph.lower().find(title.lower(), 0, len(title) + 35) + length = len(paragraph) + + if length >= 200 or (starts_with_title >= 0 and (image or length >= 150)): + first_paragraph = paragraph + break + + failed_attempts += 1 + if failed_attempts > 3: + return None + + return first_paragraph + + +# get response from search-request +def response(resp): + results = [] + + search_result = loads(resp.text) + + # wikipedia article's unique id + # first valid id is assumed to be the requested article + for article_id in search_result['query']['pages']: + page = search_result['query']['pages'][article_id] + if int(article_id) > 0: + break + + if int(article_id) < 0: + return [] + + title = page.get('title') + + image = page.get('thumbnail') + if image: + image = image.get('source') + + extract = page.get('extract') + + summary = extract_first_paragraph(extract, title, image) + + # link to wikipedia article + wikipedia_link = base_url.format(language=url_lang(resp.search_params['language'])) \ + + 'wiki/' + quote(title.replace(' ', '_').encode('utf8')) + + results.append({'url': wikipedia_link, 'title': title}) + + results.append({'infobox': title, + 'id': wikipedia_link, + 'content': summary, + 'img_src': image, + 'urls': [{'title': 'Wikipedia', 'url': wikipedia_link}]}) + + return results + + +# get supported languages from their site +def _fetch_supported_languages(resp): + supported_languages = {} + dom = fromstring(resp.text) + tables = dom.xpath('//table[contains(@class,"sortable")]') + for table in tables: + # exclude header row + trs = table.xpath('.//tr')[1:] + for tr in trs: + td = tr.xpath('./td') + code = td[3].xpath('./a')[0].text + name = td[2].xpath('./a')[0].text + english_name = td[1].xpath('./a')[0].text + articles = int(td[4].xpath('./a/b')[0].text.replace(',', '')) + # exclude languages with too few articles + if articles >= 100: + supported_languages[code] = {"name": name, "english_name": english_name, "articles": articles} + + return supported_languages diff --git a/searx/engines/wolframalpha_api.py b/searx/engines/wolframalpha_api.py new file mode 100644 index 0000000..595c6b7 --- /dev/null +++ b/searx/engines/wolframalpha_api.py @@ -0,0 +1,129 @@ +# Wolfram Alpha (Science) +# +# @website https://www.wolframalpha.com +# @provide-api yes (https://api.wolframalpha.com/v2/) +# +# @using-api yes +# @results XML +# @stable yes +# @parse url, infobox + +from lxml import etree +from searx.url_utils import urlencode + +# search-url +search_url = 'https://api.wolframalpha.com/v2/query?appid={api_key}&{query}' +site_url = 'https://www.wolframalpha.com/input/?{query}' +api_key = '' # defined in settings.yml + +# xpath variables +failure_xpath = '/queryresult[attribute::success="false"]' +input_xpath = '//pod[starts-with(attribute::id, "Input")]/subpod/plaintext' +pods_xpath = '//pod' +subpods_xpath = './subpod' +pod_primary_xpath = './@primary' +pod_id_xpath = './@id' +pod_title_xpath = './@title' +plaintext_xpath = './plaintext' +image_xpath = './img' +img_src_xpath = './@src' +img_alt_xpath = './@alt' + +# pods to display as image in infobox +# this pods do return a plaintext, but they look better and are more useful as images +image_pods = {'VisualRepresentation', + 'Illustration'} + + +# do search-request +def request(query, params): + params['url'] = search_url.format(query=urlencode({'input': query}), api_key=api_key) + params['headers']['Referer'] = site_url.format(query=urlencode({'i': query})) + + return params + + +# replace private user area characters to make text legible +def replace_pua_chars(text): + pua_chars = {u'\uf522': u'\u2192', # rigth arrow + u'\uf7b1': u'\u2115', # set of natural numbers + u'\uf7b4': u'\u211a', # set of rational numbers + u'\uf7b5': u'\u211d', # set of real numbers + u'\uf7bd': u'\u2124', # set of integer numbers + u'\uf74c': 'd', # differential + u'\uf74d': u'\u212f', # euler's number + u'\uf74e': 'i', # imaginary number + u'\uf7d9': '='} # equals sign + + for k, v in pua_chars.items(): + text = text.replace(k, v) + + return text + + +# get response from search-request +def response(resp): + results = [] + + search_results = etree.XML(resp.text) + + # return empty array if there are no results + if search_results.xpath(failure_xpath): + return [] + + try: + infobox_title = search_results.xpath(input_xpath)[0].text + except: + infobox_title = "" + + pods = search_results.xpath(pods_xpath) + result_chunks = [] + result_content = "" + for pod in pods: + pod_id = pod.xpath(pod_id_xpath)[0] + pod_title = pod.xpath(pod_title_xpath)[0] + pod_is_result = pod.xpath(pod_primary_xpath) + + subpods = pod.xpath(subpods_xpath) + if not subpods: + continue + + # Appends either a text or an image, depending on which one is more suitable + for subpod in subpods: + content = subpod.xpath(plaintext_xpath)[0].text + image = subpod.xpath(image_xpath) + + if content and pod_id not in image_pods: + + if pod_is_result or not result_content: + if pod_id != "Input": + result_content = "%s: %s" % (pod_title, content) + + # if no input pod was found, title is first plaintext pod + if not infobox_title: + infobox_title = content + + content = replace_pua_chars(content) + result_chunks.append({'label': pod_title, 'value': content}) + + elif image: + result_chunks.append({'label': pod_title, + 'image': {'src': image[0].xpath(img_src_xpath)[0], + 'alt': image[0].xpath(img_alt_xpath)[0]}}) + + if not result_chunks: + return [] + + title = "Wolfram|Alpha (%s)" % infobox_title + + # append infobox + results.append({'infobox': infobox_title, + 'attributes': result_chunks, + 'urls': [{'title': 'Wolfram|Alpha', 'url': resp.request.headers['Referer']}]}) + + # append link to site + results.append({'url': resp.request.headers['Referer'], + 'title': title, + 'content': result_content}) + + return results diff --git a/searx/engines/wolframalpha_noapi.py b/searx/engines/wolframalpha_noapi.py new file mode 100644 index 0000000..2a8642f --- /dev/null +++ b/searx/engines/wolframalpha_noapi.py @@ -0,0 +1,120 @@ +# Wolfram|Alpha (Science) +# +# @website https://www.wolframalpha.com/ +# @provide-api yes (https://api.wolframalpha.com/v2/) +# +# @using-api no +# @results JSON +# @stable no +# @parse url, infobox + +from json import loads +from time import time + +from searx.poolrequests import get as http_get +from searx.url_utils import urlencode + +# search-url +url = 'https://www.wolframalpha.com/' + +search_url = url + 'input/json.jsp'\ + '?async=false'\ + '&banners=raw'\ + '&debuggingdata=false'\ + '&format=image,plaintext,imagemap,minput,moutput'\ + '&formattimeout=2'\ + '&{query}'\ + '&output=JSON'\ + '&parsetimeout=2'\ + '&proxycode={token}'\ + '&scantimeout=0.5'\ + '&sponsorcategories=true'\ + '&statemethod=deploybutton' + +referer_url = url + 'input/?{query}' + +token = {'value': '', + 'last_updated': None} + +# pods to display as image in infobox +# this pods do return a plaintext, but they look better and are more useful as images +image_pods = {'VisualRepresentation', + 'Illustration', + 'Symbol'} + + +# seems, wolframalpha resets its token in every hour +def obtain_token(): + update_time = time() - (time() % 3600) + try: + token_response = http_get('https://www.wolframalpha.com/input/api/v1/code?ts=9999999999999999999', timeout=2.0) + token['value'] = loads(token_response.text)['code'] + token['last_updated'] = update_time + except: + pass + return token + + +obtain_token() + + +# do search-request +def request(query, params): + # obtain token if last update was more than an hour + if time() - (token['last_updated'] or 0) > 3600: + obtain_token() + params['url'] = search_url.format(query=urlencode({'input': query}), token=token['value']) + params['headers']['Referer'] = referer_url.format(query=urlencode({'i': query})) + + return params + + +# get response from search-request +def response(resp): + results = [] + + resp_json = loads(resp.text) + + if not resp_json['queryresult']['success']: + return [] + + # TODO handle resp_json['queryresult']['assumptions'] + result_chunks = [] + infobox_title = "" + result_content = "" + for pod in resp_json['queryresult']['pods']: + pod_id = pod.get('id', '') + pod_title = pod.get('title', '') + pod_is_result = pod.get('primary', None) + + if 'subpods' not in pod: + continue + + if pod_id == 'Input' or not infobox_title: + infobox_title = pod['subpods'][0]['plaintext'] + + for subpod in pod['subpods']: + if subpod['plaintext'] != '' and pod_id not in image_pods: + # append unless it's not an actual answer + if subpod['plaintext'] != '(requires interactivity)': + result_chunks.append({'label': pod_title, 'value': subpod['plaintext']}) + + if pod_is_result or not result_content: + if pod_id != "Input": + result_content = pod_title + ': ' + subpod['plaintext'] + + elif 'img' in subpod: + result_chunks.append({'label': pod_title, 'image': subpod['img']}) + + if not result_chunks: + return [] + + results.append({'infobox': infobox_title, + 'attributes': result_chunks, + 'urls': [{'title': 'Wolfram|Alpha', 'url': resp.request.headers['Referer']}]}) + + results.append({'url': resp.request.headers['Referer'], + 'title': 'Wolfram|Alpha (' + infobox_title + ')', + 'content': result_content}) + + return results diff --git a/searx/engines/www1x.py b/searx/engines/www1x.py new file mode 100644 index 0000000..5088032 --- /dev/null +++ b/searx/engines/www1x.py @@ -0,0 +1,81 @@ +""" + 1x (Images) + + @website http://1x.com/ + @provide-api no + + @using-api no + @results HTML + @stable no (HTML can change) + @parse url, title, thumbnail, img_src, content +""" + +from lxml import html +import re +from searx.url_utils import urlencode, urljoin + +# engine dependent config +categories = ['images'] +paging = False + +# search-url +base_url = 'https://1x.com' +search_url = base_url + '/backend/search.php?{query}' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(query=urlencode({'q': query})) + + return params + + +# get response from search-request +def response(resp): + results = [] + + # get links from result-text + regex = re.compile('(|', '"/>') + + dom = html.fromstring(cur_element) + link = dom.xpath('//a')[0] + + url = urljoin(base_url, link.attrib.get('href')) + title = link.attrib.get('title', '') + + thumbnail_src = urljoin(base_url, link.xpath('.//img')[0].attrib['src']) + # TODO: get image with higher resolution + img_src = thumbnail_src + + # check if url is showing to a photo + if '/photo/' not in url: + continue + + # append result + results.append({'url': url, + 'title': title, + 'img_src': img_src, + 'content': '', + 'thumbnail_src': thumbnail_src, + 'template': 'images.html'}) + + # return results + return results diff --git a/searx/engines/www500px.py b/searx/engines/www500px.py new file mode 100644 index 0000000..7a2015a --- /dev/null +++ b/searx/engines/www500px.py @@ -0,0 +1,73 @@ +""" + 500px (Images) + + @website https://500px.com + @provide-api yes (https://developers.500px.com/) + + @using-api no + @results HTML + @stable no (HTML can change) + @parse url, title, thumbnail, img_src, content + + @todo rewrite to api +""" + +from json import loads +from searx.url_utils import urlencode, urljoin + +# engine dependent config +categories = ['images'] +paging = True + +# search-url +base_url = 'https://500px.com' +search_url = 'https://api.500px.com/v1/photos/search?type=photos'\ + '&{query}'\ + '&image_size%5B%5D=4'\ + '&image_size%5B%5D=20'\ + '&image_size%5B%5D=21'\ + '&image_size%5B%5D=1080'\ + '&image_size%5B%5D=1600'\ + '&image_size%5B%5D=2048'\ + '&include_states=true'\ + '&formats=jpeg%2Clytro'\ + '&include_tags=true'\ + '&exclude_nude=true'\ + '&page={pageno}'\ + '&rpp=50'\ + '&sdk_key=b68e60cff4c929bedea36ca978830c5caca790c3' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(pageno=params['pageno'], + query=urlencode({'term': query})) + + return params + + +# get response from search-request +def response(resp): + results = [] + + response_json = loads(resp.text) + + # parse results + for result in response_json['photos']: + url = urljoin(base_url, result['url']) + title = result['name'] + # last index is the biggest resolution + img_src = result['image_url'][-1] + thumbnail_src = result['image_url'][0] + content = result['description'] or '' + + # append result + results.append({'url': url, + 'title': title, + 'img_src': img_src, + 'content': content, + 'thumbnail_src': thumbnail_src, + 'template': 'images.html'}) + + # return results + return results diff --git a/searx/engines/xpath.py b/searx/engines/xpath.py new file mode 100644 index 0000000..c8c56da --- /dev/null +++ b/searx/engines/xpath.py @@ -0,0 +1,122 @@ +from lxml import html +from lxml.etree import _ElementStringResult, _ElementUnicodeResult +from searx.utils import html_to_text +from searx.url_utils import unquote, urlencode, urljoin, urlparse + +search_url = None +url_xpath = None +content_xpath = None +title_xpath = None +paging = False +suggestion_xpath = '' +results_xpath = '' + +# parameters for engines with paging support +# +# number of results on each page +# (only needed if the site requires not a page number, but an offset) +page_size = 1 +# number of the first page (usually 0 or 1) +first_page_num = 1 + + +''' +if xpath_results is list, extract the text from each result and concat the list +if xpath_results is a xml element, extract all the text node from it + ( text_content() method from lxml ) +if xpath_results is a string element, then it's already done +''' + + +def extract_text(xpath_results): + if type(xpath_results) == list: + # it's list of result : concat everything using recursive call + result = '' + for e in xpath_results: + result = result + extract_text(e) + return result.strip() + elif type(xpath_results) in [_ElementStringResult, _ElementUnicodeResult]: + # it's a string + return ''.join(xpath_results) + else: + # it's a element + text = html.tostring(xpath_results, encoding='unicode', method='text', with_tail=False) + text = text.strip().replace('\n', ' ') + return ' '.join(text.split()) + + +def extract_url(xpath_results, search_url): + if xpath_results == []: + raise Exception('Empty url resultset') + url = extract_text(xpath_results) + + if url.startswith('//'): + # add http or https to this kind of url //example.com/ + parsed_search_url = urlparse(search_url) + url = u'{0}:{1}'.format(parsed_search_url.scheme, url) + elif url.startswith('/'): + # fix relative url to the search engine + url = urljoin(search_url, url) + + # normalize url + url = normalize_url(url) + + return url + + +def normalize_url(url): + parsed_url = urlparse(url) + + # add a / at this end of the url if there is no path + if not parsed_url.netloc: + raise Exception('Cannot parse url') + if not parsed_url.path: + url += '/' + + # FIXME : hack for yahoo + if parsed_url.hostname == 'search.yahoo.com'\ + and parsed_url.path.startswith('/r'): + p = parsed_url.path + mark = p.find('/**') + if mark != -1: + return unquote(p[mark + 3:]).decode('utf-8') + + return url + + +def request(query, params): + query = urlencode({'q': query})[2:] + + fp = {'query': query} + if paging and search_url.find('{pageno}') >= 0: + fp['pageno'] = (params['pageno'] - 1) * page_size + first_page_num + + params['url'] = search_url.format(**fp) + params['query'] = query + + return params + + +def response(resp): + results = [] + dom = html.fromstring(resp.text) + if results_xpath: + for result in dom.xpath(results_xpath): + url = extract_url(result.xpath(url_xpath), search_url) + title = extract_text(result.xpath(title_xpath)) + content = extract_text(result.xpath(content_xpath)) + results.append({'url': url, 'title': title, 'content': content}) + else: + for url, title, content in zip( + (extract_url(x, search_url) for + x in dom.xpath(url_xpath)), + map(extract_text, dom.xpath(title_xpath)), + map(extract_text, dom.xpath(content_xpath)) + ): + results.append({'url': url, 'title': title, 'content': content}) + + if not suggestion_xpath: + return results + for suggestion in dom.xpath(suggestion_xpath): + results.append({'suggestion': extract_text(suggestion)}) + return results diff --git a/searx/engines/yacy.py b/searx/engines/yacy.py new file mode 100644 index 0000000..a62a129 --- /dev/null +++ b/searx/engines/yacy.py @@ -0,0 +1,99 @@ +# Yacy (Web, Images, Videos, Music, Files) +# +# @website http://yacy.net +# @provide-api yes +# (http://www.yacy-websuche.de/wiki/index.php/Dev:APIyacysearch) +# +# @using-api yes +# @results JSON +# @stable yes +# @parse (general) url, title, content, publishedDate +# @parse (images) url, title, img_src +# +# @todo parse video, audio and file results + +from json import loads +from dateutil import parser +from searx.url_utils import urlencode + +from searx.utils import html_to_text + +# engine dependent config +categories = ['general', 'images'] # TODO , 'music', 'videos', 'files' +paging = True +language_support = True +number_of_results = 5 + +# search-url +base_url = 'http://localhost:8090' +search_url = '/yacysearch.json?{query}'\ + '&startRecord={offset}'\ + '&maximumRecords={limit}'\ + '&contentdom={search_type}'\ + '&resource=global' + +# yacy specific type-definitions +search_types = {'general': 'text', + 'images': 'image', + 'files': 'app', + 'music': 'audio', + 'videos': 'video'} + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * number_of_results + search_type = search_types.get(params.get('category'), '0') + + params['url'] = base_url +\ + search_url.format(query=urlencode({'query': query}), + offset=offset, + limit=number_of_results, + search_type=search_type) + + # add language tag if specified + if params['language'] != 'all': + params['url'] += '&lr=lang_' + params['language'].split('-')[0] + + return params + + +# get response from search-request +def response(resp): + results = [] + + raw_search_results = loads(resp.text) + + # return empty array if there are no results + if not raw_search_results: + return [] + + search_results = raw_search_results.get('channels', []) + + if len(search_results) == 0: + return [] + + for result in search_results[0].get('items', []): + # parse image results + if result.get('image'): + # append result + results.append({'url': result['url'], + 'title': result['title'], + 'content': '', + 'img_src': result['image'], + 'template': 'images.html'}) + + # parse general results + else: + publishedDate = parser.parse(result['pubDate']) + + # append result + results.append({'url': result['link'], + 'title': result['title'], + 'content': html_to_text(result['description']), + 'publishedDate': publishedDate}) + + # TODO parse video, audio and file results + + # return results + return results diff --git a/searx/engines/yahoo.py b/searx/engines/yahoo.py new file mode 100644 index 0000000..5387aaf --- /dev/null +++ b/searx/engines/yahoo.py @@ -0,0 +1,153 @@ +""" + Yahoo (Web) + + @website https://search.yahoo.com/web + @provide-api yes (https://developer.yahoo.com/boss/search/), + $0.80/1000 queries + + @using-api no (because pricing) + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content, suggestion +""" + +from lxml import html +from searx.engines.xpath import extract_text, extract_url +from searx.url_utils import unquote, urlencode + +# engine dependent config +categories = ['general'] +paging = True +language_support = True +time_range_support = True + +# search-url +base_url = 'https://search.yahoo.com/' +search_url = 'search?{query}&b={offset}&fl=1&vl=lang_{lang}' +search_url_with_time = 'search?{query}&b={offset}&fl=1&vl=lang_{lang}&age={age}&btf={btf}&fr2=time' + +supported_languages_url = 'https://search.yahoo.com/web/advanced' + +# specific xpath variables +results_xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' Sr ')]" +url_xpath = './/h3/a/@href' +title_xpath = './/h3/a' +content_xpath = './/div[@class="compText aAbs"]' +suggestion_xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' AlsoTry ')]//a" + +time_range_dict = {'day': ['1d', 'd'], + 'week': ['1w', 'w'], + 'month': ['1m', 'm']} + + +# remove yahoo-specific tracking-url +def parse_url(url_string): + endings = ['/RS', '/RK'] + endpositions = [] + start = url_string.find('http', url_string.find('/RU=') + 1) + + for ending in endings: + endpos = url_string.rfind(ending) + if endpos > -1: + endpositions.append(endpos) + + if start == 0 or len(endpositions) == 0: + return url_string + else: + end = min(endpositions) + return unquote(url_string[start:end]) + + +def _get_url(query, offset, language, time_range): + if time_range in time_range_dict: + return base_url + search_url_with_time.format(offset=offset, + query=urlencode({'p': query}), + lang=language, + age=time_range_dict[time_range][0], + btf=time_range_dict[time_range][1]) + return base_url + search_url.format(offset=offset, + query=urlencode({'p': query}), + lang=language) + + +def _get_language(params): + if params['language'] == 'all': + return 'en' + elif params['language'][:2] == 'zh': + if params['language'] == 'zh' or params['language'] == 'zh-CH': + return 'szh' + else: + return 'tzh' + else: + return params['language'].split('-')[0] + + +# do search-request +def request(query, params): + if params['time_range'] and params['time_range'] not in time_range_dict: + return params + + offset = (params['pageno'] - 1) * 10 + 1 + language = _get_language(params) + + params['url'] = _get_url(query, offset, language, params['time_range']) + + # TODO required? + params['cookies']['sB'] = 'fl=1&vl=lang_{lang}&sh=1&rw=new&v=1'\ + .format(lang=language) + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + try: + results_num = int(dom.xpath('//div[@class="compPagination"]/span[last()]/text()')[0] + .split()[0].replace(',', '')) + results.append({'number_of_results': results_num}) + except: + pass + + # parse results + for result in dom.xpath(results_xpath): + try: + url = parse_url(extract_url(result.xpath(url_xpath), search_url)) + title = extract_text(result.xpath(title_xpath)[0]) + except: + continue + + content = extract_text(result.xpath(content_xpath)[0]) + + # append result + results.append({'url': url, + 'title': title, + 'content': content}) + + # if no suggestion found, return results + suggestions = dom.xpath(suggestion_xpath) + if not suggestions: + return results + + # parse suggestion + for suggestion in suggestions: + # append suggestion + results.append({'suggestion': extract_text(suggestion)}) + + # return results + return results + + +# get supported languages from their site +def _fetch_supported_languages(resp): + supported_languages = [] + dom = html.fromstring(resp.text) + options = dom.xpath('//div[@id="yschlang"]/span/label/input') + for option in options: + code = option.xpath('./@value')[0][5:].replace('_', '-') + supported_languages.append(code) + + return supported_languages diff --git a/searx/engines/yahoo_news.py b/searx/engines/yahoo_news.py new file mode 100644 index 0000000..ae54a4a --- /dev/null +++ b/searx/engines/yahoo_news.py @@ -0,0 +1,107 @@ +# Yahoo (News) +# +# @website https://news.yahoo.com +# @provide-api yes (https://developer.yahoo.com/boss/search/) +# $0.80/1000 queries +# +# @using-api no (because pricing) +# @results HTML (using search portal) +# @stable no (HTML can change) +# @parse url, title, content, publishedDate + +import re +from datetime import datetime, timedelta +from lxml import html +from searx.engines.xpath import extract_text, extract_url +from searx.engines.yahoo import parse_url, _fetch_supported_languages, supported_languages_url +from dateutil import parser +from searx.url_utils import urlencode + +# engine dependent config +categories = ['news'] +paging = True +language_support = True + +# search-url +search_url = 'https://news.search.yahoo.com/search?{query}&b={offset}&{lang}=uh3_news_web_gs_1&pz=10&xargs=0&vl=lang_{lang}' # noqa + +# specific xpath variables +results_xpath = '//ol[contains(@class,"searchCenterMiddle")]//li' +url_xpath = './/h3/a/@href' +title_xpath = './/h3/a' +content_xpath = './/div[@class="compText"]' +publishedDate_xpath = './/span[contains(@class,"tri")]' +suggestion_xpath = '//div[contains(@class,"VerALSOTRY")]//a' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 10 + 1 + + if params['language'] == 'all': + language = 'en' + else: + language = params['language'].split('_')[0] + + params['url'] = search_url.format(offset=offset, + query=urlencode({'p': query}), + lang=language) + + # TODO required? + params['cookies']['sB'] = '"v=1&vm=p&fl=1&vl=lang_{lang}&sh=1&pn=10&rw=new'\ + .format(lang=language) + return params + + +def sanitize_url(url): + if ".yahoo.com/" in url: + return re.sub(u"\\;\\_ylt\\=.+$", "", url) + else: + return url + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + # parse results + for result in dom.xpath(results_xpath): + urls = result.xpath(url_xpath) + if len(urls) != 1: + continue + url = sanitize_url(parse_url(extract_url(urls, search_url))) + title = extract_text(result.xpath(title_xpath)[0]) + content = extract_text(result.xpath(content_xpath)[0]) + + # parse publishedDate + publishedDate = extract_text(result.xpath(publishedDate_xpath)[0]) + + # still useful ? + if re.match("^[0-9]+ minute(s|) ago$", publishedDate): + publishedDate = datetime.now() - timedelta(minutes=int(re.match(r'\d+', publishedDate).group())) + elif re.match("^[0-9]+ days? ago$", publishedDate): + publishedDate = datetime.now() - timedelta(days=int(re.match(r'\d+', publishedDate).group())) + elif re.match("^[0-9]+ hour(s|), [0-9]+ minute(s|) ago$", publishedDate): + timeNumbers = re.findall(r'\d+', publishedDate) + publishedDate = datetime.now()\ + - timedelta(hours=int(timeNumbers[0]))\ + - timedelta(minutes=int(timeNumbers[1])) + else: + try: + publishedDate = parser.parse(publishedDate) + except: + publishedDate = datetime.now() + + if publishedDate.year == 1900: + publishedDate = publishedDate.replace(year=datetime.now().year) + + # append result + results.append({'url': url, + 'title': title, + 'content': content, + 'publishedDate': publishedDate}) + + # return results + return results diff --git a/searx/engines/yandex.py b/searx/engines/yandex.py new file mode 100644 index 0000000..1c789f6 --- /dev/null +++ b/searx/engines/yandex.py @@ -0,0 +1,64 @@ +""" + Yahoo (Web) + + @website https://yandex.ru/ + @provide-api ? + @using-api no + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content +""" + +from lxml import html +from searx import logger +from searx.url_utils import urlencode + +logger = logger.getChild('yandex engine') + +# engine dependent config +categories = ['general'] +paging = True +language_support = True # TODO + +default_tld = 'com' +language_map = {'ru': 'ru', + 'ua': 'ua', + 'be': 'by', + 'kk': 'kz', + 'tr': 'com.tr'} + +# search-url +base_url = 'https://yandex.{tld}/' +search_url = 'search/?{query}&p={page}' + +results_xpath = '//li[@class="serp-item"]' +url_xpath = './/h2/a/@href' +title_xpath = './/h2/a//text()' +content_xpath = './/div[@class="text-container typo typo_text_m typo_line_m organic__text"]//text()' + + +def request(query, params): + lang = params['language'].split('-')[0] + host = base_url.format(tld=language_map.get(lang) or default_tld) + params['url'] = host + search_url.format(page=params['pageno'] - 1, + query=urlencode({'text': query})) + return params + + +# get response from search-request +def response(resp): + dom = html.fromstring(resp.text) + results = [] + + for result in dom.xpath(results_xpath): + try: + res = {'url': result.xpath(url_xpath)[0], + 'title': ''.join(result.xpath(title_xpath)), + 'content': ''.join(result.xpath(content_xpath))} + except: + logger.exception('yandex parse crash') + continue + + results.append(res) + + return results diff --git a/searx/engines/youtube_api.py b/searx/engines/youtube_api.py new file mode 100644 index 0000000..6de18aa --- /dev/null +++ b/searx/engines/youtube_api.py @@ -0,0 +1,83 @@ +# Youtube (Videos) +# +# @website https://www.youtube.com/ +# @provide-api yes (https://developers.google.com/apis-explorer/#p/youtube/v3/youtube.search.list) +# +# @using-api yes +# @results JSON +# @stable yes +# @parse url, title, content, publishedDate, thumbnail, embedded + +from json import loads +from dateutil import parser +from searx.url_utils import urlencode + +# engine dependent config +categories = ['videos', 'music'] +paging = False +language_support = True +api_key = None + +# search-url +base_url = 'https://www.googleapis.com/youtube/v3/search' +search_url = base_url + '?part=snippet&{query}&maxResults=20&key={api_key}' + +embedded_url = '' + +base_youtube_url = 'https://www.youtube.com/watch?v=' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(query=urlencode({'q': query}), + api_key=api_key) + + # add language tag if specified + if params['language'] != 'all': + params['url'] += '&relevanceLanguage=' + params['language'].split('-')[0] + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_results = loads(resp.text) + + # return empty array if there are no results + if 'items' not in search_results: + return [] + + # parse results + for result in search_results['items']: + videoid = result['id']['videoId'] + + title = result['snippet']['title'] + content = '' + thumbnail = '' + + pubdate = result['snippet']['publishedAt'] + publishedDate = parser.parse(pubdate) + + thumbnail = result['snippet']['thumbnails']['high']['url'] + + content = result['snippet']['description'] + + url = base_youtube_url + videoid + + embedded = embedded_url.format(videoid=videoid) + + # append result + results.append({'url': url, + 'title': title, + 'content': content, + 'template': 'videos.html', + 'publishedDate': publishedDate, + 'embedded': embedded, + 'thumbnail': thumbnail}) + + # return results + return results diff --git a/searx/engines/youtube_noapi.py b/searx/engines/youtube_noapi.py new file mode 100644 index 0000000..9f01841 --- /dev/null +++ b/searx/engines/youtube_noapi.py @@ -0,0 +1,89 @@ +# Youtube (Videos) +# +# @website https://www.youtube.com/ +# @provide-api yes (https://developers.google.com/apis-explorer/#p/youtube/v3/youtube.search.list) +# +# @using-api no +# @results HTML +# @stable no +# @parse url, title, content, publishedDate, thumbnail, embedded + +from lxml import html +from searx.engines.xpath import extract_text +from searx.utils import list_get +from searx.url_utils import quote_plus + +# engine dependent config +categories = ['videos', 'music'] +paging = True +language_support = False +time_range_support = True + +# search-url +base_url = 'https://www.youtube.com/results' +search_url = base_url + '?search_query={query}&page={page}' +time_range_url = '&sp=EgII{time_range}%253D%253D' +time_range_dict = {'day': 'Ag', + 'week': 'Aw', + 'month': 'BA', + 'year': 'BQ'} + +embedded_url = '' + +base_youtube_url = 'https://www.youtube.com/watch?v=' + +# specific xpath variables +results_xpath = "//ol/li/div[contains(@class, 'yt-lockup yt-lockup-tile yt-lockup-video vve-check')]" +url_xpath = './/h3/a/@href' +title_xpath = './/div[@class="yt-lockup-content"]/h3/a' +content_xpath = './/div[@class="yt-lockup-content"]/div[@class="yt-lockup-description yt-ui-ellipsis yt-ui-ellipsis-2"]' + + +# returns extract_text on the first result selected by the xpath or None +def extract_text_from_dom(result, xpath): + r = result.xpath(xpath) + if len(r) > 0: + return extract_text(r[0]) + return None + + +# do search-request +def request(query, params): + params['url'] = search_url.format(query=quote_plus(query), + page=params['pageno']) + if params['time_range'] in time_range_dict: + params['url'] += time_range_url.format(time_range=time_range_dict[params['time_range']]) + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + # parse results + for result in dom.xpath(results_xpath): + videoid = list_get(result.xpath('@data-context-item-id'), 0) + if videoid is not None: + url = base_youtube_url + videoid + thumbnail = 'https://i.ytimg.com/vi/' + videoid + '/hqdefault.jpg' + + title = extract_text_from_dom(result, title_xpath) or videoid + content = extract_text_from_dom(result, content_xpath) + + embedded = embedded_url.format(videoid=videoid) + + # append result + results.append({'url': url, + 'title': title, + 'content': content, + 'template': 'videos.html', + 'embedded': embedded, + 'thumbnail': thumbnail}) + + # return results + return results diff --git a/searx/exceptions.py b/searx/exceptions.py new file mode 100644 index 0000000..c605ddc --- /dev/null +++ b/searx/exceptions.py @@ -0,0 +1,32 @@ +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2017- by Alexandre Flament, +''' + + +class SearxException(Exception): + pass + + +class SearxParameterException(SearxException): + + def __init__(self, name, value): + if value == '' or value is None: + message = 'Empty ' + name + ' parameter' + else: + message = 'Invalid value "' + value + '" for parameter ' + name + super(SearxParameterException, self).__init__(message) + self.parameter_name = name + self.parameter_value = value diff --git a/searx/languages.py b/searx/languages.py new file mode 100644 index 0000000..22229f7 --- /dev/null +++ b/searx/languages.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# list of language codes +# this file is generated automatically by utils/update_search_languages.py + +language_codes = ( + (u"ar-SA", u"العربية", u"", u"Arabic"), + (u"bg-BG", u"Български", u"", u"Bulgarian"), + (u"cs-CZ", u"Čeština", u"", u"Czech"), + (u"da-DK", u"Dansk", u"", u"Danish"), + (u"de", u"Deutsch", u"", u"German"), + (u"de-AT", u"Deutsch", u"Österreich", u"German"), + (u"de-CH", u"Deutsch", u"Schweiz", u"German"), + (u"de-DE", u"Deutsch", u"Deutschland", u"German"), + (u"el-GR", u"Ελληνικά", u"", u"Greek"), + (u"en", u"English", u"", u"English"), + (u"en-AU", u"English", u"Australia", u"English"), + (u"en-CA", u"English", u"Canada", u"English"), + (u"en-CY", u"English", u"Cyprus", u"English"), + (u"en-GB", u"English", u"United Kingdom", u"English"), + (u"en-GD", u"English", u"Grenada", u"English"), + (u"en-ID", u"English", u"Indonesia", u"English"), + (u"en-IE", u"English", u"Ireland", u"English"), + (u"en-IN", u"English", u"India", u"English"), + (u"en-MY", u"English", u"Malaysia", u"English"), + (u"en-NZ", u"English", u"New Zealand", u"English"), + (u"en-PH", u"English", u"Philippines", u"English"), + (u"en-SG", u"English", u"Singapore", u"English"), + (u"en-US", u"English", u"United States", u"English"), + (u"en-ZA", u"English", u"South Africa", u"English"), + (u"es", u"Español", u"", u"Spanish"), + (u"es-AR", u"Español", u"Argentina", u"Spanish"), + (u"es-CL", u"Español", u"Chile", u"Spanish"), + (u"es-CO", u"Español", u"Colombia", u"Spanish"), + (u"es-ES", u"Español", u"España", u"Spanish"), + (u"es-MX", u"Español", u"México", u"Spanish"), + (u"es-PE", u"Español", u"Perú", u"Spanish"), + (u"es-US", u"Español", u"Estados Unidos", u"Spanish"), + (u"et-EE", u"Eesti", u"", u"Estonian"), + (u"fi-FI", u"Suomi", u"", u"Finnish"), + (u"fr", u"Français", u"", u"French"), + (u"fr-BE", u"Français", u"Belgique", u"French"), + (u"fr-CA", u"Français", u"Canada", u"French"), + (u"fr-CH", u"Français", u"Suisse", u"French"), + (u"fr-FR", u"Français", u"France", u"French"), + (u"he-IL", u"עברית", u"", u"Hebrew"), + (u"hr-HR", u"Hrvatski", u"", u"Croatian"), + (u"hu-HU", u"Magyar", u"", u"Hungarian"), + (u"id-ID", u"Bahasa Indonesia", u"", u"Indonesian"), + (u"it", u"Italiano", u"", u"Italian"), + (u"it-CH", u"Italiano", u"Svizzera", u"Italian"), + (u"it-IT", u"Italiano", u"Italia", u"Italian"), + (u"ja-JP", u"日本語", u"", u"Japanese"), + (u"ko-KR", u"한국어", u"", u"Korean"), + (u"lt-LT", u"Lietuvių", u"", u"Lithuanian"), + (u"lv-LV", u"Latviešu", u"", u"Latvian"), + (u"ms-MY", u"Bahasa Melayu", u"", u"Malay"), + (u"nl", u"Nederlands", u"", u"Dutch"), + (u"nl-BE", u"Nederlands", u"België", u"Dutch"), + (u"nl-NL", u"Nederlands", u"Nederland", u"Dutch"), + (u"no-NO", u"Norsk", u"", u"Norwegian"), + (u"pl-PL", u"Polski", u"", u"Polish"), + (u"pt", u"Português", u"", u"Portuguese"), + (u"pt-BR", u"Português", u"Brasil", u"Portuguese"), + (u"pt-PT", u"Português", u"Portugal", u"Portuguese"), + (u"ro-RO", u"Română", u"", u"Romanian"), + (u"ru-RU", u"Русский", u"", u"Russian"), + (u"sk-SK", u"Slovenčina", u"", u"Slovak"), + (u"sl", u"Slovenščina", u"", u"Slovenian"), + (u"sv-SE", u"Svenska", u"", u"Swedish"), + (u"th-TH", u"ไทย", u"", u"Thai"), + (u"tr-TR", u"Türkçe", u"", u"Turkish"), + (u"vi-VN", u"Tiếng Việt", u"", u"Vietnamese"), + (u"zh", u"中文", u"", u"Chinese"), + (u"zh-CN", u"中文", u"中国", u"Chinese"), + (u"zh-HK", u"中文", u"香港", u"Chinese"), + (u"zh-TW", u"中文", u"台湾", u"Chinese") +) diff --git a/searx/plugins/__init__.py b/searx/plugins/__init__.py new file mode 100644 index 0000000..46c1f89 --- /dev/null +++ b/searx/plugins/__init__.py @@ -0,0 +1,88 @@ +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2015 by Adam Tauber, +''' +from sys import exit, version_info +from searx import logger + +if version_info[0] == 3: + unicode = str + +logger = logger.getChild('plugins') + +from searx.plugins import (doai_rewrite, + https_rewrite, + infinite_scroll, + open_results_on_new_tab, + self_info, + search_on_category_select, + tracker_url_remover, + vim_hotkeys) + +required_attrs = (('name', (str, unicode)), + ('description', (str, unicode)), + ('default_on', bool)) + +optional_attrs = (('js_dependencies', tuple), + ('css_dependencies', tuple)) + + +class Plugin(): + default_on = False + name = 'Default plugin' + description = 'Default plugin description' + + +class PluginStore(): + + def __init__(self): + self.plugins = [] + + def __iter__(self): + for plugin in self.plugins: + yield plugin + + def register(self, *plugins): + for plugin in plugins: + for plugin_attr, plugin_attr_type in required_attrs: + if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type): + logger.critical('missing attribute "{0}", cannot load plugin: {1}'.format(plugin_attr, plugin)) + exit(3) + for plugin_attr, plugin_attr_type in optional_attrs: + if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type): + setattr(plugin, plugin_attr, plugin_attr_type()) + plugin.id = plugin.name.replace(' ', '_') + self.plugins.append(plugin) + + def call(self, ordered_plugin_list, plugin_type, request, *args, **kwargs): + ret = True + for plugin in ordered_plugin_list: + if hasattr(plugin, plugin_type): + ret = getattr(plugin, plugin_type)(request, *args, **kwargs) + if not ret: + break + + return ret + + +plugins = PluginStore() +plugins.register(doai_rewrite) +plugins.register(https_rewrite) +plugins.register(infinite_scroll) +plugins.register(open_results_on_new_tab) +plugins.register(self_info) +plugins.register(search_on_category_select) +plugins.register(tracker_url_remover) +plugins.register(vim_hotkeys) diff --git a/searx/plugins/doai_rewrite.py b/searx/plugins/doai_rewrite.py new file mode 100644 index 0000000..95efa8f --- /dev/null +++ b/searx/plugins/doai_rewrite.py @@ -0,0 +1,31 @@ +from flask_babel import gettext +import re +from searx.url_utils import urlparse, parse_qsl + +regex = re.compile(r'10\.\d{4,9}/[^\s]+') + +name = gettext('DOAI rewrite') +description = gettext('Avoid paywalls by redirecting to open-access versions of publications when available') +default_on = False + + +def extract_doi(url): + match = regex.search(url.path) + if match: + return match.group(0) + for _, v in parse_qsl(url.query): + match = regex.search(v) + if match: + return match.group(0) + return None + + +def on_result(request, search, result): + doi = extract_doi(result['parsed_url']) + if doi and len(doi) < 50: + for suffix in ('/', '.pdf', '/full', '/meta', '/abstract'): + if doi.endswith(suffix): + doi = doi[:-len(suffix)] + result['url'] = 'http://doai.io/' + doi + result['parsed_url'] = urlparse(result['url']) + return True diff --git a/searx/plugins/https_rewrite.py b/searx/plugins/https_rewrite.py new file mode 100644 index 0000000..4462c86 --- /dev/null +++ b/searx/plugins/https_rewrite.py @@ -0,0 +1,232 @@ +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2013- by Adam Tauber, +''' + +import re +import sys +from lxml import etree +from os import listdir, environ +from os.path import isfile, isdir, join +from searx.plugins import logger +from flask_babel import gettext +from searx import searx_dir +from searx.url_utils import urlparse + +if sys.version_info[0] == 3: + unicode = str + +name = "HTTPS rewrite" +description = gettext('Rewrite HTTP links to HTTPS if possible') +default_on = True + +if 'SEARX_HTTPS_REWRITE_PATH' in environ: + rules_path = environ['SEARX_rules_path'] +else: + rules_path = join(searx_dir, 'plugins/https_rules') + +logger = logger.getChild("https_rewrite") + +# https://gitweb.torproject.org/\ +# pde/https-everywhere.git/tree/4.0:/src/chrome/content/rules + +# HTTPS rewrite rules +https_rules = [] + + +# load single ruleset from a xml file +def load_single_https_ruleset(rules_path): + ruleset = () + + # init parser + parser = etree.XMLParser() + + # load and parse xml-file + try: + tree = etree.parse(rules_path, parser) + except: + # TODO, error message + return () + + # get root node + root = tree.getroot() + + # check if root is a node with the name ruleset + # TODO improve parsing + if root.tag != 'ruleset': + return () + + # check if rule is deactivated by default + if root.attrib.get('default_off'): + return () + + # check if rule does only work for specific platforms + if root.attrib.get('platform'): + return () + + hosts = [] + rules = [] + exclusions = [] + + # parse childs from ruleset + for ruleset in root: + # this child define a target + if ruleset.tag == 'target': + # check if required tags available + if not ruleset.attrib.get('host'): + continue + + # convert host-rule to valid regex + host = ruleset.attrib.get('host')\ + .replace('.', r'\.').replace('*', '.*') + + # append to host list + hosts.append(host) + + # this child define a rule + elif ruleset.tag == 'rule': + # check if required tags available + if not ruleset.attrib.get('from')\ + or not ruleset.attrib.get('to'): + continue + + # TODO hack, which convert a javascript regex group + # into a valid python regex group + rule_from = ruleset.attrib['from'].replace('$', '\\') + if rule_from.endswith('\\'): + rule_from = rule_from[:-1] + '$' + rule_to = ruleset.attrib['to'].replace('$', '\\') + if rule_to.endswith('\\'): + rule_to = rule_to[:-1] + '$' + + # TODO, not working yet because of the hack above, + # currently doing that in webapp.py + # rule_from_rgx = re.compile(rule_from, re.I) + + # append rule + try: + rules.append((re.compile(rule_from, re.I | re.U), rule_to)) + except: + # TODO log regex error + continue + + # this child define an exclusion + elif ruleset.tag == 'exclusion': + # check if required tags available + if not ruleset.attrib.get('pattern'): + continue + + exclusion_rgx = re.compile(ruleset.attrib.get('pattern')) + + # append exclusion + exclusions.append(exclusion_rgx) + + # convert list of possible hosts to a simple regex + # TODO compress regex to improve performance + try: + target_hosts = re.compile('^(' + '|'.join(hosts) + ')', re.I | re.U) + except: + return () + + # return ruleset + return (target_hosts, rules, exclusions) + + +# load all https rewrite rules +def load_https_rules(rules_path): + # check if directory exists + if not isdir(rules_path): + logger.error("directory not found: '" + rules_path + "'") + return + + # search all xml files which are stored in the https rule directory + xml_files = [join(rules_path, f) + for f in listdir(rules_path) + if isfile(join(rules_path, f)) and f[-4:] == '.xml'] + + # load xml-files + for ruleset_file in xml_files: + # calculate rewrite-rules + ruleset = load_single_https_ruleset(ruleset_file) + + # skip if no ruleset returned + if not ruleset: + continue + + # append ruleset + https_rules.append(ruleset) + + logger.info('{n} rules loaded'.format(n=len(https_rules))) + + +def https_url_rewrite(result): + skip_https_rewrite = False + # check if HTTPS rewrite is possible + for target, rules, exclusions in https_rules: + + # check if target regex match with url + if target.match(result['parsed_url'].netloc): + # process exclusions + for exclusion in exclusions: + # check if exclusion match with url + if exclusion.match(result['url']): + skip_https_rewrite = True + break + + # skip https rewrite if required + if skip_https_rewrite: + break + + # process rules + for rule in rules: + try: + new_result_url = rule[0].sub(rule[1], result['url']) + except: + break + + # parse new url + new_parsed_url = urlparse(new_result_url) + + # continiue if nothing was rewritten + if result['url'] == new_result_url: + continue + + # get domainname from result + # TODO, does only work correct with TLD's like + # asdf.com, not for asdf.com.de + # TODO, using publicsuffix instead of this rewrite rule + old_result_domainname = '.'.join( + result['parsed_url'].hostname.split('.')[-2:]) + new_result_domainname = '.'.join( + new_parsed_url.hostname.split('.')[-2:]) + + # check if rewritten hostname is the same, + # to protect against wrong or malicious rewrite rules + if old_result_domainname == new_result_domainname: + # set new url + result['url'] = new_result_url + + # target has matched, do not search over the other rules + break + return result + + +def on_result(request, search, result): + if result['parsed_url'].scheme == 'http': + https_url_rewrite(result) + return True + + +load_https_rules(rules_path) diff --git a/searx/plugins/https_rules/00README b/searx/plugins/https_rules/00README new file mode 100644 index 0000000..fcd8a77 --- /dev/null +++ b/searx/plugins/https_rules/00README @@ -0,0 +1,17 @@ + diff --git a/searx/plugins/https_rules/Bing.xml b/searx/plugins/https_rules/Bing.xml new file mode 100644 index 0000000..8b403f1 --- /dev/null +++ b/searx/plugins/https_rules/Bing.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/Dailymotion.xml b/searx/plugins/https_rules/Dailymotion.xml new file mode 100644 index 0000000..743100c --- /dev/null +++ b/searx/plugins/https_rules/Dailymotion.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/Deviantart.xml b/searx/plugins/https_rules/Deviantart.xml new file mode 100644 index 0000000..7830fc2 --- /dev/null +++ b/searx/plugins/https_rules/Deviantart.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/DuckDuckGo.xml b/searx/plugins/https_rules/DuckDuckGo.xml new file mode 100644 index 0000000..173a9ad --- /dev/null +++ b/searx/plugins/https_rules/DuckDuckGo.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/Flickr.xml b/searx/plugins/https_rules/Flickr.xml new file mode 100644 index 0000000..85c6e80 --- /dev/null +++ b/searx/plugins/https_rules/Flickr.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/Github-Pages.xml b/searx/plugins/https_rules/Github-Pages.xml new file mode 100644 index 0000000..d3be58a --- /dev/null +++ b/searx/plugins/https_rules/Github-Pages.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/searx/plugins/https_rules/Github.xml b/searx/plugins/https_rules/Github.xml new file mode 100644 index 0000000..a9a3a1e --- /dev/null +++ b/searx/plugins/https_rules/Github.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/Google-mismatches.xml b/searx/plugins/https_rules/Google-mismatches.xml new file mode 100644 index 0000000..de9d3eb --- /dev/null +++ b/searx/plugins/https_rules/Google-mismatches.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/Google.org.xml b/searx/plugins/https_rules/Google.org.xml new file mode 100644 index 0000000..d6cc478 --- /dev/null +++ b/searx/plugins/https_rules/Google.org.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/searx/plugins/https_rules/GoogleAPIs.xml b/searx/plugins/https_rules/GoogleAPIs.xml new file mode 100644 index 0000000..85a5a80 --- /dev/null +++ b/searx/plugins/https_rules/GoogleAPIs.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/GoogleCanada.xml b/searx/plugins/https_rules/GoogleCanada.xml new file mode 100644 index 0000000..d5eefe8 --- /dev/null +++ b/searx/plugins/https_rules/GoogleCanada.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/searx/plugins/https_rules/GoogleImages.xml b/searx/plugins/https_rules/GoogleImages.xml new file mode 100644 index 0000000..0112001 --- /dev/null +++ b/searx/plugins/https_rules/GoogleImages.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/GoogleMainSearch.xml b/searx/plugins/https_rules/GoogleMainSearch.xml new file mode 100644 index 0000000..df504d9 --- /dev/null +++ b/searx/plugins/https_rules/GoogleMainSearch.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/GoogleMaps.xml b/searx/plugins/https_rules/GoogleMaps.xml new file mode 100644 index 0000000..0f82c52 --- /dev/null +++ b/searx/plugins/https_rules/GoogleMaps.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/GoogleMelange.xml b/searx/plugins/https_rules/GoogleMelange.xml new file mode 100644 index 0000000..ec23cd4 --- /dev/null +++ b/searx/plugins/https_rules/GoogleMelange.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/searx/plugins/https_rules/GoogleSearch.xml b/searx/plugins/https_rules/GoogleSearch.xml new file mode 100644 index 0000000..66b7ffd --- /dev/null +++ b/searx/plugins/https_rules/GoogleSearch.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/GoogleServices.xml b/searx/plugins/https_rules/GoogleServices.xml new file mode 100644 index 0000000..704646b --- /dev/null +++ b/searx/plugins/https_rules/GoogleServices.xml @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/GoogleShopping.xml b/searx/plugins/https_rules/GoogleShopping.xml new file mode 100644 index 0000000..6ba69a9 --- /dev/null +++ b/searx/plugins/https_rules/GoogleShopping.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/GoogleSorry.xml b/searx/plugins/https_rules/GoogleSorry.xml new file mode 100644 index 0000000..72a1921 --- /dev/null +++ b/searx/plugins/https_rules/GoogleSorry.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/searx/plugins/https_rules/GoogleTranslate.xml b/searx/plugins/https_rules/GoogleTranslate.xml new file mode 100644 index 0000000..a004025 --- /dev/null +++ b/searx/plugins/https_rules/GoogleTranslate.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/searx/plugins/https_rules/GoogleVideos.xml b/searx/plugins/https_rules/GoogleVideos.xml new file mode 100644 index 0000000..a5e88fc --- /dev/null +++ b/searx/plugins/https_rules/GoogleVideos.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/GoogleWatchBlog.xml b/searx/plugins/https_rules/GoogleWatchBlog.xml new file mode 100644 index 0000000..afec70c --- /dev/null +++ b/searx/plugins/https_rules/GoogleWatchBlog.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/searx/plugins/https_rules/Google_App_Engine.xml b/searx/plugins/https_rules/Google_App_Engine.xml new file mode 100644 index 0000000..851e051 --- /dev/null +++ b/searx/plugins/https_rules/Google_App_Engine.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/searx/plugins/https_rules/Googleplex.com.xml b/searx/plugins/https_rules/Googleplex.com.xml new file mode 100644 index 0000000..7ddbb5b --- /dev/null +++ b/searx/plugins/https_rules/Googleplex.com.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/searx/plugins/https_rules/OpenStreetMap.xml b/searx/plugins/https_rules/OpenStreetMap.xml new file mode 100644 index 0000000..58a6618 --- /dev/null +++ b/searx/plugins/https_rules/OpenStreetMap.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/Rawgithub.com.xml b/searx/plugins/https_rules/Rawgithub.com.xml new file mode 100644 index 0000000..3868f33 --- /dev/null +++ b/searx/plugins/https_rules/Rawgithub.com.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/searx/plugins/https_rules/Soundcloud.xml b/searx/plugins/https_rules/Soundcloud.xml new file mode 100644 index 0000000..6958e8c --- /dev/null +++ b/searx/plugins/https_rules/Soundcloud.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/ThePirateBay.xml b/searx/plugins/https_rules/ThePirateBay.xml new file mode 100644 index 0000000..010387b --- /dev/null +++ b/searx/plugins/https_rules/ThePirateBay.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/Torproject.xml b/searx/plugins/https_rules/Torproject.xml new file mode 100644 index 0000000..69269af --- /dev/null +++ b/searx/plugins/https_rules/Torproject.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/Twitter.xml b/searx/plugins/https_rules/Twitter.xml new file mode 100644 index 0000000..3285f44 --- /dev/null +++ b/searx/plugins/https_rules/Twitter.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/Vimeo.xml b/searx/plugins/https_rules/Vimeo.xml new file mode 100644 index 0000000..f2a3e57 --- /dev/null +++ b/searx/plugins/https_rules/Vimeo.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/WikiLeaks.xml b/searx/plugins/https_rules/WikiLeaks.xml new file mode 100644 index 0000000..977709d --- /dev/null +++ b/searx/plugins/https_rules/WikiLeaks.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/searx/plugins/https_rules/Wikimedia.xml b/searx/plugins/https_rules/Wikimedia.xml new file mode 100644 index 0000000..9f25831 --- /dev/null +++ b/searx/plugins/https_rules/Wikimedia.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/Yahoo.xml b/searx/plugins/https_rules/Yahoo.xml new file mode 100644 index 0000000..33548c4 --- /dev/null +++ b/searx/plugins/https_rules/Yahoo.xml @@ -0,0 +1,2450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/https_rules/YouTube.xml b/searx/plugins/https_rules/YouTube.xml new file mode 100644 index 0000000..bddc2a5 --- /dev/null +++ b/searx/plugins/https_rules/YouTube.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searx/plugins/infinite_scroll.py b/searx/plugins/infinite_scroll.py new file mode 100644 index 0000000..422a4be --- /dev/null +++ b/searx/plugins/infinite_scroll.py @@ -0,0 +1,8 @@ +from flask_babel import gettext + +name = gettext('Infinite scroll') +description = gettext('Automatically load next page when scrolling to bottom of current page') +default_on = False + +js_dependencies = ('plugins/js/infinite_scroll.js',) +css_dependencies = ('plugins/css/infinite_scroll.css',) diff --git a/searx/plugins/open_results_on_new_tab.py b/searx/plugins/open_results_on_new_tab.py new file mode 100644 index 0000000..ae27ea2 --- /dev/null +++ b/searx/plugins/open_results_on_new_tab.py @@ -0,0 +1,24 @@ +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2016 by Adam Tauber, +''' +from flask_babel import gettext +name = gettext('Open result links on new browser tabs') +description = gettext('Results are opened in the same window by default. ' + 'This plugin overwrites the default behaviour to open links on new tabs/windows. ' + '(JavaScript required)') +default_on = False + +js_dependencies = ('plugins/js/open_results_on_new_tab.js',) diff --git a/searx/plugins/search_on_category_select.py b/searx/plugins/search_on_category_select.py new file mode 100644 index 0000000..f72c63d --- /dev/null +++ b/searx/plugins/search_on_category_select.py @@ -0,0 +1,23 @@ +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2015 by Adam Tauber, +''' +from flask_babel import gettext +name = gettext('Search on category select') +description = gettext('Perform search immediately if a category selected. ' + 'Disable to select multiple categories. (JavaScript required)') +default_on = True + +js_dependencies = ('plugins/js/search_on_category_select.js',) diff --git a/searx/plugins/self_info.py b/searx/plugins/self_info.py new file mode 100644 index 0000000..8d6c661 --- /dev/null +++ b/searx/plugins/self_info.py @@ -0,0 +1,46 @@ +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2015 by Adam Tauber, +''' +from flask_babel import gettext +import re +name = "Self Informations" +description = gettext('Displays your IP if the query is "ip" and your user agent if the query contains "user agent".') +default_on = True + + +# Self User Agent regex +p = re.compile(b'.*user[ -]agent.*', re.IGNORECASE) + + +# attach callback to the post search hook +# request: flask request object +# ctx: the whole local context of the pre search hook +def post_search(request, search): + if search.search_query.pageno > 1: + return True + if search.search_query.query == b'ip': + x_forwarded_for = request.headers.getlist("X-Forwarded-For") + if x_forwarded_for: + ip = x_forwarded_for[0] + else: + ip = request.remote_addr + search.result_container.answers.clear() + search.result_container.answers.add(ip) + elif p.match(search.search_query.query): + ua = request.user_agent + search.result_container.answers.clear() + search.result_container.answers.add(ua) + return True diff --git a/searx/plugins/tracker_url_remover.py b/searx/plugins/tracker_url_remover.py new file mode 100644 index 0000000..a840128 --- /dev/null +++ b/searx/plugins/tracker_url_remover.py @@ -0,0 +1,44 @@ +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2015 by Adam Tauber, +''' + +from flask_babel import gettext +import re +from searx.url_utils import urlunparse + +regexes = {re.compile(r'utm_[^&]+&?'), + re.compile(r'(wkey|wemail)[^&]+&?'), + re.compile(r'&$')} + +name = gettext('Tracker URL remover') +description = gettext('Remove trackers arguments from the returned URL') +default_on = True + + +def on_result(request, search, result): + query = result['parsed_url'].query + + if query == "": + return True + + for reg in regexes: + query = reg.sub('', query) + + if query != result['parsed_url'].query: + result['parsed_url'] = result['parsed_url']._replace(query=query) + result['url'] = urlunparse(result['parsed_url']) + + return True diff --git a/searx/plugins/vim_hotkeys.py b/searx/plugins/vim_hotkeys.py new file mode 100644 index 0000000..8f06f13 --- /dev/null +++ b/searx/plugins/vim_hotkeys.py @@ -0,0 +1,10 @@ +from flask_babel import gettext + +name = gettext('Vim-like hotkeys') +description = gettext('Navigate search results with Vim-like hotkeys ' + '(JavaScript required). ' + 'Press "h" key on main or result page to get help.') +default_on = False + +js_dependencies = ('plugins/js/vim_hotkeys.js',) +css_dependencies = ('plugins/css/vim_hotkeys.css',) diff --git a/searx/poolrequests.py b/searx/poolrequests.py new file mode 100644 index 0000000..f268df2 --- /dev/null +++ b/searx/poolrequests.py @@ -0,0 +1,112 @@ +import requests + +from itertools import cycle +from threading import RLock +from searx import settings + + +class HTTPAdapterWithConnParams(requests.adapters.HTTPAdapter): + + def __init__(self, pool_connections=requests.adapters.DEFAULT_POOLSIZE, + pool_maxsize=requests.adapters.DEFAULT_POOLSIZE, + max_retries=requests.adapters.DEFAULT_RETRIES, + pool_block=requests.adapters.DEFAULT_POOLBLOCK, + **conn_params): + if max_retries == requests.adapters.DEFAULT_RETRIES: + self.max_retries = requests.adapters.Retry(0, read=False) + else: + self.max_retries = requests.adapters.Retry.from_int(max_retries) + self.config = {} + self.proxy_manager = {} + + super(requests.adapters.HTTPAdapter, self).__init__() + + self._pool_connections = pool_connections + self._pool_maxsize = pool_maxsize + self._pool_block = pool_block + self._conn_params = conn_params + + self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block, **conn_params) + + def __setstate__(self, state): + # Can't handle by adding 'proxy_manager' to self.__attrs__ because + # because self.poolmanager uses a lambda function, which isn't pickleable. + self.proxy_manager = {} + self.config = {} + + for attr, value in state.items(): + setattr(self, attr, value) + + self.init_poolmanager(self._pool_connections, self._pool_maxsize, + block=self._pool_block, **self._conn_params) + + +connect = settings['outgoing'].get('pool_connections', 100) # Magic number kept from previous code +maxsize = settings['outgoing'].get('pool_maxsize', requests.adapters.DEFAULT_POOLSIZE) # Picked from constructor +if settings['outgoing'].get('source_ips'): + http_adapters = cycle(HTTPAdapterWithConnParams(pool_connections=connect, pool_maxsize=maxsize, + source_address=(source_ip, 0)) + for source_ip in settings['outgoing']['source_ips']) + https_adapters = cycle(HTTPAdapterWithConnParams(pool_connections=connect, pool_maxsize=maxsize, + source_address=(source_ip, 0)) + for source_ip in settings['outgoing']['source_ips']) +else: + http_adapters = cycle((HTTPAdapterWithConnParams(pool_connections=connect, pool_maxsize=maxsize), )) + https_adapters = cycle((HTTPAdapterWithConnParams(pool_connections=connect, pool_maxsize=maxsize), )) + + +class SessionSinglePool(requests.Session): + + def __init__(self): + super(SessionSinglePool, self).__init__() + + # reuse the same adapters + with RLock(): + self.adapters.clear() + self.mount('https://', next(https_adapters)) + self.mount('http://', next(http_adapters)) + + def close(self): + """Call super, but clear adapters since there are managed globaly""" + self.adapters.clear() + super(SessionSinglePool, self).close() + + +def request(method, url, **kwargs): + """same as requests/requests/api.py request(...) except it use SessionSinglePool and force proxies""" + session = SessionSinglePool() + kwargs['proxies'] = settings['outgoing'].get('proxies', None) + response = session.request(method=method, url=url, **kwargs) + session.close() + return response + + +def get(url, **kwargs): + kwargs.setdefault('allow_redirects', True) + return request('get', url, **kwargs) + + +def options(url, **kwargs): + kwargs.setdefault('allow_redirects', True) + return request('options', url, **kwargs) + + +def head(url, **kwargs): + kwargs.setdefault('allow_redirects', False) + return request('head', url, **kwargs) + + +def post(url, data=None, **kwargs): + return request('post', url, data=data, **kwargs) + + +def put(url, data=None, **kwargs): + return request('put', url, data=data, **kwargs) + + +def patch(url, data=None, **kwargs): + return request('patch', url, data=data, **kwargs) + + +def delete(url, **kwargs): + return request('delete', url, **kwargs) diff --git a/searx/preferences.py b/searx/preferences.py new file mode 100644 index 0000000..b6a2ec4 --- /dev/null +++ b/searx/preferences.py @@ -0,0 +1,304 @@ +from searx import settings, autocomplete +from searx.languages import language_codes as languages + + +COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5 # 5 years +LANGUAGE_CODES = [l[0] for l in languages] +LANGUAGE_CODES.append('all') +DISABLED = 0 +ENABLED = 1 + + +class MissingArgumentException(Exception): + pass + + +class ValidationException(Exception): + pass + + +class Setting(object): + """Base class of user settings""" + + def __init__(self, default_value, **kwargs): + super(Setting, self).__init__() + self.value = default_value + for key, value in kwargs.items(): + setattr(self, key, value) + + self._post_init() + + def _post_init(self): + pass + + def parse(self, data): + self.value = data + + def get_value(self): + return self.value + + def save(self, name, resp): + resp.set_cookie(name, self.value, max_age=COOKIE_MAX_AGE) + + +class StringSetting(Setting): + """Setting of plain string values""" + pass + + +class EnumStringSetting(Setting): + """Setting of a value which can only come from the given choices""" + + def _validate_selection(self, selection): + if selection not in self.choices: + raise ValidationException('Invalid value: "{0}"'.format(selection)) + + def _post_init(self): + if not hasattr(self, 'choices'): + raise MissingArgumentException('Missing argument: choices') + self._validate_selection(self.value) + + def parse(self, data): + self._validate_selection(data) + self.value = data + + +class MultipleChoiceSetting(EnumStringSetting): + """Setting of values which can only come from the given choices""" + + def _validate_selections(self, selections): + for item in selections: + if item not in self.choices: + raise ValidationException('Invalid value: "{0}"'.format(selections)) + + def _post_init(self): + if not hasattr(self, 'choices'): + raise MissingArgumentException('Missing argument: choices') + self._validate_selections(self.value) + + def parse(self, data): + if data == '': + self.value = [] + return + + elements = data.split(',') + self._validate_selections(elements) + self.value = elements + + def parse_form(self, data): + self.value = [] + for choice in data: + if choice in self.choices and choice not in self.value: + self.value.append(choice) + + def save(self, name, resp): + resp.set_cookie(name, ','.join(self.value), max_age=COOKIE_MAX_AGE) + + +class SearchLanguageSetting(EnumStringSetting): + """Available choices may change, so user's value may not be in choices anymore""" + + def parse(self, data): + if data not in self.choices and data != self.value: + # hack to give some backwards compatibility with old language cookies + data = str(data).replace('_', '-') + lang = data.split('-')[0] + if data in self.choices: + pass + elif lang in self.choices: + data = lang + elif data == 'nb-NO': + data = 'no-NO' + elif data == 'ar-XA': + data = 'ar-SA' + else: + data = self.value + self.value = data + + +class MapSetting(Setting): + """Setting of a value that has to be translated in order to be storable""" + + def _post_init(self): + if not hasattr(self, 'map'): + raise MissingArgumentException('missing argument: map') + if self.value not in self.map.values(): + raise ValidationException('Invalid default value') + + def parse(self, data): + if data not in self.map: + raise ValidationException('Invalid choice: {0}'.format(data)) + self.value = self.map[data] + self.key = data + + def save(self, name, resp): + if hasattr(self, 'key'): + resp.set_cookie(name, self.key, max_age=COOKIE_MAX_AGE) + + +class SwitchableSetting(Setting): + """ Base class for settings that can be turned on && off""" + + def _post_init(self): + self.disabled = set() + self.enabled = set() + if not hasattr(self, 'choices'): + raise MissingArgumentException('missing argument: choices') + + def transform_form_items(self, items): + return items + + def transform_values(self, values): + return values + + def parse_cookie(self, data): + if data[DISABLED] != '': + self.disabled = set(data[DISABLED].split(',')) + if data[ENABLED] != '': + self.enabled = set(data[ENABLED].split(',')) + + def parse_form(self, items): + items = self.transform_form_items(items) + + self.disabled = set() + self.enabled = set() + for choice in self.choices: + if choice['default_on']: + if choice['id'] in items: + self.disabled.add(choice['id']) + else: + if choice['id'] not in items: + self.enabled.add(choice['id']) + + def save(self, resp): + resp.set_cookie('disabled_{0}'.format(self.value), ','.join(self.disabled), max_age=COOKIE_MAX_AGE) + resp.set_cookie('enabled_{0}'.format(self.value), ','.join(self.enabled), max_age=COOKIE_MAX_AGE) + + def get_disabled(self): + disabled = self.disabled + for choice in self.choices: + if not choice['default_on'] and choice['id'] not in self.enabled: + disabled.add(choice['id']) + return self.transform_values(disabled) + + def get_enabled(self): + enabled = self.enabled + for choice in self.choices: + if choice['default_on'] and choice['id'] not in self.disabled: + enabled.add(choice['id']) + return self.transform_values(enabled) + + +class EnginesSetting(SwitchableSetting): + + def _post_init(self): + super(EnginesSetting, self)._post_init() + transformed_choices = [] + for engine_name, engine in self.choices.items(): + for category in engine.categories: + transformed_choice = dict() + transformed_choice['default_on'] = not engine.disabled + transformed_choice['id'] = '{}__{}'.format(engine_name, category) + transformed_choices.append(transformed_choice) + self.choices = transformed_choices + + def transform_form_items(self, items): + return [item[len('engine_'):].replace('_', ' ').replace(' ', '__') for item in items] + + def transform_values(self, values): + if len(values) == 1 and next(iter(values)) == '': + return list() + transformed_values = [] + for value in values: + engine, category = value.split('__') + transformed_values.append((engine, category)) + return transformed_values + + +class PluginsSetting(SwitchableSetting): + + def _post_init(self): + super(PluginsSetting, self)._post_init() + transformed_choices = [] + for plugin in self.choices: + transformed_choice = dict() + transformed_choice['default_on'] = plugin.default_on + transformed_choice['id'] = plugin.id + transformed_choices.append(transformed_choice) + self.choices = transformed_choices + + def transform_form_items(self, items): + return [item[len('plugin_'):] for item in items] + + +class Preferences(object): + """Stores, validates and saves preferences to cookies""" + + def __init__(self, themes, categories, engines, plugins): + super(Preferences, self).__init__() + + self.key_value_settings = {'categories': MultipleChoiceSetting(['general'], choices=categories), + 'language': SearchLanguageSetting(settings['search']['language'], + choices=LANGUAGE_CODES), + 'locale': EnumStringSetting(settings['ui']['default_locale'], + choices=list(settings['locales'].keys()) + ['']), + 'autocomplete': EnumStringSetting(settings['search']['autocomplete'], + choices=list(autocomplete.backends.keys()) + ['']), + 'image_proxy': MapSetting(settings['server']['image_proxy'], + map={'': settings['server']['image_proxy'], + '0': False, + '1': True}), + 'method': EnumStringSetting('POST', choices=('GET', 'POST')), + 'safesearch': MapSetting(settings['search']['safe_search'], map={'0': 0, + '1': 1, + '2': 2}), + 'theme': EnumStringSetting(settings['ui']['default_theme'], choices=themes), + 'results_on_new_tab': MapSetting(False, map={'0': False, '1': True})} + + self.engines = EnginesSetting('engines', choices=engines) + self.plugins = PluginsSetting('plugins', choices=plugins) + self.unknown_params = {} + + def parse_cookies(self, input_data): + for user_setting_name, user_setting in input_data.items(): + if user_setting_name in self.key_value_settings: + self.key_value_settings[user_setting_name].parse(user_setting) + elif user_setting_name == 'disabled_engines': + self.engines.parse_cookie((input_data.get('disabled_engines', ''), + input_data.get('enabled_engines', ''))) + elif user_setting_name == 'disabled_plugins': + self.plugins.parse_cookie((input_data.get('disabled_plugins', ''), + input_data.get('enabled_plugins', ''))) + + def parse_form(self, input_data): + disabled_engines = [] + enabled_categories = [] + disabled_plugins = [] + for user_setting_name, user_setting in input_data.items(): + if user_setting_name in self.key_value_settings: + self.key_value_settings[user_setting_name].parse(user_setting) + elif user_setting_name.startswith('engine_'): + disabled_engines.append(user_setting_name) + elif user_setting_name.startswith('category_'): + enabled_categories.append(user_setting_name[len('category_'):]) + elif user_setting_name.startswith('plugin_'): + disabled_plugins.append(user_setting_name) + else: + self.unknown_params[user_setting_name] = user_setting + self.key_value_settings['categories'].parse_form(enabled_categories) + self.engines.parse_form(disabled_engines) + self.plugins.parse_form(disabled_plugins) + + # cannot be used in case of engines or plugins + def get_value(self, user_setting_name): + if user_setting_name in self.key_value_settings: + return self.key_value_settings[user_setting_name].get_value() + + def save(self, resp): + for user_setting_name, user_setting in self.key_value_settings.items(): + user_setting.save(user_setting_name, resp) + self.engines.save(resp) + self.plugins.save(resp) + for k, v in self.unknown_params.items(): + resp.set_cookie(k, v, max_age=COOKIE_MAX_AGE) + return resp diff --git a/searx/query.py b/searx/query.py new file mode 100644 index 0000000..828a6fb --- /dev/null +++ b/searx/query.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python + +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2014 by Thomas Pointhuber, +''' + +from searx.languages import language_codes +from searx.engines import ( + categories, engines, engine_shortcuts +) +import re +import string +import sys + +if sys.version_info[0] == 3: + unicode = str + +VALID_LANGUAGE_CODE = re.compile(r'^[a-z]{2,3}(-[a-zA-Z]{2})?$') + + +class RawTextQuery(object): + """parse raw text query (the value from the html input)""" + + def __init__(self, query, disabled_engines): + self.query = query + self.disabled_engines = [] + + if disabled_engines: + self.disabled_engines = disabled_engines + + self.query_parts = [] + self.engines = [] + self.languages = [] + self.specific = False + + # parse query, if tags are set, which + # change the serch engine or search-language + def parse_query(self): + self.query_parts = [] + + # split query, including whitespaces + raw_query_parts = re.split(r'(\s+)', self.query) + + parse_next = True + + for query_part in raw_query_parts: + if not parse_next: + self.query_parts[-1] += query_part + continue + + parse_next = False + + # part does only contain spaces, skip + if query_part.isspace()\ + or query_part == '': + parse_next = True + self.query_parts.append(query_part) + continue + + # this force a language + if query_part[0] == ':': + lang = query_part[1:].lower().replace('_', '-') + + # user may set a valid, yet not selectable language + if VALID_LANGUAGE_CODE.match(lang): + self.languages.append(lang) + parse_next = True + + # check if any language-code is equal with + # declared language-codes + for lc in language_codes: + lang_id, lang_name, country, english_name = map(unicode.lower, lc) + + # if correct language-code is found + # set it as new search-language + if lang == lang_id\ + or lang_id.startswith(lang)\ + or lang == lang_name\ + or lang == english_name\ + or lang.replace('-', ' ') == country: + parse_next = True + self.languages.append(lang_id) + # to ensure best match (first match is not necessarily the best one) + if lang == lang_id: + break + + # this force a engine or category + if query_part[0] == '!' or query_part[0] == '?': + prefix = query_part[1:].replace('-', ' ').replace('_', ' ') + + # check if prefix is equal with engine shortcut + if prefix in engine_shortcuts: + parse_next = True + self.engines.append({'category': 'none', + 'name': engine_shortcuts[prefix]}) + + # check if prefix is equal with engine name + elif prefix in engines: + parse_next = True + self.engines.append({'category': 'none', + 'name': prefix}) + + # check if prefix is equal with categorie name + elif prefix in categories: + # using all engines for that search, which + # are declared under that categorie name + parse_next = True + self.engines.extend({'category': prefix, + 'name': engine.name} + for engine in categories[prefix] + if (engine.name, prefix) not in self.disabled_engines) + + if query_part[0] == '!': + self.specific = True + + # append query part to query_part list + self.query_parts.append(query_part) + + def changeSearchQuery(self, search_query): + if len(self.query_parts): + self.query_parts[-1] = search_query + else: + self.query_parts.append(search_query) + + def getSearchQuery(self): + if len(self.query_parts): + return self.query_parts[-1] + else: + return '' + + def getFullQuery(self): + # get full querry including whitespaces + return string.join(self.query_parts, '') + + +class SearchQuery(object): + """container for all the search parameters (query, language, etc...)""" + + def __init__(self, query, engines, categories, lang, safesearch, pageno, time_range): + self.query = query.encode('utf-8') + self.engines = engines + self.categories = categories + self.lang = lang + self.safesearch = safesearch + self.pageno = pageno + self.time_range = time_range + + def __str__(self): + return str(self.query) + ";" + str(self.engines) diff --git a/searx/results.py b/searx/results.py new file mode 100644 index 0000000..6abffb5 --- /dev/null +++ b/searx/results.py @@ -0,0 +1,306 @@ +import re +import sys +from collections import defaultdict +from operator import itemgetter +from threading import RLock +from searx.engines import engines +from searx.url_utils import urlparse, unquote + +if sys.version_info[0] == 3: + basestring = str + +CONTENT_LEN_IGNORED_CHARS_REGEX = re.compile(r'[,;:!?\./\\\\ ()-_]', re.M | re.U) +WHITESPACE_REGEX = re.compile('( |\t|\n)+', re.M | re.U) + + +# return the meaningful length of the content for a result +def result_content_len(content): + if isinstance(content, basestring): + return len(CONTENT_LEN_IGNORED_CHARS_REGEX.sub('', content)) + else: + return 0 + + +def compare_urls(url_a, url_b): + # ignore www. in comparison + if url_a.netloc.startswith('www.'): + host_a = url_a.netloc.replace('www.', '', 1) + else: + host_a = url_a.netloc + if url_b.netloc.startswith('www.'): + host_b = url_b.netloc.replace('www.', '', 1) + else: + host_b = url_b.netloc + + if host_a != host_b or url_a.query != url_b.query or url_a.fragment != url_b.fragment: + return False + + # remove / from the end of the url if required + path_a = url_a.path[:-1]\ + if url_a.path.endswith('/')\ + else url_a.path + path_b = url_b.path[:-1]\ + if url_b.path.endswith('/')\ + else url_b.path + + return unquote(path_a) == unquote(path_b) + + +def merge_two_infoboxes(infobox1, infobox2): + # get engines weights + if hasattr(engines[infobox1['engine']], 'weight'): + weight1 = engines[infobox1['engine']].weight + else: + weight1 = 1 + if hasattr(engines[infobox2['engine']], 'weight'): + weight2 = engines[infobox2['engine']].weight + else: + weight2 = 1 + + if weight2 > weight1: + infobox1['engine'] = infobox2['engine'] + + if 'urls' in infobox2: + urls1 = infobox1.get('urls', None) + if urls1 is None: + urls1 = [] + + for url2 in infobox2.get('urls', []): + unique_url = True + for url1 in infobox1.get('urls', []): + if compare_urls(urlparse(url1.get('url', '')), urlparse(url2.get('url', ''))): + unique_url = False + break + if unique_url: + urls1.append(url2) + + infobox1['urls'] = urls1 + + if 'img_src' in infobox2: + img1 = infobox1.get('img_src', None) + img2 = infobox2.get('img_src') + if img1 is None: + infobox1['img_src'] = img2 + elif weight2 > weight1: + infobox1['img_src'] = img2 + + if 'attributes' in infobox2: + attributes1 = infobox1.get('attributes', None) + if attributes1 is None: + attributes1 = [] + infobox1['attributes'] = attributes1 + + attributeSet = set() + for attribute in infobox1.get('attributes', []): + if attribute.get('label', None) not in attributeSet: + attributeSet.add(attribute.get('label', None)) + + for attribute in infobox2.get('attributes', []): + if attribute.get('label', None) not in attributeSet: + attributes1.append(attribute) + + if 'content' in infobox2: + content1 = infobox1.get('content', None) + content2 = infobox2.get('content', '') + if content1 is not None: + if result_content_len(content2) > result_content_len(content1): + infobox1['content'] = content2 + else: + infobox1['content'] = content2 + + +def result_score(result): + weight = 1.0 + + for result_engine in result['engines']: + if hasattr(engines[result_engine], 'weight'): + weight *= float(engines[result_engine].weight) + + occurences = len(result['positions']) + + return sum((occurences * weight) / position for position in result['positions']) + + +class ResultContainer(object): + """docstring for ResultContainer""" + + def __init__(self): + super(ResultContainer, self).__init__() + self.results = defaultdict(list) + self._merged_results = [] + self.infoboxes = [] + self.suggestions = set() + self.answers = set() + self.corrections = set() + self._number_of_results = [] + self._ordered = False + self.paging = False + + def extend(self, engine_name, results): + for result in list(results): + result['engine'] = engine_name + if 'suggestion' in result: + self.suggestions.add(result['suggestion']) + results.remove(result) + elif 'answer' in result: + self.answers.add(result['answer']) + results.remove(result) + elif 'correction' in result: + self.corrections.add(result['correction']) + results.remove(result) + elif 'infobox' in result: + self._merge_infobox(result) + results.remove(result) + elif 'number_of_results' in result: + self._number_of_results.append(result['number_of_results']) + results.remove(result) + + if engine_name in engines: + with RLock(): + engines[engine_name].stats['search_count'] += 1 + engines[engine_name].stats['result_count'] += len(results) + + if not results: + return + + self.results[engine_name].extend(results) + + if not self.paging and engine_name in engines and engines[engine_name].paging: + self.paging = True + + for i, result in enumerate(results): + try: + result['url'] = result['url'].decode('utf-8') + except: + pass + position = i + 1 + self._merge_result(result, position) + + def _merge_infobox(self, infobox): + add_infobox = True + infobox_id = infobox.get('id', None) + if infobox_id is not None: + for existingIndex in self.infoboxes: + if compare_urls(urlparse(existingIndex.get('id', '')), urlparse(infobox_id)): + merge_two_infoboxes(existingIndex, infobox) + add_infobox = False + + if add_infobox: + self.infoboxes.append(infobox) + + def _merge_result(self, result, position): + result['parsed_url'] = urlparse(result['url']) + + # if the result has no scheme, use http as default + if not result['parsed_url'].scheme: + result['parsed_url'] = result['parsed_url']._replace(scheme="http") + result['url'] = result['parsed_url'].geturl() + + result['engines'] = [result['engine']] + + # strip multiple spaces and cariage returns from content + if result.get('content'): + result['content'] = WHITESPACE_REGEX.sub(' ', result['content']) + + # check for duplicates + duplicated = False + for merged_result in self._merged_results: + if compare_urls(result['parsed_url'], merged_result['parsed_url'])\ + and result.get('template') == merged_result.get('template'): + duplicated = merged_result + break + + # merge duplicates together + if duplicated: + # using content with more text + if result_content_len(result.get('content', '')) >\ + result_content_len(duplicated.get('content', '')): + duplicated['content'] = result['content'] + + # add the new position + duplicated['positions'].append(position) + + # add engine to list of result-engines + duplicated['engines'].append(result['engine']) + + # using https if possible + if duplicated['parsed_url'].scheme != 'https' and result['parsed_url'].scheme == 'https': + duplicated['url'] = result['parsed_url'].geturl() + duplicated['parsed_url'] = result['parsed_url'] + + # if there is no duplicate found, append result + else: + result['positions'] = [position] + with RLock(): + self._merged_results.append(result) + + def order_results(self): + for result in self._merged_results: + score = result_score(result) + result['score'] = score + with RLock(): + for result_engine in result['engines']: + engines[result_engine].stats['score_count'] += score + + results = sorted(self._merged_results, key=itemgetter('score'), reverse=True) + + # pass 2 : group results by category and template + gresults = [] + categoryPositions = {} + + for i, res in enumerate(results): + # FIXME : handle more than one category per engine + res['category'] = engines[res['engine']].categories[0] + + # FIXME : handle more than one category per engine + category = engines[res['engine']].categories[0]\ + + ':' + res.get('template', '')\ + + ':' + ('img_src' if 'img_src' in res or 'thumbnail' in res else '') + + current = None if category not in categoryPositions\ + else categoryPositions[category] + + # group with previous results using the same category + # if the group can accept more result and is not too far + # from the current position + if current is not None and (current['count'] > 0)\ + and (len(gresults) - current['index'] < 20): + # group with the previous results using + # the same category with this one + index = current['index'] + gresults.insert(index, res) + + # update every index after the current one + # (including the current one) + for k in categoryPositions: + v = categoryPositions[k]['index'] + if v >= index: + categoryPositions[k]['index'] = v + 1 + + # update this category + current['count'] -= 1 + + else: + # same category + gresults.append(res) + + # update categoryIndex + categoryPositions[category] = {'index': len(gresults), 'count': 8} + + # update _merged_results + self._ordered = True + self._merged_results = gresults + + def get_ordered_results(self): + if not self._ordered: + self.order_results() + return self._merged_results + + def results_length(self): + return len(self._merged_results) + + def results_number(self): + resultnum_sum = sum(self._number_of_results) + if not resultnum_sum or not self._number_of_results: + return 0 + return resultnum_sum / len(self._number_of_results) diff --git a/searx/search.py b/searx/search.py new file mode 100644 index 0000000..790e7d0 --- /dev/null +++ b/searx/search.py @@ -0,0 +1,432 @@ +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2013- by Adam Tauber, +''' + +import gc +import sys +import threading +from time import time +from uuid import uuid4 +import requests.exceptions +import searx.poolrequests as requests_lib +from searx.engines import ( + categories, engines +) +from searx.answerers import ask +from searx.utils import gen_useragent +from searx.query import RawTextQuery, SearchQuery, VALID_LANGUAGE_CODE +from searx.results import ResultContainer +from searx import logger +from searx.plugins import plugins +from searx.exceptions import SearxParameterException + +try: + from thread import start_new_thread +except: + from _thread import start_new_thread + +if sys.version_info[0] == 3: + unicode = str + +logger = logger.getChild('search') + +number_of_searches = 0 + + +def send_http_request(engine, request_params, start_time, timeout_limit): + # for page_load_time stats + time_before_request = time() + + # create dictionary which contain all + # informations about the request + request_args = dict( + headers=request_params['headers'], + cookies=request_params['cookies'], + timeout=timeout_limit, + verify=request_params['verify'] + ) + + # specific type of request (GET or POST) + if request_params['method'] == 'GET': + req = requests_lib.get + else: + req = requests_lib.post + request_args['data'] = request_params['data'] + + # send the request + response = req(request_params['url'], **request_args) + + # is there a timeout (no parsing in this case) + timeout_overhead = 0.2 # seconds + time_after_request = time() + search_duration = time_after_request - start_time + if search_duration > timeout_limit + timeout_overhead: + raise requests.exceptions.Timeout(response=response) + + with threading.RLock(): + # no error : reset the suspend variables + engine.continuous_errors = 0 + engine.suspend_end_time = 0 + # update stats with current page-load-time + # only the HTTP request + engine.stats['page_load_time'] += time_after_request - time_before_request + engine.stats['page_load_count'] += 1 + + # everything is ok : return the response + return response + + +def search_one_request(engine, query, request_params, start_time, timeout_limit): + # update request parameters dependent on + # search-engine (contained in engines folder) + engine.request(query, request_params) + + # ignoring empty urls + if request_params['url'] is None: + return [] + + if not request_params['url']: + return [] + + # send request + response = send_http_request(engine, request_params, start_time, timeout_limit) + + # parse the response + response.search_params = request_params + return engine.response(response) + + +def search_one_request_safe(engine_name, query, request_params, result_container, start_time, timeout_limit): + engine = engines[engine_name] + + try: + # send requests and parse the results + search_results = search_one_request(engine, query, request_params, start_time, timeout_limit) + + # add results + result_container.extend(engine_name, search_results) + + # update engine time when there is no exception + with threading.RLock(): + engine.stats['engine_time'] += time() - start_time + engine.stats['engine_time_count'] += 1 + + return True + + except Exception as e: + engine.stats['errors'] += 1 + + search_duration = time() - start_time + requests_exception = False + + if (issubclass(e.__class__, requests.exceptions.Timeout)): + # requests timeout (connect or read) + logger.error("engine {0} : HTTP requests timeout" + "(search duration : {1} s, timeout: {2} s) : {3}" + .format(engine_name, search_duration, timeout_limit, e.__class__.__name__)) + requests_exception = True + elif (issubclass(e.__class__, requests.exceptions.RequestException)): + # other requests exception + logger.exception("engine {0} : requests exception" + "(search duration : {1} s, timeout: {2} s) : {3}" + .format(engine_name, search_duration, timeout_limit, e)) + requests_exception = True + else: + # others errors + logger.exception('engine {0} : exception : {1}'.format(engine_name, e)) + + # update continuous_errors / suspend_end_time + if requests_exception: + with threading.RLock(): + engine.continuous_errors += 1 + engine.suspend_end_time = time() + min(60, engine.continuous_errors) + + # + return False + + +def search_multiple_requests(requests, result_container, start_time, timeout_limit): + search_id = uuid4().__str__() + + for engine_name, query, request_params in requests: + th = threading.Thread( + target=search_one_request_safe, + args=(engine_name, query, request_params, result_container, start_time, timeout_limit), + name=search_id, + ) + th._engine_name = engine_name + th.start() + + for th in threading.enumerate(): + if th.name == search_id: + remaining_time = max(0.0, timeout_limit - (time() - start_time)) + th.join(remaining_time) + if th.isAlive(): + logger.warning('engine timeout: {0}'.format(th._engine_name)) + + +# get default reqest parameter +def default_request_params(): + return { + 'method': 'GET', + 'headers': {}, + 'data': {}, + 'url': '', + 'cookies': {}, + 'verify': True + } + + +def get_search_query_from_webapp(preferences, form): + # no text for the query ? + if not form.get('q'): + raise SearxParameterException('q', '') + + # set blocked engines + disabled_engines = preferences.engines.get_disabled() + + # parse query, if tags are set, which change + # the serch engine or search-language + raw_text_query = RawTextQuery(form['q'], disabled_engines) + raw_text_query.parse_query() + + # set query + query = raw_text_query.getSearchQuery() + + # get and check page number + pageno_param = form.get('pageno', '1') + if not pageno_param.isdigit() or int(pageno_param) < 1: + raise SearxParameterException('pageno', pageno_param) + query_pageno = int(pageno_param) + + # get language + # set specific language if set on request, query or preferences + # TODO support search with multible languages + if len(raw_text_query.languages): + query_lang = raw_text_query.languages[-1] + elif 'language' in form: + query_lang = form.get('language') + else: + query_lang = preferences.get_value('language') + + # check language + if not VALID_LANGUAGE_CODE.match(query_lang): + raise SearxParameterException('language', query_lang) + + # get safesearch + if 'safesearch' in form: + query_safesearch = form.get('safesearch') + # first check safesearch + if not query_safesearch.isdigit(): + raise SearxParameterException('safesearch', query_safesearch) + query_safesearch = int(query_safesearch) + else: + query_safesearch = preferences.get_value('safesearch') + + # safesearch : second check + if query_safesearch < 0 or query_safesearch > 2: + raise SearxParameterException('safesearch', query_safesearch) + + # get time_range + query_time_range = form.get('time_range') + + # check time_range + if query_time_range not in ('None', None, '', 'day', 'week', 'month', 'year'): + raise SearxParameterException('time_range', query_time_range) + + # query_engines + query_engines = raw_text_query.engines + + # query_categories + query_categories = [] + + # if engines are calculated from query, + # set categories by using that informations + if query_engines and raw_text_query.specific: + query_categories = list(set(engine['category'] + for engine in query_engines)) + + # otherwise, using defined categories to + # calculate which engines should be used + else: + # set categories/engines + load_default_categories = True + for pd_name, pd in form.items(): + if pd_name == 'categories': + query_categories.extend(categ for categ in map(unicode.strip, pd.split(',')) if categ in categories) + elif pd_name == 'engines': + pd_engines = [{'category': engines[engine].categories[0], + 'name': engine} + for engine in map(unicode.strip, pd.split(',')) if engine in engines] + if pd_engines: + query_engines.extend(pd_engines) + load_default_categories = False + elif pd_name.startswith('category_'): + category = pd_name[9:] + + # if category is not found in list, skip + if category not in categories: + continue + + if pd != 'off': + # add category to list + query_categories.append(category) + elif category in query_categories: + # remove category from list if property is set to 'off' + query_categories.remove(category) + + if not load_default_categories: + if not query_categories: + query_categories = list(set(engine['category'] + for engine in query_engines)) + else: + # if no category is specified for this search, + # using user-defined default-configuration which + # (is stored in cookie) + if not query_categories: + cookie_categories = preferences.get_value('categories') + for ccateg in cookie_categories: + if ccateg in categories: + query_categories.append(ccateg) + + # if still no category is specified, using general + # as default-category + if not query_categories: + query_categories = ['general'] + + # using all engines for that search, which are + # declared under the specific categories + for categ in query_categories: + query_engines.extend({'category': categ, + 'name': engine.name} + for engine in categories[categ] + if (engine.name, categ) not in disabled_engines) + + return SearchQuery(query, query_engines, query_categories, + query_lang, query_safesearch, query_pageno, query_time_range) + + +class Search(object): + + """Search information container""" + + def __init__(self, search_query): + # init vars + super(Search, self).__init__() + self.search_query = search_query + self.result_container = ResultContainer() + + # do search-request + def search(self): + global number_of_searches + + # start time + start_time = time() + + # answeres ? + answerers_results = ask(self.search_query) + + if answerers_results: + for results in answerers_results: + self.result_container.extend('answer', results) + return self.result_container + + # init vars + requests = [] + + # increase number of searches + number_of_searches += 1 + + # set default useragent + # user_agent = request.headers.get('User-Agent', '') + user_agent = gen_useragent() + + search_query = self.search_query + + # max of all selected engine timeout + timeout_limit = 0 + + # start search-reqest for all selected engines + for selected_engine in search_query.engines: + if selected_engine['name'] not in engines: + continue + + engine = engines[selected_engine['name']] + + # skip suspended engines + if engine.suspend_end_time >= time(): + logger.debug('Engine currently suspended: %s', selected_engine['name']) + continue + + # if paging is not supported, skip + if search_query.pageno > 1 and not engine.paging: + continue + + # if time_range is not supported, skip + if search_query.time_range and not engine.time_range_support: + continue + + # set default request parameters + request_params = default_request_params() + request_params['headers']['User-Agent'] = user_agent + request_params['category'] = selected_engine['category'] + request_params['pageno'] = search_query.pageno + + if hasattr(engine, 'language') and engine.language: + request_params['language'] = engine.language + else: + request_params['language'] = search_query.lang + + # 0 = None, 1 = Moderate, 2 = Strict + request_params['safesearch'] = search_query.safesearch + request_params['time_range'] = search_query.time_range + + # append request to list + requests.append((selected_engine['name'], search_query.query, request_params)) + + # update timeout_limit + timeout_limit = max(timeout_limit, engine.timeout) + + if requests: + # send all search-request + search_multiple_requests(requests, self.result_container, start_time, timeout_limit) + start_new_thread(gc.collect, tuple()) + + # return results, suggestions, answers and infoboxes + return self.result_container + + +class SearchWithPlugins(Search): + + """Similar to the Search class but call the plugins.""" + + def __init__(self, search_query, ordered_plugin_list, request): + super(SearchWithPlugins, self).__init__(search_query) + self.ordered_plugin_list = ordered_plugin_list + self.request = request + + def search(self): + if plugins.call(self.ordered_plugin_list, 'pre_search', self.request, self): + super(SearchWithPlugins, self).search() + + plugins.call(self.ordered_plugin_list, 'post_search', self.request, self) + + results = self.result_container.get_ordered_results() + + for result in results: + plugins.call(self.ordered_plugin_list, 'on_result', self.request, self, result) + + return self.result_container diff --git a/searx/settings.yml b/searx/settings.yml new file mode 100644 index 0000000..17b0bd5 --- /dev/null +++ b/searx/settings.yml @@ -0,0 +1,686 @@ +general: + debug : False # Debug mode, only for development + instance_name : "searx" # displayed name + +search: + safe_search : 0 # Filter results. 0: None, 1: Moderate, 2: Strict + autocomplete : "" # Existing autocomplete backends: "dbpedia", "duckduckgo", "google", "startpage", "wikipedia" - leave blank to turn it off by default + language : "all" + +server: + port : 8888 + bind_address : "127.0.0.1" # address to listen on + secret_key : "ultrasecretkey" # change this! + base_url : False # Set custom base_url. Possible values: False or "https://your.custom.host/location/" + image_proxy : False # Proxying image results through searx + http_protocol_version : "1.0" # 1.0 and 1.1 are supported + +ui: + static_path : "" # Custom static path - leave it blank if you didn't change + templates_path : "" # Custom templates path - leave it blank if you didn't change + default_theme : oscar # ui theme + default_locale : "" # Default interface locale - leave blank to detect from browser information or use codes from the 'locales' config section + +# searx supports result proxification using an external service: https://github.com/asciimoo/morty +# uncomment below section if you have running morty proxy +#result_proxy: +# url : http://127.0.0.1:3000/ +# key : your_morty_proxy_key + +outgoing: # communication with search engines + request_timeout : 2.0 # seconds + useragent_suffix : "" # suffix of searx_useragent, could contain informations like an email address to the administrator + pool_connections : 100 # Number of different hosts + pool_maxsize : 10 # Number of simultaneous requests by host +# uncomment below section if you want to use a proxy +# see http://docs.python-requests.org/en/latest/user/advanced/#proxies +# SOCKS proxies are also supported: see http://docs.python-requests.org/en/master/user/advanced/#socks +# proxies : +# http : http://127.0.0.1:8080 +# https: http://127.0.0.1:8080 +# uncomment below section only if you have more than one network interface +# which can be the source of outgoing search requests +# source_ips: +# - 1.1.1.1 +# - 1.1.1.2 + +engines: + - name : arch linux wiki + engine : archlinux + shortcut : al + + - name : archive is + engine : xpath + search_url : https://archive.is/{query} + url_xpath : (//div[@class="TEXT-BLOCK"]/a)/@href + title_xpath : (//div[@class="TEXT-BLOCK"]/a) + content_xpath : //div[@class="TEXT-BLOCK"]/ul/li + categories : general + timeout : 7.0 + disabled : True + shortcut : ai + + - name : base + engine : base + shortcut : bs + + - name : wikipedia + engine : wikipedia + shortcut : wp + base_url : 'https://{language}.wikipedia.org/' + + - name : bing + engine : bing + shortcut : bi + + - name : bing images + engine : bing_images + shortcut : bii + + - name : bing news + engine : bing_news + shortcut : bin + + - name : bitbucket + engine : xpath + paging : True + search_url : https://bitbucket.org/repo/all/{pageno}?name={query} + url_xpath : //article[@class="repo-summary"]//a[@class="repo-link"]/@href + title_xpath : //article[@class="repo-summary"]//a[@class="repo-link"] + content_xpath : //article[@class="repo-summary"]/p + categories : it + timeout : 4.0 + disabled : True + shortcut : bb + + - name : ccc-tv + engine : xpath + paging : False + search_url : https://media.ccc.de/search/?q={query} + url_xpath : //div[@class="caption"]/h3/a/@href + title_xpath : //div[@class="caption"]/h3/a/text() + content_xpath : //div[@class="caption"]/h4/@title + categories : videos + disabled : True + shortcut : c3tv + + - name : crossref + engine : json_engine + paging : True + search_url : http://search.crossref.org/dois?q={query}&page={pageno} + url_query : doi + title_query : title + content_query : fullCitation + categories : science + shortcut : cr + + - name : currency + engine : currency_convert + categories : general + shortcut : cc + + - name : deezer + engine : deezer + shortcut : dz + + - name : deviantart + engine : deviantart + shortcut : da + timeout: 3.0 + + - name : ddg definitions + engine : duckduckgo_definitions + shortcut : ddd + weight : 2 + disabled : True + + - name : digbt + engine : digbt + shortcut : dbt + timeout : 6.0 + disabled : True + + - name : digg + engine : digg + shortcut : dg + + - name : erowid + engine : xpath + paging : True + first_page_num : 0 + page_size : 30 + search_url : https://www.erowid.org/search.php?q={query}&s={pageno} + url_xpath : //dl[@class="results-list"]/dt[@class="result-title"]/a/@href + title_xpath : //dl[@class="results-list"]/dt[@class="result-title"]/a/text() + content_xpath : //dl[@class="results-list"]/dd[@class="result-details"] + categories : general + shortcut : ew + disabled : True + + - name : wikidata + engine : wikidata + shortcut : wd + weight : 2 + + - name : duckduckgo + engine : duckduckgo + shortcut : ddg + disabled : True + + - name : duckduckgo images + engine : duckduckgo_images + shortcut : ddi + timeout: 3.0 + disabled : True + + - name : etymonline + engine : xpath + paging : True + search_url : http://etymonline.com/?search={query}&p={pageno} + url_xpath : //dt/a[1]/@href + title_xpath : //dt + content_xpath : //dd + suggestion_xpath : //a[@class="crossreference"] + first_page_num : 0 + shortcut : et + disabled : True + +# api-key required: http://www.faroo.com/hp/api/api.html#key +# - name : faroo +# engine : faroo +# shortcut : fa +# api_key : 'apikey' # required! + + - name : 500px + engine : www500px + shortcut : px + + - name : 1x + engine : www1x + shortcut : 1x + disabled : True + + - name : fdroid + engine : fdroid + shortcut : fd + disabled : True + + - name : flickr + categories : images + shortcut : fl +# You can use the engine using the official stable API, but you need an API key +# See : https://www.flickr.com/services/apps/create/ +# engine : flickr +# api_key: 'apikey' # required! +# Or you can use the html non-stable engine, activated by default + engine : flickr_noapi + + - name : free software directory + engine : mediawiki + shortcut : fsd + categories : it + base_url : https://directory.fsf.org/ + number_of_results : 5 +# what part of a page matches the query string: title, text, nearmatch +# title - query matches title, text - query matches the text of page, nearmatch - nearmatch in title + search_type : title + timeout : 5.0 + disabled : True + + - name : frinkiac + engine : frinkiac + shortcut : frk + disabled : True + + - name : gigablast + engine : gigablast + shortcut : gb + timeout : 3.0 + disabled: True + + - name : gitlab + engine : xpath + paging : True + search_url : https://gitlab.com/search?page={pageno}&search={query} + url_xpath : //li[@class="project-row"]//a[@class="project"]/@href + title_xpath : //li[@class="project-row"]//span[contains(@class, "project-full-name")] + content_xpath : //li[@class="project-row"]//div[@class="description"]/p + categories : it + shortcut : gl + timeout : 5.0 + disabled : True + + - name : github + engine : github + shortcut : gh + + - name : google + engine : google + shortcut : go + + - name : google images + engine : google_images + shortcut : goi + + - name : google news + engine : google_news + shortcut : gon + + - name : google scholar + engine : xpath + paging : True + search_url : https://scholar.google.com/scholar?start={pageno}&q={query}&hl=en&as_sdt=0,5&as_vis=1 + results_xpath : //div[@class="gs_r"]/div[@class="gs_ri"] + url_xpath : .//h3/a/@href + title_xpath : .//h3/a + content_xpath : .//div[@class="gs_rs"] + suggestion_xpath : //div[@id="gs_qsuggest"]/ul/li + page_size : 10 + first_page_num : 0 + categories : science + shortcut : gos + + - name : google play apps + engine : xpath + search_url : https://play.google.com/store/search?q={query}&c=apps + url_xpath : //a[@class="title"]/@href + title_xpath : //a[@class="title"] + content_xpath : //a[@class="subtitle"] + categories : files + shortcut : gpa + disabled : True + + - name : google play movies + engine : xpath + search_url : https://play.google.com/store/search?q={query}&c=movies + url_xpath : //a[@class="title"]/@href + title_xpath : //a[@class="title"]/@title + content_xpath : //a[contains(@class, "subtitle")] + categories : videos + shortcut : gpm + disabled : True + + - name : google play music + engine : xpath + search_url : https://play.google.com/store/search?q={query}&c=music + url_xpath : //a[@class="title"]/@href + title_xpath : //a[@class="title"] + content_xpath : //a[@class="subtitle"] + categories : music + shortcut : gps + disabled : True + + - name : geektimes + engine : xpath + paging : True + search_url : https://geektimes.ru/search/page{pageno}/?q={query} + url_xpath : //div[@class="search_results"]//a[@class="post__title_link"]/@href + title_xpath : //div[@class="search_results"]//a[@class="post__title_link"] + content_xpath : //div[@class="search_results"]//div[contains(@class, "content")] + categories : it + timeout : 4.0 + disabled : True + shortcut : gt + + - name : habrahabr + engine : xpath + paging : True + search_url : https://habrahabr.ru/search/page{pageno}/?q={query} + url_xpath : //div[@class="search_results"]//a[contains(@class, "post__title_link")]/@href + title_xpath : //div[@class="search_results"]//a[contains(@class, "post__title_link")] + content_xpath : //div[@class="search_results"]//div[contains(@class, "content")] + categories : it + timeout : 4.0 + disabled : True + shortcut : habr + + - name : hoogle + engine : json_engine + paging : True + search_url : https://www.haskell.org/hoogle/?mode=json&hoogle={query}&start={pageno} + results_query : results + url_query : location + title_query : self + content_query : docs + page_size : 20 + categories : it + shortcut : ho + + - name : ina + engine : ina + shortcut : in + timeout : 6.0 + disabled : True + + - name: kickass + engine : kickass + shortcut : kc + timeout : 4.0 + disabled : True + + - name : library genesis + engine : xpath + search_url : http://libgen.io/search.php?req={query} + url_xpath : //a[contains(@href,"bookfi.net")]/@href + title_xpath : //a[contains(@href,"book/")]/text()[1] + content_xpath : //td/a[1][contains(@href,"=author")]/text() + categories : general + timeout : 7.0 + disabled : True + shortcut : lg + + - name : lobste.rs + engine : xpath + search_url : https://lobste.rs/search?utf8=%E2%9C%93&q={query}&what=stories&order=relevance + results_xpath : //li[contains(@class, "story")] + url_xpath : .//span[@class="link"]/a/@href + title_xpath : .//span[@class="link"]/a + content_xpath : .//a[@class="domain"] + categories : it + shortcut : lo + + - name : microsoft academic + engine : json_engine + paging : True + search_url : https://academic.microsoft.com/api/search/GetEntityResults?query=%40{query}%40&filters=&offset={pageno}&limit=8&correlationId=undefined + results_query : results + url_query : u + title_query : dn + content_query : d + page_size : 8 + first_page_num : 0 + categories : science + shortcut : ma + + - name : mixcloud + engine : mixcloud + shortcut : mc + + - name : nyaa + engine : nyaa + shortcut : nt + disabled : True + + - name : openstreetmap + engine : openstreetmap + shortcut : osm + + - name : openrepos + engine : xpath + paging : True + search_url : https://openrepos.net/search/node/{query}?page={pageno} + url_xpath : //li[@class="search-result"]//h3[@class="title"]/a/@href + title_xpath : //li[@class="search-result"]//h3[@class="title"]/a + content_xpath : //li[@class="search-result"]//div[@class="search-snippet-info"]//p[@class="search-snippet"] + categories : files + timeout : 4.0 + disabled : True + shortcut : or + + - name : pdbe + engine : pdbe + shortcut : pdb +# Hide obsolete PDB entries. +# Default is not to hide obsolete structures +# hide_obsolete : False + + - name : photon + engine : photon + shortcut : ph + + - name : piratebay + engine : piratebay + shortcut : tpb + url: https://pirateproxy.red/ + timeout : 3.0 + + - name : qwant + engine : qwant + shortcut : qw + categories : general + disabled : True + + - name : qwant images + engine : qwant + shortcut : qwi + categories : images + + - name : qwant news + engine : qwant + shortcut : qwn + categories : news + + - name : qwant social + engine : qwant + shortcut : qws + categories : social media + + - name : reddit + engine : reddit + shortcut : re + page_size : 25 + timeout : 10.0 + disabled : True + + - name : scanr structures + shortcut: scs + engine : scanr_structures + disabled : True + + - name : soundcloud + engine : soundcloud + shortcut : sc + + - name : stackoverflow + engine : stackoverflow + shortcut : st + + - name : searchcode doc + engine : searchcode_doc + shortcut : scd + + - name : searchcode code + engine : searchcode_code + shortcut : scc + disabled : True + + - name : framalibre + engine : framalibre + shortcut : frl + disabled : True + +# - name : searx +# engine : searx_engine +# shortcut : se +# instance_urls : +# - http://127.0.0.1:8888/ +# - ... +# disabled : True + + - name : semantic scholar + engine : xpath + paging : True + search_url : https://www.semanticscholar.org/search?q={query}&sort=relevance&page={pageno}&ae=false + results_xpath : //article + url_xpath : .//div[@class="search-result-title"]/a/@href + title_xpath : .//div[@class="search-result-title"]/a + content_xpath : .//div[@class="search-result-abstract"] + shortcut : se + categories : science + + - name : spotify + engine : spotify + shortcut : stf + + - name : subtitleseeker + engine : subtitleseeker + shortcut : ss +# The language is an option. You can put any language written in english +# Examples : English, French, German, Hungarian, Chinese... +# language : English + + - name : startpage + engine : startpage + shortcut : sp + timeout : 6.0 + disabled : True + + - name : ixquick + engine : startpage + base_url : 'https://www.ixquick.eu/' + search_url : 'https://www.ixquick.eu/do/search' + shortcut : iq + timeout : 6.0 + disabled : True + + - name : swisscows + engine : swisscows + shortcut : sw + disabled : True + + - name : tokyotoshokan + engine : tokyotoshokan + shortcut : tt + timeout : 6.0 + disabled : True + + - name : twitter + engine : twitter + shortcut : tw + +# maybe in a fun category +# - name : uncyclopedia +# engine : mediawiki +# shortcut : unc +# base_url : https://uncyclopedia.wikia.com/ +# number_of_results : 5 + +# tmp suspended - too slow, too many errors +# - name : urbandictionary +# engine : xpath +# search_url : http://www.urbandictionary.com/define.php?term={query} +# url_xpath : //*[@class="word"]/@href +# title_xpath : //*[@class="def-header"] +# content_xpath : //*[@class="meaning"] +# shortcut : ud + + - name : yahoo + engine : yahoo + shortcut : yh + + - name : yandex + engine : yandex + shortcut : yn + disabled : True + + - name : yahoo news + engine : yahoo_news + shortcut : yhn + + - name : youtube + shortcut : yt + # You can use the engine using the official stable API, but you need an API key + # See : https://console.developers.google.com/project + # engine : youtube_api + # api_key: 'apikey' # required! + # Or you can use the html non-stable engine, activated by default + engine : youtube_noapi + + - name : dailymotion + engine : dailymotion + shortcut : dm + + - name : vimeo + engine : vimeo + shortcut : vm + + - name : wolframalpha + shortcut : wa + # You can use the engine using the official stable API, but you need an API key + # See : http://products.wolframalpha.com/api/ + # engine : wolframalpha_api + # api_key: '' # required! + engine : wolframalpha_noapi + timeout: 6.0 + categories : science + + - name : seedpeer + engine : seedpeer + shortcut: speu + categories: files, music, videos + disabled: True + + - name : dictzone + engine : dictzone + shortcut : dc + + - name : mymemory translated + engine : translated + shortcut : tl + timeout : 5.0 + disabled : True + # You can use without an API key, but you are limited to 1000 words/day + # See : http://mymemory.translated.net/doc/usagelimits.php + # api_key : '' + + - name : voat + engine: xpath + shortcut: vo + categories: social media + search_url : https://voat.co/search?q={query} + url_xpath : //p[contains(@class, "title")]/a/@href + title_xpath : //p[contains(@class, "title")]/a + content_xpath : //span[@class="domain"] + timeout : 10.0 + disabled : True + + - name : 1337x + engine : 1337x + shortcut : 1337x + disabled : True + +#The blekko technology and team have joined IBM Watson! -> https://blekko.com/ +# - name : blekko images +# engine : blekko_images +# locale : en-US +# shortcut : bli + +# - name : yacy +# engine : yacy +# shortcut : ya +# base_url : 'http://localhost:8090' +# number_of_results : 5 +# timeout : 3.0 + +# Doku engine lets you access to any Doku wiki instance: +# A public one or a privete/corporate one. +# - name : ubuntuwiki +# engine : doku +# shortcut : uw +# base_url : 'http://doc.ubuntu-fr.org' + +locales: + en : English + bg : Български (Bulgarian) + cs : Čeština (Czech) + de : Deutsch (German) + de_DE : Deutsch (German_Germany) + el_GR : Ελληνικά (Greek_Greece) + eo : Esperanto (Esperanto) + es : Español (Spanish) + fi : Suomi (Finnish) + fr : Français (French) + he : עברית (Hebrew) + hu : Magyar (Hungarian) + it : Italiano (Italian) + ja : 日本語 (Japanese) + nl : Nederlands (Dutch) + pt : Português (Portuguese) + pt_BR : Português (Portuguese_Brazil) + ro : Română (Romanian) + ru : Русский (Russian) + sk : Slovenčina (Slovak) + sv : Svenska (Swedish) + tr : Türkçe (Turkish) + uk : українська мова (Ukrainian) + zh : 中文 (Chinese) diff --git a/searx/settings_robot.yml b/searx/settings_robot.yml new file mode 100644 index 0000000..070a0ed --- /dev/null +++ b/searx/settings_robot.yml @@ -0,0 +1,41 @@ +general: + debug : False + instance_name : "searx_test" + +search: + safe_search : 0 + autocomplete : "" + language: "all" + +server: + port : 11111 + bind_address : 127.0.0.1 + secret_key : "ultrasecretkey" # change this! + base_url : False + image_proxy : False + http_protocol_version : "1.0" + +ui: + static_path : "" + templates_path : "" + default_theme : oscar + default_locale : "" + +outgoing: + request_timeout : 1.0 # seconds + useragent_suffix : "" + +engines: + - name : general dummy + engine : dummy + categories : general + shortcut : gd + + - name : dummy dummy + engine : dummy + categories : dummy + shortcut : dd + +locales: + en : English + hu : Magyar diff --git a/searx/static/plugins/css/infinite_scroll.css b/searx/static/plugins/css/infinite_scroll.css new file mode 100644 index 0000000..7e0ee20 --- /dev/null +++ b/searx/static/plugins/css/infinite_scroll.css @@ -0,0 +1,16 @@ +@keyframes rotate-forever { + 0% { transform: rotate(0deg) } + 100% { transform: rotate(360deg) } +} +.loading-spinner { + animation-duration: 0.75s; + animation-iteration-count: infinite; + animation-name: rotate-forever; + animation-timing-function: linear; + height: 30px; + width: 30px; + border: 8px solid #666; + border-right-color: transparent; + border-radius: 50% !important; + margin: 0 auto; +} diff --git a/searx/static/plugins/css/vim_hotkeys.css b/searx/static/plugins/css/vim_hotkeys.css new file mode 100644 index 0000000..2ccfdc1 --- /dev/null +++ b/searx/static/plugins/css/vim_hotkeys.css @@ -0,0 +1,26 @@ +.vim-hotkeys-help { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 9999999; + overflow-y: auto; + max-height: 80%; + box-shadow: 0 0 1em; +} + +.dflex { + display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ + display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ + display: -ms-flexbox; /* TWEENER - IE 10 */ + display: -webkit-flex; /* NEW - Chrome */ + display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ +} + +.iflex { + -webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */ + -moz-box-flex: 1; /* OLD - Firefox 19- */ + -webkit-flex: 1; /* Chrome */ + -ms-flex: 1; /* IE 10 */ + flex: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */ +} diff --git a/searx/static/plugins/js/infinite_scroll.js b/searx/static/plugins/js/infinite_scroll.js new file mode 100644 index 0000000..9cd582d --- /dev/null +++ b/searx/static/plugins/js/infinite_scroll.js @@ -0,0 +1,18 @@ +$(document).ready(function() { + var win = $(window); + win.scroll(function() { + if ($(document).height() - win.height() == win.scrollTop()) { + var formData = $('#pagination form:last').serialize(); + if (formData) { + $('#pagination').html('
'); + $.post('./', formData, function (data) { + var body = $(data); + $('#pagination').remove(); + $('#main_results').append('
'); + $('#main_results').append(body.find('.result')); + $('#main_results').append(body.find('#pagination')); + }); + } + } + }); +}); diff --git a/searx/static/plugins/js/open_results_on_new_tab.js b/searx/static/plugins/js/open_results_on_new_tab.js new file mode 100644 index 0000000..99ef382 --- /dev/null +++ b/searx/static/plugins/js/open_results_on_new_tab.js @@ -0,0 +1,3 @@ +$(document).ready(function() { + $('.result_header > a').attr('target', '_blank'); +}); diff --git a/searx/static/plugins/js/search_on_category_select.js b/searx/static/plugins/js/search_on_category_select.js new file mode 100644 index 0000000..a76fd12 --- /dev/null +++ b/searx/static/plugins/js/search_on_category_select.js @@ -0,0 +1,24 @@ +$(document).ready(function() { + if($('#q').length) { + $('#categories label').click(function(e) { + $('#categories input[type="checkbox"]').each(function(i, checkbox) { + $(checkbox).prop('checked', false); + }); + $(document.getElementById($(this).attr("for"))).prop('checked', true); + if($('#q').val()) { + $('#search_form').submit(); + } + return false; + }); + $('#time-range > option').click(function(e) { + if($('#q').val()) { + $('#search_form').submit(); + } + }); + $('#language').change(function(e) { + if($('#q').val()) { + $('#search_form').submit(); + } + }); + } +}); diff --git a/searx/static/plugins/js/vim_hotkeys.js b/searx/static/plugins/js/vim_hotkeys.js new file mode 100644 index 0000000..61500d8 --- /dev/null +++ b/searx/static/plugins/js/vim_hotkeys.js @@ -0,0 +1,336 @@ +$(document).ready(function() { + highlightResult('top')(); + + $('.result').on('click', function() { + highlightResult($(this))(); + }); + + var vimKeys = { + 27: { + key: 'Escape', + fun: removeFocus, + des: 'remove focus from the focused input', + cat: 'Control' + }, + 73: { + key: 'i', + fun: searchInputFocus, + des: 'focus on the search input', + cat: 'Control' + }, + 66: { + key: 'b', + fun: scrollPage(-window.innerHeight), + des: 'scroll one page up', + cat: 'Navigation' + }, + 70: { + key: 'f', + fun: scrollPage(window.innerHeight), + des: 'scroll one page down', + cat: 'Navigation' + }, + 85: { + key: 'u', + fun: scrollPage(-window.innerHeight / 2), + des: 'scroll half a page up', + cat: 'Navigation' + }, + 68: { + key: 'd', + fun: scrollPage(window.innerHeight / 2), + des: 'scroll half a page down', + cat: 'Navigation' + }, + 71: { + key: 'g', + fun: scrollPageTo(-document.body.scrollHeight, 'top'), + des: 'scroll to the top of the page', + cat: 'Navigation' + }, + 86: { + key: 'v', + fun: scrollPageTo(document.body.scrollHeight, 'bottom'), + des: 'scroll to the bottom of the page', + cat: 'Navigation' + }, + 75: { + key: 'k', + fun: highlightResult('up'), + des: 'select previous search result', + cat: 'Results' + }, + 74: { + key: 'j', + fun: highlightResult('down'), + des: 'select next search result', + cat: 'Results' + }, + 80: { + key: 'p', + fun: pageButtonClick(0), + des: 'go to previous page', + cat: 'Results' + }, + 78: { + key: 'n', + fun: pageButtonClick(1), + des: 'go to next page', + cat: 'Results' + }, + 79: { + key: 'o', + fun: openResult(false), + des: 'open search result', + cat: 'Results' + }, + 84: { + key: 't', + fun: openResult(true), + des: 'open the result in a new tab', + cat: 'Results' + }, + 82: { + key: 'r', + fun: reloadPage, + des: 'reload page from the server', + cat: 'Control' + }, + 72: { + key: 'h', + fun: toggleHelp, + des: 'toggle help window', + cat: 'Other' + } + }; + + $(document).keyup(function(e) { + // check for modifiers so we don't break browser's hotkeys + if (vimKeys.hasOwnProperty(e.keyCode) + && !e.ctrlKey + && !e.altKey + && !e.shiftKey + && !e.metaKey) + { + if (e.keyCode === 27) { + if (e.target.tagName.toLowerCase() === 'input') { + vimKeys[e.keyCode].fun(); + } + } else { + if (e.target === document.body) { + vimKeys[e.keyCode].fun(); + } + } + } + }); + + function highlightResult(which) { + return function() { + var current = $('.result[data-vim-selected]'); + if (current.length === 0) { + current = $('.result:first'); + if (current.length === 0) { + return; + } + } + + var next; + + if (typeof which !== 'string') { + next = which; + } else { + switch (which) { + case 'visible': + var top = $(window).scrollTop(); + var bot = top + $(window).height(); + var results = $('.result'); + + for (var i = 0; i < results.length; i++) { + next = $(results[i]); + var etop = next.offset().top; + var ebot = etop + next.height(); + + if ((ebot <= bot) && (etop > top)) { + break; + } + } + break; + case 'down': + next = current.next('.result'); + if (next.length === 0) { + next = $('.result:first'); + } + break; + case 'up': + next = current.prev('.result'); + if (next.length === 0) { + next = $('.result:last'); + } + break; + case 'bottom': + next = $('.result:last'); + break; + case 'top': + default: + next = $('.result:first'); + } + } + + if (next) { + current.removeAttr('data-vim-selected').removeClass('well well-sm'); + next.attr('data-vim-selected', 'true').addClass('well well-sm'); + scrollPageToSelected(); + } + } + } + + function reloadPage() { + document.location.reload(false); + } + + function removeFocus() { + if (document.activeElement) { + document.activeElement.blur(); + } + } + + function pageButtonClick(num) { + return function() { + var buttons = $('div#pagination button[type="submit"]'); + if (buttons.length !== 2) { + console.log('page navigation with this theme is not supported'); + return; + } + if (num >= 0 && num < buttons.length) { + buttons[num].click(); + } else { + console.log('pageButtonClick(): invalid argument'); + } + } + } + + function scrollPageToSelected() { + var sel = $('.result[data-vim-selected]'); + if (sel.length !== 1) { + return; + } + + var wnd = $(window); + + var wtop = wnd.scrollTop(); + var etop = sel.offset().top; + + var offset = 30; + + if (wtop > etop) { + wnd.scrollTop(etop - offset); + } else { + var ebot = etop + sel.height(); + var wbot = wtop + wnd.height(); + + if (wbot < ebot) { + wnd.scrollTop(ebot - wnd.height() + offset); + } + } + } + + function scrollPage(amount) { + return function() { + window.scrollBy(0, amount); + highlightResult('visible')(); + } + } + + function scrollPageTo(position, nav) { + return function() { + window.scrollTo(0, position); + highlightResult(nav)(); + } + } + + function searchInputFocus() { + $('input#q').focus(); + } + + function openResult(newTab) { + return function() { + var link = $('.result[data-vim-selected] .result_header a'); + if (link.length) { + var url = link.attr('href'); + if (newTab) { + window.open(url); + } else { + window.location.href = url; + } + } + }; + } + + function toggleHelp() { + var helpPanel = $('#vim-hotkeys-help'); + if (helpPanel.length) { + helpPanel.toggleClass('hidden'); + return; + } + + var categories = {}; + + for (var k in vimKeys) { + var key = vimKeys[k]; + categories[key.cat] = categories[key.cat] || []; + categories[key.cat].push(key); + } + + var sorted = Object.keys(categories).sort(function(a, b) { + return categories[b].length - categories[a].length; + }); + + if (sorted.length === 0) { + return; + } + + var html = '
'; + html += '
'; + + html += '
'; + html += '
'; + html += '

How to navigate searx with Vim-like hotkeys

'; + html += '
'; // col-sm-12 + html += '
'; // row + + for (var i = 0; i < sorted.length; i++) { + var cat = categories[sorted[i]]; + + var lastCategory = i === (sorted.length - 1); + var first = i % 2 === 0; + + if (first) { + html += '
'; + } + html += '
'; + + html += '
'; + html += '
' + cat[0].cat + '
'; + html += '
'; + html += '
    '; + + for (var cj in cat) { + html += '
  • ' + cat[cj].key + ' ' + cat[cj].des + '
  • '; + } + + html += '
'; + html += '
'; // panel-body + html += '
'; // panel + html += '
'; // col-sm-* + + if (!first || lastCategory) { + html += '
'; // row + } + } + + html += '
'; // container-fluid + html += '
'; // vim-hotkeys-help + + $('body').append(html); + } +}); diff --git a/searx/static/themes/courgette/img/favicon.png b/searx/static/themes/courgette/img/favicon.png new file mode 100644 index 0000000..3818d3d Binary files /dev/null and b/searx/static/themes/courgette/img/favicon.png differ diff --git a/searx/static/themes/courgette/img/preference-icon.png b/searx/static/themes/courgette/img/preference-icon.png new file mode 100644 index 0000000..57e991c Binary files /dev/null and b/searx/static/themes/courgette/img/preference-icon.png differ diff --git a/searx/static/themes/courgette/img/search-icon.png b/searx/static/themes/courgette/img/search-icon.png new file mode 100644 index 0000000..9bc7a22 Binary files /dev/null and b/searx/static/themes/courgette/img/search-icon.png differ diff --git a/searx/static/themes/courgette/img/searx-mobile.png b/searx/static/themes/courgette/img/searx-mobile.png new file mode 100644 index 0000000..31dd7d1 Binary files /dev/null and b/searx/static/themes/courgette/img/searx-mobile.png differ diff --git a/searx/static/themes/courgette/img/searx.png b/searx/static/themes/courgette/img/searx.png new file mode 100644 index 0000000..68c2e4f Binary files /dev/null and b/searx/static/themes/courgette/img/searx.png differ diff --git a/searx/static/themes/courgette/img/searx_logo.svg b/searx/static/themes/courgette/img/searx_logo.svg new file mode 100644 index 0000000..67a2d45 --- /dev/null +++ b/searx/static/themes/courgette/img/searx_logo.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/searx/static/themes/courgette/js/searx.js b/searx/static/themes/courgette/js/searx.js new file mode 100644 index 0000000..92a25e3 --- /dev/null +++ b/searx/static/themes/courgette/js/searx.js @@ -0,0 +1,45 @@ +if(searx.autocompleter) { + window.addEvent('domready', function() { + new Autocompleter.Request.JSON('q', './autocompleter', { + postVar:'q', + postData:{ + 'format': 'json' + }, + ajaxOptions:{ + timeout: 5 // Correct option? + }, + 'minLength': 4, + // 'selectMode': 'type-ahead', + cache: true, + delay: 300 + }); + }); +} + +(function (w, d) { + 'use strict'; + function addListener(el, type, fn) { + if (el.addEventListener) { + el.addEventListener(type, fn, false); + } else { + el.attachEvent('on' + type, fn); + } + } + + function placeCursorAtEnd() { + if (this.setSelectionRange) { + var len = this.value.length * 2; + this.setSelectionRange(len, len); + } + } + + addListener(w, 'load', function () { + var qinput = d.getElementById('q'); + if (qinput !== null && qinput.value === "") { + addListener(qinput, 'focus', placeCursorAtEnd); + qinput.focus(); + } + }); + +})(window, document); + diff --git a/searx/static/themes/courgette/less/style-rtl.less b/searx/static/themes/courgette/less/style-rtl.less new file mode 100644 index 0000000..3e357d3 --- /dev/null +++ b/searx/static/themes/courgette/less/style-rtl.less @@ -0,0 +1,42 @@ +.q { + padding: 0.5em 1em 0.5em 3em; +} + +#search_submit { + left: 0; + right:auto; +} + +.result .favicon { + float: right; + margin-left: 0.5em; + margin-right: 0; +} + +#sidebar { + right: auto; + left: 0; +} + +#results { + padding: 0px 32px 0px 272px; +} + +.search.center { + padding-right: 0; + padding-left: 17em; +} + +.right { + right: auto; + left: 0; +} + +#pagination form + form { + float: left; + margin-top: -2em; +} + +.engine-table { + text-align:right; +} \ No newline at end of file diff --git a/searx/static/themes/courgette/less/style.less b/searx/static/themes/courgette/less/style.less new file mode 100644 index 0000000..0387af5 --- /dev/null +++ b/searx/static/themes/courgette/less/style.less @@ -0,0 +1,691 @@ + +@color-main: #3498DB; +@color-focus: #0665A2; +@color-other-links: #666; +@color-fonts: #333; +@center-width: 70em; + + + + +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +input[type="search"] { + -webkit-appearance: textfield; +} + +h2 { + color: @color-other-links; + text-transform: uppercase; +} + +body { + font-family: sans-serif; + line-height: 1.5; + margin: 0; + background: #EEE; +} + +html { + position: relative; + min-height: 100%; +} + +a { + color: @color-other-links; +} + +.title h1 { + font-size:7em; + color:@color-main; + margin:0 auto; + line-height:100px; + margin-top:-20px; + padding-bottom:20px; +} + +.center { + max-width: @center-width; + text-align: center; + background: rgba(255,255,255,0.6); + padding: 2em; + margin: 7% auto 0; + position: relative; +} + +.center.search { + position: static; + width: auto; + background: none; + margin: auto; + padding-top: 1.8em; +} + +@media screen and (min-width: 1001px) { + .center:after { + content: ""; + z-index: -1; + background: url(../img/bg-body-index.jpg) no-repeat; + background-size: cover; + width: 100%; + height: 100%; + top: 0; + left: 0; + position: fixed; + } + .center.search:after { + content: none; + } +} + +.autocompleter-choices { + position: absolute; + margin: 0; + padding: 0; + background: #FFF; +} + +.autocompleter-choices li { + padding: 0.5em 1em; +} + +.autocompleter-choices li:hover { + background: @color-main; + color: #FFF; + cursor: pointer; +} + +#categories { + text-align: center; +} + +.top_margin { + position: absolute; + bottom: -3.5em; + width: 100%; + left: 0; +} + +.top_margin a { + display: inline-block; + margin-right: 1em; + color: #FFF; + text-decoration: none; +} + +.top_margin a:hover, +.top_margin a:focus { + text-decoration: underline; +} + +@media screen and (max-width: 1000px) { + .center { background: none; } + .top_margin a { + color: @color-fonts; + } +} + +.checkbox_container { margin-top: 1.5em; } +.checkbox_container label { + padding: 0.5em 1em; + color: @color-fonts; + cursor: pointer; + font-size: 0.9em; +} + +.checkbox_container label:hover { + background: @color-main; + color: #FFF; +} + +.checkbox_container input[type="checkbox"] { + position: absolute; + top: -9999px; +} + +.checkbox_container input[type="checkbox"]:checked + label { + background: @color-main; + color: #FFF; +} + +#categories_container > div { + display: inline-block; +} + +#categories .hidden { + display: none; + position: absolute; + bottom: 1em; + left: 0; + text-align: center; + width: 100%; + font-size: 0.9em; + font-style: italic; + color: @color-fonts; +} + +#categories:hover .hidden { + display: block; +} + +@media screen and (max-width: 900px) { + #categories_container { letter-spacing: -5px; } + #categories_container > div { + letter-spacing: normal; + margin-top: 1em; + } + .checkbox_container { + margin: 0; + } + .checkbox_container label { + display: block; + background: #CCC; + padding: 1em; + border: 1px solid #FFF; + } + .top_margin { position: static; } + #categories .hidden { + position: static; + display: block; + } +} + +@media screen and (max-width: 900px) and (min-width: 501px) { + #categories_container > div { + width: 31%; + margin-left: 2.333%; + } + #categories_container > div:nth-child(3n+1) { margin-left: 0; } +} + +@media screen and (max-width: 500px) { + #categories_container > div { + width: 48%; + margin-left: 2%; + font-size: 0.9em; + } + #categories_container > div:nth-child(2n+1) { margin-left: 0; } + .title h1 { + background: url(../img/searx-mobile.png) no-repeat; + width: 200px; + height: 39px; + } +} + +#search_wrapper { + position: relative; +} + +.q { + padding: 0.5em 3em 0.5em 1em; + width: 100%; + font-size: 1.5em; + border: 0; + color: #666; +} + +#search_submit { + position: absolute; + top: 0; + right: 0; + border: 0; + background:url("../img/search-icon.png") no-repeat scroll center center / 65% auto @color-main; + text-indent: -9999px; + width: 5em; + height: 100%; + cursor: pointer; +} + +#search_submit:hover, +#search_submit:focus { + background-color: @color-focus; +} + +#sidebar { + background: @color-main; + position: fixed; + top: 0; + right: 0; + width: 15em; + height: 100%; + padding: 1.5em; + text-align: right; +} + +.right { + position: fixed; + bottom: 1.5em; + width: 15em; + right: 0; + z-index: 1; + padding: 0 1.5em; + text-align: right; +} + +.right a { + color: #FFF; + display: block; + text-decoration: none; +} + +.right a:hover, +.right a:focus { + text-decoration: underline; +} + +#preferences { + background: url("../img/preference-icon.png") no-repeat right center / 12% auto; + padding-right: 1.8em; +} + +#preferences:hover, +#preferences:focus { + +} + +#search_url input { + border: 0; + padding: 0.5em; +} + +#sidebar > div { + margin-bottom: 1em; + color: #FFF; +} + +#sidebar form { + display: inline-block; +} + +#sidebar input[type="submit"] { + background: #CCC; + border: 0; + padding: 0.5em 1em; + cursor: pointer; + margin-top: 0.5em; +} + +#sidebar input[type="submit"]:hover, +#sidebar input[type="submit"]:focus { + color: #FFF; + background-color: @color-focus; +} + +#results { + padding-right: 17em; + padding-left: 2em; + padding: 0 17em 0 2em; +} + +.result p { + font-size: 0.9em; +} + +.result .content { + margin: 0; + color: #666; +} + +.result .url { + margin-top: 0; + color: #FF6530; +} + +.result .favicon { + float: left; + position: relative; + top: 0.5em; + margin-right: 0.5em; +} + +.definition_result { + background: #CCC; + padding: 1em; +} + +.definition_result .result_title, +.definition_result p { + margin: 0; +} + +.result_title { + margin-bottom: 0; + font-weight: normal; +} + +.highlight { + font-weight: bold; +} + +.result_title a { + color: @color-main; + text-decoration: none; +} + +.result_title a:hover, +.result_title a:focus { + text-decoration: underline; +} + +.cache_link { + color: #666; + font-size: 0.9em; + font-style: italic; +} + +.search.center { + padding-right: 17em; +} + +#answers { + border: 2px solid @color-main; + padding: 20px; + color:#666; + text-align: center; + max-width:@center-width; + margin:0 auto 20px; +} + +#suggestions { margin-bottom: 1em; } + +#suggestions span { color: #666; } + +#suggestions form { + display: inline-block; + vertical-align: top; + margin-bottom: 0.5em; +} + +#suggestions input[type="submit"] { + color: @color-fonts; + padding: 0.5em 1em; + border: 0; + background: #CCC; + cursor:pointer; +} + +#suggestions input[type="submit"]:hover, +#suggestions input[type="submit"]:focus { + background: @color-main; + color: #FFF; +} + +#pagination { + margin: 1.5em 0 2em; +} + +#pagination form + form { + float: right; + margin-top: -2em; +} + +input[type="submit"] { + display: inline-block; + background: @color-main; + color: #FFF; + border: 0; + padding: 0.6em 1em; + cursor: pointer; +} + +input[type="submit"]:hover, +input[type="submit"]:focus { + background: @color-focus; +} + +.row { + max-width: 60em; + margin: auto; +} + +.row a { + color: @color-main; +} + +.row form { + letter-spacing: -5px; +} + +.row form > * { letter-spacing: normal; } + +.row p { margin: 0; } + +.row fieldset { + display: inline-block; + width: 48%; + vertical-align: top; +} + +.row fieldset:last-of-type { + display: block; + width: auto; + background: none; + padding: 0; +} + +.row fieldset:nth-child(odd) { + margin-right: 2%; +} + +.row fieldset:nth-child(2) { + min-height: 10.5em; +} + +@media screen and (max-width: 900px) { + .row { + margin: 0 1em; + } + + .row fieldset { width: 49%; } + .row fieldset, + .row fieldset:nth-child(odd) { + margin-right: 0; + } + + .row fieldset:first-child { + width: 100%; + margin-right: 0; + } + + .row fieldset:nth-child(even) { + margin-right: 2%; + } +} + +@media screen and (max-width: 800px) { + .row fieldset { width: 100%; } + + select { width: 100%; } + + table { font-size: 0.8em; } + .right {display: none;} + #sidebar { display: none; } + #results { padding: 0 2em; } + .search.center { + padding-right: 2em; + } +} + +@media screen and (max-width: 400px) { + .row #categories_container > div { + width: 100%; + margin-left: 0; + } +} + +fieldset { + border: 0; + margin: 1em 0; + background: #CCC; + padding: 1.5em; +} + +table { + width: 100%; + text-align: left; + border: 1px solid #CCC; + border-collapse: collapse; +} + +table th { + background: #999; + color: #FFF; +} + +table tr:nth-child(odd) { + background: #CCC; +} + +table th, +table td { + padding: 0.5em 1em; + border: 1px solid #FFF; +} + +.engine_checkbox label { + padding: 0.5em; + background: @color-main; + color: #FFF; + cursor: pointer; +} + +.engine_checkbox .deny { + background: @color-main; +} + +.engine_checkbox .allow { + display: none; + background: #666; +} + +.engine_checkbox input { + display: none; +} + +.engine_checkbox input:checked + .allow { + display: inline; +} + +.engine_checkbox input:checked + .allow + .deny{ + display: none; +} + +.row input[type="submit"] { + font-size: 1em; + margin: 1em 0 2em; +} + +.row .right { + position: static; + display: inline-block; + +} + +.row .right a { + color: @color-fonts; + width: auto; + text-align: left; + padding: 0; +} + +.small_font { + font-size: 0.8em; +} + +table th { + padding: 1em; +} + +legend { + background: #EEE; + padding: 0 1em; + position: relative; +} + +select { + border: 1px solid #DDD; + padding: 0.5em 0.8em; + font-size: 1em; +} + +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #408080; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #008000; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #BC7A00 } /* Comment.Preproc */ +.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #008000 } /* Keyword.Pseudo */ +.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #B00040 } /* Keyword.Type */ +.highlight .m { color: #666666 } /* Literal.Number */ +.highlight .s { color: #BA2121 } /* Literal.String */ +.highlight .na { color: #7D9029 } /* Name.Attribute */ +.highlight .nb { color: #008000 } /* Name.Builtin */ +.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.highlight .no { color: #880000 } /* Name.Constant */ +.highlight .nd { color: #AA22FF } /* Name.Decorator */ +.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #0000FF } /* Name.Function */ +.highlight .nl { color: #A0A000 } /* Name.Label */ +.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #19177C } /* Name.Variable */ +.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #BB6688 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ + +.highlight pre { overflow: auto; } + +.highlight .lineno { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default; +} + +.highlight .lineno::selection { background: transparent; } /* WebKit/Blink Browsers */ +.highlight .lineno::-moz-selection { background: transparent; } /* Gecko Browsers */ diff --git a/searx/static/themes/legacy/img/favicon.png b/searx/static/themes/legacy/img/favicon.png new file mode 100644 index 0000000..3818d3d Binary files /dev/null and b/searx/static/themes/legacy/img/favicon.png differ diff --git a/searx/static/themes/legacy/img/preference-icon.png b/searx/static/themes/legacy/img/preference-icon.png new file mode 100644 index 0000000..8bdee64 Binary files /dev/null and b/searx/static/themes/legacy/img/preference-icon.png differ diff --git a/searx/static/themes/legacy/img/search-icon.png b/searx/static/themes/legacy/img/search-icon.png new file mode 100644 index 0000000..d70310b Binary files /dev/null and b/searx/static/themes/legacy/img/search-icon.png differ diff --git a/searx/static/themes/legacy/img/searx.png b/searx/static/themes/legacy/img/searx.png new file mode 100644 index 0000000..a98f12a Binary files /dev/null and b/searx/static/themes/legacy/img/searx.png differ diff --git a/searx/static/themes/legacy/img/searx_logo.svg b/searx/static/themes/legacy/img/searx_logo.svg new file mode 100644 index 0000000..67a2d45 --- /dev/null +++ b/searx/static/themes/legacy/img/searx_logo.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/searx/static/themes/legacy/js/searx.js b/searx/static/themes/legacy/js/searx.js new file mode 100644 index 0000000..d6d5b74 --- /dev/null +++ b/searx/static/themes/legacy/js/searx.js @@ -0,0 +1,49 @@ +if(searx.autocompleter) { + window.addEvent('domready', function() { + new Autocompleter.Request.JSON('q', './autocompleter', { + postVar:'q', + postData:{ + 'format': 'json' + }, + ajaxOptions:{ + timeout: 5 // Correct option? + }, + 'minLength': 4, + 'selectMode': false, + cache: true, + delay: 300 + }); + }); +} + +(function (w, d) { + 'use strict'; + function addListener(el, type, fn) { + if (el.addEventListener) { + el.addEventListener(type, fn, false); + } else { + el.attachEvent('on' + type, fn); + } + } + + function placeCursorAtEnd() { + if (this.setSelectionRange) { + var len = this.value.length * 2; + this.setSelectionRange(len, len); + } + } + + addListener(w, 'load', function () { + var qinput = d.getElementById('q'); + if (qinput !== null && qinput.value === "") { + addListener(qinput, 'focus', placeCursorAtEnd); + qinput.focus(); + } + }); + + if (!!('ontouchstart' in window)) { + document.getElementsByTagName("html")[0].className += " touch"; + } + +})(window, document); + diff --git a/searx/static/themes/legacy/less/autocompleter.less b/searx/static/themes/legacy/less/autocompleter.less new file mode 100644 index 0000000..db9601a --- /dev/null +++ b/searx/static/themes/legacy/less/autocompleter.less @@ -0,0 +1,61 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + */ + +ul { + &.autocompleter-choices { + position: absolute; + margin: 0; + padding: 0; + list-style: none; + border: 1px solid @color-autocompleter-choices-border; + border-left-color: @color-autocompleter-choices-border-left-right; + border-right-color: @color-autocompleter-choices-border-left-right; + border-bottom-color: @color-autocompleter-choices-border-bottom; + text-align: left; + font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; + z-index: 50; + background-color: @color-autocompleter-choices-background; + color: @color-autocompleter-choices-font; + + li { + position: relative; + margin: -2px 0 0 0; + padding: 0.2em 1.5em 0.2em 1em; + display: block; + float: none !important; + cursor: pointer; + font-weight: normal; + white-space: nowrap; + font-size: 1em; + line-height: 1.5em; + + &.autocompleter-selected { + background-color: @color-autocompleter-selected-background; + color: @color-autocompleter-selected-font; + + span.autocompleter-queried { + color: @color-autocompleter-selected-queried-font; + } + } + } + + span.autocompleter-queried { + display: inline; + float: none; + font-weight: bold; + margin: 0; + padding: 0; + } + } +} + +/*.autocompleter-loading { + //background-image: url(images/spinner.gif); + background-repeat: no-repeat; + background-position: right 50%; +}*/ + +/*textarea.autocompleter-loading { + background-position: right bottom; +}*/ diff --git a/searx/static/themes/legacy/less/code.less b/searx/static/themes/legacy/less/code.less new file mode 100644 index 0000000..a688dd9 --- /dev/null +++ b/searx/static/themes/legacy/less/code.less @@ -0,0 +1,83 @@ +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #408080; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #008000; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #BC7A00 } /* Comment.Preproc */ +.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #008000 } /* Keyword.Pseudo */ +.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #B00040 } /* Keyword.Type */ +.highlight .m { color: #666666 } /* Literal.Number */ +.highlight .s { color: #BA2121 } /* Literal.String */ +.highlight .na { color: #7D9029 } /* Name.Attribute */ +.highlight .nb { color: #008000 } /* Name.Builtin */ +.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.highlight .no { color: #880000 } /* Name.Constant */ +.highlight .nd { color: #AA22FF } /* Name.Decorator */ +.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #0000FF } /* Name.Function */ +.highlight .nl { color: #A0A000 } /* Name.Label */ +.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #19177C } /* Name.Variable */ +.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #BB6688 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ + +.highlight pre { + overflow: auto; +} + +.highlight .lineno { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default; + + &::selection { + background: transparent; /* WebKit/Blink Browsers */ + } + &::-moz-selection { + background: transparent; /* Gecko Browsers */ + } +} diff --git a/searx/static/themes/legacy/less/definitions.less b/searx/static/themes/legacy/less/definitions.less new file mode 100644 index 0000000..0ac0cc9 --- /dev/null +++ b/searx/static/themes/legacy/less/definitions.less @@ -0,0 +1,119 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + * + * To change the colors of the site, simple edit this variables + */ + +/// Basic Colors + +@color-base: #3498DB; +@color-base-dark: #2980B9; +@color-base-light: #ECF0F1; +@color-highlight: #094089; +@color-black: #000000; + +/// General + +@color-font: #444; +@color-font-light: #888; + +@color-red: #C0392B; + +@color-url-font: #1a11be; +@color-url-visited-font: #8E44AD; +@results-width: 50em; + + +/// Start-Screen + +// hmarg +@color-hmarg-border: @color-base; +@color-hmarg-font: @color-base; +@color-hmarg-font-hover: @color-base; + + +/// Search-Input + +@color-search-border: @color-base; +@color-search-background: #FFF; +@color-search-font: #222; + +/// Autocompleter + +@color-autocompleter-choices-background: #FFF; +@color-autocompleter-choices-border: @color-base; +@color-autocompleter-choices-border-left-right: @color-base; +@color-autocompleter-choices-border-bottom: @color-base; + +@color-autocompleter-choices-font: #444; + +/// Answers +@color-answers-border: @color-base-dark; + +// Selected +@color-autocompleter-selected-background: #444; +@color-autocompleter-selected-font: #FFF; +@color-autocompleter-selected-queried-font: #9FCFFF; + +/// Categories + +@color-categories-item-selected: @color-base; +@color-categories-item-selected-font: #FFF; + +@color-categories-item-border-selected: @color-base-dark; +@color-categories-item-border-unselected: #E8E7E6; +@color-categories-item-border-unselected-hover: @color-base; + + +/// Results + +@color-suggestions-button-background: @color-base; +@color-suggestions-button-font: #FFF; + +@color-download-button-background: @color-base; +@color-download-button-font: #FFF; + +@color-result-search-background: @color-base-light; + +@color-result-definition-border: gray; +@color-result-torrent-border: lightgray; +@color-result-top-border: #E8E7E6; + +// Link to result +@color-result-link-font: @color-base-dark; +@color-result-link-visited-font: @color-url-visited-font; + +// Url to result +@color-result-url-font: @color-red; + +// Publish Date +@color-result-publishdate-font: @color-font-light; + +// Images +@color-result-image-span-background-hover: rgba(0, 0, 0, 0.6); +@color-result-image-span-font: #FFF; + +// Search-URL +@color-result-search-url-border: #888; +@color-result-search-url-font: #444; + + +/// Settings + +@color-settings-fieldset: @color-base; +@color-settings-tr-hover: #DDD; + +// Labels +@color-settings-label-allowed-background: #E74C3C; +@color-settings-label-allowed-font: #FFF; + +@color-settings-label-deny-background: #2ECC71; +@color-settings-label-deny-font: @color-font; + +@color-settings-return-background: @color-base; +@color-settings-return-font: #FFF; + +/// Other + +@color-engines-font: @color-font-light; +@color-percentage-div-background: #444; diff --git a/searx/static/themes/legacy/less/mixins.less b/searx/static/themes/legacy/less/mixins.less new file mode 100644 index 0000000..dbccce6 --- /dev/null +++ b/searx/static/themes/legacy/less/mixins.less @@ -0,0 +1,27 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + */ + +// Mixins + +.text-size-adjust (@property: 100%) { + -webkit-text-size-adjust: @property; + -ms-text-size-adjust: @property; + -moz-text-size-adjust: @property; + text-size-adjust: @property; +} + +.rounded-corners (@radius: 4px) { + -webkit-border-radius: @radius; + -moz-border-radius: @radius; + border-radius: @radius; +} + +.user-select () { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/searx/static/themes/legacy/less/search.less b/searx/static/themes/legacy/less/search.less new file mode 100644 index 0000000..d285ca7 --- /dev/null +++ b/searx/static/themes/legacy/less/search.less @@ -0,0 +1,68 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + */ + +.search { + padding: 0; + margin: 0; + .checkbox_container label { + font-size: 0.9em; + border-bottom: 2px solid @color-categories-item-border-unselected; + } + + .checkbox_container label:hover { + border-bottom: 2px solid @color-categories-item-border-unselected-hover; + } + + .checkbox_container input[type="checkbox"]:checked + label { + border-bottom: 2px solid @color-categories-item-border-selected; + } +} + +#search_wrapper { + position: relative; + width: @results-width; + padding: 10px; +} + +.center #search_wrapper { + margin-left: auto; + margin-right: auto; +} + +.q { + background: none repeat scroll 0 0 @color-search-background; + border: 1px solid @color-search-border; + color: @color-search-font; + font-size: 16px; + height: 28px; + margin: 0; + outline: medium none; + padding: 2px; + padding-left: 8px; + padding-right: 0px !important; + width: 100%; + z-index: 2; +} + +#search_submit { + position: absolute; + top: 13px; + right: 1px; + padding: 0; + border: 0; + background: url('../img/search-icon.png') no-repeat; + background-size: 24px 24px; + opacity: 0.8; + width: 24px; + height: 30px; + font-size: 0; +} + +@media screen and (max-width: @results-width) { + #search_wrapper { + width: 90%; + clear:both; + overflow: hidden + } +} diff --git a/searx/static/themes/legacy/less/style-rtl.less b/searx/static/themes/legacy/less/style-rtl.less new file mode 100644 index 0000000..eac53c1 --- /dev/null +++ b/searx/static/themes/legacy/less/style-rtl.less @@ -0,0 +1,11 @@ +#search_submit { + left: 1px; + right:auto; +} + +.result .favicon { + float: right; + margin-left: 0.5em; + margin-right: 0; +} + diff --git a/searx/static/themes/legacy/less/style.less b/searx/static/themes/legacy/less/style.less new file mode 100644 index 0000000..4374f7d --- /dev/null +++ b/searx/static/themes/legacy/less/style.less @@ -0,0 +1,739 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + * + * To convert "style.less" to "style.css" run: $make styles + */ + +@import "definitions.less"; + +@import "mixins.less"; + +@import "code.less"; + +// Main LESS-Code + +html { + font-family: sans-serif; + font-size: 0.9em; + .text-size-adjust; + color: @color-font; + padding: 0; + margin: 0; +} + +body, #container { + padding: 0; + margin: 0; +} + +#container { + width: 100%; + position: absolute; + top: 0; +} + +// Search-Field + +@import "search.less"; + +// Autocompleter + +@import "autocompleter.less"; + +.row { + max-width: 800px; + margin: 20px auto; + text-align: justify; + + h1 { + font-size: 3em; + margin-top: 50px; + } + + p { + padding: 0 10px; + max-width: 700px; + } + + h3,ul { + margin: 4px 8px; + } +} + +.hmarg { + margin: 0 20px; + border: 1px solid @color-hmarg-border; + padding: 4px 10px; +} + +a { + &:link.hmarg { + color: @color-hmarg-font; + } + + &:visited.hmarg { + color: @color-hmarg-font; + } + + &:active.hmarg { + color: @color-hmarg-font-hover; + } + + &:hover.hmarg { + color: @color-hmarg-font-hover; + } +} + +.top_margin { + margin-top: 60px; +} + +.center { + text-align: center; +} + +h1 { + font-size: 5em; +} + +div.title { + background: url('../img/searx.png') no-repeat; + width: 100%; + min-height: 80px; + background-position: center; + + h1 { + visibility: hidden; + } +} + +input[type="submit"] { + padding: 2px 6px; + margin: 2px 4px; + display: inline-block; + background: @color-download-button-background; + color: @color-download-button-font; + .rounded-corners; + border: 0; + cursor: pointer; +} + +input[type="checkbox"] { + visibility: hidden; +} + +fieldset { + margin: 8px; + border: 1px solid @color-settings-fieldset; +} + +#categories { + margin: 0 10px; + .user-select; +} + +.checkbox_container { + display: inline-block; + position: relative; + margin: 0 3px; + padding: 0px; + + input { + display: none; + } +} + +.checkbox_container label, .engine_checkbox label { + cursor: pointer; + padding: 4px 10px; + margin: 0; + display: block; + text-transform: capitalize; + .user-select; +} + +.checkbox_container input[type="checkbox"]:checked + label { + background: @color-categories-item-selected; + color: @color-categories-item-selected-font; +} + +.engine_checkbox { + padding: 4px; +} + +label { + &.allow { + background: @color-settings-label-allowed-background; + padding: 4px 8px; + color: @color-settings-label-allowed-font; + display: none; + } + + &.deny { + background: @color-settings-label-deny-background; + padding: 4px 8px; + color: @color-settings-label-deny-font; + display: inline; + } +} + +.engine_checkbox input[type="checkbox"]:checked + label { + &:nth-child(2) + label { + display: none; + } + + &.allow { + display: inline; + } +} + +a { + text-decoration: none; + color: @color-url-font; + + &:visited { + color: @color-url-visited-font; + } +} + +.result { + margin: 19px 0 18px 0; + padding: 0; + clear: both; +} + +.result_title { + margin-bottom: 0; + + a { + color: @color-result-link-font; + font-weight: normal; + font-size: 1.1em; + + &:hover { + text-decoration: underline; + } + + &:visited { + color: @color-result-link-visited-font; + } + } +} + +.cache_link { + font-size: 10px !important; +} + +.result { + h3 { + font-size: 1em; + word-wrap:break-word; + margin: 5px 0 1px 0; + padding: 0 + } + + .content { + font-size: 0.8em; + margin: 0; + padding: 0; + max-width: 54em; + word-wrap:break-word; + line-height: 1.24; + + img { + float: left; + margin-right: 5px; + max-width: 200px; + max-height: 100px; + } + + br.last { + clear: both; + } + } + + .url { + font-size: 0.8em; + margin: 0 0 3px 0; + padding: 0; + max-width: 54em; + word-wrap:break-word; + color: @color-result-url-font; + } + + .published_date { + font-size: 0.8em; + color: @color-result-publishdate-font; + Margin: 5px 20px; + } + + .thumbnail { + width: 400px; + } +} + +.engines { + color: @color-engines-font; +} + +.small_font { + font-size: 0.8em; +} + +.small p { + margin: 2px 0; +} + +.right { + float: right; +} + +.invisible { + display: none; +} + +.left { + float: left; +} + +.highlight { + color: @color-highlight; +} + +.content .highlight { + color: @color-black; +} + +.image_result { + display: inline-block; + margin: 10px 10px; + position: relative; + max-height: 160px; + + img { + border: 0; + max-height: 160px; + } + + p { + margin: 0; + padding: 0; + + span a { + display: none; + color: @color-result-image-span-font; + } + + &:hover span a { + display: block; + position: absolute; + bottom: 0; + right: 0; + padding: 4px; + background-color: @color-result-image-span-background-hover; + font-size: 0.7em; + } + } +} + +.torrent_result { + border-left: 10px solid @color-result-torrent-border; + padding-left: 3px; + + p { + margin: 3px; + font-size: 0.8em; + } + + a { + color: @color-result-link-font; + + &:hover { + text-decoration: underline; + } + + &:visited { + color: @color-result-link-visited-font; + } + } +} + +.definition_result { + border-left: 10px solid @color-result-definition-border; + padding-left: 3px; +} + +.percentage { + position: relative; + width: 300px; + + div { + background: @color-percentage-div-background; + } +} + +table { + width: 100%; +} + +td { + padding: 0 4px; +} + +tr { + &:hover { + background: @color-settings-tr-hover; + } +} + +#results { + margin: auto; + padding: 0; + width: @results-width; + margin-bottom: 20px; +} + +#sidebar { + position: fixed; + bottom: 10px; + left: 10px; + margin: 0 2px 5px 5px; + padding: 0 2px 2px 2px; + width: 14em; + + input { + padding: 0; + margin: 3px; + font-size: 0.8em; + display: inline-block; + background: transparent; + color: @color-result-search-url-font; + cursor: pointer; + } + input[type="submit"] { + text-decoration: underline; + } +} + +#suggestions { + + form { + display: inline; + } + +} + +#suggestions, #answers { + + margin-top: 20px; + max-width: 45em; + +} + +#suggestions, #answers, #infoboxes { + + input { + padding: 0; + margin: 3px; + font-size: 0.8em; + display: inline-block; + background: transparent; + color: @color-result-search-url-font; + cursor: pointer; + } + + input[type="submit"] { + text-decoration: underline; + } + +} + +#suggestions-title { + +color: @color-font-light; + + +} + +#answers { + + border: 2px solid @color-answers-border; + padding: 20px; + +} + +#answers, #infoboxes { + form { + min-width: 210px; + } +} + + +#infoboxes { + position: absolute; + top: 100px; + right: 20px; + margin: 0px 2px 5px 5px; + padding: 0px 2px 2px; + max-width: 21em; + word-wrap: break-word; + + .infobox { + margin: 10px 0 10px; + border: 1px solid #ddd; + padding: 5px; + font-size: 0.8em; + /* box-shadow: 0px 0px 5px #CCC; */ + + img { + max-width: 90%; + max-heigt: 12em; + display: block; + margin: 5px; + padding: 5px; + } + + h2 { + margin: 0; + } + + table { + table-layout: fixed; + + td { + vertical-align: top; + } + + } + + input { + font-size: 1em; + } + + br { + clear: both; + } + + } +} + +#search_url { + margin-top: 8px; + + input { + border: 1px solid @color-result-search-url-border; + padding: 4px; + color: @color-result-search-url-font; + width: 14em; + display: block; + margin: 4px; + font-size: 0.8em; + } +} + +#preferences { + top: 10px; + padding: 0; + border: 0; + background: url('../img/preference-icon.png') no-repeat; + background-size: 28px 28px; + opacity: 0.8; + width: 28px; + height: 30px; + display: block; + + * { + display: none; + } +} + +#pagination { + clear: both; + + br { + clear: both; + } +} + +#apis { + margin-top: 8px; + clear: both; +} + +#categories_container { + position: relative; +} + +@media screen and (max-width: @results-width) { + + #results { + margin: auto; + padding: 0; + width: 90%; + } + + .github { + display: none; + } + + .checkbox_container { + display: block; + width: 90%; + //float: left; + + label { + border-bottom: 0; + } + } + + .preferences_container { + display: none; + postion: fixed !important; + top: 100px; + right: 0px; + } + +} + +@media screen and (max-width: 75em) { + + div.title { + + h1 { + font-size: 1em; + } + } + + html.touch #categories { + width: 95%; + height: 30px; + text-align: left; + overflow-x: scroll; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + + #categories_container { + width: 1000px; + width: -moz-max-content; + width: -webkit-max-content; + width: max-content; + + .checkbox_container { + display: inline-block; + width: auto; + } + } + } + + #categories { + font-size: 90%; + clear: both; + + .checkbox_container { + margin-top: 2px; + margin: auto; + } + } + + #suggestions, #answers { + margin-top: 5px; + } + + #infoboxes { + position: inherit; + max-width: inherit; + + .infobox { + clear:both; + + img { + float: left; + max-width: 10em; + } + } + } + + #categories { + font-size: 90%; + clear: both; + + .checkbox_container { + margin-top: 2px; + margin: auto; + } + } + + #sidebar { + position: static; + max-width: @results-width; + margin: 0 0 2px 0; + padding: 0; + float: none; + border: none; + width: auto; + input { + border: 0; + } + } + + #apis { + display: none; + } + + #search_url { + display: none; + } + + .result { + border-top: 1px solid @color-result-top-border; + margin: 8px 0 8px 0; + + .thumbnail { + max-width: 98%; + } + } + + .image_result { + max-width: 98%; + img { + max-width: 98%; + } + } +} + +.favicon { + float: left; + margin-right: 4px; + margin-top: 2px; +} + +.preferences_back { + background: none repeat scroll 0 0 @color-settings-return-background; + border: 0 none; + .rounded-corners; + cursor: pointer; + display: inline-block; + margin: 2px 4px; + padding: 4px 6px; + + a { + color: @color-settings-return-font; + } +} + +.hidden { + opacity: 0; + overflow: hidden; + font-size: 0.8em; + position: absolute; + bottom: -20px; + width: 100%; + text-position: center; + background: white; + transition: opacity 1s ease; +} + +#categories_container:hover .hidden { + transition: opacity 1s ease; + opacity: 0.8; +} diff --git a/searx/static/themes/oscar/.gitignore b/searx/static/themes/oscar/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/searx/static/themes/oscar/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/searx/static/themes/oscar/README.rst b/searx/static/themes/oscar/README.rst new file mode 100644 index 0000000..e7daa87 --- /dev/null +++ b/searx/static/themes/oscar/README.rst @@ -0,0 +1,17 @@ +install dependencies +~~~~~~~~~~~~~~~~~~~~ + +run this command in the directory ``searx/static/themes/oscar`` + +``npm install`` + +compile sources +~~~~~~~~~~~~~~~ + +run this command in the directory ``searx/static/themes/oscar`` + +``grunt`` + +or in the root directory: + +``make grunt`` diff --git a/searx/static/themes/oscar/gruntfile.js b/searx/static/themes/oscar/gruntfile.js new file mode 100644 index 0000000..59d1b6d --- /dev/null +++ b/searx/static/themes/oscar/gruntfile.js @@ -0,0 +1,90 @@ +module.exports = function(grunt) { + + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + concat: { + options: { + separator: ';' + }, + dist: { + src: ['js/searx_src/*.js'], + dest: 'js/searx.js' + } + }, + uglify: { + options: { + banner: '/*! oscar/searx.min.js | <%= grunt.template.today("dd-mm-yyyy") %> | https://github.com/asciimoo/searx */\n' + }, + dist: { + files: { + 'js/searx.min.js': ['<%= concat.dist.dest %>'] + } + } + }, + jshint: { + files: ['gruntfile.js', 'js/searx_src/*.js'], + options: { + // options here to override JSHint defaults + globals: { + jQuery: true, + console: true, + module: true, + document: true + } + } + }, + less: { + development: { + options: { + paths: ["less/pointhi", "less/logicodev"] + //banner: '/*! less/oscar/oscar.css | <%= grunt.template.today("dd-mm-yyyy") %> | https://github.com/asciimoo/searx */\n' + }, + files: {"css/pointhi.css": "less/pointhi/oscar.less", + "css/logicodev.css": "less/logicodev/oscar.less"} + }, + production: { + options: { + paths: ["less/pointhi", "less/logicodev"], + //banner: '/*! less/oscar/oscar.css | <%= grunt.template.today("dd-mm-yyyy") %> | https://github.com/asciimoo/searx */\n', + cleancss: true + }, + files: {"css/pointhi.min.css": "less/pointhi/oscar.less", + "css/logicodev.min.css": "less/logicodev/oscar.less"} + }, + bootstrap: { + options: { + paths: ["less/bootstrap"], + cleancss: true + }, + files: {"css/bootstrap.min.css": "less/bootstrap/bootstrap.less"} + }, + }, + watch: { + scripts: { + files: ['<%= jshint.files %>'], + tasks: ['jshint', 'concat', 'uglify'] + }, + oscar_styles: { + files: ['less/pointhi/**/*.less'], + tasks: ['less:development', 'less:production'] + }, + bootstrap_styles: { + files: ['less/bootstrap/**/*.less'], + tasks: ['less:bootstrap'] + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-less'); + + grunt.registerTask('test', ['jshint']); + + grunt.registerTask('default', ['jshint', 'concat', 'uglify', 'less']); + + grunt.registerTask('styles', ['less']); + +}; diff --git a/searx/static/themes/oscar/img/favicon.png b/searx/static/themes/oscar/img/favicon.png new file mode 100644 index 0000000..15b4575 Binary files /dev/null and b/searx/static/themes/oscar/img/favicon.png differ diff --git a/searx/static/themes/oscar/img/icons/README.md b/searx/static/themes/oscar/img/icons/README.md new file mode 100644 index 0000000..0e3ad1c --- /dev/null +++ b/searx/static/themes/oscar/img/icons/README.md @@ -0,0 +1,2 @@ +Source: http://www.iconspedia.com/pack/flat-gradient-social-icons-4384/ +License: Free for non commercial use. diff --git a/searx/static/themes/oscar/img/loader.gif b/searx/static/themes/oscar/img/loader.gif new file mode 100644 index 0000000..419cdee Binary files /dev/null and b/searx/static/themes/oscar/img/loader.gif differ diff --git a/searx/static/themes/oscar/img/logo_searx_a.png b/searx/static/themes/oscar/img/logo_searx_a.png new file mode 100644 index 0000000..9427900 Binary files /dev/null and b/searx/static/themes/oscar/img/logo_searx_a.png differ diff --git a/searx/static/themes/oscar/img/logo_searx_a_n.png b/searx/static/themes/oscar/img/logo_searx_a_n.png new file mode 100644 index 0000000..5b24aea Binary files /dev/null and b/searx/static/themes/oscar/img/logo_searx_a_n.png differ diff --git a/searx/static/themes/oscar/img/map/layers-2x.png b/searx/static/themes/oscar/img/map/layers-2x.png new file mode 100644 index 0000000..0b30da6 Binary files /dev/null and b/searx/static/themes/oscar/img/map/layers-2x.png differ diff --git a/searx/static/themes/oscar/img/map/layers.png b/searx/static/themes/oscar/img/map/layers.png new file mode 100644 index 0000000..4297fd9 Binary files /dev/null and b/searx/static/themes/oscar/img/map/layers.png differ diff --git a/searx/static/themes/oscar/img/map/marker-icon-2x-green.png b/searx/static/themes/oscar/img/map/marker-icon-2x-green.png new file mode 100644 index 0000000..7446bb0 Binary files /dev/null and b/searx/static/themes/oscar/img/map/marker-icon-2x-green.png differ diff --git a/searx/static/themes/oscar/img/map/marker-icon-2x-orange.png b/searx/static/themes/oscar/img/map/marker-icon-2x-orange.png new file mode 100644 index 0000000..ecd6773 Binary files /dev/null and b/searx/static/themes/oscar/img/map/marker-icon-2x-orange.png differ diff --git a/searx/static/themes/oscar/img/map/marker-icon-2x-red.png b/searx/static/themes/oscar/img/map/marker-icon-2x-red.png new file mode 100644 index 0000000..1d2e197 Binary files /dev/null and b/searx/static/themes/oscar/img/map/marker-icon-2x-red.png differ diff --git a/searx/static/themes/oscar/img/map/marker-icon-2x.png b/searx/static/themes/oscar/img/map/marker-icon-2x.png new file mode 100644 index 0000000..0015b64 Binary files /dev/null and b/searx/static/themes/oscar/img/map/marker-icon-2x.png differ diff --git a/searx/static/themes/oscar/img/map/marker-icon-green.png b/searx/static/themes/oscar/img/map/marker-icon-green.png new file mode 100644 index 0000000..f48ef41 Binary files /dev/null and b/searx/static/themes/oscar/img/map/marker-icon-green.png differ diff --git a/searx/static/themes/oscar/img/map/marker-icon-orange.png b/searx/static/themes/oscar/img/map/marker-icon-orange.png new file mode 100644 index 0000000..d0d2220 Binary files /dev/null and b/searx/static/themes/oscar/img/map/marker-icon-orange.png differ diff --git a/searx/static/themes/oscar/img/map/marker-icon-red.png b/searx/static/themes/oscar/img/map/marker-icon-red.png new file mode 100644 index 0000000..7a92b9e Binary files /dev/null and b/searx/static/themes/oscar/img/map/marker-icon-red.png differ diff --git a/searx/static/themes/oscar/img/map/marker-icon.png b/searx/static/themes/oscar/img/map/marker-icon.png new file mode 100644 index 0000000..e2e9f75 Binary files /dev/null and b/searx/static/themes/oscar/img/map/marker-icon.png differ diff --git a/searx/static/themes/oscar/img/map/marker-shadow.png b/searx/static/themes/oscar/img/map/marker-shadow.png new file mode 100644 index 0000000..d1e773c Binary files /dev/null and b/searx/static/themes/oscar/img/map/marker-shadow.png differ diff --git a/searx/static/themes/oscar/img/searx_logo.png b/searx/static/themes/oscar/img/searx_logo.png new file mode 100644 index 0000000..307b42f Binary files /dev/null and b/searx/static/themes/oscar/img/searx_logo.png differ diff --git a/searx/static/themes/oscar/js/searx_src/00_requirejs_config.js b/searx/static/themes/oscar/js/searx_src/00_requirejs_config.js new file mode 100644 index 0000000..1aa4349 --- /dev/null +++ b/searx/static/themes/oscar/js/searx_src/00_requirejs_config.js @@ -0,0 +1,23 @@ +/** + * searx is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * searx is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with searx. If not, see < http://www.gnu.org/licenses/ >. + * + * (C) 2014 by Thomas Pointhuber, + */ + +requirejs.config({ + baseUrl: './static/themes/oscar/js', + paths: { + app: '../app' + } +}); diff --git a/searx/static/themes/oscar/js/searx_src/autocompleter.js b/searx/static/themes/oscar/js/searx_src/autocompleter.js new file mode 100644 index 0000000..70c66d2 --- /dev/null +++ b/searx/static/themes/oscar/js/searx_src/autocompleter.js @@ -0,0 +1,37 @@ +/** + * searx is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * searx is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with searx. If not, see < http://www.gnu.org/licenses/ >. + * + * (C) 2014 by Thomas Pointhuber, + */ + +if(searx.autocompleter) { + searx.searchResults = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: './autocompleter?q=%QUERY' + }); + searx.searchResults.initialize(); +} + +$(document).ready(function(){ + if(searx.autocompleter) { + $('#q').typeahead(null, { + name: 'search-results', + displayKey: function(result) { + return result; + }, + source: searx.searchResults.ttAdapter() + }); + } +}); diff --git a/searx/static/themes/oscar/js/searx_src/element_modifiers.js b/searx/static/themes/oscar/js/searx_src/element_modifiers.js new file mode 100644 index 0000000..8e42805 --- /dev/null +++ b/searx/static/themes/oscar/js/searx_src/element_modifiers.js @@ -0,0 +1,99 @@ +/** + * searx is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * searx is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with searx. If not, see < http://www.gnu.org/licenses/ >. + * + * (C) 2014 by Thomas Pointhuber, + */ + +$(document).ready(function(){ + /** + * focus element if class="autofocus" and id="q" + */ + $('#q.autofocus').focus(); + + /** + * select full content on click if class="select-all-on-click" + */ + $(".select-all-on-click").click(function () { + $(this).select(); + }); + + /** + * change text during btn-collapse click if possible + */ + $('.btn-collapse').click(function() { + var btnTextCollapsed = $(this).data('btn-text-collapsed'); + var btnTextNotCollapsed = $(this).data('btn-text-not-collapsed'); + + if(btnTextCollapsed !== '' && btnTextNotCollapsed !== '') { + if($(this).hasClass('collapsed')) { + new_html = $(this).html().replace(btnTextCollapsed, btnTextNotCollapsed); + } else { + new_html = $(this).html().replace(btnTextNotCollapsed, btnTextCollapsed); + } + $(this).html(new_html); + } + }); + + /** + * change text during btn-toggle click if possible + */ + $('.btn-toggle .btn').click(function() { + var btnClass = 'btn-' + $(this).data('btn-class'); + var btnLabelDefault = $(this).data('btn-label-default'); + var btnLabelToggled = $(this).data('btn-label-toggled'); + if(btnLabelToggled !== '') { + if($(this).hasClass('btn-default')) { + new_html = $(this).html().replace(btnLabelDefault, btnLabelToggled); + } else { + new_html = $(this).html().replace(btnLabelToggled, btnLabelDefault); + } + $(this).html(new_html); + } + $(this).toggleClass(btnClass); + $(this).toggleClass('btn-default'); + }); + + /** + * change text during btn-toggle click if possible + */ + $('.media-loader').click(function() { + var target = $(this).data('target'); + var iframe_load = $(target + ' > iframe'); + var srctest = iframe_load.attr('src'); + if(srctest === undefined || srctest === false){ + iframe_load.attr('src', iframe_load.data('src')); + } + }); + + /** + * Select or deselect every categories on double clic + */ + $(".btn-sm").dblclick(function() { + var btnClass = 'btn-' + $(this).data('btn-class'); // primary + if($(this).hasClass('btn-default')) { + $(".btn-sm > input").attr('checked', 'checked'); + $(".btn-sm > input").prop("checked", true); + $(".btn-sm").addClass(btnClass); + $(".btn-sm").addClass('active'); + $(".btn-sm").removeClass('btn-default'); + } else { + $(".btn-sm > input").attr('checked', ''); + $(".btn-sm > input").removeAttr('checked'); + $(".btn-sm > input").checked = false; + $(".btn-sm").removeClass(btnClass); + $(".btn-sm").removeClass('active'); + $(".btn-sm").addClass('btn-default'); + } + }); +}); diff --git a/searx/static/themes/oscar/js/searx_src/leaflet_map.js b/searx/static/themes/oscar/js/searx_src/leaflet_map.js new file mode 100644 index 0000000..4be46ac --- /dev/null +++ b/searx/static/themes/oscar/js/searx_src/leaflet_map.js @@ -0,0 +1,167 @@ +/** + * searx is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * searx is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with searx. If not, see < http://www.gnu.org/licenses/ >. + * + * (C) 2014 by Thomas Pointhuber, + */ + +$(document).ready(function(){ + $(".searx_overpass_request").on( "click", function( event ) { + var overpass_url = "https://overpass-api.de/api/interpreter?data="; + var query_start = overpass_url + "[out:json][timeout:25];("; + var query_end = ");out meta;"; + + var osm_id = $(this).data('osm-id'); + var osm_type = $(this).data('osm-type'); + var result_table = $(this).data('result-table'); + var result_table_loadicon = "#" + $(this).data('result-table-loadicon'); + + // tags which can be ignored + var osm_ignore_tags = [ "addr:city", "addr:country", "addr:housenumber", "addr:postcode", "addr:street" ]; + + if(osm_id && osm_type && result_table) { + result_table = "#" + result_table; + var query = null; + switch(osm_type) { + case 'node': + query = query_start + "node(" + osm_id + ");" + query_end; + break; + case 'way': + query = query_start + "way(" + osm_id + ");" + query_end; + break; + case 'relation': + query = query_start + "relation(" + osm_id + ");" + query_end; + break; + default: + break; + } + if(query) { + //alert(query); + var ajaxRequest = $.ajax( query ) + .done(function( html) { + if(html && html.elements && html.elements[0]) { + var element = html.elements[0]; + var newHtml = $(result_table).html(); + for (var row in element.tags) { + if(element.tags.name === null || osm_ignore_tags.indexOf(row) == -1) { + newHtml += "" + row + ""; + switch(row) { + case "phone": + case "fax": + newHtml += "" + element.tags[row] + ""; + break; + case "email": + newHtml += "" + element.tags[row] + ""; + break; + case "website": + case "url": + newHtml += "" + element.tags[row] + ""; + break; + case "wikidata": + newHtml += "" + element.tags[row] + ""; + break; + case "wikipedia": + if(element.tags[row].indexOf(":") != -1) { + newHtml += "" + element.tags[row] + ""; + break; + } + /* jshint ignore:start */ + default: + /* jshint ignore:end */ + newHtml += element.tags[row]; + break; + } + newHtml += ""; + } + } + $(result_table).html(newHtml); + $(result_table).removeClass('hidden'); + $(result_table_loadicon).addClass('hidden'); + } + }) + .fail(function() { + $(result_table_loadicon).html($(result_table_loadicon).html() + "

could not load data!

"); + }); + } + } + + // this event occour only once per element + $( this ).off( event ); + }); + + $(".searx_init_map").on( "click", function( event ) { + var leaflet_target = $(this).data('leaflet-target'); + var map_lon = $(this).data('map-lon'); + var map_lat = $(this).data('map-lat'); + var map_zoom = $(this).data('map-zoom'); + var map_boundingbox = $(this).data('map-boundingbox'); + var map_geojson = $(this).data('map-geojson'); + + require(['leaflet-0.7.3.min'], function(leaflet) { + if(map_boundingbox) { + southWest = L.latLng(map_boundingbox[0], map_boundingbox[2]); + northEast = L.latLng(map_boundingbox[1], map_boundingbox[3]); + map_bounds = L.latLngBounds(southWest, northEast); + } + + // TODO hack + // change default imagePath + L.Icon.Default.imagePath = "./static/themes/oscar/img/map"; + + // init map + var map = L.map(leaflet_target); + + // create the tile layer with correct attribution + var osmMapnikUrl='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; + var osmMapnikAttrib='Map data © OpenStreetMap contributors'; + var osmMapnik = new L.TileLayer(osmMapnikUrl, {minZoom: 1, maxZoom: 19, attribution: osmMapnikAttrib}); + + var osmWikimediaUrl='https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png'; + var osmWikimediaAttrib = 'Wikimedia maps beta | Maps data © OpenStreetMap contributors'; + var osmWikimedia = new L.TileLayer(osmWikimediaUrl, {minZoom: 1, maxZoom: 19, attribution: osmWikimediaAttrib}); + + // init map view + if(map_bounds) { + // TODO hack: https://github.com/Leaflet/Leaflet/issues/2021 + setTimeout(function () { + map.fitBounds(map_bounds, { + maxZoom:17 + }); + }, 0); + } else if (map_lon && map_lat) { + if(map_zoom) + map.setView(new L.LatLng(map_lat, map_lon),map_zoom); + else + map.setView(new L.LatLng(map_lat, map_lon),8); + } + + map.addLayer(osmMapnik); + + var baseLayers = { + "OSM Mapnik": osmMapnik/*, + "OSM Wikimedia": osmWikimedia*/ + }; + + L.control.layers(baseLayers).addTo(map); + + + if(map_geojson) + L.geoJson(map_geojson).addTo(map); + /*else if(map_bounds) + L.rectangle(map_bounds, {color: "#ff7800", weight: 3, fill:false}).addTo(map);*/ + }); + + // this event occour only once per element + $( this ).off( event ); + }); +}); diff --git a/searx/static/themes/oscar/less/logicodev/advanced.less b/searx/static/themes/oscar/less/logicodev/advanced.less new file mode 100644 index 0000000..4c3827b --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/advanced.less @@ -0,0 +1,49 @@ +#advanced-search-container { + display: none; + text-align: left; + margin-bottom: 1rem; + clear: both; + + label, .input-group-addon { + font-size: 1.2rem; + font-weight:normal; + background-color: white; + border: @mild-gray 1px solid; + border-right: none; + color: @dark-gray; + padding-bottom: 0.4rem; + padding-right: 0.7rem; + padding-left: 0.7rem; + } + + label:last-child, .input-group-addon:last-child { + border-right: @mild-gray 1px solid; + } + + input[type="radio"] { + display: none; + } + + input[type="radio"]:checked + label{ + color: @black; + font-weight: bold; + border-bottom: @light-green 5px solid; + } +} + +#check-advanced { + display: none; +} + +#check-advanced:checked ~ #advanced-search-container { + display: block; +} + +.advanced { + padding: 0; + margin-top: 0.3rem; + text-align: right; + label, select { + cursor: pointer; + } +} diff --git a/searx/static/themes/oscar/less/logicodev/checkbox.less b/searx/static/themes/oscar/less/logicodev/checkbox.less new file mode 100644 index 0000000..6428b36 --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/checkbox.less @@ -0,0 +1,9 @@ +// Hide element if checkbox is checked +input[type=checkbox]:checked + .label_hide_if_checked, input[type=checkbox]:checked + .label_hide_if_not_checked + .label_hide_if_checked { + display:none; +} + +// Hide element if checkbox is not checked +input[type=checkbox]:not(:checked) + .label_hide_if_not_checked, input[type=checkbox]:not(:checked) + .label_hide_if_checked + .label_hide_if_not_checked { + display:none; +} diff --git a/searx/static/themes/oscar/less/logicodev/code.less b/searx/static/themes/oscar/less/logicodev/code.less new file mode 100644 index 0000000..96486f5 --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/code.less @@ -0,0 +1,103 @@ +pre, code{ + font-family: 'Ubuntu Mono', 'Courier New', 'Lucida Console', monospace !important; +} + +.lineno{ + margin-right: 5px; +} + +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #556366; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid @orange } /* Error */ +.highlight .k { color: #BE74D5; font-weight: bold } /* Keyword */ +.highlight .o { color: #D19A66 } /* Operator */ +.highlight .cm { color: #556366; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #BC7A00 } /* Comment.Preproc */ +.highlight .c1 { color: #556366; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #556366; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #BE74D5; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #BE74D5; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #BE74D5; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #BE74D5 } /* Keyword.Pseudo */ +.highlight .kr { color: #BE74D5; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #D46C72 } /* Keyword.Type */ +.highlight .m { color: #D19A66 } /* Literal.Number */ +.highlight .s { color: #86C372 } /* Literal.String */ +.highlight .na { color: #7D9029 } /* Name.Attribute */ +.highlight .nb { color: #BE74D5 } /* Name.Builtin */ +.highlight .nc { color: #61AFEF; font-weight: bold } /* Name.Class */ +.highlight .no { color: #D19A66 } /* Name.Constant */ +.highlight .nd { color: #AA22FF } /* Name.Decorator */ +.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #61AFEF } /* Name.Function */ +.highlight .nl { color: #A0A000 } /* Name.Label */ +.highlight .nn { color: #61AFEF; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #BE74D5; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #DFC06F } /* Name.Variable */ +.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #D7DAE0 } /* Text.Whitespace */ +.highlight .mf { color: #D19A66 } /* Literal.Number.Float */ +.highlight .mh { color: #D19A66 } /* Literal.Number.Hex */ +.highlight .mi { color: #D19A66 } /* Literal.Number.Integer */ +.highlight .mo { color: #D19A66 } /* Literal.Number.Oct */ +.highlight .sb { color: #86C372 } /* Literal.String.Backtick */ +.highlight .sc { color: #86C372 } /* Literal.String.Char */ +.highlight .sd { color: #86C372; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #86C372 } /* Literal.String.Double */ +.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #86C372 } /* Literal.String.Heredoc */ +.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #BE74D5 } /* Literal.String.Other */ +.highlight .sr { color: #BB6688 } /* Literal.String.Regex */ +.highlight .s1 { color: #86C372 } /* Literal.String.Single */ +.highlight .ss { color: #DFC06F } /* Literal.String.Symbol */ +.highlight .bp { color: #BE74D5 } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #DFC06F } /* Name.Variable.Class */ +.highlight .vg { color: #DFC06F } /* Name.Variable.Global */ +.highlight .vi { color: #DFC06F } /* Name.Variable.Instance */ +.highlight .il { color: #D19A66 } /* Literal.Number.Integer.Long */ + +.highlight .lineno { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default; + color: #556366; + + &::selection { + background: transparent; /* WebKit/Blink Browsers */ + } + &::-moz-selection { + background: transparent; /* Gecko Browsers */ + } +} + +.highlight pre { + background-color: #282C34; + color: #D7DAE0; + border: none; + margin-bottom: 25px; + font-size: 15px; + padding: 20px 10px; +} + +.highlight { + font-weight: 700; +} + + diff --git a/searx/static/themes/oscar/less/logicodev/cursor.less b/searx/static/themes/oscar/less/logicodev/cursor.less new file mode 100644 index 0000000..cbc1ea6 --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/cursor.less @@ -0,0 +1,8 @@ +// display cursor +.cursor-text { + cursor: text !important; +} + +.cursor-pointer { + cursor: pointer !important; +} diff --git a/searx/static/themes/oscar/less/logicodev/footer.less b/searx/static/themes/oscar/less/logicodev/footer.less new file mode 100644 index 0000000..d23a0cc --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/footer.less @@ -0,0 +1,30 @@ +// Sticky footer styles +*{ + border-radius: 0 !important; +} +html { + position: relative; + min-height: 100%; + color: @black; +} + +body { + /* Margin bottom by footer height */ + font-family: 'Roboto', Helvetica, Arial, sans-serif; + margin-bottom: 80px; + background-color: white; + + a{ + color: @blue; + } +} + +.footer { + position: absolute; + bottom: 0; + width: 100%; + /* Set the fixed height of the footer here */ + height: 60px; + text-align: center; + color: #999; +} diff --git a/searx/static/themes/oscar/less/logicodev/infobox.less b/searx/static/themes/oscar/less/logicodev/infobox.less new file mode 100644 index 0000000..0d488d7 --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/infobox.less @@ -0,0 +1,37 @@ +.infobox { + + .panel-heading{ + background-color: @dim-gray; + + .panel-title{ + font-weight: 700; + } + } + + + p{ + font-family: "DejaVu Serif", Georgia, Cambria, "Times New Roman", Times, serif !important; + font-style: italic; + } + + .btn{ + background-color: @green; + border: none; + + a{ + color: white; + margin: 5px; + } + } + + .infobox_part { + margin-bottom: 20px; + word-wrap: break-word; + table-layout: fixed; + + } + + .infobox_part:last-child { + margin-bottom: 0; + } +} diff --git a/searx/static/themes/oscar/less/logicodev/navbar.less b/searx/static/themes/oscar/less/logicodev/navbar.less new file mode 100644 index 0000000..5da7115 --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/navbar.less @@ -0,0 +1,31 @@ +.searx-navbar { + background: @black; + height: 2.3rem; + font-size: 1.3rem; + line-height: 1.3rem; + padding: 0.5rem; + font-weight: bold; + margin-bottom: 0.8rem; + + a, a:hover { + margin-right: 2.0rem; + color: white; + text-decoration: none; + } + + .instance a { + color: @light-green; + margin-left: 2.0rem; + } +} + +#main-logo { + margin-top: 20vh; + margin-bottom: 25px; + + & > img { + max-width: 350px; + width: 80%; + } +} + diff --git a/searx/static/themes/oscar/less/logicodev/onoff.less b/searx/static/themes/oscar/less/logicodev/onoff.less new file mode 100644 index 0000000..f471892 --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/onoff.less @@ -0,0 +1,57 @@ +.onoff-checkbox { + width:15%; +} +.onoffswitch { + position: relative; + width: 110px; + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select: none; +} +.onoffswitch-checkbox { + display: none; +} +.onoffswitch-label { + display: block; + overflow: hidden; + cursor: pointer; + border: 2px solid #FFFFFF !important; + border-radius: 50px !important; +} +.onoffswitch-inner { + display: block; + transition: margin 0.3s ease-in 0s; +} + +.onoffswitch-inner:before, .onoffswitch-inner:after { + display: block; + float: left; + width: 50%; + height: 30px; + padding: 0; + line-height: 40px; + font-size: 20px; + box-sizing: border-box; + content: ""; + background-color: #EEEEEE; +} + +.onoffswitch-switch { + display: block; + width: 37px; + background-color: @light-green; + position: absolute; + top: 0; + bottom: 0; + right: 0px; + border: 2px solid #FFFFFF !important; + border-radius: 50px !important; + transition: all 0.3s ease-in 0s; +} +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { + margin-right: 0; +} +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { + right: 71px; + background-color: #A1A1A1; +} diff --git a/searx/static/themes/oscar/less/logicodev/oscar.less b/searx/static/themes/oscar/less/logicodev/oscar.less new file mode 100644 index 0000000..55181cb --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/oscar.less @@ -0,0 +1,21 @@ +@import "variables.less"; + +@import "navbar.less"; + +@import "footer.less"; + +@import "checkbox.less"; + +@import "onoff.less"; + +@import "results.less"; + +@import "infobox.less"; + +@import "search.less"; + +@import "advanced.less"; + +@import "cursor.less"; + +@import "code.less"; diff --git a/searx/static/themes/oscar/less/logicodev/results.less b/searx/static/themes/oscar/less/logicodev/results.less new file mode 100644 index 0000000..3b36a17 --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/results.less @@ -0,0 +1,168 @@ +.result_header { + margin-top: 0px; + margin-bottom: 2px; + font-size: 16px; + + .favicon { + margin-bottom:-3px; + } + + a { + color: @black; + text-decoration: none; + + &:hover{ + color: @blue; + } + + &:visited{ + color: @violet; + } + + .highlight { + background-color: @dim-gray; + // Chrome hack: bold is different size than normal + // https://stackoverflow.com/questions/20713988/weird-text-alignment-issue-in-css-when-bolded-lucida-sans + } + } +} + +.result-content { + margin-top: 2px; + margin-bottom: 0; + word-wrap: break-word; + color: @dark-gray; + font-size: 13px; + + + .highlight { + font-weight:bold; + } + +} + +.external-link { + color: @dark-green; + font-size: 12px; + + a { + margin-right: 3px; + } +} + +// default formating of results +.result-default, .result-code, .result-torrent, .result-videos, .result-map { + clear: both; + padding: 2px 4px; + &:hover{ + background-color: @dim-gray; + } +} + + +// image formating of results +.result-images { + float: left !important; + width: 24%; + margin: .5%; + a { + display: block; + width: 100%; + background-size: cover; + } +} + +.img-thumbnail { + margin: 5px; + max-height: 128px; + min-height: 128px; +} + +// video formating of results +.result-videos { + clear: both; + + hr{ + margin: 5px 0 15px 0; + } + + .collapse{ + width: 100%; + } + + .in{ + margin-bottom: 8px; + } +} + +// torrent formating of results +.result-torrent { + clear: both; + + b{ + margin-right: 5px; + margin-left: 5px; + } + + .seeders{ + color: @green; + } + + .leechers{ + color: @red; + } +} + +// map formating of results +.result-map { + clear: both; +} + +// code formating of results +.result-code { + clear: both; + + .code-fork, .code-fork a{ + color: @dark-gray; + } + +} + +// suggestion +.suggestion_item { + margin: 2px 5px; +} + +// download result +.result_download { + margin-right: 5px; +} + +// page forward, backward +#pagination { + margin-top: 30px; + padding-bottom: 60px; +} + +.label-default { + color: @gray; + background: transparent; +} + +.result .text-muted small { + word-wrap: break-word; +} + +.modal-wrapper { + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); +} + +.modal-wrapper { + background-clip: padding-box; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + outline: 0 none; + position: relative; +} diff --git a/searx/static/themes/oscar/less/logicodev/search.less b/searx/static/themes/oscar/less/logicodev/search.less new file mode 100644 index 0000000..fa1e0e8 --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/search.less @@ -0,0 +1,79 @@ +.search_categories, #categories { + text-transform: capitalize; + margin-bottom: 0.5rem; + display: flex; + flex-wrap: wrap; + flex-flow: row wrap; + align-content: stretch; + + label, .input-group-addon { + flex-grow: 1; + flex-basis: auto; + font-size: 1.2rem; + font-weight: normal; + background-color: white; + border: @mild-gray 1px solid; + border-right: none; + color: @dark-gray; + padding-bottom: 0.4rem; + padding-top: 0.4rem; + text-align: center; + } + label:last-child, .input-group-addon:last-child { + border-right: @mild-gray 1px solid; + } + + input[type="checkbox"]:checked + label { + color: @black; + font-weight: bold; + border-bottom: @light-green 5px solid; + } +} + +#main-logo{ + margin-top: 10vh; + margin-bottom: 25px; +} + +#main-logo > img { + max-width: 350px; + width: 80%; +} + +#q{ + box-shadow: none; + border-right: none; + border-color: @gray; +} + + #search_form .input-group-btn .btn{ + border-color: @gray; + } + + #search_form .input-group-btn .btn:hover{ + background-color: @green; + color: white; + } + +.custom-select { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + font-size: 1.2rem; + font-weight:normal; + background-color: white; + border: @mild-gray 1px solid; + color: @dark-gray; + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAQAAACR313BAAAABGdBTUEAALGPC/xhBQAAACBjSFJN +AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAJcEhZ +cwAABFkAAARZAVnbJUkAAAAHdElNRQfgBxgLDwB20OFsAAAAbElEQVQY073OsQ3CMAAEwJMYwJGn +sAehpoXJItltBkmcdZBYgIIiQoLglnz3ui+eP+bk5uneteTMZJa6OJuIqvYzSJoqwqBq8gdmTTW8 +6/dghxAUq4xsVYT9laBYXCw93Aajh7GPEF23t4fkBYevGFTANkPRAAAAJXRFWHRkYXRlOmNyZWF0 +ZQAyMDE2LTA3LTI0VDExOjU1OjU4KzAyOjAwRFqFOQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNi0w +Ny0yNFQxMToxNTowMCswMjowMP7RDgQAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb +7jwaAAAAAElFTkSuQmCC) 96% no-repeat; +} + +.search-margin { + margin-bottom: 0.6em; +} \ No newline at end of file diff --git a/searx/static/themes/oscar/less/logicodev/variables.less b/searx/static/themes/oscar/less/logicodev/variables.less new file mode 100644 index 0000000..5966ee6 --- /dev/null +++ b/searx/static/themes/oscar/less/logicodev/variables.less @@ -0,0 +1,13 @@ +@black: #29314D; +@gray: #A4A4A4; +@dim-gray: #F6F9FA; +@dark-gray: #666; +@middle-gray: #F5F5F5; +@mild-gray: #DDD; +@blue: #0088CC; +@red: #F35E77; +@violet: #684898; +@dark-green: #069025; +@green: #2ecc71; +@light-green: #01D7D4; +@orange: #FFA92F; diff --git a/searx/static/themes/oscar/less/pointhi/advanced.less b/searx/static/themes/oscar/less/pointhi/advanced.less new file mode 100644 index 0000000..23bfdb0 --- /dev/null +++ b/searx/static/themes/oscar/less/pointhi/advanced.less @@ -0,0 +1,49 @@ +#advanced-search-container { + display: none; + text-align: center; + margin-bottom: 1rem; + clear: both; + + label, .input-group-addon { + font-size: 1.3rem; + font-weight:normal; + background-color: white; + border: #DDD 1px solid; + border-right: none; + color: #333; + padding-bottom: 0.8rem; + padding-left: 1.2rem; + padding-right: 1.2rem; + } + + label:last-child, .input-group-addon:last-child { + border-right: #DDD 1px solid; + } + + input[type="radio"] { + display: none; + } + + input[type="radio"]:checked + label { + color: black; + font-weight: bold; + background-color: #EEE; + } +} + +#check-advanced { + display: none; +} + +#check-advanced:checked ~ #advanced-search-container { + display: block; +} + +.advanced { + padding: 0; + margin-top: 0.3rem; + text-align: right; + label, select { + cursor: pointer; + } +} diff --git a/searx/static/themes/oscar/less/pointhi/checkbox.less b/searx/static/themes/oscar/less/pointhi/checkbox.less new file mode 100644 index 0000000..6428b36 --- /dev/null +++ b/searx/static/themes/oscar/less/pointhi/checkbox.less @@ -0,0 +1,9 @@ +// Hide element if checkbox is checked +input[type=checkbox]:checked + .label_hide_if_checked, input[type=checkbox]:checked + .label_hide_if_not_checked + .label_hide_if_checked { + display:none; +} + +// Hide element if checkbox is not checked +input[type=checkbox]:not(:checked) + .label_hide_if_not_checked, input[type=checkbox]:not(:checked) + .label_hide_if_checked + .label_hide_if_not_checked { + display:none; +} diff --git a/searx/static/themes/oscar/less/pointhi/code.less b/searx/static/themes/oscar/less/pointhi/code.less new file mode 100644 index 0000000..90a2cd6 --- /dev/null +++ b/searx/static/themes/oscar/less/pointhi/code.less @@ -0,0 +1,79 @@ +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #408080; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #008000; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #BC7A00 } /* Comment.Preproc */ +.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #008000 } /* Keyword.Pseudo */ +.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #B00040 } /* Keyword.Type */ +.highlight .m { color: #666666 } /* Literal.Number */ +.highlight .s { color: #BA2121 } /* Literal.String */ +.highlight .na { color: #7D9029 } /* Name.Attribute */ +.highlight .nb { color: #008000 } /* Name.Builtin */ +.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.highlight .no { color: #880000 } /* Name.Constant */ +.highlight .nd { color: #AA22FF } /* Name.Decorator */ +.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #0000FF } /* Name.Function */ +.highlight .nl { color: #A0A000 } /* Name.Label */ +.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #19177C } /* Name.Variable */ +.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #BB6688 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ + +.highlight .lineno { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default; + + &::selection { + background: transparent; /* WebKit/Blink Browsers */ + } + &::-moz-selection { + background: transparent; /* Gecko Browsers */ + } +} diff --git a/searx/static/themes/oscar/less/pointhi/cursor.less b/searx/static/themes/oscar/less/pointhi/cursor.less new file mode 100644 index 0000000..cbc1ea6 --- /dev/null +++ b/searx/static/themes/oscar/less/pointhi/cursor.less @@ -0,0 +1,8 @@ +// display cursor +.cursor-text { + cursor: text !important; +} + +.cursor-pointer { + cursor: pointer !important; +} diff --git a/searx/static/themes/oscar/less/pointhi/footer.less b/searx/static/themes/oscar/less/pointhi/footer.less new file mode 100644 index 0000000..0b25e73 --- /dev/null +++ b/searx/static/themes/oscar/less/pointhi/footer.less @@ -0,0 +1,19 @@ +// Sticky footer styles + +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 80px; +} + +.footer { + position: absolute; + bottom: 0; + width: 100%; + /* Set the fixed height of the footer here */ + height: 60px; +} diff --git a/searx/static/themes/oscar/less/pointhi/infobox.less b/searx/static/themes/oscar/less/pointhi/infobox.less new file mode 100644 index 0000000..41375f2 --- /dev/null +++ b/searx/static/themes/oscar/less/pointhi/infobox.less @@ -0,0 +1,11 @@ +.infobox { + .infobox_part { + margin-bottom: 20px; + word-wrap: break-word; + table-layout: fixed; + } + + .infobox_part:last-child { + margin-bottom: 0; + } +} diff --git a/searx/static/themes/oscar/less/pointhi/navbar.less b/searx/static/themes/oscar/less/pointhi/navbar.less new file mode 100644 index 0000000..a057f82 --- /dev/null +++ b/searx/static/themes/oscar/less/pointhi/navbar.less @@ -0,0 +1,20 @@ +.searx-navbar { + background: #eee; + color: #aaa; + height: 2.3rem; + font-size: 1.3rem; + line-height: 1.3rem; + padding: 0.5rem; + font-weight: bold; + margin-bottom: 1.3rem; + + a, a:hover { + margin-right: 2.0rem; + text-decoration: none; + } + + .instance a { + color: #444; + margin-left: 2.0rem; + } +} diff --git a/searx/static/themes/oscar/less/pointhi/onoff.less b/searx/static/themes/oscar/less/pointhi/onoff.less new file mode 100644 index 0000000..72b289a --- /dev/null +++ b/searx/static/themes/oscar/less/pointhi/onoff.less @@ -0,0 +1,57 @@ +.onoff-checkbox { + width:15%; +} +.onoffswitch { + position: relative; + width: 110px; + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select: none; +} +.onoffswitch-checkbox { + display: none; +} +.onoffswitch-label { + display: block; + overflow: hidden; + cursor: pointer; + border: 2px solid #FFFFFF !important; + border-radius: 50px !important; +} +.onoffswitch-inner { + display: block; + transition: margin 0.3s ease-in 0s; +} + +.onoffswitch-inner:before, .onoffswitch-inner:after { + display: block; + float: left; + width: 50%; + height: 30px; + padding: 0; + line-height: 40px; + font-size: 20px; + box-sizing: border-box; + content: ""; + background-color: #EEEEEE; +} + +.onoffswitch-switch { + display: block; + width: 37px; + background-color: #00CC00; + position: absolute; + top: 0; + bottom: 0; + right: 0px; + border: 2px solid #FFFFFF !important; + border-radius: 50px !important; + transition: all 0.3s ease-in 0s; +} +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { + margin-right: 0; +} +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { + right: 71px; + background-color: #A1A1A1; +} diff --git a/searx/static/themes/oscar/less/pointhi/oscar.less b/searx/static/themes/oscar/less/pointhi/oscar.less new file mode 100644 index 0000000..4e2fee1 --- /dev/null +++ b/searx/static/themes/oscar/less/pointhi/oscar.less @@ -0,0 +1,19 @@ +@import "footer.less"; + +@import "checkbox.less"; + +@import "onoff.less"; + +@import "results.less"; + +@import "infobox.less"; + +@import "search.less"; + +@import "advanced.less"; + +@import "cursor.less"; + +@import "code.less"; + +@import "navbar.less"; diff --git a/searx/static/themes/oscar/less/pointhi/results.less b/searx/static/themes/oscar/less/pointhi/results.less new file mode 100644 index 0000000..beea353 --- /dev/null +++ b/searx/static/themes/oscar/less/pointhi/results.less @@ -0,0 +1,101 @@ + +.result_header { + margin-bottom:5px; + margin-top:20px; + + .favicon { + margin-bottom:-3px; + } + + a { + vertical-align: bottom; + + .highlight { + font-weight:bold; + } + } +} + +.result-content { + margin-top: 5px; + word-wrap: break-word; + + .highlight { + font-weight:bold; + } +} + +// default formating of results +.result-default { + clear: both; +} + +// image formating of results +.result-images { + float: left !important; +} + +.img-thumbnail { + margin: 5px; + max-height: 128px; + min-height: 128px; +} + +// video formating of results +.result-videos { + clear: both; +} + +// torrent formating of results +.result-torrents { + clear: both; +} + +// map formating of results +.result-map { + clear: both; +} + +// code formating of results +.result-code { + clear: both; +} + +// suggestion +.suggestion_item { + margin: 2px 5px; +} + +// download result +.result_download { + margin-right: 5px; +} + +// page forward, backward +#pagination { + margin-top: 30px; + padding-bottom: 50px; +} + +.label-default { + color: #AAA; + background: #FFF; +} + +.result .text-muted small { + word-wrap: break-word; +} + +.modal-wrapper { + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); +} + +.modal-wrapper { + background-clip: padding-box; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + outline: 0 none; + position: relative; +} diff --git a/searx/static/themes/oscar/less/pointhi/search.less b/searx/static/themes/oscar/less/pointhi/search.less new file mode 100644 index 0000000..cea6799 --- /dev/null +++ b/searx/static/themes/oscar/less/pointhi/search.less @@ -0,0 +1,32 @@ +.search_categories, #categories { + text-transform: capitalize; + margin-bottom: 1.5rem; + margin-top: 1.5rem; + display: flex; + flex-wrap: wrap; + align-content: stretch; + + label, .input-group-addon { + flex-grow: 1; + flex-basis: auto; + font-size: 1.3rem; + font-weight: normal; + background-color: white; + border: #DDD 1px solid; + border-right: none; + color: #333; + padding-bottom: 0.8rem; + padding-top: 0.8rem; + text-align: center; + } + + label:last-child, .input-group-addon:last-child { + border-right: #DDD 1px solid; + } + + input[type="checkbox"]:checked + label{ + color: black; + font-weight: bold; + background-color: #EEE; + } +} diff --git a/searx/static/themes/oscar/package.json b/searx/static/themes/oscar/package.json new file mode 100644 index 0000000..7eae9df --- /dev/null +++ b/searx/static/themes/oscar/package.json @@ -0,0 +1,16 @@ +{ + "devDependencies": { + "grunt": "~0.4.5", + "grunt-contrib-uglify": "~0.6.0", + "grunt-contrib-watch" : "~0.6.1", + "grunt-contrib-concat" : "~0.5.0", + "grunt-contrib-jshint" : "~0.10.0", + "grunt-contrib-less" : "~0.11.0" + }, + + "scripts": { + "build": "npm install && grunt", + "start": "grunt watch", + "test": "grunt" + } +} diff --git a/searx/static/themes/pix-art/img/favicon.png b/searx/static/themes/pix-art/img/favicon.png new file mode 100644 index 0000000..3818d3d Binary files /dev/null and b/searx/static/themes/pix-art/img/favicon.png differ diff --git a/searx/static/themes/pix-art/img/preference-icon-pixel.png b/searx/static/themes/pix-art/img/preference-icon-pixel.png new file mode 100644 index 0000000..424e01e Binary files /dev/null and b/searx/static/themes/pix-art/img/preference-icon-pixel.png differ diff --git a/searx/static/themes/pix-art/img/search-icon-pixel.png b/searx/static/themes/pix-art/img/search-icon-pixel.png new file mode 100644 index 0000000..8235882 Binary files /dev/null and b/searx/static/themes/pix-art/img/search-icon-pixel.png differ diff --git a/searx/static/themes/pix-art/img/searx-pixel-small.png b/searx/static/themes/pix-art/img/searx-pixel-small.png new file mode 100644 index 0000000..75b476c Binary files /dev/null and b/searx/static/themes/pix-art/img/searx-pixel-small.png differ diff --git a/searx/static/themes/pix-art/img/searx-pixel.png b/searx/static/themes/pix-art/img/searx-pixel.png new file mode 100644 index 0000000..6aee581 Binary files /dev/null and b/searx/static/themes/pix-art/img/searx-pixel.png differ diff --git a/searx/static/themes/pix-art/js/searx.js b/searx/static/themes/pix-art/js/searx.js new file mode 100644 index 0000000..5eb0af9 --- /dev/null +++ b/searx/static/themes/pix-art/js/searx.js @@ -0,0 +1,141 @@ +if(searx.autocompleter) { + window.addEvent('domready', function() { + new Autocompleter.Request.JSON('q', '/autocompleter', { + postVar:'q', + postData:{ + 'format': 'json' + }, + ajaxOptions:{ + timeout: 5 // Correct option? + }, + 'minLength': 4, + 'selectMode': false, + cache: true, + delay: 300 + }); + }); +} + +(function (w, d) { + 'use strict'; + function addListener(el, type, fn) { + if (el.addEventListener) { + el.addEventListener(type, fn, false); + } else { + el.attachEvent('on' + type, fn); + } + } + + function placeCursorAtEnd() { + if (this.setSelectionRange) { + var len = this.value.length * 2; + this.setSelectionRange(len, len); + } + } + + addListener(w, 'load', function () { + var qinput = d.getElementById('q'); + if (qinput !== null && qinput.value === "") { + addListener(qinput, 'focus', placeCursorAtEnd); + qinput.focus(); + } + }); + + if (!!('ontouchstart' in window)) { + document.getElementsByTagName("html")[0].className += " touch"; + } + +})(window, document); + +var xmlHttp + +function GetXmlHttpObject(){ + + var xmlHttp = null; + + try { + // Firefox, Opera 8.0+, Safari + xmlHttp = new XMLHttpRequest(); + } + catch (e) { + // Internet Explorer + try { + xmlHttp = new ActiveXObject("Msxml2.XMLHTTP"); + } + catch (e){ + xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + } + return xmlHttp; +} + +var timer; + +// Load more results +function load_more(query,page){ + + xmlHttp = GetXmlHttpObject(); + clearTimeout(timer); + + if(xmlHttp == null){ + alert ("Your browser does not support AJAX!"); + return; + } + + favicons[page] = []; + + xmlHttp.onreadystatechange = function(){ + + var loader = document.getElementById('load_more'); + + // If 4, response OK + if (xmlHttp.readyState == 4){ + + var res = xmlHttp.responseText; + + clearTimeout(timer); + timer = setTimeout(function(){},6000); + + var results = document.getElementById('results_list'); + + var newNode = document.createElement('span'); + newNode.innerHTML = res; + results_list.appendChild(newNode); + + var scripts = newNode.getElementsByTagName('script'); + for (var ix = 0; ix < scripts.length; ix++) { + eval(scripts[ix].text); + } + + load_images(page); + document.getElementById("load_more").onclick = function() { load_more(query, (page+1)); } + loader.removeAttribute("disabled"); + + } else { + loader.disabled = 'disabled'; + } + } + var url = "/"; + var params = "q="+query+"&pageno="+page+"&category_general=1&category_files=1&category_images=1&category_it=1&category_map=1&category_music=1&category_news=1&category_social+media=1&category_videos=1"; + xmlHttp.open("POST",url,true); + xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xmlHttp.setRequestHeader("Content-length", params.length); + xmlHttp.setRequestHeader("Connection", "close"); + xmlHttp.send(params); +} + +// Load the images on the canvas in the page +function load_images(page){ + var arrayLength = favicons[page].length; + for (var i = 1; i < arrayLength+1; i++) { + var img = new Image(); + img.setAttribute("i",i) + img.onload = function () { + var id = 'canvas-'+page+'-'+this.getAttribute("i"); + var can = document.getElementById(id); + var ctx = can.getContext("2d"); + ctx.drawImage(this, 0, 0, 16, 16); + }; + img.src = favicons[page][i]; + } +} \ No newline at end of file diff --git a/searx/static/themes/pix-art/less/definitions.less b/searx/static/themes/pix-art/less/definitions.less new file mode 100644 index 0000000..0ac0cc9 --- /dev/null +++ b/searx/static/themes/pix-art/less/definitions.less @@ -0,0 +1,119 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + * + * To change the colors of the site, simple edit this variables + */ + +/// Basic Colors + +@color-base: #3498DB; +@color-base-dark: #2980B9; +@color-base-light: #ECF0F1; +@color-highlight: #094089; +@color-black: #000000; + +/// General + +@color-font: #444; +@color-font-light: #888; + +@color-red: #C0392B; + +@color-url-font: #1a11be; +@color-url-visited-font: #8E44AD; +@results-width: 50em; + + +/// Start-Screen + +// hmarg +@color-hmarg-border: @color-base; +@color-hmarg-font: @color-base; +@color-hmarg-font-hover: @color-base; + + +/// Search-Input + +@color-search-border: @color-base; +@color-search-background: #FFF; +@color-search-font: #222; + +/// Autocompleter + +@color-autocompleter-choices-background: #FFF; +@color-autocompleter-choices-border: @color-base; +@color-autocompleter-choices-border-left-right: @color-base; +@color-autocompleter-choices-border-bottom: @color-base; + +@color-autocompleter-choices-font: #444; + +/// Answers +@color-answers-border: @color-base-dark; + +// Selected +@color-autocompleter-selected-background: #444; +@color-autocompleter-selected-font: #FFF; +@color-autocompleter-selected-queried-font: #9FCFFF; + +/// Categories + +@color-categories-item-selected: @color-base; +@color-categories-item-selected-font: #FFF; + +@color-categories-item-border-selected: @color-base-dark; +@color-categories-item-border-unselected: #E8E7E6; +@color-categories-item-border-unselected-hover: @color-base; + + +/// Results + +@color-suggestions-button-background: @color-base; +@color-suggestions-button-font: #FFF; + +@color-download-button-background: @color-base; +@color-download-button-font: #FFF; + +@color-result-search-background: @color-base-light; + +@color-result-definition-border: gray; +@color-result-torrent-border: lightgray; +@color-result-top-border: #E8E7E6; + +// Link to result +@color-result-link-font: @color-base-dark; +@color-result-link-visited-font: @color-url-visited-font; + +// Url to result +@color-result-url-font: @color-red; + +// Publish Date +@color-result-publishdate-font: @color-font-light; + +// Images +@color-result-image-span-background-hover: rgba(0, 0, 0, 0.6); +@color-result-image-span-font: #FFF; + +// Search-URL +@color-result-search-url-border: #888; +@color-result-search-url-font: #444; + + +/// Settings + +@color-settings-fieldset: @color-base; +@color-settings-tr-hover: #DDD; + +// Labels +@color-settings-label-allowed-background: #E74C3C; +@color-settings-label-allowed-font: #FFF; + +@color-settings-label-deny-background: #2ECC71; +@color-settings-label-deny-font: @color-font; + +@color-settings-return-background: @color-base; +@color-settings-return-font: #FFF; + +/// Other + +@color-engines-font: @color-font-light; +@color-percentage-div-background: #444; diff --git a/searx/static/themes/pix-art/less/mixins.less b/searx/static/themes/pix-art/less/mixins.less new file mode 100644 index 0000000..dbccce6 --- /dev/null +++ b/searx/static/themes/pix-art/less/mixins.less @@ -0,0 +1,27 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + */ + +// Mixins + +.text-size-adjust (@property: 100%) { + -webkit-text-size-adjust: @property; + -ms-text-size-adjust: @property; + -moz-text-size-adjust: @property; + text-size-adjust: @property; +} + +.rounded-corners (@radius: 4px) { + -webkit-border-radius: @radius; + -moz-border-radius: @radius; + border-radius: @radius; +} + +.user-select () { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/searx/static/themes/pix-art/less/search.less b/searx/static/themes/pix-art/less/search.less new file mode 100644 index 0000000..f5ac33e --- /dev/null +++ b/searx/static/themes/pix-art/less/search.less @@ -0,0 +1,57 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + */ + +.search { + padding: 0; + margin: 0; +} + +#search_wrapper { + position: relative; + width: @results-width; + padding: 10px; +} + +.center #search_wrapper { + margin-left: auto; + margin-right: auto; +} + +.q { + background: none repeat scroll 0 0 @color-search-background; + border: 1px solid @color-search-border; + color: @color-search-font; + font-size: 16px; + font-family: "Courier New", Courier, monospace; + height: 28px; + margin: 0; + outline: medium none; + padding: 2px; + padding-left: 8px; + padding-right: 0px !important; + width: 100%; + z-index: 2; +} + +#search_submit { + position: absolute; + top: 15px; + right: 5px; + padding: 0; + border: 0; + background: url('../img/search-icon-pixel.png') no-repeat; + background-size: 24px 24px; + opacity: 0.8; + width: 24px; + height: 24px; + font-size: 0; +} + +@media screen and (max-width: @results-width) { + #search_wrapper { + width: 90%; + clear:both; + overflow: hidden + } +} diff --git a/searx/static/themes/pix-art/less/style.less b/searx/static/themes/pix-art/less/style.less new file mode 100644 index 0000000..a2088e9 --- /dev/null +++ b/searx/static/themes/pix-art/less/style.less @@ -0,0 +1,451 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + * + * To convert "style.less" to "style.css" run: $make styles + */ + +@import "definitions.less"; + +@import "mixins.less"; + + +// Main LESS-Code + +html { + font-family: "Courier New", Courier, monospace; + font-size: 0.9em; + .text-size-adjust; + color: @color-font; + padding: 0; + margin: 0; +} + +body, #container { + padding: 0; + margin: 0; +} + +canvas { + image-rendering: optimizeSpeed; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-optimize-contrast; + image-rendering: optimize-contrast; + image-rendering: pixelated; + -ms-interpolation-mode: nearest-neighbor; + width:32px; + height:32px; +} + +#container { + width: 100%; + position: absolute; + top: 0; +} + +// Search-Field + +@import "search.less"; + +.row { + max-width: 800px; + margin: 20px auto; + text-align: justify; + + h1 { + font-size: 3em; + margin-top: 50px; + } + + p { + padding: 0 10px; + max-width: 700px; + } + + h3,ul { + margin: 4px 8px; + } +} + +.hmarg { + margin: 0 20px; + border: 1px solid @color-hmarg-border; + padding: 4px 10px; +} + +a { + &:link.hmarg { + color: @color-hmarg-font; + } + + &:visited.hmarg { + color: @color-hmarg-font; + } + + &:active.hmarg { + color: @color-hmarg-font-hover; + } + + &:hover.hmarg { + color: @color-hmarg-font-hover; + } +} + +.top_margin { + margin-top: 60px; +} + +.center { + text-align: center; +} + +h1 { + font-size: 5em; +} + +div.title { + background: url('../img/searx-pixel.png') no-repeat; + width: 100%; + min-height: 80px; + background-position: center; + + h1 { + visibility: hidden; + } +} + +input[type="button"], +input[type="submit"] { + font-family: "Courier New", Courier, monospace; + padding: 4px 12px; + margin: 2px 4px; + display: inline-block; + background: @color-download-button-background; + color: @color-download-button-font; + .rounded-corners; + border: 0; + cursor: pointer; +} + +input[type="button"]:disabled { + cursor: progress; +} + +input[type="checkbox"] { + visibility: hidden; +} + +fieldset { + margin: 8px; + border: 1px solid @color-settings-fieldset; +} + +#logo { + position: absolute; + top: 13px; + left: 10px; +} + +#categories { + margin: 0 10px; + .user-select; +} + +.checkbox_container { + display: inline-block; + position: relative; + margin: 0 3px; + padding: 0px; + + input { + display: none; + } +} + +.checkbox_container label, .engine_checkbox label { + cursor: pointer; + padding: 4px 10px; + margin: 0; + display: block; + text-transform: capitalize; + .user-select; +} + +.checkbox_container input[type="checkbox"]:checked + label { + background: @color-categories-item-selected; + color: @color-categories-item-selected-font; +} + +.engine_checkbox { + padding: 4px; +} + +label { + &.allow { + background: @color-settings-label-allowed-background; + padding: 4px 8px; + color: @color-settings-label-allowed-font; + display: none; + } + + &.deny { + background: @color-settings-label-deny-background; + padding: 4px 8px; + color: @color-settings-label-deny-font; + display: inline; + } +} + +.engine_checkbox input[type="checkbox"]:checked + label { + &:nth-child(2) + label { + display: none; + } + + &.allow { + display: inline; + } +} + +a { + text-decoration: none; + color: @color-url-font; + + &:visited { + color: @color-url-visited-font; + } +} + +.engines { + color: @color-engines-font; +} + +.small_font { + font-size: 0.8em; +} + +.small p { + margin: 2px 0; +} + +.right { + float: right; +} + +.invisible { + display: none; +} + +.left { + float: left; +} + +.highlight { + color: @color-highlight; +} + +.content .highlight { + color: @color-black; +} + +.percentage { + position: relative; + width: 300px; + + div { + background: @color-percentage-div-background; + } +} + +table { + width: 100%; +} + +td { + padding: 0 4px; +} + +tr { + &:hover { + background: @color-settings-tr-hover; + } +} + +#results { + margin: auto; + padding: 0; + width: @results-width; + margin-bottom: 20px; +} + +#search_url { + margin-top: 8px; + + input { + border: 1px solid @color-result-search-url-border; + padding: 4px; + color: @color-result-search-url-font; + width: 14em; + display: block; + margin: 4px; + font-size: 0.8em; + } +} + +#preferences { + top: 10px; + padding: 0; + border: 0; + background: url('../img/preference-icon-pixel.png') no-repeat; + background-size: 28px 28px; + opacity: 0.8; + width: 28px; + height: 30px; + display: block; + + * { + display: none; + } +} + +#pagination { + clear: both; + text-align: center; + br { + clear: both; + } +} + +#apis { + margin-top: 8px; + clear: both; +} + +#categories_container { + position: relative; +} + +@media screen and (max-width: @results-width) { + + #results { + margin: auto; + padding: 0; + width: 90%; + } + + .checkbox_container { + display: block; + width: 90%; + //float: left; + + label { + border-bottom: 0; + } + } + + .preferences_container { + display: none; + postion: fixed !important; + top: 100px; + right: 0px; + } + +} + +@media screen and (max-width: 75em) { + + div.title { + + h1 { + font-size: 1em; + } + } + + html.touch #categories { + width: 95%; + height: 30px; + text-align: left; + overflow-x: scroll; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + + #categories_container { + width: 1000px; + width: -moz-max-content; + width: -webkit-max-content; + width: max-content; + + .checkbox_container { + display: inline-block; + width: auto; + } + } + } + + #categories { + font-size: 90%; + clear: both; + + .checkbox_container { + margin-top: 2px; + margin: auto; + } + } + + #categories { + font-size: 90%; + clear: both; + + .checkbox_container { + margin-top: 2px; + margin: auto; + } + } + + #apis { + display: none; + } + + #search_url { + display: none; + } + + #logo { + display: none; + } +} + +.favicon { + float: left; + margin-right: 4px; + margin-top: 2px; +} + +.preferences_back { + background: none repeat scroll 0 0 @color-settings-return-background; + border: 0 none; + .rounded-corners; + cursor: pointer; + display: inline-block; + margin: 2px 4px; + padding: 4px 6px; + + a { + color: @color-settings-return-font; + } +} + +.hidden { + opacity: 0; + overflow: hidden; + font-size: 0.8em; + position: absolute; + bottom: -20px; + width: 100%; + text-position: center; + background: white; + transition: opacity 1s ease; +} + +#categories_container:hover .hidden { + transition: opacity 1s ease; + opacity: 0.8; +} diff --git a/searx/templates/__common__/about.html b/searx/templates/__common__/about.html new file mode 100644 index 0000000..d8afab7 --- /dev/null +++ b/searx/templates/__common__/about.html @@ -0,0 +1,62 @@ + +

About searx

+ +

Searx is a metasearch engine, aggregating the results of other search engines while not storing information about its users. +

+

Why use searx?

+
    +
  • searx may not offer you as personalised results as Google, but it doesn't generate a profile about you
  • +
  • searx doesn't care about what you search for, never shares anything with a third party, and it can't be used to compromise you
  • +
  • searx is free software, the code is 100% open and you can help to make it better. See more on github
  • +
+

If you do care about privacy, want to be a conscious user, or otherwise believe + in digital freedom, make searx your default search engine or run it on your own server

+ +

Technical details - How does it work?

+ +

Searx is a metasearch engine, +inspired by the seeks project.
+It provides basic privacy by mixing your queries with searches on other platforms without storing search data. Queries are made using a POST request on every browser (except chrome*). Therefore they show up in neither our logs, nor your url history. In case of Chrome* users there is an exception, searx uses the search bar to perform GET requests.
+Searx can be added to your browser's search bar; moreover, it can be set as the default search engine. +

+ +

How can I make it my own?

+ +

Searx appreciates your concern regarding logs, so take the code and run it yourself!
Add your Searx to this list to help other people reclaim their privacy and make the Internet freer! +
The more decentralized the Internet is, the more freedom we have!

+ + +

More about searx

+ + + + +
+ +

FAQ

+ +

How to add to firefox?

+

Install searx as a search engine on any version of Firefox! (javascript required)

+ +

Developer FAQ

+ +

New engines?

+ +

Don't forget to restart searx after config edit!

+ +

Installation/WSGI support?

+

See the installation and setup wiki page

+ +

How to debug engines?

+

Stats page contains some useful data about the engines used.

+ + diff --git a/searx/templates/__common__/opensearch.xml b/searx/templates/__common__/opensearch.xml new file mode 100644 index 0000000..15d3eb7 --- /dev/null +++ b/searx/templates/__common__/opensearch.xml @@ -0,0 +1,28 @@ + + + {{ instance_name }} + a privacy-respecting, hackable metasearch engine + UTF-8 + {{ urljoin(host, url_for('static', filename='img/favicon.png')) }} + searx metasearch + {% if opensearch_method == 'get' %} + + {% if autocomplete %} + + + + + {% endif %} + {% else %} + + + + {% if autocomplete %} + + + + + + {% endif %} + {% endif %} + diff --git a/searx/templates/__common__/opensearch_response_rss.xml b/searx/templates/__common__/opensearch_response_rss.xml new file mode 100644 index 0000000..32c42e7 --- /dev/null +++ b/searx/templates/__common__/opensearch_response_rss.xml @@ -0,0 +1,29 @@ + + + + Searx search: {{ q|e }} + {{ base_url }}?q={{ q|e }} + Search results for "{{ q|e }}" - searx + {{ number_of_results }} + 1 + {{ number_of_results }} + + diff --git a/searx/templates/courgette/404.html b/searx/templates/courgette/404.html new file mode 100644 index 0000000..9e3b8ac --- /dev/null +++ b/searx/templates/courgette/404.html @@ -0,0 +1,9 @@ +{% extends "courgette/base.html" %} +{% block content %} +
+

{{ _('Page not found') }}

+ {% autoescape false %} +

{{ _('Go to %(search_page)s.', search_page=unicode('{}').format(url_for('index'), _('search page'))) }}

+ {% endautoescape %} +
+{% endblock %} diff --git a/searx/templates/courgette/about.html b/searx/templates/courgette/about.html new file mode 100644 index 0000000..08948ee --- /dev/null +++ b/searx/templates/courgette/about.html @@ -0,0 +1,5 @@ +{% extends 'courgette/base.html' %} +{% block content %} +{% include 'courgette/github_ribbon.html' %} +{% include '__common__/about.html' %} +{% endblock %} diff --git a/searx/templates/courgette/base.html b/searx/templates/courgette/base.html new file mode 100644 index 0000000..8e27258 --- /dev/null +++ b/searx/templates/courgette/base.html @@ -0,0 +1,43 @@ + + + + + + + + + + {% block title %}{% endblock %}searx + + {% if rtl %} + + {% endif %} + {% if cookies['courgette-color'] %} + + {% endif %} + + {% block styles %} + {% endblock %} + {% block meta %}{% endblock %} + {% block head %} + + {% endblock %} + + + +
+ {% block content %} + {% endblock %} + {% if autocomplete %} + + + {% endif %} + +
+ + diff --git a/searx/templates/courgette/categories.html b/searx/templates/courgette/categories.html new file mode 100644 index 0000000..b8d6a75 --- /dev/null +++ b/searx/templates/courgette/categories.html @@ -0,0 +1,9 @@ +
+
+ {% for category in categories %} +
+ +
+ {% endfor %} +
+
\ No newline at end of file diff --git a/searx/templates/courgette/color.css b/searx/templates/courgette/color.css new file mode 100644 index 0000000..3e0d730 --- /dev/null +++ b/searx/templates/courgette/color.css @@ -0,0 +1,34 @@ + .autocompleter-choices li:hover, +.checkbox_container label:hover, +.checkbox_container input[type="checkbox"]:checked + label, +#sidebar, +#suggestions input[type="submit"]:hover, +#suggestions input[type="submit"]:focus, +input[type="submit"], +.engine_checkbox label, +.engine_checkbox .deny, +#search_submit{ + background-color: {{ cookies['courgette-color'].split()[0] }}; +} + +.result_title a, +.row a, +.title h1{ + color: {{ cookies['courgette-color'].split()[0] }}; +} + +#answers { + border-color: {{ cookies['courgette-color'].split()[0] }}; +} + +#search_submit:hover, +#search_submit:focus, +#sidebar input[type="submit"]:hover, +#sidebar input[type="submit"]:focus { + background-color: {{ cookies['courgette-color'].split()[1] }}; +} + +input[type="submit"]:hover, +input[type="submit"]:focus { + background: {{ cookies['courgette-color'].split()[1] }}; +} \ No newline at end of file diff --git a/searx/templates/courgette/github_ribbon.html b/searx/templates/courgette/github_ribbon.html new file mode 100644 index 0000000..67c6e67 --- /dev/null +++ b/searx/templates/courgette/github_ribbon.html @@ -0,0 +1,3 @@ + + Fork me on GitHub + \ No newline at end of file diff --git a/searx/templates/courgette/index.html b/searx/templates/courgette/index.html new file mode 100644 index 0000000..0d34e1c --- /dev/null +++ b/searx/templates/courgette/index.html @@ -0,0 +1,17 @@ +{% extends "courgette/base.html" %} +{% block content %} +{% include 'courgette/github_ribbon.html' %} +
+

searx

+ {% include 'courgette/search.html' %} +

+ {% if rtl %} + {{ _('preferences') }} + {% endif %} + {{ _('about') }} + {% if not rtl %} + {{ _('preferences') }} + {% endif %} +

+
+{% endblock %} \ No newline at end of file diff --git a/searx/templates/courgette/preferences.html b/searx/templates/courgette/preferences.html new file mode 100644 index 0000000..56a6e02 --- /dev/null +++ b/searx/templates/courgette/preferences.html @@ -0,0 +1,132 @@ +{% extends "courgette/base.html" %} +{% block head %} {% endblock %} +{% block content %} +
+

{{ _('Preferences') }}

+ +
+
+ {{ _('Default categories') }} + {% include 'courgette/categories.html' %} +
+
+ {{ _('Search language') }} +

+ +

+
+
+ {{ _('Interface language') }} +

+ +

+
+
+ {{ _('Autocomplete') }} +

+ +

+
+
+ {{ _('Image proxy') }} +

+ +

+
+
+ {{ _('Method') }} +

+ +

+
+
+ {{ _('SafeSearch') }} +

+ +

+
+
+ {{ _('Themes') }} +

+ +

+
+
+ {{ _('Color') }} +

+ +

+
+
+ {{ _('Currently used search engines') }} + + + + + + + + {% for categ in all_categories %} + {% for search_engine in engines_by_category[categ] %} + + {% if not search_engine.private %} + + + + + + {% endif %} + {% endfor %} + {% endfor %} +
{{ _('Engine name') }}{{ _('Category') }}{{ _('Allow') }} / {{ _('Block') }}
{{ search_engine.name }} ({{ shortcuts[search_engine.name] }})‎{{ _(categ) }} + + + +
+
+

{{ _('These settings are stored in your cookies, this allows us not to store this data about you.') }} +
+ {{ _("These cookies serve your sole convenience, we don't use these cookies to track you.") }} +

+ + + + +
+
+{% endblock %} diff --git a/searx/templates/courgette/result_templates/code.html b/searx/templates/courgette/result_templates/code.html new file mode 100644 index 0000000..953617e --- /dev/null +++ b/searx/templates/courgette/result_templates/code.html @@ -0,0 +1,11 @@ +
+

{% if result['favicon'] %}{{result['favicon']}}{% endif %}{{ result.title|safe }}

+ {% if result.publishedDate %}{{ result.publishedDate }}{% endif %} +

{% if result.img_src %}{% endif %}{% if result.content %}{{ result.content|safe }}
{% endif %}

+ {% if result.repository %}

{{ result.repository }}

{% endif %} +
+ {{ result.codelines|code_highlighter(result.code_language)|safe }} +
+ +

{{ result.pretty_url }}‎

+
diff --git a/searx/templates/courgette/result_templates/default.html b/searx/templates/courgette/result_templates/default.html new file mode 100644 index 0000000..5f2ead6 --- /dev/null +++ b/searx/templates/courgette/result_templates/default.html @@ -0,0 +1,13 @@ +
+ + {% if "icon_"~result.engine~".ico" in favicons %} + {{result.engine}} + {% endif %} + +
+

{{ result.title|safe }}

+ {% if result.publishedDate %}{{ result.publishedDate }}{% endif %} +

{% if result.content %}{{ result.content|safe }}
{% endif %}

+

{{ result.pretty_url }}‎

+
+
diff --git a/searx/templates/courgette/result_templates/images.html b/searx/templates/courgette/result_templates/images.html new file mode 100644 index 0000000..49acb3b --- /dev/null +++ b/searx/templates/courgette/result_templates/images.html @@ -0,0 +1,6 @@ + diff --git a/searx/templates/courgette/result_templates/map.html b/searx/templates/courgette/result_templates/map.html new file mode 100644 index 0000000..5f2ead6 --- /dev/null +++ b/searx/templates/courgette/result_templates/map.html @@ -0,0 +1,13 @@ +
+ + {% if "icon_"~result.engine~".ico" in favicons %} + {{result.engine}} + {% endif %} + +
+

{{ result.title|safe }}

+ {% if result.publishedDate %}{{ result.publishedDate }}{% endif %} +

{% if result.content %}{{ result.content|safe }}
{% endif %}

+

{{ result.pretty_url }}‎

+
+
diff --git a/searx/templates/courgette/result_templates/torrent.html b/searx/templates/courgette/result_templates/torrent.html new file mode 100644 index 0000000..2fd8395 --- /dev/null +++ b/searx/templates/courgette/result_templates/torrent.html @@ -0,0 +1,13 @@ +
+ {% if "icon_"~result.engine~".ico" in favicons %} + {{result.engine}} + {% endif %} +

{{ result.title|safe }}

+ {% if result.content %}{{ result.content|safe }}
{% endif %} + {{ _('Seeder') }} : {{ result.seed }}, {{ _('Leecher') }} : {{ result.leech }}
+ + {% if result.magnetlink %}{{ _('magnet link') }}{% endif %} + {% if result.torrentfile %}{{ _('torrent file') }}{% endif %} + +

{{ result.pretty_url }}‎

+
diff --git a/searx/templates/courgette/result_templates/videos.html b/searx/templates/courgette/result_templates/videos.html new file mode 100644 index 0000000..b3e19e0 --- /dev/null +++ b/searx/templates/courgette/result_templates/videos.html @@ -0,0 +1,10 @@ +
+ {% if "icon_"~result.engine~".ico" in favicons %} + {{result.engine}} + {% endif %} + +

{{ result.title|safe }}

+ {% if result.publishedDate %}{{ result.publishedDate }}
{% endif %} + {{ result.title|striptags }} +

{{ result.pretty_url }}‎

+
diff --git a/searx/templates/courgette/results.html b/searx/templates/courgette/results.html new file mode 100644 index 0000000..c72b7c3 --- /dev/null +++ b/searx/templates/courgette/results.html @@ -0,0 +1,87 @@ +{% extends "courgette/base.html" %} +{% block title %}{{ q|e }} - {% endblock %} +{% block meta %}{% endblock %} +{% block content %} + + +
+ + + {% if answers %} +
{{ _('Answers') }} + {% for answer in answers %} + {{ answer }} + {% endfor %} +
+ {% endif %} + + {% if suggestions %} +
{{ _('Suggestions') }} + {% for suggestion in suggestions %} +
+ + +
+ {% endfor %} +
+ {% endif %} + + {% for result in results %} + {% if result['template'] %} + {% include get_result_template('courgette', result['template']) %} + {% else %} + {% include 'courgette/result_templates/default.html' %} + {% endif %} + {% endfor %} + + {% if paging %} + + {% endif %} +
+{% endblock %} diff --git a/searx/templates/courgette/search.html b/searx/templates/courgette/search.html new file mode 100644 index 0000000..bd4efd4 --- /dev/null +++ b/searx/templates/courgette/search.html @@ -0,0 +1,7 @@ +
+
+ + +
+ {% include 'courgette/categories.html' %} +
\ No newline at end of file diff --git a/searx/templates/courgette/stats.html b/searx/templates/courgette/stats.html new file mode 100644 index 0000000..b9aafbb --- /dev/null +++ b/searx/templates/courgette/stats.html @@ -0,0 +1,22 @@ +{% extends "courgette/base.html" %} +{% block head %} {% endblock %} +{% block content %} +

{{ _('Engine stats') }}

+ +{% for stat_name,stat_category in stats %} +
+ + + + + {% for engine in stat_category %} + + + + + + {% endfor %} +
{{ stat_name }}
{{ engine.name }}{{ '%.02f'|format(engine.avg) }}
 
+
+{% endfor %} +{% endblock %} \ No newline at end of file diff --git a/searx/templates/legacy/404.html b/searx/templates/legacy/404.html new file mode 100644 index 0000000..3e889dd --- /dev/null +++ b/searx/templates/legacy/404.html @@ -0,0 +1,9 @@ +{% extends "legacy/base.html" %} +{% block content %} +
+

{{ _('Page not found') }}

+ {% autoescape false %} +

{{ _('Go to %(search_page)s.', search_page=unicode('{}').format(url_for('index'), _('search page'))) }}

+ {% endautoescape %} +
+{% endblock %} diff --git a/searx/templates/legacy/about.html b/searx/templates/legacy/about.html new file mode 100644 index 0000000..f773e3a --- /dev/null +++ b/searx/templates/legacy/about.html @@ -0,0 +1,5 @@ +{% extends 'legacy/base.html' %} +{% block content %} +{% include 'legacy/github_ribbon.html' %} +{% include '__common__/about.html' %} +{% endblock %} diff --git a/searx/templates/legacy/base.html b/searx/templates/legacy/base.html new file mode 100644 index 0000000..da19741 --- /dev/null +++ b/searx/templates/legacy/base.html @@ -0,0 +1,38 @@ + + + + + + + + + + {% block title %}{% endblock %}searx + + {% if rtl %} + + {% endif %} + + {% block styles %} + {% endblock %} + {% block meta %}{% endblock %} + {% block head %} + + {% endblock %} + + +
+ {% block content %} + {% endblock %} + {% if autocomplete %} + + + {% endif %} + + +
+ + diff --git a/searx/templates/legacy/categories.html b/searx/templates/legacy/categories.html new file mode 100644 index 0000000..1c46678 --- /dev/null +++ b/searx/templates/legacy/categories.html @@ -0,0 +1,10 @@ +
+
+ {% for category in categories %} +
+ +
+ {% endfor %} + {% if display_tooltip %}{% endif %} +
+
diff --git a/searx/templates/legacy/github_ribbon.html b/searx/templates/legacy/github_ribbon.html new file mode 100644 index 0000000..bdd9cf1 --- /dev/null +++ b/searx/templates/legacy/github_ribbon.html @@ -0,0 +1,3 @@ + + Fork me on GitHub + diff --git a/searx/templates/legacy/index.html b/searx/templates/legacy/index.html new file mode 100644 index 0000000..de956d5 --- /dev/null +++ b/searx/templates/legacy/index.html @@ -0,0 +1,18 @@ +{% extends "legacy/base.html" %} +{% block content %} +
+

searx

+ {% include 'legacy/search.html' %} +

+ {% if rtl %} + {{ _('preferences') }} + {% endif %} + {{ _('about') }} + {% if not rtl %} + {{ _('preferences') }} + {% endif %} +

+
+{% include 'legacy/github_ribbon.html' %} +{% endblock %} + diff --git a/searx/templates/legacy/infobox.html b/searx/templates/legacy/infobox.html new file mode 100644 index 0000000..4dd25fa --- /dev/null +++ b/searx/templates/legacy/infobox.html @@ -0,0 +1,51 @@ +
+

{{ infobox.infobox }}

+ {% if infobox.img_src %}{{ infobox.infobox|striptags }}{% endif %} +

{{ infobox.entity }}

+

{{ infobox.content | safe }}

+ {% if infobox.attributes %} +
+ + {% for attribute in infobox.attributes %} + + + {% if attribute.image %} + + {% else %} + + {% endif %} + + {% endfor %} +
{{ attribute.label }}{{ attribute.image.alt }}{{ attribute.value }}
+
+ {% endif %} + + {% if infobox.urls %} +
+ +
+ {% endif %} + + {% if infobox.relatedTopics %} +
+ {% for topic in infobox.relatedTopics %} +
+

{{ topic.name }}

+ {% for suggestion in topic.suggestions %} +
+ + +
+ {% endfor %} +
+ {% endfor %} +
+ {% endif %} + +
+ +
diff --git a/searx/templates/legacy/preferences.html b/searx/templates/legacy/preferences.html new file mode 100644 index 0000000..f418dcd --- /dev/null +++ b/searx/templates/legacy/preferences.html @@ -0,0 +1,129 @@ +{% extends "legacy/base.html" %} +{% block head %} {% endblock %} +{% block content %} +
+

{{ _('Preferences') }}

+ +
+
+ {{ _('Default categories') }} + {% set display_tooltip = false %} + {% include 'legacy/categories.html' %} +
+
+ {{ _('Search language') }} +

+ +

+
+
+ {{ _('Interface language') }} +

+ +

+
+
+ {{ _('Autocomplete') }} +

+ +

+
+
+ {{ _('Image proxy') }} +

+ +

+
+
+ {{ _('Method') }} +

+ +

+
+
+ {{ _('SafeSearch') }} +

+ +

+
+
+ {{ _('Themes') }} +

+ +

+
+
+ {{ _('Results on new tabs') }} +

+ +

+
+
+ {{ _('Currently used search engines') }} + + + + + + + + {% for categ in all_categories %} + {% for search_engine in engines_by_category[categ] %} + + {% if not search_engine.private %} + + + + + + {% endif %} + {% endfor %} + {% endfor %} +
{{ _('Engine name') }}{{ _('Category') }}{{ _('Allow') }} / {{ _('Block') }}
{{ search_engine.name }} ({{ shortcuts[search_engine.name] }})‎{{ _(categ) }} + + + +
+
+

{{ _('These settings are stored in your cookies, this allows us not to store this data about you.') }} +
+ {{ _("These cookies serve your sole convenience, we don't use these cookies to track you.") }} +

+ + + + +
+
+{% endblock %} diff --git a/searx/templates/legacy/result_templates/code.html b/searx/templates/legacy/result_templates/code.html new file mode 100644 index 0000000..9e3ed20 --- /dev/null +++ b/searx/templates/legacy/result_templates/code.html @@ -0,0 +1,11 @@ +
+

{% if result['favicon'] %}{{result['favicon']}}{% endif %}{{ result.title|safe }}

+

{{ result.pretty_url }}‎ {{ _('cached') }}

+ {% if result.publishedDate %}

{{ result.publishedDate }}

{% endif %} +

{% if result.img_src %}{% endif %}{% if result.content %}{{ result.content|safe }}
{% endif %}

+ {% if result.repository %}

{{ result.repository }}

{% endif %} + +
+ {{ result.codelines|code_highlighter(result.code_language)|safe }} +
+
diff --git a/searx/templates/legacy/result_templates/default.html b/searx/templates/legacy/result_templates/default.html new file mode 100644 index 0000000..da09117 --- /dev/null +++ b/searx/templates/legacy/result_templates/default.html @@ -0,0 +1,6 @@ +
+

{% if "icon_"~result.engine~".ico" in favicons %}{{result.engine}}{% endif %}{{ result.title|safe }}

+

{{ result.pretty_url }}‎ {{ _('cached') }} + {% if result.publishedDate %}{{ result.publishedDate }}{% endif %}

+

{% if result.img_src %}{% endif %}{% if result.content %}{{ result.content|safe }}
{% endif %}

+
diff --git a/searx/templates/legacy/result_templates/images.html b/searx/templates/legacy/result_templates/images.html new file mode 100644 index 0000000..00f62ab --- /dev/null +++ b/searx/templates/legacy/result_templates/images.html @@ -0,0 +1,6 @@ + diff --git a/searx/templates/legacy/result_templates/map.html b/searx/templates/legacy/result_templates/map.html new file mode 100644 index 0000000..0200e0f --- /dev/null +++ b/searx/templates/legacy/result_templates/map.html @@ -0,0 +1,13 @@ +
+ + {% if "icon_"~result.engine~".ico" in favicons %} + {{result.engine}} + {% endif %} + +
+

{{ result.title|safe }}

+

{{ result.pretty_url }}‎ {{ _('cached') }} + {% if result.publishedDate %}{{ result.publishedDate }}{% endif %}

+

{% if result.img_src %}{% endif %}{% if result.content %}{{ result.content|safe }}
{% endif %}

+
+
diff --git a/searx/templates/legacy/result_templates/torrent.html b/searx/templates/legacy/result_templates/torrent.html new file mode 100644 index 0000000..67e058a --- /dev/null +++ b/searx/templates/legacy/result_templates/torrent.html @@ -0,0 +1,13 @@ +
+ {% if "icon_"~result.engine~".ico" in favicons %} + {{result.engine}} + {% endif %} +

{{ result.title|safe }}

+

{{ result.pretty_url }}‎

+ {% if result.content %}

{{ result.content|safe }}

{% endif %} +

+ {% if result.magnetlink %}{{ _('magnet link') }}{% endif %} + {% if result.torrentfile %}{{ _('torrent file') }}{% endif %} - + {{ _('Seeder') }} : {{ result.seed }}, {{ _('Leecher') }} : {{ result.leech }} +

+
diff --git a/searx/templates/legacy/result_templates/videos.html b/searx/templates/legacy/result_templates/videos.html new file mode 100644 index 0000000..727f44c --- /dev/null +++ b/searx/templates/legacy/result_templates/videos.html @@ -0,0 +1,6 @@ +
+

{% if "icon_"~result.engine~".ico" in favicons %}{{result.engine}}{% endif %}{{ result.title|safe }}

+ {% if result.publishedDate %}{{ result.publishedDate }}
{% endif %} + {{ result.title|striptags }} +

{{ result.url }}‎

+
diff --git a/searx/templates/legacy/results.html b/searx/templates/legacy/results.html new file mode 100644 index 0000000..f0d7839 --- /dev/null +++ b/searx/templates/legacy/results.html @@ -0,0 +1,100 @@ +{% extends "legacy/base.html" %} +{% block title %}{{ q|e }} - {% endblock %} +{% block meta %}{% endblock %} +{% block content %} + + +
+ + + {% if answers %} +
{{ _('Answers') }} + {% for answer in answers %} + {{ answer }} + {% endfor %} +
+ {% endif %} + + {% if suggestions %} +
{{ _('Suggestions') }} : + {% set first = true %} + {% for suggestion in suggestions %} + {% if not first %} • {% endif %}
+ + +
+ {% set first = false %} + {% endfor %} +
+ {% endif %} + + {% if infoboxes %} +
+ {% for infobox in infoboxes %} + {% include 'legacy/infobox.html' %} + {% endfor %} +
+ {% endif %} + + {% for result in results %} + {% if result['template'] %} + {% include get_result_template('legacy', result['template']) %} + {% else %} + {% include 'legacy/result_templates/default.html' %} + {% endif %} + {% endfor %} + + {% if paging %} + + {% endif %} +
+{% endblock %} diff --git a/searx/templates/legacy/search.html b/searx/templates/legacy/search.html new file mode 100644 index 0000000..4d37f9b --- /dev/null +++ b/searx/templates/legacy/search.html @@ -0,0 +1,8 @@ +
+
+ + +
+ {% set display_tooltip = true %} + {% include 'legacy/categories.html' %} +
diff --git a/searx/templates/legacy/stats.html b/searx/templates/legacy/stats.html new file mode 100644 index 0000000..372447e --- /dev/null +++ b/searx/templates/legacy/stats.html @@ -0,0 +1,22 @@ +{% extends "legacy/base.html" %} +{% block head %} {% endblock %} +{% block content %} +

{{ _('Engine stats') }}

+ +{% for stat_name,stat_category in stats %} +
+ + + + + {% for engine in stat_category %} + + + + + + {% endfor %} +
{{ stat_name }}
{{ engine.name }}{{ '%.02f'|format(engine.avg) }}
 
+
+{% endfor %} +{% endblock %} diff --git a/searx/templates/oscar/404.html b/searx/templates/oscar/404.html new file mode 100644 index 0000000..5a50880 --- /dev/null +++ b/searx/templates/oscar/404.html @@ -0,0 +1,9 @@ +{% extends "oscar/base.html" %} +{% block content %} +
+

{{ _('Page not found') }}

+ {% autoescape false %} +

{{ _('Go to %(search_page)s.', search_page=unicode('{}').format(url_for('index'), _('search page'))) }}

+ {% endautoescape %} +
+{% endblock %} diff --git a/searx/templates/oscar/about.html b/searx/templates/oscar/about.html new file mode 100644 index 0000000..bc7fed8 --- /dev/null +++ b/searx/templates/oscar/about.html @@ -0,0 +1,5 @@ +{% extends "oscar/base.html" %} +{% block title %}{{ _('about') }} - {% endblock %} +{% block content %} +{% include '__common__/about.html' %} +{% endblock %} diff --git a/searx/templates/oscar/advanced.html b/searx/templates/oscar/advanced.html new file mode 100644 index 0000000..95d99ba --- /dev/null +++ b/searx/templates/oscar/advanced.html @@ -0,0 +1,16 @@ + + +
+ {% include 'oscar/categories.html' %} +
+
+ {% include 'oscar/time-range.html' %} +
+
+ {% include 'oscar/languages.html' %} +
+
+
diff --git a/searx/templates/oscar/base.html b/searx/templates/oscar/base.html new file mode 100644 index 0000000..890204c --- /dev/null +++ b/searx/templates/oscar/base.html @@ -0,0 +1,107 @@ +{% from 'oscar/macros.html' import icon %} + + + + + + + + + + + {% block meta %}{% endblock %} + {% block title %}{% endblock %}{{ instance_name }} + + + {% if cookies['oscar-style'] %} + + {% else %} + + {% endif %} + + {% for css in styles %} + + {% endfor %} + + + + + + + {% block styles %} + {% endblock %} + {% block head %} + {% endblock %} + + + + + + + + {% include 'oscar/navbar.html' %} +
+ {% if errors %} + + {% endif %} + + {% block site_alert_error %} + {% endblock %} + {% block site_alert_warning %} + {% endblock %} + {% block site_alert_info %} + {% endblock %} + {% block site_alert_success %} + {% endblock %} + + {% block content %} + {% endblock %} + +
+ + + + {% if autocomplete %}{% endif %} + + + {% for script in scripts %} + + {% endfor %} + + + diff --git a/searx/templates/oscar/categories.html b/searx/templates/oscar/categories.html new file mode 100644 index 0000000..1ace10f --- /dev/null +++ b/searx/templates/oscar/categories.html @@ -0,0 +1,13 @@ +
+{% if rtl %} + {% for category in categories | reverse %} + + + {% endfor %} +{% else %} + {% for category in categories %} + + + {% endfor %} +{% endif %} +
diff --git a/searx/templates/oscar/index.html b/searx/templates/oscar/index.html new file mode 100644 index 0000000..b941f5f --- /dev/null +++ b/searx/templates/oscar/index.html @@ -0,0 +1,22 @@ +{% extends "oscar/base.html" %} +{% block content %} +
+
+
+ {% if cookies['oscar-style'] == 'pointhi' %} +

searx logosearx

+ {% else %} +

+ searx logo + searx +

+ {% endif %} +
+
+
+
+ {% include 'oscar/search_full.html' %} +
+
+
+{% endblock %} diff --git a/searx/templates/oscar/infobox.html b/searx/templates/oscar/infobox.html new file mode 100644 index 0000000..c98fb0e --- /dev/null +++ b/searx/templates/oscar/infobox.html @@ -0,0 +1,35 @@ +{% from 'oscar/macros.html' import result_link with context %} +
+
+

{{ infobox.infobox }}

+
+
+ {% if infobox.img_src %}{{ infobox.infobox }}{% endif %} + {% if infobox.content %}

{{ infobox.content }}

{% endif %} + + {% if infobox.attributes %} + + {% for attribute in infobox.attributes %} + + + {% if attribute.image %} + + {% else %} + + {% endif %} + + {% endfor %} +
{{ attribute.label }}{{ attribute.image.alt }}{{ attribute.value }}
+ {% endif %} + + {% if infobox.urls %} +
+ + {% for url in infobox.urls %} +

{{ result_link(url.url, url.title) }}

+ {% endfor %} +
+
+ {% endif %} +
+
diff --git a/searx/templates/oscar/languages.html b/searx/templates/oscar/languages.html new file mode 100644 index 0000000..96c1c3a --- /dev/null +++ b/searx/templates/oscar/languages.html @@ -0,0 +1,12 @@ +{% if preferences %} + +{% endif %} + + {% for lang_id,lang_name,country_name,english_name in language_codes | sort(attribute=1) %} + + {% endfor %} + diff --git a/searx/templates/oscar/macros.html b/searx/templates/oscar/macros.html new file mode 100644 index 0000000..e71091e --- /dev/null +++ b/searx/templates/oscar/macros.html @@ -0,0 +1,88 @@ + +{% macro icon(action) -%} + +{%- endmacro %} + + + +{% macro draw_favicon(favicon) -%} + {{ favicon }} +{%- endmacro %} + +{%- macro result_link(url, title, classes='') -%} +{{ title }} +{%- endmacro -%} + + +{% macro result_header(result, favicons) -%} +

{% if result.engine~".png" in favicons %}{{ draw_favicon(result.engine) }} {% endif %}{{ result_link(result.url, result.title|safe) }}

+{%- endmacro %} + + +{% macro result_sub_header(result) -%} + {% if result.publishedDate %}{% endif %} + {% if result.magnetlink %} • {{ result_link(result.magnetlink, icon('magnet') + _('magnet link'), "magnetlink") }}{% endif %} + {% if result.torrentfile %} • {{ result_link(result.torrentfile, icon('download-alt') + _('torrent file'), "torrentfile") }}{% endif %} +{%- endmacro %} + + +{% macro result_footer(result) -%} +
+
+ {% for engine in result.engines %} + {{ engine }} + {% endfor %} + {{ result_link("https://web.archive.org/web/" + result.url, icon('link') + _('cached'), "text-info") }} + {% if proxify %} + {{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }} + {% endif %} +
+ +{%- endmacro %} + + +{% macro result_footer_rtl(result) -%} +
+ {% for engine in result.engines %} + {{ engine }} + {% endfor %} + {{ result_link("https://web.archive.org/web/" + result.url, icon('link') + _('cached'), "text-info") }} + {% if proxify %} + {{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }} + {% endif %} + +{%- endmacro %} + +{% macro preferences_item_header(info, label, rtl) -%} + {% if rtl %} +
+ + {{ info }} +
+ {% else %} +
+ +
+ {% endif %} +{%- endmacro %} + +{% macro preferences_item_footer(info, label, rtl) -%} + {% if rtl %} +
+
+ {% else %} +
+ {{ info }} +
+ {% endif %} +{%- endmacro %} + +{% macro checkbox_toggle(id, blocked) -%} +
+ + +
+{%- endmacro %} diff --git a/searx/templates/oscar/messages/first_time.html b/searx/templates/oscar/messages/first_time.html new file mode 100644 index 0000000..38db62b --- /dev/null +++ b/searx/templates/oscar/messages/first_time.html @@ -0,0 +1,8 @@ + diff --git a/searx/templates/oscar/messages/no_cookies.html b/searx/templates/oscar/messages/no_cookies.html new file mode 100644 index 0000000..9bebc8a --- /dev/null +++ b/searx/templates/oscar/messages/no_cookies.html @@ -0,0 +1,5 @@ +{% from 'oscar/macros.html' import icon %} + diff --git a/searx/templates/oscar/messages/no_data_available.html b/searx/templates/oscar/messages/no_data_available.html new file mode 100644 index 0000000..aee7917 --- /dev/null +++ b/searx/templates/oscar/messages/no_data_available.html @@ -0,0 +1,5 @@ +{% from 'oscar/macros.html' import icon %} + diff --git a/searx/templates/oscar/messages/no_results.html b/searx/templates/oscar/messages/no_results.html new file mode 100644 index 0000000..ac3705e --- /dev/null +++ b/searx/templates/oscar/messages/no_results.html @@ -0,0 +1,9 @@ +{% from 'oscar/macros.html' import icon %} + diff --git a/searx/templates/oscar/messages/save_settings_successfull.html b/searx/templates/oscar/messages/save_settings_successfull.html new file mode 100644 index 0000000..63e578c --- /dev/null +++ b/searx/templates/oscar/messages/save_settings_successfull.html @@ -0,0 +1,9 @@ +{% from 'oscar/macros.html' import icon %} + diff --git a/searx/templates/oscar/messages/unknow_error.html b/searx/templates/oscar/messages/unknow_error.html new file mode 100644 index 0000000..3c4c9c1 --- /dev/null +++ b/searx/templates/oscar/messages/unknow_error.html @@ -0,0 +1,9 @@ +{% from 'oscar/macros.html' import icon %} + diff --git a/searx/templates/oscar/navbar.html b/searx/templates/oscar/navbar.html new file mode 100644 index 0000000..12bf14f --- /dev/null +++ b/searx/templates/oscar/navbar.html @@ -0,0 +1,9 @@ + diff --git a/searx/templates/oscar/preferences.html b/searx/templates/oscar/preferences.html new file mode 100644 index 0000000..e5477e7 --- /dev/null +++ b/searx/templates/oscar/preferences.html @@ -0,0 +1,292 @@ +{% from 'oscar/macros.html' import preferences_item_header, preferences_item_header_rtl, preferences_item_footer, preferences_item_footer_rtl, checkbox_toggle %} +{% extends "oscar/base.html" %} +{% block title %}{{ _('preferences') }} - {% endblock %} +{% block content %} +
+ +

{{ _('Preferences') }}

+
+ + + + + + +
+
+
+
+
+ {% if rtl %} +
+ {% include 'oscar/categories.html' %} +
+ + {% else %} + +
+ {% include 'oscar/categories.html' %} +
+ {% endif %} +
+ {% set language_label = _('Search language') %} + {% set language_info = _('What language do you prefer for search?') %} + {{ preferences_item_header(language_info, language_label, rtl) }} + {% include 'oscar/languages.html' %} + {{ preferences_item_footer(language_info, language_label, rtl) }} + + {% set locale_label = _('Interface language') %} + {% set locale_info = _('Change the language of the layout') %} + {{ preferences_item_header(locale_info, locale_label, rtl) }} + + {{ preferences_item_footer(locale_info, locale_label, rtl) }} + + {% set autocomplete_label = _('Autocomplete') %} + {% set autocomplete_info = _('Find stuff as you type') %} + {{ preferences_item_header(autocomplete_info, autocomplete_label, rtl) }} + + {{ preferences_item_footer(autocomplete_info, autocomplete_label, rtl) }} + + {% set image_proxy_label = _('Image proxy') %} + {% set image_proxy_info = _('Proxying image results through searx') %} + {{ preferences_item_header(image_proxy_info, image_proxy_label, rtl) }} + + {{ preferences_item_footer(image_proxy_info, image_proxy_label, rtl) }} + + {% set method_label = _('Method') %} + {% set method_info = _('Change how forms are submited, learn more about request methods') %} + {{ preferences_item_header(method_info, method_label, rtl) }} + + {{ preferences_item_footer(method_info, method_label, rtl) }} + + {% set safesearch_label = _('SafeSearch') %} + {% set safesearch_info = _('Filter content') %} + {{ preferences_item_header(safesearch_info, safesearch_label, rtl) }} + + {{ preferences_item_footer(safesearch_info, safesearch_label, rtl) }} + + {% set theme_label = _('Themes') %} + {% set theme_info = _('Change searx layout') %} + {{ preferences_item_header(theme_info, theme_label, rtl) }} + + {{ preferences_item_footer(theme_info, theme_label, rtl) }} + + {{ preferences_item_header(_('Choose style for this theme'), _('Style'), rtl) }} + + {{ preferences_item_footer(_('Choose style for this theme'), _('Style'), rtl) }} + + {% set label = _('Results on new tabs') %} + {% set info = _('Open result links on new browser tabs') %} + {{ preferences_item_header(info, label, rtl) }} + + {{ preferences_item_footer(info, label, rtl) }} +
+
+
+
+ + + + + + + +
+ {% for categ in all_categories %} + +
+
+
+
+ + + {% if not rtl %} + + + + + + + + + {% else %} + + + + + + + + {% endif %} + + {% for search_engine in engines_by_category[categ] %} + {% if not search_engine.private %} + + {% if not rtl %} + + + + + + + + + {% else %} + + + + + + + + {% endif %} + + {% endif %} + {% endfor %} +
{{ _("Allow") }}{{ _("Engine name") }}{{ _("Shortcut") }}{{ _("Supports selected language") }}{{ _("SafeSearch") }}{{ _("Time range") }}{{ _("Avg. time") }}{{ _("Max time") }}{{ _("Max time") }}{{ _("Avg. time") }}{{ _("SafeSearch") }}{{ _("Supports selected language") }}{{ _("Shortcut") }}{{ _("Engine name") }}{{ _("Allow") }}
+ {{ checkbox_toggle('engine_' + search_engine.name|replace(' ', '_') + '__' + categ|replace(' ', '_'), (search_engine.name, categ) in disabled_engines) }} + {{ search_engine.name }}{{ shortcuts[search_engine.name] }}{{ 'N/A' if stats[search_engine.name].time==None else stats[search_engine.name].time }}{{ search_engine.timeout }}{{ search_engine.timeout }}{{ 'N/A' if stats[search_engine.name].time==None else stats[search_engine.name].time }}{{ shortcuts[search_engine.name] }}{{ search_engine.name }} + {{ checkbox_toggle('engine_' + search_engine.name|replace(' ', '_') + '__' + categ|replace(' ', '_'), (search_engine.name, categ) in disabled_engines) }} +
+
+
+
+
+ {% endfor %} +
+
+
+ +
+
+ {% for plugin in plugins %} +
+
+

{{ _(plugin.name) }}

+
+
+
{{ _(plugin.description) }}
+
+
+ {{ checkbox_toggle('plugin_' + plugin.id, plugin.id not in allowed_plugins) }} +
+
+
+
+ {% endfor %} +
+
+
+ + {% if answerers %} +
+ +

+ {{ _('This is the list of searx\'s instant answering modules.') }} +

+ + + + + + + + + {% for answerer in answerers %} + + + + + + + {% endfor %} +
{{ _('Name') }}{{ _('Keywords') }}{{ _('Description') }}{{ _('Examples') }}
{{ answerer.info.name }}{{ answerer.keywords|join(', ') }}{{ answerer.info.description }}{{ answerer.info.examples|join(', ') }}
+
+ {% endif %} + +
+ +

+ {{ _('This is the list of cookies and their values searx is storing on your computer.') }}
+ {{ _('With that list, you can assess searx transparency.') }}
+

+ {% if cookies %} + + + + + + + {% for cookie in cookies %} + + + + + {% endfor %} +
{{ _('Cookie name') }}{{ _('Value') }}
{{ cookie }}{{ cookies[cookie] }}
+ {% else %} + {% include 'oscar/messages/no_cookies.html' %} + {% endif %} +
+
+

{{ _('These settings are stored in your cookies, this allows us not to store this data about you.') }} +
+ {{ _("These cookies serve your sole convenience, we don't use these cookies to track you.") }} +

+ + +
{{ _('back') }}
+
{{ _('Reset defaults') }}
+
+
+{% endblock %} diff --git a/searx/templates/oscar/result_templates/code.html b/searx/templates/oscar/result_templates/code.html new file mode 100644 index 0000000..ba74d03 --- /dev/null +++ b/searx/templates/oscar/result_templates/code.html @@ -0,0 +1,18 @@ +{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon %} + +{{ result_header(result, favicons) }} +{{ result_sub_header(result) }} + +{% if result.content %}

{{ result.content|safe }}

{% endif %} + +{% if result.repository %}

{{ icon('file') }} {{ result.repository }}

{% endif %} + +
+{{ result.codelines|code_highlighter(result.code_language)|safe }} +
+ +{% if rtl %} +{{ result_footer_rtl(result) }} +{% else %} +{{ result_footer(result) }} +{% endif %} diff --git a/searx/templates/oscar/result_templates/default.html b/searx/templates/oscar/result_templates/default.html new file mode 100644 index 0000000..3ed0f31 --- /dev/null +++ b/searx/templates/oscar/result_templates/default.html @@ -0,0 +1,31 @@ +{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon with context %} + +{{ result_header(result, favicons) }} +{{ result_sub_header(result) }} + +{% if result.embedded %} + +{% endif %} + +{% if result.embedded %} +
+ {{ result.embedded|safe }} +
+{% endif %} + +{% if result.img_src %} +
+
+{{ result.title|striptags }} +{% if result.content %}

{{ result.content|safe }}

{% endif %} +
+
+{% else %} +{% if result.content %}

{{ result.content|safe }}

{% endif %} +{% endif %} + +{% if rtl %} +{{ result_footer_rtl(result) }} +{% else %} +{{ result_footer(result) }} +{% endif %} diff --git a/searx/templates/oscar/result_templates/images.html b/searx/templates/oscar/result_templates/images.html new file mode 100644 index 0000000..b23f349 --- /dev/null +++ b/searx/templates/oscar/result_templates/images.html @@ -0,0 +1,39 @@ +{% from 'oscar/macros.html' import draw_favicon %} + + + {{ result.title|striptags }} + + + diff --git a/searx/templates/oscar/result_templates/map.html b/searx/templates/oscar/result_templates/map.html new file mode 100644 index 0000000..822c7cd --- /dev/null +++ b/searx/templates/oscar/result_templates/map.html @@ -0,0 +1,72 @@ +{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon %} + +{{ result_header(result, favicons) }} +{{ result_sub_header(result) }} + +{% if (result.latitude and result.longitude) or result.boundingbox %} + +{% endif %} + +{% if result.osm and (result.osm.type and result.osm.id) %} + +{% endif %} + +{# {% if (result.latitude and result.longitude) %} + +{% endif %} #} + +
+ +{% if result.address %} +

+ {% if result.address.name %} + {{ result.address.name }}
+ {% endif %} + {% if result.address.road %} + + {% if result.address.house_number %}{{ result.address.house_number }}, {% endif %} + {{ result.address.road }} +
+ {% endif %} + {% if result.address.locality %} + {{ result.address.locality }} + {% if result.address.postcode %}, {{ result.address.postcode }}{% endif %} +
+ {% endif %} + {% if result.address.country %} + {{ result.address.country }} + {% endif %} +

+{% endif %} + +{% if result.osm and (result.osm.type and result.osm.id) %} +
+
Loading ...
+ + + +
+{% endif %} + +{# {% if (result.latitude and result.longitude) %} +
+ Longitude: {{ result.longitude }}
+ Latitude: {{ result.latitude }} +
+{% endif %} #} + +{% if result.content %}

{{ result.content|safe }}

{% endif %} + +
+ +{% if (result.latitude and result.longitude) or result.boundingbox %} +
+
+
+{% endif %} + +{% if rtl %} +{{ result_footer_rtl(result) }} +{% else %} +{{ result_footer(result) }} +{% endif %} diff --git a/searx/templates/oscar/result_templates/torrent.html b/searx/templates/oscar/result_templates/torrent.html new file mode 100644 index 0000000..bc2b30f --- /dev/null +++ b/searx/templates/oscar/result_templates/torrent.html @@ -0,0 +1,25 @@ +{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon %} + +{{ result_header(result, favicons) }} +{{ result_sub_header(result) }} + +

{{ icon('transfer') }} {{ _('Seeder') }} {{ result.seed }} • {{ _('Leecher') }} {{ result.leech }} +{% if result.filesize %}
{{ icon('floppy-disk') }} {{ _('Filesize') }} + + {% if result.filesize < 1024 %}{{ result.filesize }} {{ _('Bytes') }} + {% elif result.filesize < 1024*1024 %}{{ '{0:0.2f}'.format(result.filesize/1024) }} {{ _('kiB') }} + {% elif result.filesize < 1024*1024*1024 %}{{ '{0:0.2f}'.format(result.filesize/1024/1024) }} {{ _('MiB') }} + {% elif result.filesize < 1024*1024*1024*1024 %}{{ '{0:0.2f}'.format(result.filesize/1024/1024/1024) }} {{ _('GiB') }} + {% else %}{{ '{0:0.2f}'.format(result.filesize/1024/1024/1024/1024) }} {{ _('TiB') }}{% endif %} + {% endif %} +{% if result.files %}
{{ icon('file') }} {{ _('Number of Files') }} {{ result.files }}{% endif %} + +{% if result.content %}
{{ result.content|safe }}{% endif %} + +

+ +{% if rtl %} +{{ result_footer_rtl(result) }} +{% else %} +{{ result_footer(result) }} +{% endif %} diff --git a/searx/templates/oscar/result_templates/videos.html b/searx/templates/oscar/result_templates/videos.html new file mode 100644 index 0000000..36fb262 --- /dev/null +++ b/searx/templates/oscar/result_templates/videos.html @@ -0,0 +1,27 @@ +{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon %} + +{{ result_header(result, favicons) }} +{{ result_sub_header(result) }} + +{% if result.embedded %} + +{% endif %} + +{% if result.embedded %} +
+ {{ result.embedded|safe }} +
+{% endif %} + +
+
+ {{ result.title|striptags }} {{ result.engine }} + {% if result.content %}

{{ result.content|safe }}

{% endif %} +
+
+ +{% if rtl %} +{{ result_footer_rtl(result) }} +{% else %} +{{ result_footer(result) }} +{% endif %} diff --git a/searx/templates/oscar/results.html b/searx/templates/oscar/results.html new file mode 100644 index 0000000..060b2a1 --- /dev/null +++ b/searx/templates/oscar/results.html @@ -0,0 +1,145 @@ +{% extends "oscar/base.html" %} +{% macro search_form_attrs(pageno) -%} + {% for category in selected_categories %}{% endfor %} + + + + +{%- endmacro %} +{%- macro search_url() %}{{ base_url }}?q={{ q|urlencode }}{% if selected_categories %}&categories={{ selected_categories|join(",") | replace(' ','+') }}{% endif %}{% if pageno > 1 %}&pageno={{ pageno }}{% endif %}{% if time_range %}&time_range={{ time_range }}{% endif %}{% if current_language != 'all' %}&language={{ current_language }}{% endif %}{% endmacro -%} + +{% block title %}{{ q|e }} - {% endblock %} +{% block meta %}{% endblock %} +{% block content %} + {% include 'oscar/search.html' %} +
+
+

{{ _('Search results') }}

+ + {% if corrections %} +
+ {{ _('Try searching for:') }} + {% for correction in corrections %} + + {% endfor %} +
+ {% endif %} + + {% if answers %} + {% for answer in answers %} +
+ {{ answer }} +
+ {% endfor %} + {% endif %} + + {% for result in results %} +
+ {% set index = loop.index %} + {% if result.template %} + {% include get_result_template('oscar', result['template']) %} + {% else %} + {% include 'oscar/result_templates/default.html' %} + {% endif %} +
+ {% endfor %} + + {% if not results and not answers %} + {% include 'oscar/messages/no_results.html' %} + {% endif %} + +
+ + {% if paging %} + {% if rtl %} + +
+ {% else %} + +
+ {% endif %} + {% endif %} +
+ + +
+{% endblock %} diff --git a/searx/templates/oscar/search.html b/searx/templates/oscar/search.html new file mode 100644 index 0000000..59ee468 --- /dev/null +++ b/searx/templates/oscar/search.html @@ -0,0 +1,24 @@ +{% from 'oscar/macros.html' import icon %} + diff --git a/searx/templates/oscar/search_full.html b/searx/templates/oscar/search_full.html new file mode 100644 index 0000000..6fdae40 --- /dev/null +++ b/searx/templates/oscar/search_full.html @@ -0,0 +1,18 @@ +{% from 'oscar/macros.html' import icon %} + +