mirror of
https://github.com/searxng/searxng.git
synced 2026-06-22 17:48:33 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8dd88add9 |
@@ -0,0 +1,163 @@
|
||||
;;; .dir-locals.el
|
||||
;;
|
||||
;; Per-Directory Local Variables:
|
||||
;; https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html
|
||||
;;
|
||||
;; For full fledge developer tools install emacs packages:
|
||||
;;
|
||||
;; M-x package-install ...
|
||||
;;
|
||||
;; magit gitconfig
|
||||
;; nvm lsp-mode lsp-pyright lsp-eslint
|
||||
;; pyvenv pylint pip-requirements
|
||||
;; jinja2-mode
|
||||
;; json-mode
|
||||
;; company company-jedi company-quickhelp company-shell
|
||||
;; realgud
|
||||
;; sphinx-doc markdown-mode graphviz-dot-mode
|
||||
;; apache-mode nginx-mode
|
||||
;;
|
||||
;; To setup a developer environment, build target::
|
||||
;;
|
||||
;; $ make node.env.dev pyenv.install
|
||||
;;
|
||||
;; Some buffer locals are referencing the project environment:
|
||||
;;
|
||||
;; - prj-root --> <repo>/
|
||||
;; - nvm-dir --> <repo>/.nvm
|
||||
;; - python-environment-directory --> <repo>/local
|
||||
;; - python-environment-default-root-name --> py3
|
||||
;; - python-shell-virtualenv-root --> <repo>/local/py3
|
||||
;; When this variable is set with the path of the virtualenv to use,
|
||||
;; `process-environment' and `exec-path' get proper values in order to run
|
||||
;; shells inside the specified virtualenv, example::
|
||||
;; (setq python-shell-virtualenv-root "/path/to/env/")
|
||||
;; - python-shell-interpreter --> <repo>/local/py3/bin/python
|
||||
;;
|
||||
;; Python development:
|
||||
;;
|
||||
;; Jedi, flycheck & other python stuff should use the 'python-shell-interpreter'
|
||||
;; from the local py3 environment.
|
||||
;;
|
||||
|
||||
((nil
|
||||
. ((fill-column . 80)
|
||||
(indent-tabs-mode . nil)
|
||||
(eval . (progn
|
||||
|
||||
(add-to-list 'auto-mode-alist '("\\.html\\'" . jinja2-mode))
|
||||
|
||||
;; project root folder is where the `.dir-locals.el' is located
|
||||
(setq-local prj-root
|
||||
(locate-dominating-file default-directory ".dir-locals.el"))
|
||||
|
||||
(setq-local python-environment-directory
|
||||
(expand-file-name "./local" prj-root))
|
||||
|
||||
;; to get in use of NVM environment, install https://github.com/rejeep/nvm.el
|
||||
(setq-local nvm-dir (expand-file-name "./.nvm" prj-root))
|
||||
|
||||
;; use nodejs from the (local) NVM environment (see nvm-dir)
|
||||
(nvm-use-for-buffer)
|
||||
(ignore-errors (require 'lsp))
|
||||
(setq-local lsp-server-install-dir (car (cdr nvm-current-version)))
|
||||
(setq-local lsp-enable-file-watchers nil)
|
||||
|
||||
;; use 'py3' environment as default
|
||||
(setq-local python-environment-default-root-name
|
||||
"py3")
|
||||
|
||||
(setq-local python-shell-virtualenv-root
|
||||
(expand-file-name
|
||||
python-environment-default-root-name python-environment-directory))
|
||||
|
||||
(setq-local python-shell-interpreter
|
||||
(expand-file-name
|
||||
"bin/python" python-shell-virtualenv-root))))))
|
||||
(makefile-gmake-mode
|
||||
. ((indent-tabs-mode . t)))
|
||||
|
||||
(yaml-mode
|
||||
. ((eval . (progn
|
||||
|
||||
;; flycheck should use the local py3 environment
|
||||
(setq-local flycheck-yaml-yamllint-executable
|
||||
(expand-file-name "bin/yamllint" python-shell-virtualenv-root))
|
||||
|
||||
(setq-local flycheck-yamllintrc
|
||||
(expand-file-name ".yamllint.yml" prj-root))
|
||||
|
||||
(flycheck-checker . yaml-yamllint)))))
|
||||
|
||||
(json-mode
|
||||
. ((eval . (progn
|
||||
(setq-local js-indent-level 4)
|
||||
(flycheck-checker . json-python-json)))))
|
||||
|
||||
(js-mode
|
||||
. ((eval . (progn
|
||||
(ignore-errors (require 'lsp-eslint))
|
||||
(setq-local js-indent-level 2)
|
||||
;; flycheck should use the eslint checker from developer tools
|
||||
(setq-local flycheck-javascript-eslint-executable
|
||||
(expand-file-name "node_modules/.bin/eslint" prj-root))
|
||||
;; (flycheck-mode)
|
||||
|
||||
(if (featurep 'lsp-eslint)
|
||||
(lsp))
|
||||
))))
|
||||
|
||||
(python-mode
|
||||
. ((eval . (progn
|
||||
(ignore-errors (require 'jedi-core))
|
||||
(ignore-errors (require 'lsp-pyright))
|
||||
(ignore-errors (sphinx-doc-mode))
|
||||
(setq-local python-environment-virtualenv
|
||||
(list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
|
||||
;;"--system-site-packages"
|
||||
"--quiet"))
|
||||
|
||||
(setq-local pylint-command
|
||||
(expand-file-name "bin/pylint" python-shell-virtualenv-root))
|
||||
|
||||
(if (featurep 'lsp-pyright)
|
||||
(lsp))
|
||||
|
||||
;; pylint will find the '.pylintrc' file next to the CWD
|
||||
;; https://pylint.readthedocs.io/en/latest/user_guide/run.html#command-line-options
|
||||
(setq-local flycheck-pylintrc
|
||||
".pylintrc")
|
||||
|
||||
;; flycheck & other python stuff should use the local py3 environment
|
||||
(setq-local flycheck-python-pylint-executable
|
||||
python-shell-interpreter)
|
||||
|
||||
;; use 'M-x jedi:show-setup-info' and 'M-x epc:controller' to inspect jedi server
|
||||
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-root -- You
|
||||
;; can specify a full path instead of a name (relative path). In that case,
|
||||
;; python-environment-directory is ignored and Python virtual environment
|
||||
;; is created at the specified path.
|
||||
(setq-local jedi:environment-root
|
||||
python-shell-virtualenv-root)
|
||||
|
||||
;; https://tkf.github.io/emacs-jedi/latest/#jedi:server-command
|
||||
(setq-local jedi:server-command
|
||||
(list python-shell-interpreter
|
||||
jedi:server-script))
|
||||
|
||||
;; jedi:environment-virtualenv --> see above 'python-environment-virtualenv'
|
||||
;; is set buffer local! No need to setup jedi:environment-virtualenv:
|
||||
;;
|
||||
;; Virtualenv command to use. A list of string. If it is nil,
|
||||
;; python-environment-virtualenv is used instead. You must set non-nil
|
||||
;; value to jedi:environment-root in order to make this setting work.
|
||||
;;
|
||||
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-virtualenv
|
||||
;;
|
||||
;; (setq-local jedi:environment-virtualenv
|
||||
;; (list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
|
||||
;; "--python"
|
||||
;; "/usr/bin/python3.4"
|
||||
;; ))
|
||||
))))
|
||||
)
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
fetch-depth: "0"
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
fetch-depth: "0"
|
||||
@@ -50,14 +50,11 @@ jobs:
|
||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup dependencies
|
||||
run: sudo ./utils/searxng.sh install buildhost
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
- name: Build documentation
|
||||
run: make V=1 docs.html
|
||||
run: make V=1 docs.clean docs.html
|
||||
|
||||
- if: github.ref_name == 'master'
|
||||
name: Release
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
||||
fetch-depth: "0"
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
||||
fetch-depth: "0"
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
@@ -41,6 +41,6 @@ jobs:
|
||||
write-comment: "false"
|
||||
|
||||
- name: Upload SARIFs
|
||||
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
with:
|
||||
sarif_file: "./scout.sarif"
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[[language]]
|
||||
name = "python"
|
||||
language-servers = ["basedpyright", "pylsp"]
|
||||
auto-format = true
|
||||
|
||||
[language-server.pylsp.config.pylsp]
|
||||
plugins.pylint.enabled = true
|
||||
plugins.isort.enabled = true
|
||||
plugins.black.enabled = true
|
||||
plugins.black.skip_string_normalization = true
|
||||
plugins.black.line_length = 120
|
||||
+29
-15
@@ -2,12 +2,12 @@
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": ["**", "!node_modules", "!src/brand", "!src/svg"]
|
||||
"includes": ["**", "!node_modules"]
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"preset": "recommended",
|
||||
"recommended": true,
|
||||
"source": {
|
||||
"useSortedAttributes": "on",
|
||||
"useSortedProperties": "on"
|
||||
@@ -27,14 +27,12 @@
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"preset": "recommended",
|
||||
"recommended": true,
|
||||
"complexity": {
|
||||
"noForEach": "error",
|
||||
"noImplicitCoercions": "error",
|
||||
"noRedundantDefaultExport": "error",
|
||||
"noUselessCatchBinding": "error",
|
||||
"noUselessUndefined": "error",
|
||||
"useArrayFind": "error",
|
||||
"useSimplifiedLogicExpression": "error"
|
||||
},
|
||||
"correctness": {
|
||||
@@ -44,11 +42,25 @@
|
||||
"useSingleJsDocAsterisk": "error"
|
||||
},
|
||||
"nursery": {
|
||||
"noContinue": "warn",
|
||||
"noEqualsToNull": "warn",
|
||||
"noFloatingPromises": "warn",
|
||||
"noForIn": "warn",
|
||||
"noIncrementDecrement": "warn",
|
||||
"noMisusedPromises": "warn",
|
||||
"noMultiAssign": "warn",
|
||||
"noMultiStr": "warn",
|
||||
"noNestedPromises": "warn",
|
||||
"noParametersOnlyUsedInRecursion": "warn",
|
||||
"noRedundantDefaultExport": "warn",
|
||||
"noReturnAssign": "warn",
|
||||
"noUselessReturn": "off",
|
||||
"useAwaitThenable": "off",
|
||||
"useConsistentEnumValueType": "warn",
|
||||
"useDestructuring": "warn",
|
||||
"useExhaustiveSwitchCases": "warn",
|
||||
"useExplicitType": "off",
|
||||
"useFind": "warn",
|
||||
"useRegexpExec": "warn"
|
||||
},
|
||||
"performance": {
|
||||
@@ -63,15 +75,23 @@
|
||||
"noCommonJs": "error",
|
||||
"noEnum": "error",
|
||||
"noImplicitBoolean": "error",
|
||||
"noIncrementDecrement": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noMultiAssign": "error",
|
||||
"noMultilineString": "error",
|
||||
"noNamespace": "error",
|
||||
"noNegationElse": "error",
|
||||
"noNestedTernary": "error",
|
||||
"noParameterAssign": "error",
|
||||
"noParameterProperties": "error",
|
||||
"noRestrictedTypes": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"types": {
|
||||
"Element": {
|
||||
"message": "Element is too generic",
|
||||
"use": "HTMLElement"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"noSubstr": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"noUselessElse": "error",
|
||||
@@ -87,7 +107,6 @@
|
||||
}
|
||||
},
|
||||
"useConsistentBuiltinInstantiation": "error",
|
||||
"useConsistentEnumValueType": "error",
|
||||
"useConsistentMemberAccessibility": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
@@ -107,7 +126,6 @@
|
||||
}
|
||||
},
|
||||
"useDefaultSwitchClause": "error",
|
||||
"useDestructuring": "error",
|
||||
"useExplicitLengthCheck": "error",
|
||||
"useForOf": "error",
|
||||
"useGroupedAccessorPairs": "error",
|
||||
@@ -124,17 +142,13 @@
|
||||
"useUnifiedTypeSignatures": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noAlert": "error",
|
||||
"noBitwiseOperators": "error",
|
||||
"noConstantBinaryExpressions": "error",
|
||||
"noDeprecatedImports": "error",
|
||||
"noEmptyBlockStatements": "error",
|
||||
"noEqualsToNull": "error",
|
||||
"noEvolvingTypes": "error",
|
||||
"noForIn": "error",
|
||||
"noImportCycles": "error",
|
||||
"noNestedPromises": "error",
|
||||
"noParametersOnlyUsedInRecursion": "error",
|
||||
"noReturnAssign": "error",
|
||||
"noUnassignedVariables": "error",
|
||||
"noVar": "error",
|
||||
"useNumberToFixedDigitsArgument": "error",
|
||||
|
||||
Generated
+313
-361
File diff suppressed because it is too large
Load Diff
@@ -29,21 +29,21 @@
|
||||
"swiped-events": "1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.0",
|
||||
"@types/node": "^25.9.3",
|
||||
"@biomejs/biome": "2.4.15",
|
||||
"@types/node": "^25.8.0",
|
||||
"browserslist": "^4.28.2",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"edge.js": "^6.5.1",
|
||||
"edge.js": "^6.5.0",
|
||||
"less": "^4.6.4",
|
||||
"mathjs": "^15.2.0",
|
||||
"sharp": "~0.35.1",
|
||||
"sort-package-json": "^4.0.0",
|
||||
"stylelint": "^17.13.0",
|
||||
"sharp": "~0.34.5",
|
||||
"sort-package-json": "^3.6.1",
|
||||
"stylelint": "^17.11.1",
|
||||
"stylelint-config-standard-less": "^4.1.0",
|
||||
"stylelint-prettier": "^5.0.3",
|
||||
"svgo": "^4.0.1",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "^8.0.16",
|
||||
"vite": "^8.0.13",
|
||||
"vite-bundle-analyzer": "^1.3.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { KeyBindingLayout } from "./main/keyboard.ts";
|
||||
// synced with searx/webapp.py get_client_settings
|
||||
type Settings = {
|
||||
plugins?: string[];
|
||||
advanced_search?: boolean;
|
||||
autocomplete?: string;
|
||||
autocomplete_min?: number;
|
||||
doi_resolver?: string;
|
||||
|
||||
@@ -19,7 +19,6 @@ Settings
|
||||
settings_search
|
||||
settings_server
|
||||
settings_ui
|
||||
settings_preferences
|
||||
settings_redis
|
||||
settings_valkey
|
||||
settings_outgoing
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.. _settings preferences:
|
||||
|
||||
================
|
||||
``preferences:``
|
||||
================
|
||||
|
||||
.. autoclass:: searx._settings.SettingsPref
|
||||
:members:
|
||||
@@ -47,7 +47,6 @@
|
||||
activated:
|
||||
|
||||
- :py:obj:`searx.botdetection.link_token` in the :ref:`limiter`
|
||||
- :ref:`image_proxy`
|
||||
|
||||
.. _image_proxy:
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.. _kagi engines:
|
||||
|
||||
============
|
||||
Kagi Engines
|
||||
============
|
||||
|
||||
.. automodule:: searx.engines.kagi
|
||||
:members:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.. _karmasearch engine:
|
||||
|
||||
===========
|
||||
Karmasearch
|
||||
===========
|
||||
|
||||
.. automodule:: searx.engines.karmasearch
|
||||
:members:
|
||||
@@ -23,6 +23,6 @@ coloredlogs==15.0.1
|
||||
docutils>=0.23;python_version <= "3.11"
|
||||
docutils>=0.22.4; python_version > "3.11"
|
||||
parameterized==0.9.0
|
||||
granian[reload]==2.7.6
|
||||
basedpyright==1.39.7
|
||||
granian[reload]==2.7.5
|
||||
basedpyright==1.39.6
|
||||
types-lxml==2026.2.16
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
granian==2.7.6
|
||||
granian[pname]==2.7.6
|
||||
granian==2.7.5
|
||||
granian[pname]==2.7.5
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ sniffio==1.3.1
|
||||
valkey==6.1.1
|
||||
markdown-it-py==4.2.0
|
||||
msgspec==0.21.1
|
||||
typer==0.26.7
|
||||
typer==0.26.3
|
||||
isodate==0.7.2
|
||||
whitenoise==6.12.0
|
||||
typing-extensions==4.15.0
|
||||
|
||||
+1
-8
@@ -10,7 +10,6 @@ from os.path import dirname, abspath
|
||||
import logging
|
||||
|
||||
import msgspec
|
||||
from ._settings import SettingsPref
|
||||
|
||||
# Debug
|
||||
LOG_FORMAT_DEBUG: str = '%(levelname)-7s %(name)-30.30s: %(message)s'
|
||||
@@ -48,12 +47,6 @@ def init_settings():
|
||||
settings.clear()
|
||||
settings.update(cfg)
|
||||
|
||||
if get_setting("server.public_instance"):
|
||||
# enable image proxy for public instances #6125
|
||||
settings["server"]["image_proxy"] = True
|
||||
pref: SettingsPref = get_setting("preferences")
|
||||
pref.lock.add("image_proxy")
|
||||
|
||||
sxng_debug = get_setting("general.debug")
|
||||
if sxng_debug:
|
||||
_logging_config_debug()
|
||||
@@ -73,7 +66,7 @@ def init_settings():
|
||||
if settings['server']['public_instance']:
|
||||
logger.warning(
|
||||
"Be aware you have activated features intended only for public instances. "
|
||||
"This force the usage of the limiter, link_token and image proxy / "
|
||||
"This force the usage of the limiter and link_token / "
|
||||
"see https://docs.searxng.org/admin/searx.limiter.html"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Implementation of the :py:obj:`preference <searx.preference>` settings."""
|
||||
# pylint: disable = too-few-public-methods
|
||||
|
||||
import typing as t
|
||||
import msgspec
|
||||
|
||||
|
||||
class SettingsPref(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
|
||||
"""Options for configuring the preferences
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
preferences:
|
||||
lock:
|
||||
- favicon_resolver
|
||||
- image_proxy
|
||||
- method
|
||||
# ...
|
||||
|
||||
"""
|
||||
|
||||
lock: set[
|
||||
t.Literal[
|
||||
"categories",
|
||||
"language",
|
||||
"locale",
|
||||
"autocomplete",
|
||||
"favicon_resolver",
|
||||
"image_proxy",
|
||||
"method",
|
||||
"safesearch",
|
||||
"theme",
|
||||
"results_on_new_tab",
|
||||
"doi_resolver",
|
||||
"simple_style",
|
||||
"center_alignment",
|
||||
"query_in_title",
|
||||
"search_on_category_select",
|
||||
]
|
||||
] = set()
|
||||
"""Lock arbitrary settings on the preferences page."""
|
||||
+5
-8
@@ -444,10 +444,12 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
|
||||
def get(self, key: str, default: typing.Any = None, ctx: str | None = None) -> typing.Any:
|
||||
"""Get value of ``key`` from table given by argument ``ctx``. If
|
||||
``ctx`` argument is ``None`` (the default), a table name is generated
|
||||
from the :py:obj:`ExpireCacheCfg.name`. If ``key`` not exists in
|
||||
the table or the table not exists, the ``default`` value is returned.
|
||||
from the :py:obj:`ExpireCacheCfg.name`. If ``key`` not exists (in
|
||||
table), the ``default`` value is returned.
|
||||
|
||||
"""
|
||||
table = ctx
|
||||
self.maintenance()
|
||||
|
||||
if not table:
|
||||
table = self.normalize_name(self.cfg.name)
|
||||
@@ -455,9 +457,6 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
|
||||
if table not in self.table_names:
|
||||
return default
|
||||
|
||||
# Before values are taken from the table, a maintenance interval may
|
||||
# need to be carried out.
|
||||
self.maintenance()
|
||||
sql = f"SELECT value FROM {table} WHERE key = ?"
|
||||
row = self.DB.execute(sql, (key,)).fetchone()
|
||||
if row is None:
|
||||
@@ -470,14 +469,12 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
|
||||
If ``ctx`` argument is ``None`` (the default), a table name is
|
||||
generated from the :py:obj:`ExpireCacheCfg.name`."""
|
||||
table = ctx
|
||||
self.maintenance()
|
||||
|
||||
if not table:
|
||||
table = self.normalize_name(self.cfg.name)
|
||||
|
||||
if table in self.table_names:
|
||||
# Before values are taken from the table, a maintenance interval may
|
||||
# need to be carried out.
|
||||
self.maintenance()
|
||||
for row in self.DB.execute(f"SELECT key, value FROM {table}"):
|
||||
yield row[0], self.deserialize(row[1])
|
||||
|
||||
|
||||
@@ -5740,6 +5740,186 @@
|
||||
"zu-ZA": "ZA"
|
||||
}
|
||||
},
|
||||
"karmasearch": {
|
||||
"all_locale": null,
|
||||
"custom": {},
|
||||
"data_type": "traits_v1",
|
||||
"languages": {},
|
||||
"regions": {
|
||||
"da-DK": "da-DK",
|
||||
"de-AT": "de-AT",
|
||||
"de-CH": "de-CH",
|
||||
"de-DE": "de-DE",
|
||||
"en-AU": "en-AU",
|
||||
"en-CA": "en-CA",
|
||||
"en-GB": "en-GB",
|
||||
"en-ID": "en-ID",
|
||||
"en-IN": "en-IN",
|
||||
"en-MY": "en-MY",
|
||||
"en-NZ": "en-NZ",
|
||||
"en-PH": "en-PH",
|
||||
"en-US": "en-US",
|
||||
"en-ZA": "en-ZA",
|
||||
"es-AR": "es-AR",
|
||||
"es-CL": "es-CL",
|
||||
"es-ES": "es-ES",
|
||||
"es-MX": "es-MX",
|
||||
"es-US": "es-US",
|
||||
"fi-FI": "fi-FI",
|
||||
"fr-BE": "fr-BE",
|
||||
"fr-CA": "fr-CA",
|
||||
"fr-CH": "fr-CH",
|
||||
"fr-FR": "fr-FR",
|
||||
"it-IT": "it-IT",
|
||||
"ja-JP": "ja-JP",
|
||||
"ko-KR": "ko-KR",
|
||||
"nl-BE": "nl-BE",
|
||||
"nl-NL": "nl-NL",
|
||||
"pl-PL": "pl-PL",
|
||||
"pt-BR": "pt-BR",
|
||||
"ru-RU": "ru-RU",
|
||||
"sv-SE": "sv-SE",
|
||||
"tr-TR": "tr-TR",
|
||||
"zh-CN": "zh-CN",
|
||||
"zh-HK": "zh-HK",
|
||||
"zh-TW": "zh-TW"
|
||||
}
|
||||
},
|
||||
"karmasearch images": {
|
||||
"all_locale": null,
|
||||
"custom": {},
|
||||
"data_type": "traits_v1",
|
||||
"languages": {},
|
||||
"regions": {
|
||||
"da-DK": "da-DK",
|
||||
"de-AT": "de-AT",
|
||||
"de-CH": "de-CH",
|
||||
"de-DE": "de-DE",
|
||||
"en-AU": "en-AU",
|
||||
"en-CA": "en-CA",
|
||||
"en-GB": "en-GB",
|
||||
"en-ID": "en-ID",
|
||||
"en-IN": "en-IN",
|
||||
"en-MY": "en-MY",
|
||||
"en-NZ": "en-NZ",
|
||||
"en-PH": "en-PH",
|
||||
"en-US": "en-US",
|
||||
"en-ZA": "en-ZA",
|
||||
"es-AR": "es-AR",
|
||||
"es-CL": "es-CL",
|
||||
"es-ES": "es-ES",
|
||||
"es-MX": "es-MX",
|
||||
"es-US": "es-US",
|
||||
"fi-FI": "fi-FI",
|
||||
"fr-BE": "fr-BE",
|
||||
"fr-CA": "fr-CA",
|
||||
"fr-CH": "fr-CH",
|
||||
"fr-FR": "fr-FR",
|
||||
"it-IT": "it-IT",
|
||||
"ja-JP": "ja-JP",
|
||||
"ko-KR": "ko-KR",
|
||||
"nl-BE": "nl-BE",
|
||||
"nl-NL": "nl-NL",
|
||||
"pl-PL": "pl-PL",
|
||||
"pt-BR": "pt-BR",
|
||||
"ru-RU": "ru-RU",
|
||||
"sv-SE": "sv-SE",
|
||||
"tr-TR": "tr-TR",
|
||||
"zh-CN": "zh-CN",
|
||||
"zh-HK": "zh-HK",
|
||||
"zh-TW": "zh-TW"
|
||||
}
|
||||
},
|
||||
"karmasearch news": {
|
||||
"all_locale": null,
|
||||
"custom": {},
|
||||
"data_type": "traits_v1",
|
||||
"languages": {},
|
||||
"regions": {
|
||||
"da-DK": "da-DK",
|
||||
"de-AT": "de-AT",
|
||||
"de-CH": "de-CH",
|
||||
"de-DE": "de-DE",
|
||||
"en-AU": "en-AU",
|
||||
"en-CA": "en-CA",
|
||||
"en-GB": "en-GB",
|
||||
"en-ID": "en-ID",
|
||||
"en-IN": "en-IN",
|
||||
"en-MY": "en-MY",
|
||||
"en-NZ": "en-NZ",
|
||||
"en-PH": "en-PH",
|
||||
"en-US": "en-US",
|
||||
"en-ZA": "en-ZA",
|
||||
"es-AR": "es-AR",
|
||||
"es-CL": "es-CL",
|
||||
"es-ES": "es-ES",
|
||||
"es-MX": "es-MX",
|
||||
"es-US": "es-US",
|
||||
"fi-FI": "fi-FI",
|
||||
"fr-BE": "fr-BE",
|
||||
"fr-CA": "fr-CA",
|
||||
"fr-CH": "fr-CH",
|
||||
"fr-FR": "fr-FR",
|
||||
"it-IT": "it-IT",
|
||||
"ja-JP": "ja-JP",
|
||||
"ko-KR": "ko-KR",
|
||||
"nl-BE": "nl-BE",
|
||||
"nl-NL": "nl-NL",
|
||||
"pl-PL": "pl-PL",
|
||||
"pt-BR": "pt-BR",
|
||||
"ru-RU": "ru-RU",
|
||||
"sv-SE": "sv-SE",
|
||||
"tr-TR": "tr-TR",
|
||||
"zh-CN": "zh-CN",
|
||||
"zh-HK": "zh-HK",
|
||||
"zh-TW": "zh-TW"
|
||||
}
|
||||
},
|
||||
"karmasearch videos": {
|
||||
"all_locale": null,
|
||||
"custom": {},
|
||||
"data_type": "traits_v1",
|
||||
"languages": {},
|
||||
"regions": {
|
||||
"da-DK": "da-DK",
|
||||
"de-AT": "de-AT",
|
||||
"de-CH": "de-CH",
|
||||
"de-DE": "de-DE",
|
||||
"en-AU": "en-AU",
|
||||
"en-CA": "en-CA",
|
||||
"en-GB": "en-GB",
|
||||
"en-ID": "en-ID",
|
||||
"en-IN": "en-IN",
|
||||
"en-MY": "en-MY",
|
||||
"en-NZ": "en-NZ",
|
||||
"en-PH": "en-PH",
|
||||
"en-US": "en-US",
|
||||
"en-ZA": "en-ZA",
|
||||
"es-AR": "es-AR",
|
||||
"es-CL": "es-CL",
|
||||
"es-ES": "es-ES",
|
||||
"es-MX": "es-MX",
|
||||
"es-US": "es-US",
|
||||
"fi-FI": "fi-FI",
|
||||
"fr-BE": "fr-BE",
|
||||
"fr-CA": "fr-CA",
|
||||
"fr-CH": "fr-CH",
|
||||
"fr-FR": "fr-FR",
|
||||
"it-IT": "it-IT",
|
||||
"ja-JP": "ja-JP",
|
||||
"ko-KR": "ko-KR",
|
||||
"nl-BE": "nl-BE",
|
||||
"nl-NL": "nl-NL",
|
||||
"pl-PL": "pl-PL",
|
||||
"pt-BR": "pt-BR",
|
||||
"ru-RU": "ru-RU",
|
||||
"sv-SE": "sv-SE",
|
||||
"tr-TR": "tr-TR",
|
||||
"zh-CN": "zh-CN",
|
||||
"zh-HK": "zh-HK",
|
||||
"zh-TW": "zh-TW"
|
||||
}
|
||||
},
|
||||
"mojeek": {
|
||||
"all_locale": null,
|
||||
"custom": {
|
||||
|
||||
@@ -12,7 +12,6 @@ import typing as t
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import os
|
||||
from os.path import realpath, dirname
|
||||
|
||||
import types
|
||||
@@ -279,8 +278,6 @@ def load_engines(engine_list: list[dict[str, t.Any]]):
|
||||
else:
|
||||
# if an engine can't be loaded (if for example the engine is missing
|
||||
# tor or some other requirements) its set to inactive!
|
||||
logger.error(
|
||||
f"(PID {os.getpid()}) loading engine %s failed: set engine to inactive!", engine_data.get("name", "???")
|
||||
)
|
||||
logger.error("loading engine %s failed: set engine to inactive!", engine_data.get("name", "???"))
|
||||
engine_data["inactive"] = True
|
||||
return engines
|
||||
|
||||
@@ -40,7 +40,7 @@ if t.TYPE_CHECKING:
|
||||
|
||||
about = {
|
||||
"website": "https://www.aol.com",
|
||||
"wikidata_id": "Q27585",
|
||||
"wikidata_id": "Q2407",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
|
||||
@@ -51,10 +51,11 @@ def request(query, params):
|
||||
}
|
||||
|
||||
params["url"] = f"{base_url}?{urlencode(query_params)}"
|
||||
params["headers"]["Referer"] = "https://www.bilibili.com/"
|
||||
params["headers"]["Accept"] = "application/json, text/javascript, */*; q=0.01"
|
||||
params["headers"]["Referer"] = "https://www.bilibili.com"
|
||||
params["cookies"] = cookie
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
search_res = resp.json()
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Dogpile is a metasearch engine by the American advertising company `System1`_.
|
||||
|
||||
.. _System1: https://system1.com/
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
from datetime import datetime, timezone
|
||||
import html
|
||||
|
||||
from searx.utils import format_duration, html_to_text, humanize_number
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
about = {
|
||||
"website": "https://www.dogpile.com",
|
||||
"wikidata_id": "Q3595363",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
paging = True
|
||||
safesearch = True
|
||||
|
||||
categories = ["general"]
|
||||
dogpile_categ = "search"
|
||||
"""Category to search in. Can be either "search", "images", "videos" or "news"."""
|
||||
|
||||
|
||||
base_url = "https://www.dogpile.com"
|
||||
safe_search_map = {0: "none", 1: "moderate", 2: "heavy"}
|
||||
|
||||
|
||||
def init(_):
|
||||
if dogpile_categ not in ("search", "images", "videos", "news"):
|
||||
raise ValueError("invalid search type: %s" % dogpile_categ)
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams"):
|
||||
params["url"] = f"{base_url}/api/{dogpile_categ}"
|
||||
params["method"] = "POST"
|
||||
params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]}
|
||||
return params
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response"):
|
||||
res = EngineResults()
|
||||
|
||||
json_resp = resp.json()
|
||||
|
||||
for result in json_resp["results"]:
|
||||
if dogpile_categ == "search":
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=result["clickUrl"],
|
||||
title=html_to_text(result["title"]),
|
||||
content=html_to_text(result["description"]),
|
||||
)
|
||||
)
|
||||
elif dogpile_categ == "news":
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=result["clickUrl"],
|
||||
title=html_to_text(html.unescape(result["title"])),
|
||||
content=html_to_text(html.unescape(result["description"])),
|
||||
thumbnail=result["thumbnailUrl"],
|
||||
publishedDate=datetime.fromtimestamp(result["date"], tz=timezone.utc),
|
||||
)
|
||||
)
|
||||
elif dogpile_categ == "videos":
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
template="videos.html",
|
||||
url=result["clickUrl"],
|
||||
title=html_to_text(result["title"]),
|
||||
content=html_to_text(result["description"]),
|
||||
thumbnail=result["thumbnailUrl"],
|
||||
publishedDate=datetime.fromisoformat(result["publishDate"]),
|
||||
length=format_duration(result["duration"]),
|
||||
views=humanize_number(result["viewCount"]),
|
||||
)
|
||||
)
|
||||
elif dogpile_categ == "images":
|
||||
res.add(
|
||||
res.types.Image(
|
||||
url=result["altClickUrl"],
|
||||
title=html_to_text(result["title"]),
|
||||
content=html_to_text(result["description"]),
|
||||
img_src=result["clickUrl"],
|
||||
thumbnail_src=result["thumbnailUrl"],
|
||||
resolution=f"{result['width']}x{result['height']}",
|
||||
img_format=result["format"],
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
@@ -41,9 +41,7 @@ safesearch_cookies = {0: "-2", 1: None, 2: "1"}
|
||||
safesearch_args = {0: "1", 1: None, 2: "1"}
|
||||
|
||||
search_path_map = {"images": "i", "videos": "v", "news": "news"}
|
||||
|
||||
_HTTP_User_Agent: str = gen_useragent()
|
||||
send_accept_language_header = False
|
||||
|
||||
|
||||
def init(engine_settings: dict[str, t.Any]):
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""DuckDuckGo Web (general)
|
||||
|
||||
This implementation fetches the link to the first API page
|
||||
(i.e. ``links.duckduckgo.com/d.js?...``) from duckduckgo.com and uses the ``n``
|
||||
parameter of the API to fetch all subsequent pages.
|
||||
|
||||
This also means that it's not possible to immediately search for the third
|
||||
page - the first and the second page would need to be loaded first.
|
||||
|
||||
The reason why we can't just normally use the `vqd` value is that the API URLs
|
||||
require an additional parameter `dp` which seems generated at server-side, so we
|
||||
can't build it ourselves and must scrape it from the HTML pages.
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
|
||||
from urllib.parse import quote_plus
|
||||
from lxml import html
|
||||
|
||||
from searx.utils import html_to_text, gen_useragent, extract_text, eval_xpath
|
||||
from searx.result_types import EngineResults
|
||||
from searx.enginelib import EngineCache
|
||||
from searx.network import get
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
about = {
|
||||
"website": "https://duckduckgo.com/",
|
||||
"wikidata_id": "Q12805",
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
# engine dependent config
|
||||
categories = ["general"]
|
||||
paging = True
|
||||
_HTTP_User_Agent: str = gen_useragent()
|
||||
|
||||
base_url = "https://duckduckgo.com"
|
||||
|
||||
CACHE: EngineCache
|
||||
"""Cache to store the API URLs for combinations of (query, page)."""
|
||||
|
||||
|
||||
def setup(engine_settings: dict[str, str]):
|
||||
global CACHE # pylint:disable=global-statement
|
||||
CACHE = EngineCache(engine_settings["name"])
|
||||
return CACHE
|
||||
|
||||
|
||||
def _fetch_first_page_link(
|
||||
query: str,
|
||||
headers: dict[str, str],
|
||||
):
|
||||
"""Search for a::
|
||||
|
||||
<link id="deep_preload_link" rel="preload" as="script"
|
||||
href="https://links.duckduckgo.com/d.js?q=rust&t=D&l=us-en&s=0&a=h_&ct=DE&vqd=VQD_VALUE&bing_market=en-US&p_ent=&ex=-1&dp=LONG_TOKEN
|
||||
>
|
||||
|
||||
This points to the first page
|
||||
""" # pylint:disable=line-too-long
|
||||
|
||||
cache_key = _cache_key(query, 1)
|
||||
cached: str | None = CACHE.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
resp = get(
|
||||
url=f"{base_url}/?q={quote_plus(query)}&t=h_&ia=web",
|
||||
headers=headers,
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
logger.error("vqd: got HTTP %s from duckduckgo.com", resp.status_code)
|
||||
|
||||
dom = html.fromstring(resp.text)
|
||||
first_page_link = extract_text(eval_xpath(dom, "//link[@id='deep_preload_link']/@href"))
|
||||
|
||||
if not first_page_link:
|
||||
logger.error("vqd: failed to load first page JS url from ddg response (return empty string)")
|
||||
return ""
|
||||
|
||||
logger.debug("got link to first page from duckduckgo.com request: '%s'", first_page_link)
|
||||
CACHE.set(cache_key, first_page_link, expire=7200)
|
||||
|
||||
return first_page_link
|
||||
|
||||
|
||||
def _cache_key(query: str, pageno: int) -> str:
|
||||
return f"nextpage_url|{query}|{pageno}"
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
|
||||
if len(query) >= 500:
|
||||
# DDG does not accept queries with more than 499 chars
|
||||
params["url"] = None
|
||||
return
|
||||
|
||||
headers = params["headers"]
|
||||
|
||||
# The vqd value is generated from the query and the UA header. To be able
|
||||
# to reuse the vqd value, the UA header must be static.
|
||||
headers["User-Agent"] = _HTTP_User_Agent
|
||||
headers["Accept"] = "*/*"
|
||||
headers["Referer"] = f"{base_url}/"
|
||||
headers["Host"] = "duckduckgo.com"
|
||||
|
||||
# Sec-Fetch headers are required to not get blocked when sending a Firefox user agent
|
||||
headers["Sec-Fetch-Dest"] = "script"
|
||||
headers["Sec-Fetch-Mode"] = "no-cors"
|
||||
headers["Sec-Fetch-Site"] = "same-site"
|
||||
|
||||
api_url = ""
|
||||
if params["pageno"] > 1:
|
||||
api_url = CACHE.get(_cache_key(query, params["pageno"]))
|
||||
else:
|
||||
api_url = _fetch_first_page_link(query, headers)
|
||||
|
||||
if not api_url:
|
||||
params["url"] = None
|
||||
return
|
||||
|
||||
params["url"] = api_url.replace("/d.js?", "/d.js?o=json&")
|
||||
|
||||
# TODO: support safesearch, timerange and engine traits # pylint:disable=fixme
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response"):
|
||||
res = EngineResults()
|
||||
res_json = resp.json()
|
||||
|
||||
for result in res_json["results"]:
|
||||
if "u" not in result:
|
||||
continue
|
||||
|
||||
res.add(res.types.MainResult(url=result["u"], title=result["t"], content=html_to_text(result["a"])))
|
||||
|
||||
# link to next page
|
||||
next_page_path = res_json["results"][-1].get("n")
|
||||
if next_page_path:
|
||||
CACHE.set(
|
||||
_cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1),
|
||||
base_url + next_page_path,
|
||||
expire=60 * 60,
|
||||
)
|
||||
|
||||
return res
|
||||
@@ -1,169 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Fireball_ is a Germany-based, privacy-focused search engine.
|
||||
|
||||
It likely doesn't have its own index, but it's unclear where its results come
|
||||
from.
|
||||
|
||||
.. _Fireball: https://fireball.com
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from searx.enginelib import EngineCache
|
||||
from searx.exceptions import SearxEngineAPIException
|
||||
from searx.extended_types import SXNG_Response
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
from searx.network import post
|
||||
from searx.utils import html_to_text
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
about = {
|
||||
"website": "https://fireball.com",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
base_url = "https://fireball.com"
|
||||
categories = ["general"]
|
||||
fireball_category = "web" # values: "web", "news", "videos"
|
||||
|
||||
paging = False
|
||||
safesearch = True
|
||||
|
||||
safe_search_map = {0: "off", 1: "moderate", 2: "strict"}
|
||||
|
||||
CACHE: EngineCache
|
||||
"""Cache to store the settings cookie (contains e.g. language, safesearch, ...)."""
|
||||
|
||||
CACHE_VALID_DURATION = 30 * 24 * 3600 # one month, same as website
|
||||
"""Duration how long settings cookies are valid."""
|
||||
|
||||
|
||||
def init(engine_settings: dict[str, t.Any]):
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache(engine_settings["name"])
|
||||
|
||||
if fireball_category not in ("web", "news", "videos"):
|
||||
raise ValueError(f"Unsupported category: {fireball_category}")
|
||||
|
||||
|
||||
def _cache_key(fireball_settings: dict[str, str]) -> str:
|
||||
return f"fireball_settings_{fireball_settings['safesearch']}_{fireball_settings['market']}"
|
||||
|
||||
|
||||
def _get_search_settings_cookie(params: 'OnlineParams') -> str:
|
||||
"""Get a 'fireball' cookie for the given locale and safesearch setting set
|
||||
in params."""
|
||||
|
||||
# the language is set by only specifying the search country on their
|
||||
# website, they only list DE and US, but in fact it supports much more
|
||||
# countries
|
||||
country = "US"
|
||||
if params["searxng_locale"] != "all":
|
||||
language_parts = params["searxng_locale"].split("-")
|
||||
country = language_parts[-1].upper()
|
||||
|
||||
fireball_settings = {
|
||||
"action": "save",
|
||||
"language": "en", # language is irrelevant, only changes UI language
|
||||
"market": country,
|
||||
"adprovider": "automatic",
|
||||
"target": "_blank",
|
||||
"tiles": "on",
|
||||
"safesearch": safe_search_map[params["safesearch"]],
|
||||
}
|
||||
cache_key = _cache_key(fireball_settings)
|
||||
|
||||
cached_cookie = CACHE.get(cache_key)
|
||||
if cached_cookie:
|
||||
return cached_cookie
|
||||
|
||||
resp = post("https://fireball.com/settings", data=fireball_settings)
|
||||
if not resp.ok:
|
||||
raise SearxEngineAPIException("failed to obtain cookie for settings")
|
||||
|
||||
cookie = resp.cookies.get("fireball")
|
||||
if not cookie:
|
||||
raise SearxEngineAPIException("failed to obtain cookie for settings")
|
||||
|
||||
CACHE.set(cache_key, cookie, expire=CACHE_VALID_DURATION)
|
||||
return cookie
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams"):
|
||||
# no matter the category, the request is always the same, i.e. we get all
|
||||
# different categories with one HTTP request
|
||||
|
||||
args = {
|
||||
"f": "web",
|
||||
"q": query,
|
||||
}
|
||||
|
||||
params["url"] = f"{base_url}/getResults/?{urlencode(args)}"
|
||||
params["cookies"]["fireball"] = _get_search_settings_cookie(params)
|
||||
|
||||
# referer header has to be set, otherwise the requests get blocked
|
||||
params["headers"]["Referer"] = f"{base_url}/search?{urlencode(args)}"
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
res = EngineResults()
|
||||
|
||||
json_data = resp.json()
|
||||
|
||||
for result in json_data.get(fireball_category, {}).get("results", []):
|
||||
published_date = None
|
||||
if result.get("page_age"):
|
||||
published_date = datetime.fromisoformat(result["page_age"])
|
||||
|
||||
if fireball_category == "web":
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=result["url"],
|
||||
title=html_to_text(result["title"]),
|
||||
content=html_to_text(result["description"]),
|
||||
publishedDate=published_date,
|
||||
)
|
||||
)
|
||||
elif fireball_category == "news":
|
||||
thumbnail: str | None = None
|
||||
if result.get("thumbnail"):
|
||||
thumbnail = result["thumbnail"]["src"]
|
||||
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=result["url"],
|
||||
title=html_to_text(result["title"]),
|
||||
content=html_to_text(result["description"]),
|
||||
thumbnail=thumbnail or "",
|
||||
publishedDate=published_date,
|
||||
)
|
||||
)
|
||||
elif fireball_category == "videos":
|
||||
length = None
|
||||
if result.get("video"):
|
||||
length = result["video"].get("duration")
|
||||
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
{
|
||||
"template": "videos.html",
|
||||
"url": result["url"],
|
||||
"title": html_to_text(result["title"]),
|
||||
"content": html_to_text(result["description"]),
|
||||
"thumbnail": result.get("thumbnail", {}).get("original"),
|
||||
"length": length,
|
||||
"publishedDate": published_date,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
@@ -53,13 +53,10 @@ def response(resp: "SXNG_Response"):
|
||||
|
||||
result: dict[str, str] # TBH: dict[str, t.Any]
|
||||
for result in resp.json()["items"]:
|
||||
tags = [
|
||||
tag_info["tag"] for tag_info in result["tags"] if tag_info["tag"] # pyright: ignore[reportArgumentType]
|
||||
]
|
||||
res.add(
|
||||
res.types.Image(
|
||||
title=result["name"],
|
||||
content=", ".join(tags),
|
||||
content=", ".join([tag["tag"] for tag in result["tags"]]), # pyright: ignore[reportArgumentType]
|
||||
url=_fix_url(result["slug"]),
|
||||
thumbnail_src=_fix_url(result["png"]),
|
||||
img_src=_fix_url(result["png512"]),
|
||||
|
||||
@@ -10,12 +10,10 @@ import time
|
||||
import typing as t
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
from searx.exceptions import SearxEngineCaptchaException
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.utils import extr, gen_useragent, html_to_text, eval_xpath
|
||||
from searx.utils import extr, gen_useragent, html_to_text
|
||||
from searx.network import get
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
@@ -42,11 +40,6 @@ time_range_map = {"day": "d", "week": "w", "month": "m", "year": "y"}
|
||||
def _get_page_hash(query: str, page: int, headers: dict[str, str]) -> str:
|
||||
resp = get(f"{base_url}/web/result?q={query}&page={page}", headers=headers)
|
||||
|
||||
# detect captcha (if any)
|
||||
doc = html.fromstring(resp.text)
|
||||
if eval_xpath(doc, "//*[@id='spam-messages']"):
|
||||
raise SearxEngineCaptchaException()
|
||||
|
||||
# the text we search for looks like:
|
||||
# load("/desk?lang="+eV.p.param['hl']+"&q="+eV['p']['q_encode']+"&page=5&h=aa45603&t=177582576&origin=web&comp=web_serp_pag&p=gmx-com&sp=&lr="+eV.p.param['lr0']+"&mkt="+eV.p.param['mkt0']+"&family="+eV.p.param['familyFilter']+"&fcons="+eV.p.perm.fCons,"google", "eMMO", "eMH","eMP"); # pylint: disable=line-too-long
|
||||
return extr(resp.text, "&h=", "&t=")
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Heexy_ is a minimalist search engine that focuses on privacy.
|
||||
|
||||
Although it also supports news and videos, these are not implemented here
|
||||
because they usually return no result to very few irrelevant ones.
|
||||
|
||||
It seems to use Bing internally, as the image thumbnails are loaded from Bing.
|
||||
|
||||
.. _Heexy: https://docs.heexy.org/introduction
|
||||
|
||||
"""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import typing as t
|
||||
|
||||
from searx.exceptions import SearxEngineAccessDeniedException
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
about = {
|
||||
"website": "https://heexy.org",
|
||||
"wikidata_id": None,
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
paging = True
|
||||
safesearch = True
|
||||
|
||||
categories = ["general"]
|
||||
heexy_categ = "web"
|
||||
"""Category to search in. Can be either "web" or "image"."""
|
||||
|
||||
|
||||
base_url = "https://seapi.heexy.org"
|
||||
safe_search_map = {0: "off", 1: "on", 2: "on"}
|
||||
|
||||
|
||||
def init(_):
|
||||
if heexy_categ not in ("web", "image"):
|
||||
raise ValueError("invalid search category: %s" % heexy_categ)
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
args = {
|
||||
"q": query,
|
||||
"page": params["pageno"],
|
||||
"safe": safe_search_map[params["safesearch"]],
|
||||
}
|
||||
if params["searxng_locale"] != "all":
|
||||
args["lang"] = params["searxng_locale"].split("-")[0]
|
||||
|
||||
params["url"] = f"{base_url}/search/{heexy_categ}?{urlencode(args)}"
|
||||
params["headers"]["Origin"] = base_url
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response"):
|
||||
res = EngineResults()
|
||||
|
||||
json_resp = resp.json()
|
||||
if not json_resp["success"]:
|
||||
raise SearxEngineAccessDeniedException()
|
||||
|
||||
result: dict[str, str]
|
||||
for result in json_resp["results"]:
|
||||
if heexy_categ == "web":
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=result["url"],
|
||||
title=result["title"],
|
||||
content=result["description"],
|
||||
)
|
||||
)
|
||||
elif heexy_categ == "image":
|
||||
res.add(
|
||||
res.types.Image(
|
||||
title=result["description"],
|
||||
url=result["url"],
|
||||
thumbnail_src=result["image"],
|
||||
img_src=result["rawImage"],
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
@@ -20,7 +20,6 @@ Paging:
|
||||
- :py:obj:`paging`
|
||||
- :py:obj:`page_size`
|
||||
- :py:obj:`first_page_num`
|
||||
- :py:obj:`send_page_num_on_first_page`
|
||||
|
||||
Time Range:
|
||||
|
||||
@@ -170,10 +169,6 @@ number, but an offset.'''
|
||||
first_page_num = 1
|
||||
'''Number of the first page (usually 0 or 1).'''
|
||||
|
||||
send_page_num_on_first_page = True
|
||||
'''Whether to include the page number in the request for the first page.
|
||||
This can help if an engine blocks request that send a page number for the first page.'''
|
||||
|
||||
results_query = ''
|
||||
'''JSON query for the list of result items.
|
||||
|
||||
@@ -327,13 +322,10 @@ def request(query, params): # pylint: disable=redefined-outer-name
|
||||
if params['safesearch']:
|
||||
safe_search = safe_search_map[params['safesearch']]
|
||||
|
||||
pageno = ""
|
||||
if send_page_num_on_first_page or params["pageno"] != 1:
|
||||
pageno = (params['pageno'] - 1) * page_size + first_page_num
|
||||
fp = { # pylint: disable=invalid-name
|
||||
'query': urlencode({'q': query})[2:],
|
||||
'lang': lang,
|
||||
'pageno': pageno,
|
||||
'pageno': (params['pageno'] - 1) * page_size + first_page_num,
|
||||
'time_range': time_range,
|
||||
'safe_search': safe_search,
|
||||
}
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Kagi_ is a paid, privacy-focused search engine.
|
||||
|
||||
Using it requires an API key. If you have a Kagi account, you can obtain an API
|
||||
key in the `API portal`_.
|
||||
|
||||
To enable Kagi, add the following to the ``engines`` seciton of
|
||||
``settings.yml``:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: kagi
|
||||
engine: kagi
|
||||
categories: [general, web]
|
||||
shortcut: kg
|
||||
api_key: ""
|
||||
kagi_categ: search
|
||||
|
||||
- name: kagi.news
|
||||
engine: kagi
|
||||
categories: [news, web]
|
||||
shortcut: kgn
|
||||
api_key: ""
|
||||
kagi_categ: news
|
||||
|
||||
- name: kagi.images
|
||||
engine: kagi
|
||||
categories: [images, web]
|
||||
shortcut: kgi
|
||||
paging: false
|
||||
api_key: ""
|
||||
kagi_categ: images
|
||||
|
||||
- name: kagi.videos
|
||||
engine: kagi
|
||||
categories: [videos, web]
|
||||
shortcut: kgv
|
||||
api_key: ""
|
||||
kagi_categ: videos
|
||||
|
||||
.. _Kagi: https://kagi.com
|
||||
.. _Api Portal: https://help.kagi.com/kagi/api/overview.html
|
||||
"""
|
||||
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import typing as t
|
||||
import html
|
||||
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.result_types import EngineResults
|
||||
from searx.utils import parse_duration_string
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
TimeRangeType = t.Literal["day", "week", "month", "year"]
|
||||
about = {
|
||||
"website": "https://kagi.com",
|
||||
"wikidata_id": "Q26000117",
|
||||
"official_api_documentation": "https://kagi.com/api/docs/openapi",
|
||||
"use_official_api": True,
|
||||
"require_api_key": True,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
paging = True
|
||||
"""All categories except the ``images`` category support paging."""
|
||||
safesearch = True
|
||||
time_range_support = True
|
||||
|
||||
categories = ["general"]
|
||||
kagi_categ: t.Literal["search", "images", "news", "videos"] = "search"
|
||||
"""Search category. Supported values: "search" (general), "images", "news", "videos"."""
|
||||
|
||||
base_url = "https://kagi.com"
|
||||
|
||||
safe_search_map = {0: False, 1: True, 2: True}
|
||||
time_range_to_days_map: dict[TimeRangeType, int] = {"day": 1, "week": 7, "month": 30, "year": 365}
|
||||
|
||||
api_key = ""
|
||||
"""Kagi API key. Required for using this engine."""
|
||||
|
||||
|
||||
def init(_):
|
||||
if not api_key:
|
||||
raise ValueError("api_key is required for using kagi")
|
||||
|
||||
if kagi_categ not in ("search", "images", "news", "videos"):
|
||||
raise ValueError(f"Unsupported category: {kagi_categ}") # pyright: ignore[reportUnreachable]
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams"):
|
||||
# According to the API docs, Kagi supports at maximum page 10
|
||||
if params["pageno"] > 10:
|
||||
return
|
||||
|
||||
params["headers"]["Authorization"] = f"Bearer {api_key}"
|
||||
params["url"] = f"{base_url}/api/v1/search"
|
||||
|
||||
filters = {}
|
||||
time_range = params.get("time_range")
|
||||
if time_range:
|
||||
# Kagi expects the minimum date to return results from as argument to `after`
|
||||
time_period = timedelta(days=time_range_to_days_map[time_range])
|
||||
oldest_result_date = datetime.now() - time_period
|
||||
filters["after"] = oldest_result_date.strftime("%Y-%m-%d")
|
||||
|
||||
# there doesn't seem to be a list of languages anywhere,
|
||||
# so we just assume that it supports all languages
|
||||
|
||||
filters["region"] = "no_region"
|
||||
if params["searxng_locale"] != "all":
|
||||
_locale = params["searxng_locale"].split("-")
|
||||
if len(_locale) > 1:
|
||||
filters["region"] = _locale[-1].lower()
|
||||
|
||||
args: dict[str, t.Any] = {
|
||||
"query": query,
|
||||
"page": params["pageno"],
|
||||
"workflow": kagi_categ,
|
||||
"safe_search": safe_search_map[params["safesearch"]],
|
||||
"filters": filters,
|
||||
}
|
||||
|
||||
params["method"] = "POST"
|
||||
params["json"] = args
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
res = EngineResults()
|
||||
|
||||
json_data: dict[str, t.Any] = resp.json()
|
||||
|
||||
if kagi_categ in ("images", "videos"):
|
||||
# the JSON key is "image" for "images" and "video" for "videos"
|
||||
json_results = json_data["data"][kagi_categ[:-1]]
|
||||
else:
|
||||
json_results = json_data["data"][kagi_categ]
|
||||
|
||||
for result in json_results:
|
||||
published_date: datetime | None = None
|
||||
if result.get("time"):
|
||||
published_date = datetime.fromisoformat(result["time"])
|
||||
|
||||
if kagi_categ in ("search", "news"):
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=result["url"],
|
||||
title=html.unescape(result["title"]),
|
||||
content=html.unescape(result["snippet"]),
|
||||
thumbnail=result.get("image", {}).get("url") or "",
|
||||
publishedDate=published_date,
|
||||
)
|
||||
)
|
||||
elif kagi_categ == "images":
|
||||
res.add(
|
||||
res.types.Image(
|
||||
url=result["url"],
|
||||
title=html.unescape(result.get("title")),
|
||||
img_src=result.get("image", {}).get("url"),
|
||||
resolution=f"{result['image']['width']}x{result['image']['height']}",
|
||||
thumbnail_src=result.get("props", {}).get("thumbnail", {}).get("url"),
|
||||
)
|
||||
)
|
||||
elif kagi_categ == "videos":
|
||||
length: timedelta | None = None
|
||||
if result["props"].get("duration"):
|
||||
length = parse_duration_string(result["props"]["duration"])
|
||||
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
{
|
||||
"template": "videos.html",
|
||||
"url": result["url"],
|
||||
"title": html.unescape(result["title"]),
|
||||
"content": html.unescape(result["snippet"]),
|
||||
"thumbnail": result.get("image", {}).get("url"),
|
||||
"publishedDate": published_date,
|
||||
"author": result["props"].get("creator_name"),
|
||||
"length": length,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
for suggestion in json_data["data"].get("related_search", []):
|
||||
res.add(res.types.LegacyResult({"suggestion": suggestion["title"]}))
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,205 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Karmasearch uses Brave's index, so the results should be the same as Brave's.
|
||||
|
||||
However, the advantages of this engine are:
|
||||
|
||||
- it has less strict rate-limits
|
||||
- it has a JSON API, so it's less likely to break
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlencode
|
||||
import typing as t
|
||||
|
||||
from dateutil import parser
|
||||
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
|
||||
from searx.utils import html_to_text
|
||||
from searx.result_types import EngineResults, MainResult
|
||||
from searx.result_types._base import LegacyResult
|
||||
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
about = {
|
||||
"website": "https://karmasearch.org",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
base_url = "https://api.karmasearch.org"
|
||||
categories = ["web", "general"]
|
||||
search_type = "web" # supported: web, images, videos, news
|
||||
|
||||
# all types except "images" support pagination
|
||||
paging = True
|
||||
safesearch = True
|
||||
time_range_support = True
|
||||
|
||||
safe_search_map = {0: "off", 1: "moderate", 2: "strict"}
|
||||
time_range_map = {"day": "Day", "week": "Week", "month": "Month", "year": "Year"}
|
||||
|
||||
|
||||
def init(_):
|
||||
if search_type not in ("web", "images", "videos", "news"):
|
||||
raise ValueError(f"invalid search type: {search_type}")
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
engine_region: str = traits.get_region(params["searxng_locale"]) or "en-US"
|
||||
|
||||
args: dict[str, str | int] = {
|
||||
"searchTerm": query,
|
||||
"adultFilter": safe_search_map[params["safesearch"]],
|
||||
"pageNumber": params["pageno"],
|
||||
"country": engine_region.split("-")[-1],
|
||||
"userLanguage": "en", # UI language: en, es or fr / no effect on search results
|
||||
"market": engine_region,
|
||||
}
|
||||
if params["time_range"]:
|
||||
args["freshness"] = time_range_map[params["time_range"]]
|
||||
|
||||
# Needed to circumvent Cloudflare bot protection
|
||||
params['headers']['Referer'] = "https://karmasearch.org"
|
||||
|
||||
params["url"] = f"{base_url}/search/{search_type}?{urlencode(args)}"
|
||||
|
||||
|
||||
def _parse_date(date_string: str) -> datetime | None:
|
||||
try:
|
||||
return parser.parse(date_string)
|
||||
except parser.ParserError:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_general(result: dict[str, str]):
|
||||
return MainResult(
|
||||
url=result["url"],
|
||||
title=result["title"],
|
||||
content=html_to_text(result["description"]),
|
||||
thumbnail=result.get("thumbnail", ""),
|
||||
)
|
||||
|
||||
|
||||
def _parse_news(result: dict[str, str]) -> LegacyResult:
|
||||
return LegacyResult(
|
||||
{
|
||||
"url": result["url"],
|
||||
"title": result["title"],
|
||||
"content": html_to_text(result["description"]),
|
||||
"thumbnail": result.get("thumbnail"),
|
||||
"publishedDate": _parse_date(result.get("age", "")),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _parse_videos(result: dict[str, t.Any]) -> LegacyResult:
|
||||
return LegacyResult(
|
||||
{
|
||||
"template": "videos.html",
|
||||
"url": result["url"],
|
||||
"title": result["title"],
|
||||
"content": html_to_text(result["description"]),
|
||||
"thumbnail": result.get("thumbnail"),
|
||||
"publishedDate": _parse_date(result.get("age", "")),
|
||||
"length": result.get("video", {}).get("duration"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _parse_images(result: dict[str, t.Any]) -> LegacyResult:
|
||||
return LegacyResult(
|
||||
{
|
||||
"template": "images.html",
|
||||
"url": result["url"],
|
||||
"title": result["title"],
|
||||
"content": "",
|
||||
"img_src": result.get("properties", {}).get("url"),
|
||||
"thumbnail_src": result.get("thumbnail", {}).get("src"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
res = EngineResults()
|
||||
|
||||
json_resp: dict[str, t.Any] = resp.json()
|
||||
if not isinstance(json_resp, dict):
|
||||
return res # pyright: ignore[reportUnreachable]
|
||||
|
||||
for result in json_resp["results"]:
|
||||
# hide sponsored results
|
||||
if result.get("sponsored", False):
|
||||
continue
|
||||
|
||||
if "videos" in result:
|
||||
for videos_result in result["videos"]:
|
||||
res.add(_parse_videos(videos_result))
|
||||
continue
|
||||
|
||||
if "news" in result:
|
||||
for news_result in result["news"]:
|
||||
res.add(_parse_news(news_result))
|
||||
continue
|
||||
|
||||
if search_type == "news":
|
||||
res.add(_parse_news(result))
|
||||
elif search_type == "videos":
|
||||
res.add(_parse_videos(result))
|
||||
elif search_type == "images":
|
||||
res.add(_parse_images(result))
|
||||
else:
|
||||
res.add(_parse_general(result))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def fetch_traits(engine_traits: EngineTraits):
|
||||
"""Fetch :ref:`languages <brave languages>` and :ref:`regions <brave
|
||||
regions>` from Brave."""
|
||||
|
||||
# pylint: disable=import-outside-toplevel, too-many-branches
|
||||
|
||||
from lxml import html
|
||||
import babel
|
||||
|
||||
from searx.locales import region_tag
|
||||
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
||||
|
||||
# from searx.engines.xpath import extract_text
|
||||
from searx.utils import gen_useragent
|
||||
|
||||
headers = {
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Cache-Control": "no-cache",
|
||||
"DNT": "1",
|
||||
"Connection": "keep-alive",
|
||||
"Accept-Language": "en,en-US;q=0.7,en;q=0.3",
|
||||
"User-Agent": gen_useragent(),
|
||||
}
|
||||
|
||||
resp = get("https://karmasearch.org/settings", headers=headers, timeout=5)
|
||||
if not resp.ok:
|
||||
raise RuntimeError("Response from Brave languages is not OK.")
|
||||
|
||||
dom = html.fromstring(resp.text)
|
||||
for option in dom.xpath("//select[@name='country']/option"):
|
||||
country_tag: str = option.get("value", "")
|
||||
try:
|
||||
sxng_tag = region_tag(babel.Locale.parse(country_tag, sep="-"))
|
||||
except babel.UnknownLocaleError:
|
||||
# silently ignore unknown languages
|
||||
continue
|
||||
# print("%-20s: %s <-- %s" % (extract_text(option), country_tag, sxng_tag))
|
||||
|
||||
conflict = engine_traits.regions.get(sxng_tag)
|
||||
if conflict:
|
||||
if conflict != country_tag:
|
||||
print("CONFLICT: babel %s --> %s, %s" % (sxng_tag, conflict, country_tag))
|
||||
continue
|
||||
engine_traits.regions[sxng_tag] = country_tag
|
||||
@@ -45,7 +45,7 @@ about = {
|
||||
base_url = "https://api2.marginalia-search.com"
|
||||
safesearch = True
|
||||
categories = ["general"]
|
||||
paging = True
|
||||
paging = False
|
||||
results_per_page = 20
|
||||
api_key = None
|
||||
"""To get an API key, please follow the instructions from `Key and license`_
|
||||
@@ -85,12 +85,7 @@ class ApiSearchResults(t.TypedDict):
|
||||
|
||||
def request(query: str, params: dict[str, t.Any]):
|
||||
|
||||
query_params = {
|
||||
"page": params["pageno"],
|
||||
"count": results_per_page,
|
||||
"nsfw": min(params["safesearch"], 1),
|
||||
"query": query,
|
||||
}
|
||||
query_params = {"count": results_per_page, "nsfw": min(params["safesearch"], 1), "query": query}
|
||||
|
||||
params["url"] = f"{base_url}/search?{urlencode(query_params)}"
|
||||
params["headers"]["User-Agent"] = searxng_useragent()
|
||||
|
||||
+10
-1
@@ -4,6 +4,7 @@
|
||||
.. _Odysee: https://github.com/OdyseeTeam/odysee-frontend
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlencode
|
||||
|
||||
@@ -11,7 +12,6 @@ import babel
|
||||
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.locales import language_tag
|
||||
from searx.utils import format_duration
|
||||
|
||||
# Engine metadata
|
||||
about = {
|
||||
@@ -61,6 +61,15 @@ def request(query, params):
|
||||
return params
|
||||
|
||||
|
||||
# Format the video duration
|
||||
def format_duration(duration):
|
||||
seconds = int(duration)
|
||||
length = time.gmtime(seconds)
|
||||
if length.tm_hour:
|
||||
return time.strftime("%H:%M:%S", length)
|
||||
return time.strftime("%M:%S", length)
|
||||
|
||||
|
||||
def response(resp):
|
||||
data = resp.json()
|
||||
results = []
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Public domain image archive"""
|
||||
|
||||
import re
|
||||
|
||||
from urllib.parse import urlencode, urlparse, urlunparse, parse_qsl
|
||||
from json import dumps
|
||||
|
||||
@@ -51,8 +49,6 @@ paging = True
|
||||
|
||||
__CACHED_API_URL = None
|
||||
|
||||
_API_URL_RE = re.compile(r"\"(https://.*?/search-proxy)\"")
|
||||
|
||||
|
||||
def _clean_url(url):
|
||||
parsed = urlparse(url)
|
||||
@@ -78,11 +74,10 @@ def _get_algolia_api_url():
|
||||
if resp.status_code != 200:
|
||||
raise LookupError("Failed to obtain AWS api url for PDImageArchive")
|
||||
|
||||
api_url_match = _API_URL_RE.search(resp.text)
|
||||
if api_url_match is None:
|
||||
raise LookupError("Couldn't obtain AWS api url for PDImageArchive")
|
||||
api_url = extr(resp.text, 'const r="', '"', default=None)
|
||||
|
||||
api_url = api_url_match.group(1)
|
||||
if api_url is None:
|
||||
raise LookupError("Couldn't obtain AWS api url for PDImageArchive")
|
||||
|
||||
__CACHED_API_URL = api_url
|
||||
return api_url
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import socket
|
||||
from urllib.parse import urlencode
|
||||
@@ -60,19 +59,7 @@ seconds."""
|
||||
def init(_):
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache("radio_browser")
|
||||
|
||||
# In an environment with competing processes, the initial loading of the
|
||||
# cache is required only once.
|
||||
eng_state: str | None = CACHE.get("eng_state")
|
||||
if not eng_state or not eng_state.startswith("STATE:"):
|
||||
CACHE.set("eng_state", f"STATE: being initialized by PID {os.getpid()}")
|
||||
try:
|
||||
server_list()
|
||||
except Exception:
|
||||
CACHE.set("eng_state", f"ERROR: initialization by PID {os.getpid()} failed.")
|
||||
raise
|
||||
else:
|
||||
logger.debug(eng_state)
|
||||
|
||||
|
||||
def server_list() -> list[str]:
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Seek ninja (general)"""
|
||||
|
||||
from json import loads
|
||||
from hashlib import sha256
|
||||
from urllib.parse import urlencode, quote_plus
|
||||
|
||||
import typing as t
|
||||
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.network import get
|
||||
from searx.result_types import EngineResults
|
||||
from searx.utils import extr, html_to_text
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
about = {
|
||||
"website": "https://seek.ninja",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
safesearch = True
|
||||
|
||||
base_url = "https://seek.ninja"
|
||||
categories = ["general"]
|
||||
|
||||
safe_search_map = {0: "off", 1: "moderate", 2: "strict"}
|
||||
|
||||
PowChallenge = dict[str, t.Any]
|
||||
|
||||
|
||||
def _get_challenge(query: str) -> PowChallenge:
|
||||
"""Extract the challenge parameters (i.e. nonce, difficulty, ...) from the
|
||||
search website."""
|
||||
|
||||
resp = get(f"{base_url}/s?q={quote_plus(query)}")
|
||||
challenge_raw_json = "{" + extr(resp.text, "pow: {", "},") + "}"
|
||||
return loads(challenge_raw_json)
|
||||
|
||||
|
||||
def _solve_pow(challenge: PowChallenge) -> list[int]:
|
||||
"""Solves a Proof of Work SHA256 challenges. This is a 1:1 port of the
|
||||
site's JS code.
|
||||
|
||||
On a high-level, it tries to ``k`` amount of solutions, where its sha256
|
||||
hash begins with: ``leading`` 0s, i.e.
|
||||
|
||||
.. code: js
|
||||
|
||||
sha256(nonce || solution).startswith("0" * leading)
|
||||
"""
|
||||
nonce = challenge["nonce"]
|
||||
k = int(challenge["k"])
|
||||
indifficulty = float(challenge["indifficulty"])
|
||||
|
||||
leading = int(indifficulty)
|
||||
frac = indifficulty - leading
|
||||
prefix = "".join("0" for _ in range(0, leading))
|
||||
|
||||
maxNib = 15 - int(frac * 16) if frac else 15
|
||||
|
||||
solutions: list[int] = []
|
||||
ans = 0
|
||||
while len(solutions) < k:
|
||||
h = sha256(f"{nonce}{ans}".encode()).hexdigest()
|
||||
if h.startswith(prefix) and (not frac or int(h[leading], base=16) <= maxNib):
|
||||
solutions.append(ans)
|
||||
ans += 1
|
||||
return solutions
|
||||
|
||||
|
||||
def request(query: str, params: 'OnlineParams') -> None:
|
||||
challenge = _get_challenge(query)
|
||||
solution = _solve_pow(challenge)
|
||||
args = {
|
||||
"q": query,
|
||||
"panswers": ",".join(str(s) for s in solution),
|
||||
"pid": challenge["challengeId"],
|
||||
"adult": safe_search_map[params["safesearch"]],
|
||||
}
|
||||
params["url"] = f"{base_url}/search-sse?{urlencode(args)}"
|
||||
|
||||
|
||||
def response(resp: 'SXNG_Response') -> EngineResults:
|
||||
res = EngineResults()
|
||||
# The response is a stream of server-side events,
|
||||
# so it is split into `event: <type>` and `data: {"results": ...}`
|
||||
# see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/
|
||||
events = resp.text.split("\n\n")
|
||||
for event in events:
|
||||
event_parts = event.split("\n", maxsplit=2)
|
||||
if len(event_parts) != 2:
|
||||
continue
|
||||
|
||||
event_name, data = event_parts
|
||||
if not event_name.endswith("resultsUpdate"):
|
||||
continue
|
||||
|
||||
json_data = loads(data.removeprefix("data: "))
|
||||
for result in json_data["results"]:
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=result["url"],
|
||||
title=result["title"],
|
||||
content=html_to_text(result["blurb"]),
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,44 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Svgrepo (icons)"""
|
||||
|
||||
from lxml import html
|
||||
from searx.utils import extract_text, eval_xpath, eval_xpath_list
|
||||
|
||||
about = {
|
||||
"website": 'https://www.svgrepo.com',
|
||||
"official_api_documentation": 'https://svgapi.com',
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": 'HTML',
|
||||
}
|
||||
|
||||
paging = True
|
||||
categories = ['images', 'icons']
|
||||
base_url = "https://www.svgrepo.com"
|
||||
|
||||
results_xpath = "//div[@class='style_nodeListing__7Nmro']/div"
|
||||
url_xpath = ".//a/@href"
|
||||
title_xpath = ".//a/@title"
|
||||
img_src_xpath = ".//img/@src"
|
||||
|
||||
|
||||
def request(query, params):
|
||||
params['url'] = f"{base_url}/vectors/{query}/{params['pageno']}/"
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
results = []
|
||||
|
||||
dom = html.fromstring(resp.text)
|
||||
for result in eval_xpath_list(dom, results_xpath):
|
||||
results.append(
|
||||
{
|
||||
'template': 'images.html',
|
||||
'url': base_url + extract_text(eval_xpath(result, url_xpath)),
|
||||
'title': extract_text(eval_xpath(result, title_xpath)).replace(" SVG File", "").replace("Show ", ""),
|
||||
'img_src': extract_text(eval_xpath(result, img_src_xpath)),
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -1,287 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=invalid-name
|
||||
"""Swisscows (general, images, videos)"""
|
||||
|
||||
import typing as t
|
||||
|
||||
import base64
|
||||
import codecs
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from babel.core import get_global
|
||||
|
||||
from searx.result_types import EngineResults, LegacyResult # pyright: ignore[reportPrivateLocalImportUsage]
|
||||
from searx.utils import humanize_number, html_to_text
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
|
||||
about = {
|
||||
"website": "https://swisscows.com",
|
||||
"wikidata_id": "Q22937452",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
|
||||
categories = ["general"]
|
||||
swisscows_category = "web" # possible: "web", "videos", "images"
|
||||
|
||||
results_per_page = 50
|
||||
|
||||
time_range_support = True
|
||||
paging = True
|
||||
|
||||
base_url = "https://api.swisscows.com"
|
||||
|
||||
CAESAR_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
NONCE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
||||
|
||||
time_range_map = {"day": "Day", "week": "Week", "month": "Month", "year": "Year"}
|
||||
|
||||
# fmt: off
|
||||
swisscows_regions: list[str] = [
|
||||
"AR", "AU", "AT", "BE", "BR", "CA", "CL", "CN", "DK", "FI",
|
||||
"FR", "DE", "HK", "HU", "IN", "ID", "IT", "JP", "KR", "LV",
|
||||
"MY", "MX", "NL", "NZ", "NO", "PH", "PL", "PT", "RU", "SA",
|
||||
"ZA", "ES", "SE", "CH", "TW", "TR", "UA", "GB", "US"
|
||||
]
|
||||
"""Regions supported by swisscows."""
|
||||
# fmt: on
|
||||
|
||||
# swisscows_languages = [
|
||||
# "GB", "DE", "ES", "FR", "IT", "LV", "HU", "NL", "PT", "RU", "UA"
|
||||
# ]
|
||||
|
||||
|
||||
def appropriate_locale(searxng_locale: str, regions: list[str], default: str) -> str:
|
||||
"""Returns the appropriate swisscows locale for the region or language
|
||||
selected by the user. If no value is determined, ``default`` is returned
|
||||
"""
|
||||
_locale = searxng_locale.split("-")
|
||||
|
||||
if _locale[0] == "all":
|
||||
return default
|
||||
|
||||
if len(_locale) == 1 or _locale[1] in regions:
|
||||
return searxng_locale
|
||||
|
||||
sxng_lang = _locale[0]
|
||||
if sxng_lang.upper() in regions:
|
||||
return f"{sxng_lang}-{sxng_lang.upper()}"
|
||||
|
||||
likely_subtag: str | None = get_global("likely_subtags").get(sxng_lang)
|
||||
if likely_subtag:
|
||||
_tag: list[str] = likely_subtag.split("_")
|
||||
if _tag[-1] in regions:
|
||||
return f"{_tag[0]}-{_tag[-1]}"
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def generate_nonce(length: int = 32) -> str:
|
||||
"""
|
||||
Generate a random char sequence with the given length.
|
||||
"""
|
||||
return "".join([random.choice(NONCE_ALPHABET) for _ in range(length)])
|
||||
|
||||
|
||||
def caesar_shift_with_switch_case(s: str, offset: int = 13) -> str:
|
||||
"""
|
||||
Caesar shift by :py:obj:`offset` that additionally inverts the casing of all letters
|
||||
(i.e. from lowercase to uppercase and vice versa).
|
||||
"""
|
||||
out = ""
|
||||
for c in s:
|
||||
if c.upper() in CAESAR_ALPHABET:
|
||||
alphabet_index = ord(c.upper()) - ord("A")
|
||||
shifted = CAESAR_ALPHABET[(alphabet_index + offset) % len(CAESAR_ALPHABET)]
|
||||
case_switched = shifted.lower() if c.isupper() else shifted.upper()
|
||||
out += case_switched
|
||||
else:
|
||||
out += c
|
||||
return out
|
||||
|
||||
|
||||
def sha256_hash_b64_url(s: str) -> str:
|
||||
"""
|
||||
Calculate the SHA256 hash and base64 URL-encodes it.
|
||||
"""
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update(s.encode())
|
||||
hashed_bytes = hasher.digest()
|
||||
|
||||
# hashlib generates a byte digest, but since we need to convert it to base64, we
|
||||
# need to do that by hand
|
||||
hash_base64 = codecs.encode(hashed_bytes, "base64").decode("utf-8").rstrip('\n')
|
||||
|
||||
hash_base64_url_encoded = hash_base64.replace("=", "").replace("+", '-').replace("/", '_')
|
||||
return hash_base64_url_encoded
|
||||
|
||||
|
||||
def generate_nonce_and_signature(base_path: str, args: dict[str, t.Any]) -> tuple[str, str]:
|
||||
"""
|
||||
Generate "X-Request-Nonce" and "X-Request-Signature" which are required for accessing
|
||||
Swisscows images (reverse engineered from their official website).
|
||||
"""
|
||||
nonce = generate_nonce()
|
||||
nonce_shifted = caesar_shift_with_switch_case(nonce, 13)
|
||||
|
||||
# in the path, all keys must be sorted in alphabetic order,
|
||||
# otherwise the generated signature won't be accepted!
|
||||
# additionally, the values may not be URL encoded, they have to be plain text
|
||||
# hence we don't use urlencode here
|
||||
args_sorted = sorted(args.items(), key=lambda arg: arg[0])
|
||||
query_string = "&".join(f"{key}={value}" for (key, value) in args_sorted)
|
||||
full_path = f"{base_path}?{query_string}"
|
||||
|
||||
signature = sha256_hash_b64_url(full_path + nonce_shifted)
|
||||
return (nonce, signature)
|
||||
|
||||
|
||||
maximum_page_size = {"web": 20, "images": 50, "videos": 10}
|
||||
|
||||
|
||||
def init(_):
|
||||
if swisscows_category not in ("web", "images", "videos"):
|
||||
raise ValueError("illegal swisscows category: %s" % swisscows_category)
|
||||
|
||||
if results_per_page > maximum_page_size[swisscows_category]:
|
||||
raise ValueError(
|
||||
"results_per_page for swisscows %s can be at most %d"
|
||||
% (swisscows_category, maximum_page_size[swisscows_category])
|
||||
)
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
# swisscows images only supports 2 pages
|
||||
if swisscows_category == "images" and params["pageno"] > 2:
|
||||
params["url"] = None
|
||||
return
|
||||
|
||||
locale = appropriate_locale(params["searxng_locale"], swisscows_regions, "en-US")
|
||||
base_path = ""
|
||||
args = dict[str, t.Any]
|
||||
if swisscows_category == "web":
|
||||
freshness = "All"
|
||||
if params["time_range"]:
|
||||
freshness = time_range_map[params["time_range"]]
|
||||
args = {
|
||||
"freshness": freshness,
|
||||
"itemsCount": results_per_page,
|
||||
"locale": locale,
|
||||
"offset": (params["pageno"] - 1) * results_per_page,
|
||||
"query": query,
|
||||
"spellcheck": True,
|
||||
}
|
||||
base_path = "/v5/web/search"
|
||||
elif swisscows_category == "images":
|
||||
args = {
|
||||
"itemsCount": results_per_page,
|
||||
"locale": locale,
|
||||
"offset": (params["pageno"] - 1) * results_per_page,
|
||||
"query": query,
|
||||
"spellcheck": True,
|
||||
}
|
||||
base_path = "/v5/images/search"
|
||||
else:
|
||||
args = {
|
||||
"itemsCount": results_per_page,
|
||||
"offset": (params["pageno"] - 1) * results_per_page,
|
||||
"query": query,
|
||||
"region": locale,
|
||||
"spellcheck": True,
|
||||
}
|
||||
base_path = "/v2/videos/search"
|
||||
|
||||
nonce, signature = generate_nonce_and_signature(base_path, args)
|
||||
|
||||
params["headers"].update(
|
||||
{
|
||||
"X-Request-Nonce": nonce,
|
||||
"X-Request-Signature": signature,
|
||||
}
|
||||
)
|
||||
params["url"] = f"{base_url}{base_path}?{urlencode(args)}"
|
||||
|
||||
|
||||
def _video_result(result: dict[str, str]) -> LegacyResult:
|
||||
published_date = None
|
||||
if result.get("datePublished"):
|
||||
published_date = datetime.fromisoformat(result["datePublished"])
|
||||
|
||||
view_count = None
|
||||
if result.get("viewCount"):
|
||||
view_count = humanize_number(result["viewCount"]) # pyright: ignore[reportArgumentType]
|
||||
|
||||
return LegacyResult(
|
||||
{
|
||||
"template": "videos.html",
|
||||
"url": result["url"],
|
||||
"title": html_to_text(result.get("title") or result["name"]),
|
||||
"content": result["description"],
|
||||
"thumbnail": result.get("thumbnailUrl")
|
||||
or result.get("thumbnail", {}).get("url"), # pyright: ignore[reportAttributeAccessIssue]
|
||||
"length": result.get("duration"),
|
||||
"iframe_src": result.get("embedUrl"),
|
||||
"publishedDate": published_date,
|
||||
"views": view_count,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
res = EngineResults()
|
||||
|
||||
json_data = resp.json()
|
||||
|
||||
# the payload encoding is only used for general and images,
|
||||
# for videos the data gets returned directly as a normal JSON response
|
||||
# payload is encoded as a JSON web token -> 3 parts, separated by "."
|
||||
# the actual data is in the center of the encoded string
|
||||
if "payload" in json_data:
|
||||
payload = json_data["payload"].split(".")[1]
|
||||
# pad with '=' to be valid base64
|
||||
payload = payload + '=' * (4 - len(payload) % 4)
|
||||
decoded = base64.urlsafe_b64decode(payload)
|
||||
json_data = json.loads(decoded.decode())
|
||||
|
||||
result: dict[str, t.Any]
|
||||
for result in json_data["items"]:
|
||||
if result["type"] == "WebPage":
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=result["url"],
|
||||
title=result["name"],
|
||||
content=html_to_text(result["description"]),
|
||||
thumbnail=result.get("thumbnail", {}).get("url"),
|
||||
)
|
||||
)
|
||||
elif swisscows_category == "videos" and result["type"] == "VideoCollection":
|
||||
for video in result["hasPart"]:
|
||||
res.add(_video_result(video))
|
||||
elif result["type"] == "ImageObject":
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
{
|
||||
"template": "images.html",
|
||||
"url": result["url"],
|
||||
"thumbnail_src": result["thumbnail"]["url"],
|
||||
"img_src": result["contentUrl"],
|
||||
"title": result["name"],
|
||||
}
|
||||
)
|
||||
)
|
||||
elif result["type"] == "video":
|
||||
res.add(_video_result(result))
|
||||
|
||||
return res
|
||||
@@ -1,83 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=invalid-name
|
||||
"""Swisscows news"""
|
||||
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import typing as t
|
||||
|
||||
from searx.utils import html_to_text
|
||||
from searx.result_types import EngineResults
|
||||
from searx.engines.swisscows import appropriate_locale
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
|
||||
about = {
|
||||
"website": "https://swisscows.com",
|
||||
"wikidata_id": "Q22937452",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
|
||||
categories = ["news"]
|
||||
results_per_page = 20
|
||||
|
||||
time_range_support = True
|
||||
paging = True
|
||||
|
||||
base_url = "https://api.swisscows.com"
|
||||
time_range_map = {"day": "Day", "week": "Week", "month": "Month", "year": "Year"}
|
||||
|
||||
swisscows_regions: list[str] = ["DE"]
|
||||
"""Regions supported by swisscows News."""
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
|
||||
sxng_locale = params["searxng_locale"].split("-", maxsplit=1)[0]
|
||||
locale: str = appropriate_locale(sxng_locale, swisscows_regions, default="de-DE")
|
||||
if not locale:
|
||||
return
|
||||
|
||||
freshness = "All"
|
||||
if params["time_range"]:
|
||||
freshness = time_range_map[params["time_range"]]
|
||||
|
||||
args = {
|
||||
"query": query,
|
||||
"itemsCount": results_per_page,
|
||||
"region": locale,
|
||||
"language": locale.split("-", maxsplit=1)[0],
|
||||
"offset": (params["pageno"] - 1) * results_per_page,
|
||||
"freshness": freshness,
|
||||
"sortOrder": "Desc",
|
||||
"sortBy": "Created",
|
||||
}
|
||||
url_path = f"/news/search?{urlencode(args)}"
|
||||
|
||||
params["url"] = base_url + url_path
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
res = EngineResults()
|
||||
|
||||
result: dict[str, str]
|
||||
for result in resp.json()["items"]: # pyright: ignore[reportAny]
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=result["uri"],
|
||||
title=html_to_text(result["title"]),
|
||||
content=result["description"],
|
||||
publishedDate=datetime.fromisoformat(result["created"]),
|
||||
thumbnail=result.get("og:image") or "",
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
@@ -1,167 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Tiger_ is a Swiss meta search engine.
|
||||
|
||||
.. _Tiger: https://tiger.ch
|
||||
"""
|
||||
|
||||
from json import loads
|
||||
import random
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import typing as t
|
||||
|
||||
from dateutil import parser
|
||||
from lxml import html
|
||||
|
||||
from searx.exceptions import SearxEngineAPIException
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.network import get, post
|
||||
from searx.result_types import EngineResults
|
||||
from searx.utils import extr, eval_xpath_list, eval_xpath, extract_text
|
||||
from searx.enginelib import EngineCache
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
about = {
|
||||
"website": "https://tiger.ch",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "HTML",
|
||||
}
|
||||
|
||||
paging = True
|
||||
|
||||
base_url = "https://tiger.ch"
|
||||
categories = []
|
||||
tiger_category = "Websuche"
|
||||
"""
|
||||
Possible values: "Websuche", "News".
|
||||
"""
|
||||
|
||||
|
||||
CACHE: EngineCache
|
||||
"""Cache to store session codes (result of solved CAPTCHA)."""
|
||||
|
||||
|
||||
def init(_):
|
||||
if tiger_category not in ("Websuche", "News"):
|
||||
raise ValueError("invalid search category: %s" % tiger_category)
|
||||
|
||||
|
||||
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache(engine_settings["name"])
|
||||
return True
|
||||
|
||||
|
||||
def _obtain_session_code() -> str:
|
||||
"""The challenge works like this:
|
||||
|
||||
- We first generate 3 random numbers.
|
||||
- Then we send them to /Human.svc/Make to get the operands (+, -) for the
|
||||
math challenge (i.e. a simple calculation)
|
||||
- Based on the operands, we calculate a result (usually done by the user by
|
||||
hand)
|
||||
- We send the result of the math calculation to the server to obtain a
|
||||
session "code" that has to be sent as cookie parameter for all searches
|
||||
|
||||
E.g., challenges look like ``19-3+5``.
|
||||
"""
|
||||
cached_session = CACHE.get("session")
|
||||
if cached_session:
|
||||
return cached_session
|
||||
|
||||
results_page = get(f"{base_url}/_internCode.aspx")
|
||||
doc = html.fromstring(results_page.text)
|
||||
|
||||
extra_data: dict[str, str] = {}
|
||||
for extra_param in ("__VIEWSTATE", "__VIEWSTATEGENERATOR", "__EVENTVALIDATION"):
|
||||
extra_data[extra_param] = doc.xpath(f"//input[@name='{extra_param}']/@value")[0]
|
||||
|
||||
# var z1 = Math.floor((Math.random() * 8) + 11);
|
||||
# var z2 = Math.floor((Math.random() * 8) + 1);
|
||||
# var z3 = Math.floor((Math.random() * 8) + 1);
|
||||
num1 = random.randint(11, 19)
|
||||
num2 = random.randint(1, 9)
|
||||
num3 = random.randint(1, 9)
|
||||
|
||||
challenge = get(f"{base_url}/Services/Human.svc/Make?M1={num1}&M2={num2}&M3={num3}", cookies=results_page.cookies)
|
||||
signs = loads(challenge.json()["d"])[0]
|
||||
sign1 = signs["Z1"]
|
||||
sign2 = signs["Z2"]
|
||||
|
||||
result = num1
|
||||
for num, sign in [(num2, sign1), (num3, sign2)]:
|
||||
if sign == "+":
|
||||
result += num
|
||||
else:
|
||||
result -= num
|
||||
|
||||
logger.debug(f"got challenge: {num1} {sign1} {num2} {sign2} {num3} = {result}")
|
||||
data = {
|
||||
**extra_data,
|
||||
"txtM": str(result),
|
||||
"btnHuman": "OK",
|
||||
}
|
||||
|
||||
challenge_response = post(
|
||||
f"{base_url}/_internCode.aspx",
|
||||
cookies=results_page.cookies,
|
||||
data=data,
|
||||
)
|
||||
|
||||
cookie = challenge_response.cookies["Tiger.ch"]
|
||||
code = extr(cookie, "Code=", "&")
|
||||
if not code:
|
||||
raise SearxEngineAPIException("failed to obtain session code")
|
||||
|
||||
CACHE.set("session", code, expire=60 * 24 * 60) # cookie is valid for two months
|
||||
return code
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams"):
|
||||
code = _obtain_session_code()
|
||||
args = {"w": query, "page": params["pageno"]}
|
||||
params["url"] = f"{base_url}/{tiger_category}?{urlencode(args)}"
|
||||
params["cookies"]["Tiger.ch"] = f"Code={code}"
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
res = EngineResults()
|
||||
doc = html.fromstring(resp.text)
|
||||
|
||||
if tiger_category == "Websuche":
|
||||
for result in eval_xpath_list(doc, "//div[@id='mainContainer']//table/tr"):
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=extract_text(eval_xpath(result, ".//a[contains(@class, 'weblink')]/@href")),
|
||||
title=extract_text(eval_xpath(result, ".//a[contains(@class, 'weblink')]")) or "",
|
||||
content=extract_text(eval_xpath(result, ".//*[contains(@class, 'webbodynopic')]")) or "",
|
||||
)
|
||||
)
|
||||
elif tiger_category == "News":
|
||||
for result in eval_xpath_list(doc, "//div[@id='panNews']/div"):
|
||||
publishedDate = None
|
||||
try:
|
||||
date_str = extract_text(eval_xpath(result, ".//span[contains(@class, 'help')]/span")) or ""
|
||||
date_str = date_str.strip().removeprefix("-").strip()
|
||||
publishedDate = parser.parse(date_str)
|
||||
except parser.ParserError:
|
||||
pass
|
||||
|
||||
thumbnail = extract_text(eval_xpath(result, "./img/@src"))
|
||||
if thumbnail:
|
||||
thumbnail = base_url + thumbnail
|
||||
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=extract_text(eval_xpath(result, ".//a[contains(@class, 'webLink')]/@href")),
|
||||
title=extract_text(eval_xpath(result, ".//a[contains(@class, 'webLink')]")) or "",
|
||||
thumbnail=thumbnail or "",
|
||||
publishedDate=publishedDate,
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
@@ -7,7 +7,6 @@ Some implementations are shared from :ref:`wikipedia engine`.
|
||||
|
||||
import typing as t
|
||||
|
||||
import os
|
||||
from hashlib import md5
|
||||
from urllib.parse import urlencode, unquote
|
||||
from json import loads
|
||||
@@ -828,19 +827,7 @@ def debug_explain_wikidata_query(query: str, method: str = "GET"):
|
||||
def init(_):
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache("wikidata")
|
||||
|
||||
# In an environment with competing processes, the initial loading of the
|
||||
# cache is required only once.
|
||||
eng_state: str | None = CACHE.get("eng_state")
|
||||
if not eng_state or not eng_state.startswith("STATE:"):
|
||||
CACHE.set("eng_state", f"STATE: being initialized by PID {os.getpid()}")
|
||||
try:
|
||||
init_wikidata_properties()
|
||||
except Exception:
|
||||
CACHE.set("eng_state", f"ERROR: initialization by PID {os.getpid()} failed.")
|
||||
raise
|
||||
else:
|
||||
logger.debug(eng_state)
|
||||
|
||||
|
||||
def init_wikidata_properties():
|
||||
|
||||
+1
-10
@@ -22,7 +22,6 @@ Paging:
|
||||
- :py:obj:`paging`
|
||||
- :py:obj:`page_size`
|
||||
- :py:obj:`first_page_num`
|
||||
- :py:obj:`send_page_num_on_first_page`
|
||||
|
||||
Time Range:
|
||||
|
||||
@@ -175,10 +174,6 @@ number, but an offset.'''
|
||||
first_page_num = 1
|
||||
'''Number of the first page (usually 0 or 1).'''
|
||||
|
||||
send_page_num_on_first_page = True
|
||||
'''Whether to include the page number in the request for the first page.
|
||||
This can help if an engine blocks request that send a page number for the first page.'''
|
||||
|
||||
time_range_support = False
|
||||
'''Engine supports search time range.'''
|
||||
|
||||
@@ -243,14 +238,10 @@ def request(query, params):
|
||||
if safe_search_val is not None:
|
||||
safe_search = safe_search_map[safe_search_val]
|
||||
|
||||
pageno = ""
|
||||
if send_page_num_on_first_page or params["pageno"] != 1:
|
||||
pageno = (params['pageno'] - 1) * page_size + first_page_num
|
||||
|
||||
fargs = {
|
||||
'query': urlencode({'q': query})[2:],
|
||||
'lang': lang,
|
||||
'pageno': pageno,
|
||||
'pageno': (params['pageno'] - 1) * page_size + first_page_num,
|
||||
'time_range': time_range,
|
||||
'safe_search': safe_search,
|
||||
}
|
||||
|
||||
+20
-27
@@ -16,18 +16,17 @@ if t.TYPE_CHECKING:
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
about = {
|
||||
"website": "https://yep.com/",
|
||||
"official_api_documentation": "https://docs.developer.yelp.com",
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
'website': 'https://yep.com/',
|
||||
'official_api_documentation': 'https://docs.developer.yelp.com',
|
||||
'use_official_api': False,
|
||||
'require_api_key': False,
|
||||
'results': 'JSON',
|
||||
}
|
||||
|
||||
base_url = "https://api.yep.com"
|
||||
web_base_url = "https://yep.com"
|
||||
|
||||
safesearch = True
|
||||
safesearch_map = {0: "off", 1: "moderate", 2: "strict"}
|
||||
safesearch_map = {0: 'off', 1: 'moderate', 2: 'strict'}
|
||||
|
||||
enable_http2 = False
|
||||
|
||||
@@ -37,42 +36,34 @@ _IMPORT_RE = re.compile(r"import\"(.*?)\";")
|
||||
_LANGUAGE_RE = re.compile(r"\{english:\".*?\",code_string:\"(.*?)\",code:\".*?\"\}")
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
args = {"query": query, "safeSearch": safesearch_map[params["safesearch"]], "limit": results_per_page}
|
||||
def request(query: str, params: 'OnlineParams') -> None:
|
||||
args = {'query': query, 'safeSearch': safesearch_map[params['safesearch']], 'limit': results_per_page}
|
||||
|
||||
engine_language: str | None = traits.get_language(params["searxng_locale"])
|
||||
engine_language: str = traits.get_language(params["searxng_locale"])
|
||||
if engine_language:
|
||||
args["hl"] = engine_language
|
||||
|
||||
params["url"] = f"{base_url}/search?{urlencode(args)}"
|
||||
params["headers"].update(
|
||||
{
|
||||
"Referer": f"{web_base_url}/",
|
||||
"Origin": web_base_url,
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-site",
|
||||
}
|
||||
)
|
||||
params['url'] = f"{base_url}/search?{urlencode(args)}"
|
||||
params['headers']['Referer'] = 'https://yep.com/'
|
||||
params['headers']['Origin'] = 'https://yep.com'
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
def response(resp: 'SXNG_Response') -> EngineResults:
|
||||
res = EngineResults()
|
||||
|
||||
result: dict[str, str]
|
||||
for result in resp.json()[1]["results"]:
|
||||
for result in resp.json()[1]['results']:
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=result["url"],
|
||||
title=result["title"],
|
||||
content=html_to_text(result["snippet"]),
|
||||
url=result['url'],
|
||||
title=result['title'],
|
||||
content=html_to_text(result['snippet']),
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def fetch_traits(engine_traits: "EngineTraits"):
|
||||
def fetch_traits(engine_traits: 'EngineTraits'):
|
||||
"""Fetch :ref:`languages <yep languages>` and :ref:`regions <yep
|
||||
regions>` from Yep.
|
||||
|
||||
@@ -92,6 +83,8 @@ def fetch_traits(engine_traits: "EngineTraits"):
|
||||
|
||||
from searx.utils import gen_useragent
|
||||
|
||||
web_base_url = "https://yep.com"
|
||||
|
||||
headers = {
|
||||
"User-Agent": gen_useragent(),
|
||||
"Referer": f"{web_base_url}/",
|
||||
|
||||
+63
-52
@@ -17,14 +17,13 @@ import babel.core
|
||||
|
||||
import searx.plugins
|
||||
|
||||
from searx import get_setting, settings, autocomplete, favicons
|
||||
from searx import settings, autocomplete, favicons
|
||||
from searx.enginelib import Engine
|
||||
from searx.engines import DEFAULT_CATEGORY
|
||||
from searx.extended_types import SXNG_Request
|
||||
from searx.locales import LOCALE_NAMES
|
||||
from searx.webutils import VALID_LANGUAGE_CODE
|
||||
|
||||
from ._settings import SettingsPref
|
||||
|
||||
COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5 # 5 years
|
||||
DOI_RESOLVERS = list(settings['doi_resolvers'])
|
||||
@@ -387,7 +386,6 @@ class ClientPref:
|
||||
return cls(locale=locale)
|
||||
|
||||
|
||||
@t.final
|
||||
class Preferences:
|
||||
"""Validates and saves preferences to cookies"""
|
||||
|
||||
@@ -402,91 +400,95 @@ class Preferences:
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.cfg: SettingsPref = get_setting("preferences")
|
||||
|
||||
self.key_value_settings: dict[str, Setting] = {
|
||||
# fmt: off
|
||||
'categories': MultipleChoiceSetting(
|
||||
["general"],
|
||||
locked="categories" in self.cfg.lock,
|
||||
choices=categories + ["none"],
|
||||
['general'],
|
||||
locked=is_locked('categories'),
|
||||
choices=categories + ['none']
|
||||
),
|
||||
'language': SearchLanguageSetting(
|
||||
get_setting("search.default_lang"),
|
||||
locked="language" in self.cfg.lock,
|
||||
choices=get_setting("search.languages") + [""],
|
||||
settings['search']['default_lang'],
|
||||
locked=is_locked('language'),
|
||||
choices=settings['search']['languages'] + ['']
|
||||
),
|
||||
'locale': EnumStringSetting(
|
||||
get_setting("ui.default_locale"),
|
||||
locked="locale" in self.cfg.lock,
|
||||
choices=list(LOCALE_NAMES.keys()) + [""],
|
||||
settings['ui']['default_locale'],
|
||||
locked=is_locked('locale'),
|
||||
choices=list(LOCALE_NAMES.keys()) + ['']
|
||||
),
|
||||
'autocomplete': EnumStringSetting(
|
||||
get_setting("search.autocomplete"),
|
||||
locked="autocomplete" in self.cfg.lock,
|
||||
choices=list(autocomplete.backends.keys()) + [""],
|
||||
settings['search']['autocomplete'],
|
||||
locked=is_locked('autocomplete'),
|
||||
choices=list(autocomplete.backends.keys()) + ['']
|
||||
),
|
||||
'favicon_resolver': EnumStringSetting(
|
||||
get_setting("search.favicon_resolver"),
|
||||
locked="favicon_resolver" in self.cfg.lock,
|
||||
choices=list(favicons.proxy.CFG.resolver_map.keys()) + [''],
|
||||
settings['search']['favicon_resolver'],
|
||||
locked=is_locked('favicon_resolver'),
|
||||
choices=list(favicons.proxy.CFG.resolver_map.keys()) + ['']
|
||||
),
|
||||
'image_proxy': BooleanSetting(
|
||||
get_setting("server.image_proxy"),
|
||||
locked="image_proxy" in self.cfg.lock,
|
||||
settings['server']['image_proxy'],
|
||||
locked=is_locked('image_proxy')
|
||||
),
|
||||
'method': EnumStringSetting(
|
||||
get_setting("server.method"),
|
||||
locked="method" in self.cfg.lock,
|
||||
choices=("GET", "POST"),
|
||||
settings['server']['method'],
|
||||
locked=is_locked('method'),
|
||||
choices=('GET', 'POST')
|
||||
),
|
||||
'safesearch': MapSetting(
|
||||
get_setting("search.safe_search"),
|
||||
locked="safesearch" in self.cfg.lock,
|
||||
settings['search']['safe_search'],
|
||||
locked=is_locked('safesearch'),
|
||||
map={
|
||||
"0": 0,
|
||||
"1": 1,
|
||||
"2": 2,
|
||||
},
|
||||
'0': 0,
|
||||
'1': 1,
|
||||
'2': 2
|
||||
}
|
||||
),
|
||||
'theme': EnumStringSetting(
|
||||
get_setting("ui.default_theme"),
|
||||
locked="theme" in self.cfg.lock,
|
||||
choices=themes,
|
||||
settings['ui']['default_theme'],
|
||||
locked=is_locked('theme'),
|
||||
choices=themes
|
||||
),
|
||||
'results_on_new_tab': BooleanSetting(
|
||||
get_setting("ui.results_on_new_tab"),
|
||||
locked="results_on_new_tab" in self.cfg.lock,
|
||||
settings['ui']['results_on_new_tab'],
|
||||
locked=is_locked('results_on_new_tab')
|
||||
),
|
||||
'doi_resolver': MultipleChoiceSetting(
|
||||
[get_setting("default_doi_resolver")],
|
||||
locked="doi_resolver" in self.cfg.lock,
|
||||
choices=DOI_RESOLVERS,
|
||||
[settings['default_doi_resolver'], ],
|
||||
locked=is_locked('doi_resolver'),
|
||||
choices=DOI_RESOLVERS
|
||||
),
|
||||
'simple_style': EnumStringSetting(
|
||||
get_setting("ui.theme_args.simple_style"),
|
||||
locked="simple_style" in self.cfg.lock,
|
||||
choices=["", "auto", "light", "dark", "black"],
|
||||
settings['ui']['theme_args']['simple_style'],
|
||||
locked=is_locked('simple_style'),
|
||||
choices=['', 'auto', 'light', 'dark', 'black']
|
||||
),
|
||||
'center_alignment': BooleanSetting(
|
||||
get_setting("ui.center_alignment"),
|
||||
locked="center_alignment" in self.cfg.lock,
|
||||
settings['ui']['center_alignment'],
|
||||
locked=is_locked('center_alignment')
|
||||
),
|
||||
'advanced_search': BooleanSetting(
|
||||
settings['ui']['advanced_search'],
|
||||
locked=is_locked('advanced_search')
|
||||
),
|
||||
'query_in_title': BooleanSetting(
|
||||
get_setting("ui.query_in_title"),
|
||||
locked="query_in_title" in self.cfg.lock,
|
||||
settings['ui']['query_in_title'],
|
||||
locked=is_locked('query_in_title')
|
||||
),
|
||||
'search_on_category_select': BooleanSetting(
|
||||
get_setting("ui.search_on_category_select"),
|
||||
locked="search_on_category_select" in self.cfg.lock,
|
||||
settings['ui']['search_on_category_select'],
|
||||
locked=is_locked('search_on_category_select')
|
||||
),
|
||||
'hotkeys': EnumStringSetting(
|
||||
get_setting("ui.hotkeys"),
|
||||
choices=["default", "vim"],
|
||||
settings['ui']['hotkeys'],
|
||||
choices=['default', 'vim']
|
||||
),
|
||||
'url_formatting': EnumStringSetting(
|
||||
get_setting("ui.url_formatting"),
|
||||
choices=["pretty", "full", "host"],
|
||||
settings['ui']['url_formatting'],
|
||||
choices=['pretty', 'full', 'host']
|
||||
),
|
||||
# fmt: on
|
||||
}
|
||||
|
||||
self.engines = EnginesSetting('engines', engines=engines.values())
|
||||
@@ -595,3 +597,12 @@ class Preferences:
|
||||
break
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def is_locked(setting_name: str):
|
||||
"""Checks if a given setting name is locked by settings.yml"""
|
||||
if 'preferences' not in settings:
|
||||
return False
|
||||
if 'lock' not in settings['preferences']:
|
||||
return False
|
||||
return setting_name in settings['preferences']['lock']
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import typing as t
|
||||
|
||||
import os
|
||||
import logging
|
||||
import threading
|
||||
from abc import abstractmethod, ABC
|
||||
@@ -155,9 +154,7 @@ class EngineProcessor(ABC):
|
||||
try:
|
||||
init_ok = self.engine.init(eng_setting)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.exception(
|
||||
f"(PID {os.getpid()}) Init method of engine %s failed due to an exception.", self.engine.name
|
||||
)
|
||||
logger.exception("Init method of engine %s failed due to an exception.", self.engine.name)
|
||||
init_ok = False
|
||||
# In older engines, None is returned from the init method, which is
|
||||
# equivalent to indicating that the initialization was successful.
|
||||
|
||||
@@ -152,13 +152,11 @@ class OnlineProcessor(EngineProcessor):
|
||||
# add Accept-Language header
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language
|
||||
|
||||
if self.engine.send_accept_language_header:
|
||||
if search_query.locale:
|
||||
headers["Accept-Language"] = "en,en-US;q=0.7,en;q=0.3"
|
||||
if self.engine.send_accept_language_header and search_query.locale:
|
||||
_l = search_query.locale.language
|
||||
_t = search_query.locale.territory or _l
|
||||
headers["Accept-Language"] = f"{_l},{_l}-{_t};q=0.7,en;q=0.3"
|
||||
else:
|
||||
headers["Accept-Language"] = "en-US,en;q=0.9"
|
||||
self.logger.debug("HTTP Accept-Language: %s", headers.get("Accept-Language", ""))
|
||||
|
||||
return params
|
||||
|
||||
+39
-193
@@ -154,13 +154,14 @@ ui:
|
||||
# URL formatting: pretty, full or host
|
||||
url_formatting: pretty
|
||||
|
||||
preferences:
|
||||
# Lock arbitrary settings on the preferences page.
|
||||
lock: []
|
||||
#
|
||||
# preferences:
|
||||
# lock:
|
||||
# - categories
|
||||
# - language
|
||||
# - autocomplete
|
||||
# - favicon_resolver
|
||||
# - favicon
|
||||
# - safesearch
|
||||
# - method
|
||||
# - doi_resolver
|
||||
@@ -803,45 +804,10 @@ engines:
|
||||
display_type: ["infobox"]
|
||||
categories: [general]
|
||||
|
||||
- name: dogpile
|
||||
engine: dogpile
|
||||
shortcut: dog
|
||||
dogpile_categ: search
|
||||
categories: general
|
||||
disabled: true
|
||||
|
||||
- name: dogpile images
|
||||
engine: dogpile
|
||||
shortcut: dogi
|
||||
dogpile_categ: images
|
||||
categories: images
|
||||
disabled: true
|
||||
|
||||
- name: dogpile videos
|
||||
engine: dogpile
|
||||
shortcut: dogv
|
||||
dogpile_categ: videos
|
||||
categories: videos
|
||||
disabled: true
|
||||
|
||||
- name: dogpile news
|
||||
engine: dogpile
|
||||
shortcut: dogn
|
||||
dogpile_categ: news
|
||||
categories: news
|
||||
disabled: true
|
||||
|
||||
# duckduckgo uses html.duckduckgo.com,
|
||||
# duckduckgo web uses duckduckgo.com
|
||||
- name: duckduckgo
|
||||
engine: duckduckgo
|
||||
shortcut: ddg
|
||||
|
||||
- name: duckduckgo web
|
||||
engine: duckduckgo_web
|
||||
shortcut: ddgw
|
||||
disabled: true
|
||||
|
||||
- name: duckduckgo images
|
||||
engine: duckduckgo_extra
|
||||
categories: [images]
|
||||
@@ -924,27 +890,6 @@ engines:
|
||||
shortcut: ftm
|
||||
disabled: true
|
||||
|
||||
- name: fireball
|
||||
engine: fireball
|
||||
shortcut: fire
|
||||
categories: general
|
||||
fireball_category: web
|
||||
disabled: true
|
||||
|
||||
- name: fireball news
|
||||
engine: fireball
|
||||
shortcut: firen
|
||||
categories: news
|
||||
fireball_category: news
|
||||
disabled: true
|
||||
|
||||
- name: fireball videos
|
||||
engine: fireball
|
||||
shortcut: firev
|
||||
categories: videos
|
||||
fireball_category: videos
|
||||
disabled: true
|
||||
|
||||
- name: flaticon
|
||||
engine: flaticon
|
||||
shortcut: fli
|
||||
@@ -1020,22 +965,6 @@ engines:
|
||||
timeout: 8.0
|
||||
disabled: true
|
||||
|
||||
- name: gabanza
|
||||
engine: xpath
|
||||
search_url: https://www.gabanza.com/search?query={query}
|
||||
shortcut: gab
|
||||
timeout: 4
|
||||
disabled: true
|
||||
results_xpath: //div[contains(@class, "border-t")]/div/div
|
||||
url_xpath: (.//a/@href)[1]
|
||||
title_xpath: ./a
|
||||
content_xpath: .//p
|
||||
about:
|
||||
website: https://www.gabanza.com
|
||||
use_official_api: false
|
||||
require_api_key: false
|
||||
results: HTML
|
||||
|
||||
- name: geizhals
|
||||
engine: geizhals
|
||||
shortcut: geiz
|
||||
@@ -1187,22 +1116,6 @@ engines:
|
||||
shortcut: hn
|
||||
disabled: true
|
||||
|
||||
- name: heexy
|
||||
engine: heexy
|
||||
categories: general
|
||||
heexy_categ: web
|
||||
shortcut: he
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: heexy images
|
||||
engine: heexy
|
||||
categories: images
|
||||
heexy_categ: image
|
||||
shortcut: hei
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: hex
|
||||
engine: hex
|
||||
shortcut: hex
|
||||
@@ -1294,41 +1207,41 @@ engines:
|
||||
shortcut: iq
|
||||
disabled: true
|
||||
|
||||
# - name: kagi
|
||||
# engine: kagi
|
||||
# categories: [general, web]
|
||||
# shortcut: kg
|
||||
# api_key: "" # required
|
||||
# kagi_categ: search
|
||||
|
||||
# - name: kagi.news
|
||||
# engine: kagi
|
||||
# categories: [news, web]
|
||||
# shortcut: kgn
|
||||
# api_key: "" # required
|
||||
# kagi_categ: news
|
||||
|
||||
# - name: kagi.images
|
||||
# engine: kagi
|
||||
# categories: [images, web]
|
||||
# paging: false
|
||||
# shortcut: kgi
|
||||
# api_key: "" # required
|
||||
# kagi_categ: images
|
||||
|
||||
# - name: kagi.videos
|
||||
# engine: kagi
|
||||
# categories: [videos, web]
|
||||
# shortcut: kgv
|
||||
# api_key: "" # required
|
||||
# kagi_categ: videos
|
||||
|
||||
- name: jisho
|
||||
engine: jisho
|
||||
shortcut: js
|
||||
timeout: 3.0
|
||||
disabled: true
|
||||
|
||||
- name: karmasearch
|
||||
engine: karmasearch
|
||||
categories: [general, web]
|
||||
search_type: web
|
||||
shortcut: ka
|
||||
inactive: true
|
||||
|
||||
- name: karmasearch images
|
||||
engine: karmasearch
|
||||
categories: [images, web]
|
||||
search_type: images
|
||||
shortcut: kai
|
||||
paging: false
|
||||
inactive: true
|
||||
|
||||
- name: karmasearch videos
|
||||
engine: karmasearch
|
||||
categories: [videos, web]
|
||||
search_type: videos
|
||||
shortcut: kav
|
||||
inactive: true
|
||||
|
||||
- name: karmasearch news
|
||||
engine: karmasearch
|
||||
categories: [news, web]
|
||||
search_type: news
|
||||
shortcut: kan
|
||||
inactive: true
|
||||
|
||||
- name: kickass
|
||||
engine: kickass
|
||||
base_url:
|
||||
@@ -2061,14 +1974,6 @@ engines:
|
||||
# - ...
|
||||
# disabled: true
|
||||
|
||||
- name: seekninja
|
||||
engine: seekninja
|
||||
shortcut: sen
|
||||
# very slow due to its server-side events architecture
|
||||
timeout: 10
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: semantic scholar
|
||||
engine: semantic_scholar
|
||||
shortcut: se
|
||||
@@ -2163,22 +2068,6 @@ engines:
|
||||
shortcut: ts
|
||||
disabled: true
|
||||
|
||||
- name: tiger
|
||||
engine: tiger
|
||||
categories: general
|
||||
tiger_category: Websuche
|
||||
shortcut: tig
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: tiger news
|
||||
engine: tiger
|
||||
categories: news
|
||||
tiger_category: News
|
||||
shortcut: tign
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: tmdb
|
||||
engine: xpath
|
||||
paging: true
|
||||
@@ -2636,38 +2525,6 @@ engines:
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: swisscows
|
||||
engine: swisscows
|
||||
categories: general
|
||||
swisscows_category: web
|
||||
results_per_page: 20
|
||||
shortcut: sw
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: swisscows images
|
||||
engine: swisscows
|
||||
categories: images
|
||||
swisscows_category: images
|
||||
shortcut: swi
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: swisscows videos
|
||||
engine: swisscows
|
||||
categories: videos
|
||||
swisscows_category: videos
|
||||
results_per_page: 10
|
||||
shortcut: swv
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: swisscows news
|
||||
engine: swisscows_news
|
||||
shortcut: swn
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: wordnik
|
||||
engine: wordnik
|
||||
shortcut: wnik
|
||||
@@ -2692,6 +2549,12 @@ engines:
|
||||
results: HTML
|
||||
language: de
|
||||
|
||||
- name: svgrepo
|
||||
engine: svgrepo
|
||||
shortcut: svg
|
||||
timeout: 10.0
|
||||
disabled: true
|
||||
|
||||
- name: tootfinder
|
||||
engine: tootfinder
|
||||
shortcut: toot
|
||||
@@ -2737,23 +2600,6 @@ engines:
|
||||
shortcut: wttr
|
||||
timeout: 9.0
|
||||
|
||||
- name: zapmeta
|
||||
engine: xpath
|
||||
shortcut: zpm
|
||||
search_url: https://www.zapmeta.com/search?q={query}&pg={pageno}
|
||||
results_xpath: //article[contains(@class, "organic-results-item")]
|
||||
url_xpath: ./h2/a/@href
|
||||
title_xpath: ./h2
|
||||
content_xpath: ./p
|
||||
paging: true
|
||||
send_page_num_on_first_page: false # otherwise blocks requests
|
||||
disabled: true
|
||||
about:
|
||||
website: https://www.zapmeta.com/
|
||||
use_official_api: false
|
||||
require_api_key: false
|
||||
results: HTML
|
||||
|
||||
- name: braveapi
|
||||
engine: braveapi
|
||||
# read https://docs.searxng.org/dev/engines/online/brave.html
|
||||
|
||||
@@ -15,7 +15,6 @@ import msgspec
|
||||
from typing_extensions import override
|
||||
from .brand import SettingsBrand
|
||||
from .sxng_locales import sxng_locales
|
||||
from ._settings import SettingsPref
|
||||
|
||||
searx_dir = abspath(dirname(__file__))
|
||||
|
||||
@@ -147,8 +146,6 @@ def apply_schema(settings: dict[str, t.Any], schema: dict[str, t.Any], path_list
|
||||
# Type Validation at runtime:
|
||||
# https://jcristharif.com/msgspec/structs.html#type-validation
|
||||
cfg_dict = settings.get(key)
|
||||
if cfg_dict is None:
|
||||
cfg_dict = {}
|
||||
cfg_json = msgspec.json.encode(cfg_dict)
|
||||
settings[key] = msgspec.json.decode(cfg_json, type=value)
|
||||
except msgspec.ValidationError as e:
|
||||
@@ -239,13 +236,16 @@ SCHEMA: dict[str, t.Any] = {
|
||||
},
|
||||
'center_alignment': SettingsValue(bool, False),
|
||||
'results_on_new_tab': SettingsValue(bool, False),
|
||||
'advanced_search': SettingsValue(bool, False),
|
||||
'query_in_title': SettingsValue(bool, False),
|
||||
'cache_url': SettingsValue(str, 'https://web.archive.org/web/'),
|
||||
'search_on_category_select': SettingsValue(bool, True),
|
||||
'hotkeys': SettingsValue(('default', 'vim'), 'default'),
|
||||
'url_formatting': SettingsValue(('pretty', 'full', 'host'), 'pretty'),
|
||||
},
|
||||
"preferences": SettingsPref,
|
||||
'preferences': {
|
||||
'lock': SettingsValue(list, []),
|
||||
},
|
||||
'outgoing': {
|
||||
'useragent_suffix': SettingsValue(str, ''),
|
||||
'request_timeout': SettingsValue(numbers.Real, 3.0),
|
||||
|
||||
+4
-13
@@ -121,8 +121,8 @@ class SQLiteAppl(abc.ABC):
|
||||
|
||||
.. _WAL: https://sqlite.org/wal.html
|
||||
"""
|
||||
SQLITE_CONNECT_ARGS: dict[str, str | float | int | bool | None] = {
|
||||
"timeout": 3.0, # default is 5sec
|
||||
SQLITE_CONNECT_ARGS: dict[str,str|int|bool|None] = {
|
||||
# "timeout": 5.0,
|
||||
# "detect_types": 0,
|
||||
"check_same_thread": bool(SQLITE_THREADING_MODE != "serialized"),
|
||||
"cached_statements": 0, # https://github.com/python/cpython/issues/118172
|
||||
@@ -195,7 +195,6 @@ class SQLiteAppl(abc.ABC):
|
||||
self.db_url: str = db_url
|
||||
self.properties: SQLiteProperties = SQLiteProperties(db_url)
|
||||
self._init_done: bool = False
|
||||
self._DB: sqlite3.Connection | None = None
|
||||
self._compatibility()
|
||||
# atexit.register(self.tear_down)
|
||||
|
||||
@@ -210,7 +209,7 @@ class SQLiteAppl(abc.ABC):
|
||||
def _compatibility(self):
|
||||
|
||||
if self.SQLITE_THREADING_MODE == "serialized":
|
||||
self._DB = None
|
||||
self._DB: sqlite3.Connection | None = None
|
||||
else:
|
||||
msg = (
|
||||
f"SQLite library is compiled with {self.SQLITE_THREADING_MODE} mode,"
|
||||
@@ -229,13 +228,7 @@ class SQLiteAppl(abc.ABC):
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
conn = sqlite3.Connection(self.db_url, **self.SQLITE_CONNECT_ARGS) # type: ignore
|
||||
try:
|
||||
with conn:
|
||||
conn.execute(f"PRAGMA journal_mode={self.SQLITE_JOURNAL_MODE}")
|
||||
except sqlite3.OperationalError:
|
||||
# when database is locked, the journal_mode is already set by
|
||||
# different but concurrent process (no need to set it once more)
|
||||
pass
|
||||
self.register_functions(conn)
|
||||
return conn
|
||||
|
||||
@@ -319,7 +312,6 @@ class SQLiteAppl(abc.ABC):
|
||||
# Since more than one instance of SQLiteAppl share the same DB
|
||||
# connection, we need to make sure that each SQLiteAppl instance has run
|
||||
# its init method at least once.
|
||||
with conn:
|
||||
self.init(conn)
|
||||
|
||||
return conn
|
||||
@@ -338,7 +330,6 @@ class SQLiteAppl(abc.ABC):
|
||||
self._init_done = True
|
||||
|
||||
logger.debug("init DB: %s", self.db_url)
|
||||
with conn:
|
||||
self.properties.init(conn)
|
||||
|
||||
ver = self.properties("DB_SCHEMA")
|
||||
@@ -418,7 +409,7 @@ CREATE TABLE IF NOT EXISTS properties (
|
||||
self._init_done = True
|
||||
logger.debug("init properties of DB: %s", self.db_url)
|
||||
res = conn.execute(self.SQL_TABLE_EXISTS)
|
||||
if res.fetchone() is None: # DB schema needs to be created
|
||||
if res.fetchone() is None: # DB schema needs to be be created
|
||||
self.create_schema(conn)
|
||||
return True
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -1,2 +1,2 @@
|
||||
import{n as e}from"../sxng-core.min.js";import{t}from"./DcK-mo-Y.min.js";var n=t(`search`),r=t(`q`),i=t(`clear_search`),a=window.matchMedia(`(max-width: 50em)`).matches,o=document.querySelector(`main`)?.id===`main_results`,s=Array.from(document.querySelectorAll(`#categories_container button.category`));r.value.length===0&&i.classList.add(`empty`),a||o||r.focus(),a&&e(`focus`,r,()=>{requestAnimationFrame(()=>{let e=r.value.length;r.setSelectionRange(e,e),r.scrollLeft=r.scrollWidth})}),e(`input`,r,()=>{i.classList.toggle(`empty`,r.value.length===0)}),e(`click`,i,e=>{e.preventDefault(),r.value=``,r.focus(),i.classList.add(`empty`)});for(let t of s)e(`click`,t,e=>{if(e.shiftKey){e.preventDefault(),t.classList.toggle(`selected`);return}for(let e of s)e.classList.toggle(`selected`,e===t)});if(document.querySelector(`div.search_filters`)){let t=document.getElementById(`safesearch`);t&&e(`change`,t,()=>n.submit());let r=document.getElementById(`time_range`);r&&e(`change`,r,()=>n.submit());let i=document.getElementById(`language`);i&&e(`change`,i,()=>n.submit())}e(`submit`,n,e=>{if(e.preventDefault(),s.length>0){let e=t(`selected-categories`);e.value=s.filter(e=>e.classList.contains(`selected`)).map(e=>e.name.replace(`category_`,``)).join(`,`)}n.submit()});
|
||||
//# sourceMappingURL=5Ako-qGW.min.js.map
|
||||
import{n as e}from"../sxng-core.min.js";import{t}from"./chlzpS6K.min.js";var n=t(`search`),r=t(`q`),i=t(`clear_search`),a=window.matchMedia(`(max-width: 50em)`).matches,o=document.querySelector(`main`)?.id===`main_results`,s=Array.from(document.querySelectorAll(`#categories_container button.category`));r.value.length===0&&i.classList.add(`empty`),a||o||r.focus(),a&&e(`focus`,r,()=>{requestAnimationFrame(()=>{let e=r.value.length;r.setSelectionRange(e,e),r.scrollLeft=r.scrollWidth})}),e(`input`,r,()=>{i.classList.toggle(`empty`,r.value.length===0)}),e(`click`,i,e=>{e.preventDefault(),r.value=``,r.focus(),i.classList.add(`empty`)});for(let t of s)e(`click`,t,e=>{if(e.shiftKey){e.preventDefault(),t.classList.toggle(`selected`);return}for(let e of s)e.classList.toggle(`selected`,e===t)});if(document.querySelector(`div.search_filters`)){let t=document.getElementById(`safesearch`);t&&e(`change`,t,()=>n.submit());let r=document.getElementById(`time_range`);r&&e(`change`,r,()=>n.submit());let i=document.getElementById(`language`);i&&e(`change`,i,()=>n.submit())}e(`submit`,n,e=>{if(e.preventDefault(),s.length>0){let e=t(`selected-categories`);e.value=s.filter(e=>e.classList.contains(`selected`)).map(e=>e.name.replace(`category_`,``)).join(`,`)}n.submit()});
|
||||
//# sourceMappingURL=BnP4vIuG.min.js.map
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"5Ako-qGW.min.js","names":[],"sources":["../../../../../client/simple/src/js/main/search.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { listen } from \"../toolkit.ts\";\nimport { getElement } from \"../util/getElement.ts\";\n\nconst searchForm: HTMLFormElement = getElement<HTMLFormElement>(\"search\");\nconst searchInput: HTMLInputElement = getElement<HTMLInputElement>(\"q\");\nconst searchReset: HTMLButtonElement = getElement<HTMLButtonElement>(\"clear_search\");\n\nconst isMobile: boolean = window.matchMedia(\"(max-width: 50em)\").matches;\nconst isResultsPage: boolean = document.querySelector(\"main\")?.id === \"main_results\";\n\nconst categoryButtons: HTMLButtonElement[] = Array.from(\n document.querySelectorAll<HTMLButtonElement>(\"#categories_container button.category\")\n);\n\nif (searchInput.value.length === 0) {\n searchReset.classList.add(\"empty\");\n}\n\n// focus search input on large screens\nif (!(isMobile || isResultsPage)) {\n searchInput.focus();\n}\n\n// On mobile, move cursor to the end of the input on focus\nif (isMobile) {\n listen(\"focus\", searchInput, () => {\n // Defer cursor move until the next frame to prevent a visual jump\n requestAnimationFrame(() => {\n const end = searchInput.value.length;\n searchInput.setSelectionRange(end, end);\n searchInput.scrollLeft = searchInput.scrollWidth;\n });\n });\n}\n\nlisten(\"input\", searchInput, () => {\n searchReset.classList.toggle(\"empty\", searchInput.value.length === 0);\n});\n\nlisten(\"click\", searchReset, (event: MouseEvent) => {\n event.preventDefault();\n searchInput.value = \"\";\n searchInput.focus();\n searchReset.classList.add(\"empty\");\n});\n\nfor (const button of categoryButtons) {\n listen(\"click\", button, (event: MouseEvent) => {\n if (event.shiftKey) {\n event.preventDefault();\n button.classList.toggle(\"selected\");\n return;\n }\n\n // deselect all other categories\n for (const categoryButton of categoryButtons) {\n categoryButton.classList.toggle(\"selected\", categoryButton === button);\n }\n });\n}\n\nif (document.querySelector(\"div.search_filters\")) {\n const safesearchElement = document.getElementById(\"safesearch\");\n if (safesearchElement) {\n listen(\"change\", safesearchElement, () => searchForm.submit());\n }\n\n const timeRangeElement = document.getElementById(\"time_range\");\n if (timeRangeElement) {\n listen(\"change\", timeRangeElement, () => searchForm.submit());\n }\n\n const languageElement = document.getElementById(\"language\");\n if (languageElement) {\n listen(\"change\", languageElement, () => searchForm.submit());\n }\n}\n\n// override searchForm submit event\nlisten(\"submit\", searchForm, (event: Event) => {\n event.preventDefault();\n\n if (categoryButtons.length > 0) {\n const searchCategories = getElement<HTMLInputElement>(\"selected-categories\");\n searchCategories.value = categoryButtons\n .filter((button) => button.classList.contains(\"selected\"))\n .map((button) => button.name.replace(\"category_\", \"\"))\n .join(\",\");\n }\n\n searchForm.submit();\n});\n"],"mappings":"yEAKA,IAAM,EAA8B,EAA4B,QAAQ,EAClE,EAAgC,EAA6B,GAAG,EAChE,EAAiC,EAA8B,cAAc,EAE7E,EAAoB,OAAO,WAAW,mBAAmB,EAAE,QAC3D,EAAyB,SAAS,cAAc,MAAM,GAAG,KAAO,eAEhE,EAAuC,MAAM,KACjD,SAAS,iBAAoC,uCAAuC,CACtF,EAEI,EAAY,MAAM,SAAW,GAC/B,EAAY,UAAU,IAAI,OAAO,EAI7B,GAAY,GAChB,EAAY,MAAM,EAIhB,GACF,EAAO,QAAS,MAAmB,CAEjC,0BAA4B,CAC1B,IAAM,EAAM,EAAY,MAAM,OAC9B,EAAY,kBAAkB,EAAK,CAAG,EACtC,EAAY,WAAa,EAAY,WACvC,CAAC,CACH,CAAC,EAGH,EAAO,QAAS,MAAmB,CACjC,EAAY,UAAU,OAAO,QAAS,EAAY,MAAM,SAAW,CAAC,CACtE,CAAC,EAED,EAAO,QAAS,EAAc,GAAsB,CAClD,EAAM,eAAe,EACrB,EAAY,MAAQ,GACpB,EAAY,MAAM,EAClB,EAAY,UAAU,IAAI,OAAO,CACnC,CAAC,EAED,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,EAAS,GAAsB,CAC7C,GAAI,EAAM,SAAU,CAClB,EAAM,eAAe,EACrB,EAAO,UAAU,OAAO,UAAU,EAClC,MACF,CAGA,IAAK,IAAM,KAAkB,EAC3B,EAAe,UAAU,OAAO,WAAY,IAAmB,CAAM,CAEzE,CAAC,EAGH,GAAI,SAAS,cAAc,oBAAoB,EAAG,CAChD,IAAM,EAAoB,SAAS,eAAe,YAAY,EAC1D,GACF,EAAO,SAAU,MAAyB,EAAW,OAAO,CAAC,EAG/D,IAAM,EAAmB,SAAS,eAAe,YAAY,EACzD,GACF,EAAO,SAAU,MAAwB,EAAW,OAAO,CAAC,EAG9D,IAAM,EAAkB,SAAS,eAAe,UAAU,EACtD,GACF,EAAO,SAAU,MAAuB,EAAW,OAAO,CAAC,CAE/D,CAGA,EAAO,SAAU,EAAa,GAAiB,CAG7C,GAFA,EAAM,eAAe,EAEjB,EAAgB,OAAS,EAAG,CAC9B,IAAM,EAAmB,EAA6B,qBAAqB,EAC3E,EAAiB,MAAQ,EACtB,OAAQ,GAAW,EAAO,UAAU,SAAS,UAAU,CAAC,EACxD,IAAK,GAAW,EAAO,KAAK,QAAQ,YAAa,EAAE,CAAC,EACpD,KAAK,GAAG,CACb,CAEA,EAAW,OAAO,CACpB,CAAC"}
|
||||
{"version":3,"file":"BnP4vIuG.min.js","names":[],"sources":["../../../../../client/simple/src/js/main/search.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { listen } from \"../toolkit.ts\";\nimport { getElement } from \"../util/getElement.ts\";\n\nconst searchForm: HTMLFormElement = getElement<HTMLFormElement>(\"search\");\nconst searchInput: HTMLInputElement = getElement<HTMLInputElement>(\"q\");\nconst searchReset: HTMLButtonElement = getElement<HTMLButtonElement>(\"clear_search\");\n\nconst isMobile: boolean = window.matchMedia(\"(max-width: 50em)\").matches;\nconst isResultsPage: boolean = document.querySelector(\"main\")?.id === \"main_results\";\n\nconst categoryButtons: HTMLButtonElement[] = Array.from(\n document.querySelectorAll<HTMLButtonElement>(\"#categories_container button.category\")\n);\n\nif (searchInput.value.length === 0) {\n searchReset.classList.add(\"empty\");\n}\n\n// focus search input on large screens\nif (!(isMobile || isResultsPage)) {\n searchInput.focus();\n}\n\n// On mobile, move cursor to the end of the input on focus\nif (isMobile) {\n listen(\"focus\", searchInput, () => {\n // Defer cursor move until the next frame to prevent a visual jump\n requestAnimationFrame(() => {\n const end = searchInput.value.length;\n searchInput.setSelectionRange(end, end);\n searchInput.scrollLeft = searchInput.scrollWidth;\n });\n });\n}\n\nlisten(\"input\", searchInput, () => {\n searchReset.classList.toggle(\"empty\", searchInput.value.length === 0);\n});\n\nlisten(\"click\", searchReset, (event: MouseEvent) => {\n event.preventDefault();\n searchInput.value = \"\";\n searchInput.focus();\n searchReset.classList.add(\"empty\");\n});\n\nfor (const button of categoryButtons) {\n listen(\"click\", button, (event: MouseEvent) => {\n if (event.shiftKey) {\n event.preventDefault();\n button.classList.toggle(\"selected\");\n return;\n }\n\n // deselect all other categories\n for (const categoryButton of categoryButtons) {\n categoryButton.classList.toggle(\"selected\", categoryButton === button);\n }\n });\n}\n\nif (document.querySelector(\"div.search_filters\")) {\n const safesearchElement = document.getElementById(\"safesearch\");\n if (safesearchElement) {\n listen(\"change\", safesearchElement, () => searchForm.submit());\n }\n\n const timeRangeElement = document.getElementById(\"time_range\");\n if (timeRangeElement) {\n listen(\"change\", timeRangeElement, () => searchForm.submit());\n }\n\n const languageElement = document.getElementById(\"language\");\n if (languageElement) {\n listen(\"change\", languageElement, () => searchForm.submit());\n }\n}\n\n// override searchForm submit event\nlisten(\"submit\", searchForm, (event: Event) => {\n event.preventDefault();\n\n if (categoryButtons.length > 0) {\n const searchCategories = getElement<HTMLInputElement>(\"selected-categories\");\n searchCategories.value = categoryButtons\n .filter((button) => button.classList.contains(\"selected\"))\n .map((button) => button.name.replace(\"category_\", \"\"))\n .join(\",\");\n }\n\n searchForm.submit();\n});\n"],"mappings":"yEAKA,IAAM,EAA8B,EAA4B,QAAQ,EAClE,EAAgC,EAA6B,GAAG,EAChE,EAAiC,EAA8B,cAAc,EAE7E,EAAoB,OAAO,WAAW,mBAAmB,EAAE,QAC3D,EAAyB,SAAS,cAAc,MAAM,GAAG,KAAO,eAEhE,EAAuC,MAAM,KACjD,SAAS,iBAAoC,uCAAuC,CACtF,EAEI,EAAY,MAAM,SAAW,GAC/B,EAAY,UAAU,IAAI,OAAO,EAI7B,GAAY,GAChB,EAAY,MAAM,EAIhB,GACF,EAAO,QAAS,MAAmB,CAEjC,0BAA4B,CAC1B,IAAM,EAAM,EAAY,MAAM,OAC9B,EAAY,kBAAkB,EAAK,CAAG,EACtC,EAAY,WAAa,EAAY,WACvC,CAAC,CACH,CAAC,EAGH,EAAO,QAAS,MAAmB,CACjC,EAAY,UAAU,OAAO,QAAS,EAAY,MAAM,SAAW,CAAC,CACtE,CAAC,EAED,EAAO,QAAS,EAAc,GAAsB,CAClD,EAAM,eAAe,EACrB,EAAY,MAAQ,GACpB,EAAY,MAAM,EAClB,EAAY,UAAU,IAAI,OAAO,CACnC,CAAC,EAED,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,EAAS,GAAsB,CAC7C,GAAI,EAAM,SAAU,CAClB,EAAM,eAAe,EACrB,EAAO,UAAU,OAAO,UAAU,EAClC,MACF,CAGA,IAAK,IAAM,KAAkB,EAC3B,EAAe,UAAU,OAAO,WAAY,IAAmB,CAAM,CAEzE,CAAC,EAGH,GAAI,SAAS,cAAc,oBAAoB,EAAG,CAChD,IAAM,EAAoB,SAAS,eAAe,YAAY,EAC1D,GACF,EAAO,SAAU,MAAyB,EAAW,OAAO,CAAC,EAG/D,IAAM,EAAmB,SAAS,eAAe,YAAY,EACzD,GACF,EAAO,SAAU,MAAwB,EAAW,OAAO,CAAC,EAG9D,IAAM,EAAkB,SAAS,eAAe,UAAU,EACtD,GACF,EAAO,SAAU,MAAuB,EAAW,OAAO,CAAC,CAE/D,CAGA,EAAO,SAAU,EAAa,GAAiB,CAG7C,GAFA,EAAM,eAAe,EAEjB,EAAgB,OAAS,EAAG,CAC9B,IAAM,EAAmB,EAA6B,qBAAqB,EAC3E,EAAiB,MAAQ,EACtB,OAAQ,GAAW,EAAO,UAAU,SAAS,UAAU,CAAC,EACxD,IAAK,GAAW,EAAO,KAAK,QAAQ,YAAa,EAAE,CAAC,EACpD,KAAK,GAAG,CACb,CAEA,EAAW,OAAO,CACpB,CAAC"}
|
||||
Vendored
+2
-2
@@ -1,2 +1,2 @@
|
||||
import{i as e,n as t,t as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";var i=async(i,a)=>{try{let o;o=e.method===`GET`?await n(`GET`,`./autocompleter?q=${a}`):await n(`POST`,`./autocompleter`,{body:new URLSearchParams({q:a})});let s=await o.json(),c=document.querySelector(`.autocomplete`);r(c);let l=document.querySelector(`.autocomplete ul`);if(r(l),c.classList.add(`open`),l.replaceChildren(),s?.[1]?.length===0){let t=Object.assign(document.createElement(`li`),{className:`no-item-found`,textContent:e.translations?.no_item_found??`No results found`});l.append(t);return}let u=new DocumentFragment;for(let e of s[1]){let n=Object.assign(document.createElement(`li`),{textContent:e});t(`mousedown`,n,()=>{i.value=e,document.querySelector(`#search`)?.submit()}),u.append(n)}l.append(u)}catch(e){console.error(`Error fetching autocomplete results:`,e)}},a=document.getElementById(`q`);r(a);var o;t(`input`,a,()=>{clearTimeout(o);let t=a.value,n=e.autocomplete_min??2;t.length<n||(o=window.setTimeout(async()=>{t===a.value&&await i(a,t)},300))});var s=document.querySelector(`.autocomplete`),c=document.querySelector(`.autocomplete ul`);c&&(t(`keydown`,a,e=>{e.key===`Escape`&&s?.classList.remove(`open`)}),t(`keyup`,a,e=>{let t=[...c.children],n=t.findIndex(e=>e.classList.contains(`active`)),r=-1;switch(e.key){case`ArrowUp`:{let e=t[n];e&&n>=0&&e.classList.remove(`active`),r=(n-1+t.length)%t.length;break}case`ArrowDown`:{let e=t[n];e&&n>=0&&e.classList.remove(`active`),r=(n+1)%t.length;break}case`Enter`:s&&s.classList.remove(`open`);break;default:break}if(r!==-1){let e=t[r];if(e&&(e.classList.add(`active`),!e.classList.contains(`no-item-found`))){let t=document.getElementById(`q`);t&&(t.value=e.textContent??``)}}}),t(`blur`,a,()=>{s?.classList.remove(`open`)}),t(`focus`,a,()=>{s?.classList.add(`open`)}));
|
||||
//# sourceMappingURL=DvCYLbJr.min.js.map
|
||||
import{i as e,n as t,t as n}from"../sxng-core.min.js";import{t as r}from"./DH1EQbEY.min.js";var i=async(i,a)=>{try{let o;o=e.method===`GET`?await n(`GET`,`./autocompleter?q=${a}`):await n(`POST`,`./autocompleter`,{body:new URLSearchParams({q:a})});let s=await o.json(),c=document.querySelector(`.autocomplete`);r(c);let l=document.querySelector(`.autocomplete ul`);if(r(l),c.classList.add(`open`),l.replaceChildren(),s?.[1]?.length===0){let t=Object.assign(document.createElement(`li`),{className:`no-item-found`,textContent:e.translations?.no_item_found??`No results found`});l.append(t);return}let u=new DocumentFragment;for(let e of s[1]){let n=Object.assign(document.createElement(`li`),{textContent:e});t(`mousedown`,n,()=>{i.value=e,document.querySelector(`#search`)?.submit()}),u.append(n)}l.append(u)}catch(e){console.error(`Error fetching autocomplete results:`,e)}},a=document.getElementById(`q`);r(a);var o;t(`input`,a,()=>{clearTimeout(o);let t=a.value,n=e.autocomplete_min??2;t.length<n||(o=window.setTimeout(async()=>{t===a.value&&await i(a,t)},300))});var s=document.querySelector(`.autocomplete`),c=document.querySelector(`.autocomplete ul`);c&&(t(`keydown`,a,e=>{e.key===`Escape`&&s?.classList.remove(`open`)}),t(`keyup`,a,e=>{let t=[...c.children],n=t.findIndex(e=>e.classList.contains(`active`)),r=-1;switch(e.key){case`ArrowUp`:{let e=t[n];e&&n>=0&&e.classList.remove(`active`),r=(n-1+t.length)%t.length;break}case`ArrowDown`:{let e=t[n];e&&n>=0&&e.classList.remove(`active`),r=(n+1)%t.length;break}case`Enter`:s&&s.classList.remove(`open`);break;default:break}if(r!==-1){let e=t[r];if(e&&(e.classList.add(`active`),!e.classList.contains(`no-item-found`))){let t=document.getElementById(`q`);t&&(t.value=e.textContent??``)}}}),t(`blur`,a,()=>{s?.classList.remove(`open`)}),t(`focus`,a,()=>{s?.classList.add(`open`)}));
|
||||
//# sourceMappingURL=CQ8vfMdp.min.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -1,2 +1,2 @@
|
||||
import{a as e,i as t,t as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";import{t as i}from"./DcK-mo-Y.min.js";var a=class extends e{constructor(){super(`infiniteScroll`)}async run(){let e=i(`results`).classList.contains(`only_template_images`),a=`article.result:last-child`,o=document.createElement(`div`);o.className=`loader`;let s=async i=>{let a=document.querySelector(`#search`);r(a);let s=document.querySelector(`#pagination form.next_page`);r(s);let c=a.getAttribute(`action`);if(!c)throw Error(`Form action not defined`);let l=document.querySelector(`#pagination`);r(l),l.replaceChildren(o);try{let t=await(await n(`POST`,c,{body:new FormData(s)})).text();if(!t)return;let r=new DOMParser().parseFromString(t,`text/html`),a=r.querySelectorAll(`#urls article`),o=r.querySelector(`#pagination`);document.querySelector(`#pagination`)?.remove();let l=document.querySelector(`#urls`);if(!l)throw Error(`URLs element not found`);a.length>0&&!e&&l.appendChild(document.createElement(`hr`)),l.append(...a),o&&(document.querySelector(`#results`)?.appendChild(o),i())}catch(e){console.error(`Error loading next page:`,e);let n=Object.assign(document.createElement(`div`),{textContent:t.translations?.error_loading_next_page??`Error loading next page`,className:`dialog-error`});n.setAttribute(`role`,`alert`),document.querySelector(`#pagination`)?.replaceChildren(n)}},c=new IntersectionObserver(async e=>{let[t]=e;t?.isIntersecting&&(c.unobserve(t.target),await s(()=>{let e=document.querySelector(a);e&&c.observe(e)}))},{rootMargin:`320px`}),l=document.querySelector(a);l&&c.observe(l)}async post(){}};export{a as default};
|
||||
//# sourceMappingURL=DpvWr1cn.min.js.map
|
||||
import{a as e,i as t,t as n}from"../sxng-core.min.js";import{t as r}from"./DH1EQbEY.min.js";import{t as i}from"./chlzpS6K.min.js";var a=class extends e{constructor(){super(`infiniteScroll`)}async run(){let e=i(`results`).classList.contains(`only_template_images`),a=`article.result:last-child`,o=document.createElement(`div`);o.className=`loader`;let s=async i=>{let a=document.querySelector(`#search`);r(a);let s=document.querySelector(`#pagination form.next_page`);r(s);let c=a.getAttribute(`action`);if(!c)throw Error(`Form action not defined`);let l=document.querySelector(`#pagination`);r(l),l.replaceChildren(o);try{let t=await(await n(`POST`,c,{body:new FormData(s)})).text();if(!t)return;let r=new DOMParser().parseFromString(t,`text/html`),a=r.querySelectorAll(`#urls article`),o=r.querySelector(`#pagination`);document.querySelector(`#pagination`)?.remove();let l=document.querySelector(`#urls`);if(!l)throw Error(`URLs element not found`);a.length>0&&!e&&l.appendChild(document.createElement(`hr`)),l.append(...a),o&&(document.querySelector(`#results`)?.appendChild(o),i())}catch(e){console.error(`Error loading next page:`,e);let n=Object.assign(document.createElement(`div`),{textContent:t.translations?.error_loading_next_page??`Error loading next page`,className:`dialog-error`});n.setAttribute(`role`,`alert`),document.querySelector(`#pagination`)?.replaceChildren(n)}},c=new IntersectionObserver(async e=>{let[t]=e;t?.isIntersecting&&(c.unobserve(t.target),await s(()=>{let e=document.querySelector(a);e&&c.observe(e)}))},{rootMargin:`320px`}),l=document.querySelector(a);l&&c.observe(l)}async post(){}};export{a as default};
|
||||
//# sourceMappingURL=Cx4rGXMm.min.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
||||
import{i as e,n as t,r as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";
|
||||
import{i as e,n as t,r as n}from"../sxng-core.min.js";import{t as r}from"./DH1EQbEY.min.js";
|
||||
/*!
|
||||
* swiped-events.js - v@version@
|
||||
* Pure JavaScript swipe events
|
||||
@@ -8,4 +8,4 @@ import{i as e,n as t,r as n}from"../sxng-core.min.js";import{t as r}from"./DK4yU
|
||||
* @license MIT
|
||||
*/
|
||||
(function(e,t){typeof e.CustomEvent!=`function`&&(e.CustomEvent=function(e,n){n||={bubbles:!1,cancelable:!1,detail:void 0};var r=t.createEvent(`CustomEvent`);return r.initCustomEvent(e,n.bubbles,n.cancelable,n.detail),r},e.CustomEvent.prototype=e.Event.prototype),t.addEventListener(`touchstart`,u,!1),t.addEventListener(`touchmove`,d,!1),t.addEventListener(`touchend`,l,!1);var n=null,r=null,i=null,a=null,o=null,s=null,c=0;function l(e){if(s===e.target){var l=parseInt(f(s,`data-swipe-threshold`,`20`),10),u=f(s,`data-swipe-unit`,`px`),d=parseInt(f(s,`data-swipe-timeout`,`500`),10),p=Date.now()-o,m=``,h=e.changedTouches||e.touches||[];if(u===`vh`&&(l=Math.round(l/100*t.documentElement.clientHeight)),u===`vw`&&(l=Math.round(l/100*t.documentElement.clientWidth)),Math.abs(i)>Math.abs(a)?Math.abs(i)>l&&p<d&&(m=i>0?`swiped-left`:`swiped-right`):Math.abs(a)>l&&p<d&&(m=a>0?`swiped-up`:`swiped-down`),m!==``){var g={dir:m.replace(/swiped-/,``),touchType:(h[0]||{}).touchType||`direct`,fingers:c,xStart:parseInt(n,10),xEnd:parseInt((h[0]||{}).clientX||-1,10),yStart:parseInt(r,10),yEnd:parseInt((h[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent(`swiped`,{bubbles:!0,cancelable:!0,detail:g})),s.dispatchEvent(new CustomEvent(m,{bubbles:!0,cancelable:!0,detail:g}))}n=null,r=null,o=null}}function u(e){e.target.getAttribute(`data-swipe-ignore`)!==`true`&&(s=e.target,o=Date.now(),n=e.touches[0].clientX,r=e.touches[0].clientY,i=0,a=0,c=e.touches.length)}function d(e){if(!(!n||!r)){var t=e.touches[0].clientX,o=e.touches[0].clientY;i=n-t,a=r-o}}function f(e,n,r){for(;e&&e!==t.documentElement;){var i=e.getAttribute(n);if(i)return i;e=e.parentNode}return r}})(window,document);var i,a=t=>{i&&clearTimeout(i);let n=t.querySelector(`.result-images-source img`);if(!n)return;let r=t.querySelector(`.image_thumbnail`);if(r){if(r.src===`${e.theme_static_path}/img/img_load_error.svg`)return;n.onerror=()=>{n.src=r.src},n.src=r.src}let a=n.getAttribute(`data-src`);a&&(i=setTimeout(()=>{n.src=a,n.removeAttribute(`data-src`)},1e3))},o=document.querySelectorAll(`#urls img.image_thumbnail`);for(let t of o)t.complete&&t.naturalWidth===0&&(t.src=`${e.theme_static_path}/img/img_load_error.svg`),t.onerror=()=>{t.src=`${e.theme_static_path}/img/img_load_error.svg`};document.querySelector(`#search_url button#copy_url`)?.style.setProperty(`display`,`block`),n.selectImage=e=>{document.getElementById(`results`)?.classList.add(`image-detail-open`),window.location.hash=`#image-viewer`,n.scrollPageToSelected?.(),e&&a(e)},n.closeDetail=()=>{document.getElementById(`results`)?.classList.remove(`image-detail-open`),window.location.hash===`#image-viewer`&&window.history.back(),n.scrollPageToSelected?.()},t(`click`,`.btn-collapse`,function(){let e=this.getAttribute(`data-btn-text-collapsed`),t=this.getAttribute(`data-btn-text-not-collapsed`),n=this.getAttribute(`data-target`);if(!(n&&e&&t))return;let i=document.querySelector(n);r(i);let a=this.classList.contains(`collapsed`),o=a?t:e,s=a?e:t;this.innerHTML=this.innerHTML.replace(s,o),this.classList.toggle(`collapsed`),i.classList.toggle(`invisible`)}),t(`click`,`.media-loader`,function(){let e=this.getAttribute(`data-target`);if(!e)return;let t=document.querySelector(`${e} > iframe`);if(r(t),!t.getAttribute(`src`)){let e=t.getAttribute(`data-src`);e&&t.setAttribute(`src`,e)}}),t(`click`,`#copy_url`,async function(){let e=this.parentElement?.querySelector(`pre`);if(r(e),window.isSecureContext)await navigator.clipboard.writeText(e.innerText);else{let t=window.getSelection();if(t){let n=document.createRange();n.selectNodeContents(e),t.removeAllRanges(),t.addRange(n),document.execCommand(`copy`)}}this.dataset.copiedText&&(this.innerText=this.dataset.copiedText)}),t(`click`,`.result-detail-close`,e=>{e.preventDefault(),n.closeDetail?.()}),t(`click`,`.result-detail-previous`,e=>{e.preventDefault(),n.selectPrevious?.(!1)}),t(`click`,`.result-detail-next`,e=>{e.preventDefault(),n.selectNext?.(!1)}),window.addEventListener(`hashchange`,()=>{window.location.hash!==`#image-viewer`&&n.closeDetail?.()});var s=document.querySelectorAll(`.swipe-horizontal`);for(let e of s)t(`swiped-left`,e,()=>{n.selectNext?.(!1)}),t(`swiped-right`,e,()=>{n.selectPrevious?.(!1)});window.addEventListener(`scroll`,()=>{let e=document.getElementById(`backToTop`),t=document.getElementById(`results`);if(e&&t){let e=(document.documentElement.scrollTop||document.body.scrollTop)>=100;t.classList.toggle(`scrolling`,e)}},!0);
|
||||
//# sourceMappingURL=B8prKeWj.min.js.map
|
||||
//# sourceMappingURL=DGJ63wI6.min.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
||||
var e=e=>{if(!e)throw Error(`DOM element not found`)};export{e as t};
|
||||
//# sourceMappingURL=DK4yUVpy.min.js.map
|
||||
//# sourceMappingURL=DH1EQbEY.min.js.map
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"DK4yUVpy.min.js","names":[],"sources":["../../../../../client/simple/src/js/util/assertElement.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\ntype AssertElement = <T>(element?: T | null) => asserts element is T;\nexport const assertElement: AssertElement = <T>(element?: T | null): asserts element is T => {\n if (!element) {\n throw new Error(\"DOM element not found\");\n }\n};\n"],"mappings":"AAGA,IAAa,EAAmC,GAA6C,CAC3F,GAAI,CAAC,EACH,MAAU,MAAM,uBAAuB,CAE3C"}
|
||||
{"version":3,"file":"DH1EQbEY.min.js","names":[],"sources":["../../../../../client/simple/src/js/util/assertElement.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\ntype AssertElement = <T>(element?: T | null) => asserts element is T;\nexport const assertElement: AssertElement = <T>(element?: T | null): asserts element is T => {\n if (!element) {\n throw new Error(\"DOM element not found\");\n }\n};\n"],"mappings":"AAGA,IAAa,EAAmC,GAA6C,CAC3F,GAAI,CAAC,EACH,MAAU,MAAM,uBAAuB,CAE3C"}
|
||||
Vendored
+2
-2
@@ -1,2 +1,2 @@
|
||||
import{i as e,n as t,t as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";var i,a=async()=>{if(!i){try{i=await(await n(`GET`,`engine_descriptions.json`)).json()}catch(e){console.error(`Error fetching engineDescriptions:`,e)}if(i)for(let[t,[n,r]]of Object.entries(i)){let i=document.querySelectorAll(`[data-engine-name="${t}"] .engine-description`),a=` (<i>${e.translations?.Source}: ${r}</i>)`;for(let e of i)e.innerHTML=n+a}}},o=(e,t)=>{for(let n of t)n.offsetParent&&(n.checked=!e)},s=document.querySelectorAll(`[data-engine-name]`);for(let e of s)t(`mouseenter`,e,a);var c=document.querySelectorAll(`tbody input[type=checkbox][class~=checkbox-onoff]`),l=document.querySelectorAll(`.enable-all-engines`);for(let e of l)t(`click`,e,()=>o(!0,c));var u=document.querySelectorAll(`.disable-all-engines`);for(let e of u)t(`click`,e,()=>o(!1,c));t(`click`,`#copy-hash`,async function(){let e=this.parentElement?.querySelector(`pre`);if(r(e),window.isSecureContext)await navigator.clipboard.writeText(e.innerText);else{let t=window.getSelection();if(t){let n=document.createRange();n.selectNodeContents(e),t.removeAllRanges(),t.addRange(n),document.execCommand(`copy`)}}this.dataset.copiedText&&(this.innerText=this.dataset.copiedText)});
|
||||
//# sourceMappingURL=e2-9fzwE.min.js.map
|
||||
import{i as e,n as t,t as n}from"../sxng-core.min.js";import{t as r}from"./DH1EQbEY.min.js";var i,a=async()=>{if(!i){try{i=await(await n(`GET`,`engine_descriptions.json`)).json()}catch(e){console.error(`Error fetching engineDescriptions:`,e)}if(i)for(let[t,[n,r]]of Object.entries(i)){let i=document.querySelectorAll(`[data-engine-name="${t}"] .engine-description`),a=` (<i>${e.translations?.Source}: ${r}</i>)`;for(let e of i)e.innerHTML=n+a}}},o=(e,t)=>{for(let n of t)n.offsetParent&&(n.checked=!e)},s=document.querySelectorAll(`[data-engine-name]`);for(let e of s)t(`mouseenter`,e,a);var c=document.querySelectorAll(`tbody input[type=checkbox][class~=checkbox-onoff]`),l=document.querySelectorAll(`.enable-all-engines`);for(let e of l)t(`click`,e,()=>o(!0,c));var u=document.querySelectorAll(`.disable-all-engines`);for(let e of u)t(`click`,e,()=>o(!1,c));t(`click`,`#copy-hash`,async function(){let e=this.parentElement?.querySelector(`pre`);if(r(e),window.isSecureContext)await navigator.clipboard.writeText(e.innerText);else{let t=window.getSelection();if(t){let n=document.createRange();n.selectNodeContents(e),t.removeAllRanges(),t.addRange(n),document.execCommand(`copy`)}}this.dataset.copiedText&&(this.innerText=this.dataset.copiedText)});
|
||||
//# sourceMappingURL=DZidprJh.min.js.map
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"e2-9fzwE.min.js","names":[],"sources":["../../../../../client/simple/src/js/main/preferences.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { http, listen, settings } from \"../toolkit.ts\";\nimport { assertElement } from \"../util/assertElement.ts\";\n\nlet engineDescriptions: Record<string, [string, string]> | undefined;\n\nconst loadEngineDescriptions = async (): Promise<void> => {\n if (engineDescriptions) return;\n try {\n const res = await http(\"GET\", \"engine_descriptions.json\");\n engineDescriptions = await res.json();\n } catch (error) {\n console.error(\"Error fetching engineDescriptions:\", error);\n }\n if (!engineDescriptions) return;\n\n for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {\n const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name=\"${engine_name}\"] .engine-description`);\n const sourceText = ` (<i>${settings.translations?.Source}: ${source}</i>)`;\n\n for (const element of elements) {\n element.innerHTML = description + sourceText;\n }\n }\n};\n\nconst toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputElement>): void => {\n for (const engineToggle of engineToggles) {\n // check if element visible, so that only engines of the current category are modified\n if (engineToggle.offsetParent) {\n engineToggle.checked = !enable;\n }\n }\n};\n\nconst engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\"[data-engine-name]\");\nfor (const engineElement of engineElements) {\n listen(\"mouseenter\", engineElement, loadEngineDescriptions);\n}\n\nconst engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(\n \"tbody input[type=checkbox][class~=checkbox-onoff]\"\n);\n\nconst enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".enable-all-engines\");\nfor (const engine of enableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(true, engineToggles));\n}\n\nconst disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".disable-all-engines\");\nfor (const engine of disableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(false, engineToggles));\n}\n\nlisten(\"click\", \"#copy-hash\", async function (this: HTMLElement) {\n const target = this.parentElement?.querySelector<HTMLPreElement>(\"pre\");\n assertElement(target);\n\n if (window.isSecureContext) {\n await navigator.clipboard.writeText(target.innerText);\n } else {\n const selection = window.getSelection();\n if (selection) {\n const range = document.createRange();\n range.selectNodeContents(target);\n selection.removeAllRanges();\n selection.addRange(range);\n document.execCommand(\"copy\");\n }\n }\n\n if (this.dataset.copiedText) {\n this.innerText = this.dataset.copiedText;\n }\n});\n"],"mappings":"4FAKA,IAAI,EAEE,EAAyB,SAA2B,CACpD,MACJ,IAAI,CAEF,EAAqB,MAAM,MADT,EAAK,MAAO,0BAA0B,GACzB,KAAK,CACtC,OAAS,EAAO,CACd,QAAQ,MAAM,qCAAsC,CAAK,CAC3D,CACK,KAEL,IAAK,GAAM,CAAC,EAAa,CAAC,EAAa,MAAY,OAAO,QAAQ,CAAkB,EAAG,CACrF,IAAM,EAAW,SAAS,iBAA8B,sBAAsB,EAAY,uBAAuB,EAC3G,EAAa,QAAQ,EAAS,cAAc,OAAO,SAAS,EAAO,OAEzE,IAAK,IAAM,KAAW,EACpB,EAAQ,UAAY,EAAc,CAEtC,CAVA,CAWF,EAEM,GAAiB,EAAiB,IAAsD,CAC5F,IAAK,IAAM,KAAgB,EAErB,EAAa,eACf,EAAa,QAAU,CAAC,EAG9B,EAEM,EAA0C,SAAS,iBAA8B,oBAAoB,EAC3G,IAAK,IAAM,KAAiB,EAC1B,EAAO,aAAc,EAAe,CAAsB,EAG5D,IAAM,EAA8C,SAAS,iBAC3D,mDACF,EAEM,EAA4C,SAAS,iBAA8B,qBAAqB,EAC9G,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAM,CAAa,CAAC,EAGlE,IAAM,EAA6C,SAAS,iBAA8B,sBAAsB,EAChH,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAO,CAAa,CAAC,EAGnE,EAAO,QAAS,aAAc,gBAAmC,CAC/D,IAAM,EAAS,KAAK,eAAe,cAA8B,KAAK,EAGtE,GAFA,EAAc,CAAM,EAEhB,OAAO,gBACT,MAAM,UAAU,UAAU,UAAU,EAAO,SAAS,MAC/C,CACL,IAAM,EAAY,OAAO,aAAa,EACtC,GAAI,EAAW,CACb,IAAM,EAAQ,SAAS,YAAY,EACnC,EAAM,mBAAmB,CAAM,EAC/B,EAAU,gBAAgB,EAC1B,EAAU,SAAS,CAAK,EACxB,SAAS,YAAY,MAAM,CAC7B,CACF,CAEI,KAAK,QAAQ,aACf,KAAK,UAAY,KAAK,QAAQ,WAElC,CAAC"}
|
||||
{"version":3,"file":"DZidprJh.min.js","names":[],"sources":["../../../../../client/simple/src/js/main/preferences.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { http, listen, settings } from \"../toolkit.ts\";\nimport { assertElement } from \"../util/assertElement.ts\";\n\nlet engineDescriptions: Record<string, [string, string]> | undefined;\n\nconst loadEngineDescriptions = async (): Promise<void> => {\n if (engineDescriptions) return;\n try {\n const res = await http(\"GET\", \"engine_descriptions.json\");\n engineDescriptions = await res.json();\n } catch (error) {\n console.error(\"Error fetching engineDescriptions:\", error);\n }\n if (!engineDescriptions) return;\n\n for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {\n const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name=\"${engine_name}\"] .engine-description`);\n const sourceText = ` (<i>${settings.translations?.Source}: ${source}</i>)`;\n\n for (const element of elements) {\n element.innerHTML = description + sourceText;\n }\n }\n};\n\nconst toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputElement>): void => {\n for (const engineToggle of engineToggles) {\n // check if element visible, so that only engines of the current category are modified\n if (engineToggle.offsetParent) {\n engineToggle.checked = !enable;\n }\n }\n};\n\nconst engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\"[data-engine-name]\");\nfor (const engineElement of engineElements) {\n listen(\"mouseenter\", engineElement, loadEngineDescriptions);\n}\n\nconst engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(\n \"tbody input[type=checkbox][class~=checkbox-onoff]\"\n);\n\nconst enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".enable-all-engines\");\nfor (const engine of enableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(true, engineToggles));\n}\n\nconst disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".disable-all-engines\");\nfor (const engine of disableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(false, engineToggles));\n}\n\nlisten(\"click\", \"#copy-hash\", async function (this: HTMLElement) {\n const target = this.parentElement?.querySelector<HTMLPreElement>(\"pre\");\n assertElement(target);\n\n if (window.isSecureContext) {\n await navigator.clipboard.writeText(target.innerText);\n } else {\n const selection = window.getSelection();\n if (selection) {\n const range = document.createRange();\n range.selectNodeContents(target);\n selection.removeAllRanges();\n selection.addRange(range);\n document.execCommand(\"copy\");\n }\n }\n\n if (this.dataset.copiedText) {\n this.innerText = this.dataset.copiedText;\n }\n});\n"],"mappings":"4FAKA,IAAI,EAEE,EAAyB,SAA2B,CACpD,MACJ,IAAI,CAEF,EAAqB,MAAM,MADT,EAAK,MAAO,0BAA0B,GACzB,KAAK,CACtC,OAAS,EAAO,CACd,QAAQ,MAAM,qCAAsC,CAAK,CAC3D,CACK,KAEL,IAAK,GAAM,CAAC,EAAa,CAAC,EAAa,MAAY,OAAO,QAAQ,CAAkB,EAAG,CACrF,IAAM,EAAW,SAAS,iBAA8B,sBAAsB,EAAY,uBAAuB,EAC3G,EAAa,QAAQ,EAAS,cAAc,OAAO,SAAS,EAAO,OAEzE,IAAK,IAAM,KAAW,EACpB,EAAQ,UAAY,EAAc,CAEtC,CAVA,CAWF,EAEM,GAAiB,EAAiB,IAAsD,CAC5F,IAAK,IAAM,KAAgB,EAErB,EAAa,eACf,EAAa,QAAU,CAAC,EAG9B,EAEM,EAA0C,SAAS,iBAA8B,oBAAoB,EAC3G,IAAK,IAAM,KAAiB,EAC1B,EAAO,aAAc,EAAe,CAAsB,EAG5D,IAAM,EAA8C,SAAS,iBAC3D,mDACF,EAEM,EAA4C,SAAS,iBAA8B,qBAAqB,EAC9G,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAM,CAAa,CAAC,EAGlE,IAAM,EAA6C,SAAS,iBAA8B,sBAAsB,EAChH,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAO,CAAa,CAAC,EAGnE,EAAO,QAAS,aAAc,gBAAmC,CAC/D,IAAM,EAAS,KAAK,eAAe,cAA8B,KAAK,EAGtE,GAFA,EAAc,CAAM,EAEhB,OAAO,gBACT,MAAM,UAAU,UAAU,UAAU,EAAO,SAAS,MAC/C,CACL,IAAM,EAAY,OAAO,aAAa,EACtC,GAAI,EAAW,CACb,IAAM,EAAQ,SAAS,YAAY,EACnC,EAAM,mBAAmB,CAAM,EAC/B,EAAU,gBAAgB,EAC1B,EAAU,SAAS,CAAK,EACxB,SAAS,YAAY,MAAM,CAC7B,CACF,CAEI,KAAK,QAAQ,aACf,KAAK,UAAY,KAAK,QAAQ,WAElC,CAAC"}
|
||||
@@ -1,2 +0,0 @@
|
||||
import{t as e}from"./DK4yUVpy.min.js";function t(t,n={}){n.assert??=!0;let r=document.getElementById(t);return n.assert&&e(r),r}export{t};
|
||||
//# sourceMappingURL=DcK-mo-Y.min.js.map
|
||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
import{t as e}from"./DH1EQbEY.min.js";function t(t,n={}){n.assert??=!0;let r=document.getElementById(t);return n.assert&&e(r),r}export{t};
|
||||
//# sourceMappingURL=chlzpS6K.min.js.map
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"DcK-mo-Y.min.js","names":[],"sources":["../../../../../client/simple/src/js/util/getElement.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { assertElement } from \"./assertElement.ts\";\n\ntype Options = {\n assert?: boolean;\n};\n\nexport function getElement<T>(id: string, options?: { assert: true }): T;\nexport function getElement<T>(id: string, options?: { assert: false }): T | null;\nexport function getElement<T>(id: string, options: Options = {}): T | null {\n options.assert ??= true;\n\n const element = document.getElementById(id) as T | null;\n\n if (options.assert) {\n assertElement(element);\n }\n\n return element;\n}\n"],"mappings":"sCAUA,SAAgB,EAAc,EAAY,EAAmB,CAAC,EAAa,CACzE,EAAQ,SAAW,GAEnB,IAAM,EAAU,SAAS,eAAe,CAAE,EAM1C,OAJI,EAAQ,QACV,EAAc,CAAO,EAGhB,CACT"}
|
||||
{"version":3,"file":"chlzpS6K.min.js","names":[],"sources":["../../../../../client/simple/src/js/util/getElement.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { assertElement } from \"./assertElement.ts\";\n\ntype Options = {\n assert?: boolean;\n};\n\nexport function getElement<T>(id: string, options?: { assert: true }): T;\nexport function getElement<T>(id: string, options?: { assert: false }): T | null;\nexport function getElement<T>(id: string, options: Options = {}): T | null {\n options.assert ??= true;\n\n const element = document.getElementById(id) as T | null;\n\n if (options.assert) {\n assertElement(element);\n }\n\n return element;\n}\n"],"mappings":"sCAUA,SAAgB,EAAc,EAAY,EAAmB,CAAC,EAAa,CACzE,EAAQ,SAAW,GAEnB,IAAM,EAAU,SAAS,eAAe,CAAE,EAM1C,OAJI,EAAQ,QACV,EAAc,CAAO,EAGhB,CACT"}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"_DK4yUVpy.min.js": {
|
||||
"file": "chunk/DK4yUVpy.min.js",
|
||||
"_DH1EQbEY.min.js": {
|
||||
"file": "chunk/DH1EQbEY.min.js",
|
||||
"name": "assertelement"
|
||||
},
|
||||
"_DcK-mo-Y.min.js": {
|
||||
"file": "chunk/DcK-mo-Y.min.js",
|
||||
"_chlzpS6K.min.js": {
|
||||
"file": "chunk/chlzpS6K.min.js",
|
||||
"name": "getelement",
|
||||
"imports": [
|
||||
"_DK4yUVpy.min.js"
|
||||
"_DH1EQbEY.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/index.ts": {
|
||||
@@ -27,78 +27,78 @@
|
||||
]
|
||||
},
|
||||
"src/js/main/autocomplete.ts": {
|
||||
"file": "chunk/DvCYLbJr.min.js",
|
||||
"file": "chunk/CQ8vfMdp.min.js",
|
||||
"name": "autocomplete",
|
||||
"src": "src/js/main/autocomplete.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DK4yUVpy.min.js"
|
||||
"_DH1EQbEY.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/main/keyboard.ts": {
|
||||
"file": "chunk/C93hSkpT.min.js",
|
||||
"file": "chunk/aUw47Wy0.min.js",
|
||||
"name": "keyboard",
|
||||
"src": "src/js/main/keyboard.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DK4yUVpy.min.js"
|
||||
"_DH1EQbEY.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/main/preferences.ts": {
|
||||
"file": "chunk/e2-9fzwE.min.js",
|
||||
"file": "chunk/DZidprJh.min.js",
|
||||
"name": "preferences",
|
||||
"src": "src/js/main/preferences.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DK4yUVpy.min.js"
|
||||
"_DH1EQbEY.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/main/results.ts": {
|
||||
"file": "chunk/B8prKeWj.min.js",
|
||||
"file": "chunk/DGJ63wI6.min.js",
|
||||
"name": "results",
|
||||
"src": "src/js/main/results.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DK4yUVpy.min.js"
|
||||
"_DH1EQbEY.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/main/search.ts": {
|
||||
"file": "chunk/5Ako-qGW.min.js",
|
||||
"file": "chunk/BnP4vIuG.min.js",
|
||||
"name": "search",
|
||||
"src": "src/js/main/search.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DcK-mo-Y.min.js"
|
||||
"_chlzpS6K.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/plugin/Calculator.ts": {
|
||||
"file": "chunk/DDL5uWMz.min.js",
|
||||
"file": "chunk/DyePpW7L.min.js",
|
||||
"name": "calculator",
|
||||
"src": "src/js/plugin/Calculator.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DcK-mo-Y.min.js"
|
||||
"_chlzpS6K.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/plugin/InfiniteScroll.ts": {
|
||||
"file": "chunk/DpvWr1cn.min.js",
|
||||
"file": "chunk/Cx4rGXMm.min.js",
|
||||
"name": "infinitescroll",
|
||||
"src": "src/js/plugin/InfiniteScroll.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DK4yUVpy.min.js",
|
||||
"_DcK-mo-Y.min.js"
|
||||
"_DH1EQbEY.min.js",
|
||||
"_chlzpS6K.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/plugin/MapView.ts": {
|
||||
"file": "chunk/U6YV4Y8e.min.js",
|
||||
"file": "chunk/DwAGgYJF.min.js",
|
||||
"name": "mapview",
|
||||
"src": "src/js/plugin/MapView.ts",
|
||||
"isDynamicEntry": true,
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./chunk/U6YV4Y8e.min.js","./sxng-mapview.min.css","./chunk/DpvWr1cn.min.js","./chunk/DK4yUVpy.min.js","./chunk/DcK-mo-Y.min.js","./chunk/DDL5uWMz.min.js","./chunk/C93hSkpT.min.js","./chunk/5Ako-qGW.min.js","./chunk/DvCYLbJr.min.js","./chunk/B8prKeWj.min.js","./chunk/e2-9fzwE.min.js"])))=>i.map(i=>d[i]);
|
||||
var e=class{id;constructor(e){this.id=e,queueMicrotask(()=>this.invoke())}async invoke(){try{console.debug(`[PLUGIN] ${this.id}: Running...`);let e=await this.run();if(!e)return;console.debug(`[PLUGIN] ${this.id}: Running post-exec...`),await this.post(e)}catch(e){console.error(`[PLUGIN] ${this.id}:`,e)}finally{console.debug(`[PLUGIN] ${this.id}: Done.`)}}},t={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},n={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},r=()=>{let e=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return e&&e in t?e:t.unknown},i=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},a=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},o=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},s=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?o(`DOMContentLoaded`,document,e,{once:!0}):e()},c=r(),l=i(),u=(e,t)=>{d(t)&&e()},d=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(c)}},f=`modulepreload`,p=function(e,t){return new URL(e,t).href},m={},h=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=p(t,n),t in m)return;m[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:f,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};s(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),o(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),o(`click`,`.searxng_init_map`,async function(e){e.preventDefault(),this.classList.remove(`searxng_init_map`),u(()=>h(async()=>{let{default:e}=await import(`./chunk/U6YV4Y8e.min.js`);return{default:e}},__vite__mapDeps([0,1]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[t.results]})}),l.plugins?.includes(`infiniteScroll`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DpvWr1cn.min.js`);return{default:e}},__vite__mapDeps([2,3,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]}),l.plugins?.includes(`calculator`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DDL5uWMz.min.js`);return{default:e}},__vite__mapDeps([5,4,3]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]})}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.index]}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/B8prKeWj.min.js`),__vite__mapDeps([9,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.results]}),s(()=>{h(()=>import(`./chunk/e2-9fzwE.min.js`),__vite__mapDeps([10,3]),import.meta.url)},{on:[c===t.preferences]});export{e as a,l as i,o as n,n as r,a as t};
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./chunk/DwAGgYJF.min.js","./sxng-mapview.min.css","./chunk/Cx4rGXMm.min.js","./chunk/DH1EQbEY.min.js","./chunk/chlzpS6K.min.js","./chunk/DyePpW7L.min.js","./chunk/aUw47Wy0.min.js","./chunk/BnP4vIuG.min.js","./chunk/CQ8vfMdp.min.js","./chunk/DGJ63wI6.min.js","./chunk/DZidprJh.min.js"])))=>i.map(i=>d[i]);
|
||||
var e=class{id;constructor(e){this.id=e,queueMicrotask(()=>this.invoke())}async invoke(){try{console.debug(`[PLUGIN] ${this.id}: Running...`);let e=await this.run();if(!e)return;console.debug(`[PLUGIN] ${this.id}: Running post-exec...`),await this.post(e)}catch(e){console.error(`[PLUGIN] ${this.id}:`,e)}finally{console.debug(`[PLUGIN] ${this.id}: Done.`)}}},t={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},n={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},r=()=>{let e=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return e&&e in t?e:t.unknown},i=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},a=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},o=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},s=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?o(`DOMContentLoaded`,document,e,{once:!0}):e()},c=r(),l=i(),u=(e,t)=>{d(t)&&e()},d=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(c)}},f=`modulepreload`,p=function(e,t){return new URL(e,t).href},m={},h=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=p(t,n),t in m)return;m[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:f,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};s(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),o(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),o(`click`,`.searxng_init_map`,async function(e){e.preventDefault(),this.classList.remove(`searxng_init_map`),u(()=>h(async()=>{let{default:e}=await import(`./chunk/DwAGgYJF.min.js`);return{default:e}},__vite__mapDeps([0,1]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[t.results]})}),l.plugins?.includes(`infiniteScroll`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/Cx4rGXMm.min.js`);return{default:e}},__vite__mapDeps([2,3,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]}),l.plugins?.includes(`calculator`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DyePpW7L.min.js`);return{default:e}},__vite__mapDeps([5,4,3]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]})}),s(()=>{h(()=>import(`./chunk/aUw47Wy0.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/BnP4vIuG.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/CQ8vfMdp.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.index]}),s(()=>{h(()=>import(`./chunk/aUw47Wy0.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/DGJ63wI6.min.js`),__vite__mapDeps([9,3]),import.meta.url),h(()=>import(`./chunk/BnP4vIuG.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/CQ8vfMdp.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.results]}),s(()=>{h(()=>import(`./chunk/DZidprJh.min.js`),__vite__mapDeps([10,3]),import.meta.url)},{on:[c===t.preferences]});export{e as a,l as i,o as n,n as r,a as t};
|
||||
//# sourceMappingURL=sxng-core.min.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -180,7 +180,7 @@
|
||||
{%- if 'autocomplete' not in locked_preferences -%}
|
||||
{%- include 'simple/preferences/autocomplete.html' -%}
|
||||
{%- endif -%}
|
||||
{%- if 'favicon_resolver' not in locked_preferences -%}
|
||||
{%- if 'favicon' not in locked_preferences -%}
|
||||
{%- include 'simple/preferences/favicon.html' -%}
|
||||
{%- endif -%}
|
||||
{% if 'safesearch' not in locked_preferences %}
|
||||
|
||||
Binary file not shown.
@@ -17,22 +17,20 @@
|
||||
# yoonhahwang <yoonhahwang@noreply.codeberg.org>, 2025.
|
||||
# choonarine <choonarine@noreply.codeberg.org>, 2025.
|
||||
# pywc <pywc@noreply.codeberg.org>, 2025.
|
||||
# daemul72 <daemul72@noreply.codeberg.org>, 2026.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-05-25 10:44+0000\n"
|
||||
"PO-Revision-Date: 2026-05-31 08:03+0000\n"
|
||||
"Last-Translator: daemul72 <daemul72@noreply.codeberg.org>\n"
|
||||
"Language-Team: Korean <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/ko/>\n"
|
||||
"PO-Revision-Date: 2026-05-19 12:07+0000\n"
|
||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||
"Language: ko\n"
|
||||
"Language-Team: Korean "
|
||||
"<https://translate.codeberg.org/projects/searxng/searxng/ko/>\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 2026.5\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||
@@ -649,7 +647,7 @@ msgstr "인수들의 {func}를 계산하세요"
|
||||
#: searx/engines/boardreader.py:107
|
||||
#, python-brace-format
|
||||
msgid "Posted by {author}"
|
||||
msgstr "작성자: {author}"
|
||||
msgstr ""
|
||||
|
||||
#: searx/engines/openstreetmap.py:155
|
||||
msgid "Show route in map .."
|
||||
@@ -732,11 +730,11 @@ msgstr "Onion 검색 결과중 Ahmia 블랙리스트에 포함된 페이지를
|
||||
|
||||
#: searx/plugins/calculator.py:25
|
||||
msgid "Calculator"
|
||||
msgstr "계산기"
|
||||
msgstr ""
|
||||
|
||||
#: searx/plugins/calculator.py:26
|
||||
msgid "Parses and solves mathematical expressions."
|
||||
msgstr "수식들을 분석하고 풀이합니다."
|
||||
msgstr ""
|
||||
|
||||
#: searx/plugins/hash_plugin.py:33
|
||||
msgid "Hash plugin"
|
||||
@@ -972,16 +970,14 @@ msgid ""
|
||||
"This is a preview of the settings used by the 'Search URL' you used to "
|
||||
"get here."
|
||||
msgstr ""
|
||||
"이는 사용자가 이 페이지에 접속하기 위해 사용한 '검색 URL'에서 사용되는 "
|
||||
"설정의 미리보기입니다."
|
||||
|
||||
#: searx/templates/simple/preferences.html:158
|
||||
msgid "Press save to copy these preferences to your browser."
|
||||
msgstr "이 설정을 브라우저에 복사하려면 '저장'을 누르세요."
|
||||
msgstr ""
|
||||
|
||||
#: searx/templates/simple/preferences.html:159
|
||||
msgid "Click here to view your browser preferences instead:"
|
||||
msgstr "브라우저 설정을 확인하려면 여기를 클릭하세요:"
|
||||
msgstr ""
|
||||
|
||||
#: searx/templates/simple/preferences.html:169
|
||||
msgid "General"
|
||||
@@ -2196,3 +2192,4 @@ msgstr "비디오 숨기기"
|
||||
|
||||
#~ msgid "Number of results"
|
||||
#~ msgstr "결과 수"
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Utility functions for the engines"""
|
||||
|
||||
import time
|
||||
|
||||
import re
|
||||
import importlib
|
||||
@@ -810,12 +809,3 @@ def parse_duration_string(duration_str: str) -> timedelta | None:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Format the video duration
|
||||
def format_duration(duration: str | int) -> str:
|
||||
seconds = int(duration)
|
||||
length = time.gmtime(seconds)
|
||||
if length.tm_hour:
|
||||
return time.strftime("%H:%M:%S", length)
|
||||
return time.strftime("%M:%S", length)
|
||||
|
||||
+6
-6
@@ -8,7 +8,7 @@ from searx.webutils import VALID_LANGUAGE_CODE
|
||||
from searx.query import RawTextQuery
|
||||
from searx.engines import categories, engines
|
||||
from searx.search.models import SearchQuery, EngineRef
|
||||
from searx.preferences import Preferences
|
||||
from searx.preferences import Preferences, is_locked
|
||||
|
||||
|
||||
# remove duplicate queries.
|
||||
@@ -53,7 +53,7 @@ def parse_pageno(form: Dict[str, str]) -> int:
|
||||
|
||||
|
||||
def parse_lang(preferences: Preferences, form: Dict[str, str], raw_text_query: RawTextQuery) -> str:
|
||||
if "language" in preferences.cfg.lock:
|
||||
if is_locked('language'):
|
||||
return preferences.get_value('language')
|
||||
# get language
|
||||
# set specific language if set on request, query or preferences
|
||||
@@ -73,7 +73,7 @@ def parse_lang(preferences: Preferences, form: Dict[str, str], raw_text_query: R
|
||||
|
||||
|
||||
def parse_safesearch(preferences: Preferences, form: Dict[str, str]) -> int:
|
||||
if "safesearch" in preferences.cfg.lock:
|
||||
if is_locked('safesearch'):
|
||||
return preferences.get_value('safesearch')
|
||||
|
||||
if 'safesearch' in form:
|
||||
@@ -135,7 +135,7 @@ def parse_category_form(query_categories: List[str], name: str, value: str) -> N
|
||||
def get_selected_categories(preferences: Preferences, form: Optional[Dict[str, str]]) -> List[str]:
|
||||
selected_categories = []
|
||||
|
||||
if not "categories" in preferences.cfg.lock and form is not None:
|
||||
if not is_locked('categories') and form is not None:
|
||||
for name, value in form.items():
|
||||
parse_category_form(selected_categories, name, value)
|
||||
|
||||
@@ -175,7 +175,7 @@ def parse_generic(preferences: Preferences, form: Dict[str, str], disabled_engin
|
||||
|
||||
# set categories/engines
|
||||
explicit_engine_list = False
|
||||
if not "categories" in preferences.cfg.lock:
|
||||
if not is_locked('categories'):
|
||||
# parse the form only if the categories are not locked
|
||||
for pd_name, pd in form.items(): # pylint: disable=invalid-name
|
||||
if pd_name == 'engines':
|
||||
@@ -266,7 +266,7 @@ def get_search_query_from_webapp(
|
||||
if query_lang == 'auto':
|
||||
query_lang = preferences.client.locale_tag or 'all'
|
||||
|
||||
if not "categories" in preferences.cfg.lock and raw_text_query.specific:
|
||||
if not is_locked('categories') and raw_text_query.specific:
|
||||
# if engines are calculated from query,
|
||||
# set categories by using that information
|
||||
query_engineref_list = raw_text_query.enginerefs
|
||||
|
||||
+3
-15
@@ -377,6 +377,7 @@ def get_client_settings():
|
||||
'theme_static_path': custom_url_for('static', filename='themes/simple'),
|
||||
'results_on_new_tab': req_pref.get_value('results_on_new_tab'),
|
||||
'favicon_resolver': req_pref.get_value('favicon_resolver'),
|
||||
'advanced_search': req_pref.get_value('advanced_search'),
|
||||
'query_in_title': req_pref.get_value('query_in_title'),
|
||||
'safesearch': req_pref.get_value('safesearch'),
|
||||
'theme': req_pref.get_value('theme'),
|
||||
@@ -976,7 +977,7 @@ def preferences():
|
||||
current_doi_resolver = get_doi_resolver(),
|
||||
allowed_plugins = allowed_plugins,
|
||||
preferences_url_params = sxng_request.preferences.get_as_url_params(),
|
||||
locked_preferences = get_setting("preferences").lock,
|
||||
locked_preferences = get_setting("preferences.lock", []),
|
||||
doi_resolvers = get_setting("doi_resolvers", {}),
|
||||
# fmt: on
|
||||
)
|
||||
@@ -1348,8 +1349,6 @@ def run():
|
||||
|
||||
def init():
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
|
||||
if searx.sxng_debug or app.debug:
|
||||
app.debug = True
|
||||
searx.sxng_debug = True
|
||||
@@ -1360,18 +1359,6 @@ def init():
|
||||
logger.error("server.secret_key is not changed. Please use something else instead of ultrasecretkey.")
|
||||
sys.exit(1)
|
||||
|
||||
# init database schema first / DB schema is created with the first connect
|
||||
from searx.data import get_cache
|
||||
from searx.enginelib import ENGINES_CACHE
|
||||
|
||||
conn = get_cache().connect()
|
||||
conn.close()
|
||||
conn = ENGINES_CACHE.connect()
|
||||
conn.close()
|
||||
|
||||
favicons.init()
|
||||
|
||||
# init application
|
||||
locales_initialize()
|
||||
valkey_initialize()
|
||||
searx.plugins.initialize(app)
|
||||
@@ -1380,6 +1367,7 @@ def init():
|
||||
searx.search.initialize(check_network=True, enable_metrics=metrics)
|
||||
|
||||
limiter.initialize(app, settings)
|
||||
favicons.init()
|
||||
|
||||
|
||||
def static_headers(headers: Headers, _path: str, _url: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user