mirror of
https://github.com/searxng/searxng.git
synced 2026-06-22 01:28:31 +02:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23ab3bef91 | |||
| ab81c77533 | |||
| cc196f2a5b | |||
| dd3022d680 | |||
| de8a3de15a | |||
| 4dd0bf4867 | |||
| 1957876dd6 | |||
| ab13451086 | |||
| a1490676e3 | |||
| 3a382cb3f3 | |||
| 9d9d605b15 | |||
| de03f4eb11 | |||
| 00f7c68a6f | |||
| 41c98b3b41 | |||
| f4c63c8eb0 | |||
| 26801e92af | |||
| f3fab143be | |||
| 72a827ae93 | |||
| 6ca9d3784c | |||
| 63f264220b | |||
| 41fcf0be4b | |||
| 86903a2c66 | |||
| 70de3cc561 | |||
| 51b6fd4f23 | |||
| 9d49a9f344 | |||
| e260a732c8 | |||
| 0429198415 | |||
| e7cf57e9ae | |||
| ed369ac0ec | |||
| 94bdbb5c63 | |||
| 465b5229c6 | |||
| cbf97fd262 | |||
| 37187dc2d8 | |||
| 2f049cb037 | |||
| eb39bc0dc1 | |||
| 007a4e2155 | |||
| 13ce187e64 | |||
| 26fa181b84 | |||
| 0f35ef7cd6 | |||
| b1ae576b2d | |||
| e6559c9ad6 | |||
| 5bae05514b | |||
| 00ca5776f2 | |||
| 577f5f2f30 | |||
| 253dc86c10 | |||
| 3066bc19eb | |||
| e964708c00 | |||
| 7159b8aed3 | |||
| 246f5a5499 | |||
| 300695de5c | |||
| bd863f16b1 | |||
| 4ac822fd7f | |||
| e1d25c5078 |
@@ -1,163 +0,0 @@
|
||||
;;; .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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
fetch-depth: "0"
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
fetch-depth: "0"
|
||||
@@ -50,11 +50,14 @@ 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.clean docs.html
|
||||
run: make V=1 docs.html
|
||||
|
||||
- if: github.ref_name == 'master'
|
||||
name: Release
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
||||
fetch-depth: "0"
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
||||
fetch-depth: "0"
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
@@ -41,6 +41,6 @@ jobs:
|
||||
write-comment: "false"
|
||||
|
||||
- name: Upload SARIFs
|
||||
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
sarif_file: "./scout.sarif"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[[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
|
||||
+15
-29
@@ -2,12 +2,12 @@
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": ["**", "!node_modules"]
|
||||
"includes": ["**", "!node_modules", "!src/brand", "!src/svg"]
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"recommended": true,
|
||||
"preset": "recommended",
|
||||
"source": {
|
||||
"useSortedAttributes": "on",
|
||||
"useSortedProperties": "on"
|
||||
@@ -27,12 +27,14 @@
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"preset": "recommended",
|
||||
"complexity": {
|
||||
"noForEach": "error",
|
||||
"noImplicitCoercions": "error",
|
||||
"noRedundantDefaultExport": "error",
|
||||
"noUselessCatchBinding": "error",
|
||||
"noUselessUndefined": "error",
|
||||
"useArrayFind": "error",
|
||||
"useSimplifiedLogicExpression": "error"
|
||||
},
|
||||
"correctness": {
|
||||
@@ -42,25 +44,11 @@
|
||||
"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": {
|
||||
@@ -75,23 +63,15 @@
|
||||
"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",
|
||||
@@ -107,6 +87,7 @@
|
||||
}
|
||||
},
|
||||
"useConsistentBuiltinInstantiation": "error",
|
||||
"useConsistentEnumValueType": "error",
|
||||
"useConsistentMemberAccessibility": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
@@ -126,6 +107,7 @@
|
||||
}
|
||||
},
|
||||
"useDefaultSwitchClause": "error",
|
||||
"useDestructuring": "error",
|
||||
"useExplicitLengthCheck": "error",
|
||||
"useForOf": "error",
|
||||
"useGroupedAccessorPairs": "error",
|
||||
@@ -142,13 +124,17 @@
|
||||
"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
+363
-315
File diff suppressed because it is too large
Load Diff
@@ -29,21 +29,21 @@
|
||||
"swiped-events": "1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.15",
|
||||
"@types/node": "^25.8.0",
|
||||
"@biomejs/biome": "2.5.0",
|
||||
"@types/node": "^25.9.3",
|
||||
"browserslist": "^4.28.2",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"edge.js": "^6.5.0",
|
||||
"edge.js": "^6.5.1",
|
||||
"less": "^4.6.4",
|
||||
"mathjs": "^15.2.0",
|
||||
"sharp": "~0.34.5",
|
||||
"sort-package-json": "^3.6.1",
|
||||
"stylelint": "^17.11.1",
|
||||
"sharp": "~0.35.1",
|
||||
"sort-package-json": "^4.0.0",
|
||||
"stylelint": "^17.13.0",
|
||||
"stylelint-config-standard-less": "^4.1.0",
|
||||
"stylelint-prettier": "^5.0.3",
|
||||
"svgo": "^4.0.1",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "^8.0.13",
|
||||
"vite": "^8.0.16",
|
||||
"vite-bundle-analyzer": "^1.3.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ 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,6 +19,7 @@ Settings
|
||||
settings_search
|
||||
settings_server
|
||||
settings_ui
|
||||
settings_preferences
|
||||
settings_redis
|
||||
settings_valkey
|
||||
settings_outgoing
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.. _settings preferences:
|
||||
|
||||
================
|
||||
``preferences:``
|
||||
================
|
||||
|
||||
.. autoclass:: searx._settings.SettingsPref
|
||||
:members:
|
||||
@@ -47,6 +47,7 @@
|
||||
activated:
|
||||
|
||||
- :py:obj:`searx.botdetection.link_token` in the :ref:`limiter`
|
||||
- :ref:`image_proxy`
|
||||
|
||||
.. _image_proxy:
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.. _kagi engines:
|
||||
|
||||
============
|
||||
Kagi Engines
|
||||
============
|
||||
|
||||
.. automodule:: searx.engines.kagi
|
||||
:members:
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.. _karmasearch engine:
|
||||
|
||||
===========
|
||||
Karmasearch
|
||||
===========
|
||||
|
||||
.. automodule:: searx.engines.karmasearch
|
||||
:members:
|
||||
@@ -20,9 +20,9 @@ aiounittest==1.5.0
|
||||
yamllint==1.38.0
|
||||
wlc==2.0.0
|
||||
coloredlogs==15.0.1
|
||||
docutils>=0.21.2;python_version <= "3.11"
|
||||
docutils>=0.23;python_version <= "3.11"
|
||||
docutils>=0.22.4; python_version > "3.11"
|
||||
parameterized==0.9.0
|
||||
granian[reload]==2.7.5
|
||||
basedpyright==1.39.6
|
||||
granian[reload]==2.7.6
|
||||
basedpyright==1.39.7
|
||||
types-lxml==2026.2.16
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
granian==2.7.5
|
||||
granian[pname]==2.7.5
|
||||
granian==2.7.6
|
||||
granian[pname]==2.7.6
|
||||
|
||||
+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.3
|
||||
typer==0.26.7
|
||||
isodate==0.7.2
|
||||
whitenoise==6.12.0
|
||||
typing-extensions==4.15.0
|
||||
|
||||
+8
-1
@@ -10,6 +10,7 @@ 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'
|
||||
@@ -47,6 +48,12 @@ 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()
|
||||
@@ -66,7 +73,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 and link_token / "
|
||||
"This force the usage of the limiter, link_token and image proxy / "
|
||||
"see https://docs.searxng.org/admin/searx.limiter.html"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# 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."""
|
||||
+8
-5
@@ -444,12 +444,10 @@ 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
|
||||
table), the ``default`` value is returned.
|
||||
|
||||
from the :py:obj:`ExpireCacheCfg.name`. If ``key`` not exists in
|
||||
the table or the table not exists, the ``default`` value is returned.
|
||||
"""
|
||||
table = ctx
|
||||
self.maintenance()
|
||||
|
||||
if not table:
|
||||
table = self.normalize_name(self.cfg.name)
|
||||
@@ -457,6 +455,9 @@ 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:
|
||||
@@ -469,12 +470,14 @@ 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,186 +5740,6 @@
|
||||
"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,6 +12,7 @@ import typing as t
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import os
|
||||
from os.path import realpath, dirname
|
||||
|
||||
import types
|
||||
@@ -278,6 +279,8 @@ 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("loading engine %s failed: set engine to inactive!", engine_data.get("name", "???"))
|
||||
logger.error(
|
||||
f"(PID {os.getpid()}) 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": "Q2407",
|
||||
"wikidata_id": "Q27585",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
|
||||
@@ -51,11 +51,10 @@ def request(query, params):
|
||||
}
|
||||
|
||||
params["url"] = f"{base_url}?{urlencode(query_params)}"
|
||||
params["headers"]["Referer"] = "https://www.bilibili.com"
|
||||
params["headers"]["Referer"] = "https://www.bilibili.com/"
|
||||
params["headers"]["Accept"] = "application/json, text/javascript, */*; q=0.01"
|
||||
params["cookies"] = cookie
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
search_res = resp.json()
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# 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,7 +41,9 @@ 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]):
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
# 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
|
||||
@@ -0,0 +1,169 @@
|
||||
# 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,10 +53,13 @@ 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([tag["tag"] for tag in result["tags"]]), # pyright: ignore[reportArgumentType]
|
||||
content=", ".join(tags),
|
||||
url=_fix_url(result["slug"]),
|
||||
thumbnail_src=_fix_url(result["png"]),
|
||||
img_src=_fix_url(result["png512"]),
|
||||
|
||||
@@ -10,10 +10,12 @@ 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
|
||||
from searx.utils import extr, gen_useragent, html_to_text, eval_xpath
|
||||
from searx.network import get
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
@@ -40,6 +42,11 @@ 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=")
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# 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,6 +20,7 @@ Paging:
|
||||
- :py:obj:`paging`
|
||||
- :py:obj:`page_size`
|
||||
- :py:obj:`first_page_num`
|
||||
- :py:obj:`send_page_num_on_first_page`
|
||||
|
||||
Time Range:
|
||||
|
||||
@@ -169,6 +170,10 @@ 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.
|
||||
|
||||
@@ -322,10 +327,13 @@ 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': (params['pageno'] - 1) * page_size + first_page_num,
|
||||
'pageno': pageno,
|
||||
'time_range': time_range,
|
||||
'safe_search': safe_search,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
# 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
|
||||
@@ -1,205 +0,0 @@
|
||||
# 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 = False
|
||||
paging = True
|
||||
results_per_page = 20
|
||||
api_key = None
|
||||
"""To get an API key, please follow the instructions from `Key and license`_
|
||||
@@ -85,7 +85,12 @@ class ApiSearchResults(t.TypedDict):
|
||||
|
||||
def request(query: str, params: dict[str, t.Any]):
|
||||
|
||||
query_params = {"count": results_per_page, "nsfw": min(params["safesearch"], 1), "query": query}
|
||||
query_params = {
|
||||
"page": params["pageno"],
|
||||
"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()
|
||||
|
||||
+1
-10
@@ -4,7 +4,6 @@
|
||||
.. _Odysee: https://github.com/OdyseeTeam/odysee-frontend
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlencode
|
||||
|
||||
@@ -12,6 +11,7 @@ import babel
|
||||
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.locales import language_tag
|
||||
from searx.utils import format_duration
|
||||
|
||||
# Engine metadata
|
||||
about = {
|
||||
@@ -61,15 +61,6 @@ 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,6 +1,8 @@
|
||||
# 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
|
||||
|
||||
@@ -49,6 +51,8 @@ paging = True
|
||||
|
||||
__CACHED_API_URL = None
|
||||
|
||||
_API_URL_RE = re.compile(r"\"(https://.*?/search-proxy)\"")
|
||||
|
||||
|
||||
def _clean_url(url):
|
||||
parsed = urlparse(url)
|
||||
@@ -74,11 +78,12 @@ def _get_algolia_api_url():
|
||||
if resp.status_code != 200:
|
||||
raise LookupError("Failed to obtain AWS api url for PDImageArchive")
|
||||
|
||||
api_url = extr(resp.text, 'const r="', '"', default=None)
|
||||
|
||||
if api_url is None:
|
||||
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 = api_url_match.group(1)
|
||||
|
||||
__CACHED_API_URL = api_url
|
||||
return api_url
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import socket
|
||||
from urllib.parse import urlencode
|
||||
@@ -59,7 +60,19 @@ seconds."""
|
||||
def init(_):
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache("radio_browser")
|
||||
server_list()
|
||||
|
||||
# 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]:
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# 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
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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
|
||||
@@ -0,0 +1,287 @@
|
||||
# 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
|
||||
@@ -0,0 +1,83 @@
|
||||
# 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
|
||||
@@ -0,0 +1,167 @@
|
||||
# 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,6 +7,7 @@ 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
|
||||
@@ -827,7 +828,19 @@ def debug_explain_wikidata_query(query: str, method: str = "GET"):
|
||||
def init(_):
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache("wikidata")
|
||||
init_wikidata_properties()
|
||||
|
||||
# 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():
|
||||
|
||||
+10
-1
@@ -22,6 +22,7 @@ Paging:
|
||||
- :py:obj:`paging`
|
||||
- :py:obj:`page_size`
|
||||
- :py:obj:`first_page_num`
|
||||
- :py:obj:`send_page_num_on_first_page`
|
||||
|
||||
Time Range:
|
||||
|
||||
@@ -174,6 +175,10 @@ 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.'''
|
||||
|
||||
@@ -238,10 +243,14 @@ 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': (params['pageno'] - 1) * page_size + first_page_num,
|
||||
'pageno': pageno,
|
||||
'time_range': time_range,
|
||||
'safe_search': safe_search,
|
||||
}
|
||||
|
||||
+27
-20
@@ -16,17 +16,18 @@ 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
|
||||
|
||||
@@ -36,34 +37,42 @@ _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 = traits.get_language(params["searxng_locale"])
|
||||
engine_language: str | None = traits.get_language(params["searxng_locale"])
|
||||
if engine_language:
|
||||
args["hl"] = engine_language
|
||||
|
||||
params['url'] = f"{base_url}/search?{urlencode(args)}"
|
||||
params['headers']['Referer'] = 'https://yep.com/'
|
||||
params['headers']['Origin'] = 'https://yep.com'
|
||||
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",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def response(resp: 'SXNG_Response') -> EngineResults:
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
res = EngineResults()
|
||||
|
||||
for result in resp.json()[1]['results']:
|
||||
result: dict[str, str]
|
||||
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.
|
||||
|
||||
@@ -83,8 +92,6 @@ 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}/",
|
||||
|
||||
+52
-63
@@ -17,13 +17,14 @@ import babel.core
|
||||
|
||||
import searx.plugins
|
||||
|
||||
from searx import settings, autocomplete, favicons
|
||||
from searx import get_setting, 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'])
|
||||
@@ -386,6 +387,7 @@ class ClientPref:
|
||||
return cls(locale=locale)
|
||||
|
||||
|
||||
@t.final
|
||||
class Preferences:
|
||||
"""Validates and saves preferences to cookies"""
|
||||
|
||||
@@ -400,95 +402,91 @@ class Preferences:
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.cfg: SettingsPref = get_setting("preferences")
|
||||
|
||||
self.key_value_settings: dict[str, Setting] = {
|
||||
# fmt: off
|
||||
'categories': MultipleChoiceSetting(
|
||||
['general'],
|
||||
locked=is_locked('categories'),
|
||||
choices=categories + ['none']
|
||||
["general"],
|
||||
locked="categories" in self.cfg.lock,
|
||||
choices=categories + ["none"],
|
||||
),
|
||||
'language': SearchLanguageSetting(
|
||||
settings['search']['default_lang'],
|
||||
locked=is_locked('language'),
|
||||
choices=settings['search']['languages'] + ['']
|
||||
get_setting("search.default_lang"),
|
||||
locked="language" in self.cfg.lock,
|
||||
choices=get_setting("search.languages") + [""],
|
||||
),
|
||||
'locale': EnumStringSetting(
|
||||
settings['ui']['default_locale'],
|
||||
locked=is_locked('locale'),
|
||||
choices=list(LOCALE_NAMES.keys()) + ['']
|
||||
get_setting("ui.default_locale"),
|
||||
locked="locale" in self.cfg.lock,
|
||||
choices=list(LOCALE_NAMES.keys()) + [""],
|
||||
),
|
||||
'autocomplete': EnumStringSetting(
|
||||
settings['search']['autocomplete'],
|
||||
locked=is_locked('autocomplete'),
|
||||
choices=list(autocomplete.backends.keys()) + ['']
|
||||
get_setting("search.autocomplete"),
|
||||
locked="autocomplete" in self.cfg.lock,
|
||||
choices=list(autocomplete.backends.keys()) + [""],
|
||||
),
|
||||
'favicon_resolver': EnumStringSetting(
|
||||
settings['search']['favicon_resolver'],
|
||||
locked=is_locked('favicon_resolver'),
|
||||
choices=list(favicons.proxy.CFG.resolver_map.keys()) + ['']
|
||||
get_setting("search.favicon_resolver"),
|
||||
locked="favicon_resolver" in self.cfg.lock,
|
||||
choices=list(favicons.proxy.CFG.resolver_map.keys()) + [''],
|
||||
),
|
||||
'image_proxy': BooleanSetting(
|
||||
settings['server']['image_proxy'],
|
||||
locked=is_locked('image_proxy')
|
||||
get_setting("server.image_proxy"),
|
||||
locked="image_proxy" in self.cfg.lock,
|
||||
),
|
||||
'method': EnumStringSetting(
|
||||
settings['server']['method'],
|
||||
locked=is_locked('method'),
|
||||
choices=('GET', 'POST')
|
||||
get_setting("server.method"),
|
||||
locked="method" in self.cfg.lock,
|
||||
choices=("GET", "POST"),
|
||||
),
|
||||
'safesearch': MapSetting(
|
||||
settings['search']['safe_search'],
|
||||
locked=is_locked('safesearch'),
|
||||
get_setting("search.safe_search"),
|
||||
locked="safesearch" in self.cfg.lock,
|
||||
map={
|
||||
'0': 0,
|
||||
'1': 1,
|
||||
'2': 2
|
||||
}
|
||||
"0": 0,
|
||||
"1": 1,
|
||||
"2": 2,
|
||||
},
|
||||
),
|
||||
'theme': EnumStringSetting(
|
||||
settings['ui']['default_theme'],
|
||||
locked=is_locked('theme'),
|
||||
choices=themes
|
||||
get_setting("ui.default_theme"),
|
||||
locked="theme" in self.cfg.lock,
|
||||
choices=themes,
|
||||
),
|
||||
'results_on_new_tab': BooleanSetting(
|
||||
settings['ui']['results_on_new_tab'],
|
||||
locked=is_locked('results_on_new_tab')
|
||||
get_setting("ui.results_on_new_tab"),
|
||||
locked="results_on_new_tab" in self.cfg.lock,
|
||||
),
|
||||
'doi_resolver': MultipleChoiceSetting(
|
||||
[settings['default_doi_resolver'], ],
|
||||
locked=is_locked('doi_resolver'),
|
||||
choices=DOI_RESOLVERS
|
||||
[get_setting("default_doi_resolver")],
|
||||
locked="doi_resolver" in self.cfg.lock,
|
||||
choices=DOI_RESOLVERS,
|
||||
),
|
||||
'simple_style': EnumStringSetting(
|
||||
settings['ui']['theme_args']['simple_style'],
|
||||
locked=is_locked('simple_style'),
|
||||
choices=['', 'auto', 'light', 'dark', 'black']
|
||||
get_setting("ui.theme_args.simple_style"),
|
||||
locked="simple_style" in self.cfg.lock,
|
||||
choices=["", "auto", "light", "dark", "black"],
|
||||
),
|
||||
'center_alignment': BooleanSetting(
|
||||
settings['ui']['center_alignment'],
|
||||
locked=is_locked('center_alignment')
|
||||
),
|
||||
'advanced_search': BooleanSetting(
|
||||
settings['ui']['advanced_search'],
|
||||
locked=is_locked('advanced_search')
|
||||
get_setting("ui.center_alignment"),
|
||||
locked="center_alignment" in self.cfg.lock,
|
||||
),
|
||||
'query_in_title': BooleanSetting(
|
||||
settings['ui']['query_in_title'],
|
||||
locked=is_locked('query_in_title')
|
||||
get_setting("ui.query_in_title"),
|
||||
locked="query_in_title" in self.cfg.lock,
|
||||
),
|
||||
'search_on_category_select': BooleanSetting(
|
||||
settings['ui']['search_on_category_select'],
|
||||
locked=is_locked('search_on_category_select')
|
||||
get_setting("ui.search_on_category_select"),
|
||||
locked="search_on_category_select" in self.cfg.lock,
|
||||
),
|
||||
'hotkeys': EnumStringSetting(
|
||||
settings['ui']['hotkeys'],
|
||||
choices=['default', 'vim']
|
||||
get_setting("ui.hotkeys"),
|
||||
choices=["default", "vim"],
|
||||
),
|
||||
'url_formatting': EnumStringSetting(
|
||||
settings['ui']['url_formatting'],
|
||||
choices=['pretty', 'full', 'host']
|
||||
get_setting("ui.url_formatting"),
|
||||
choices=["pretty", "full", "host"],
|
||||
),
|
||||
# fmt: on
|
||||
}
|
||||
|
||||
self.engines = EnginesSetting('engines', engines=engines.values())
|
||||
@@ -597,12 +595,3 @@ 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,6 +3,7 @@
|
||||
|
||||
import typing as t
|
||||
|
||||
import os
|
||||
import logging
|
||||
import threading
|
||||
from abc import abstractmethod, ABC
|
||||
@@ -154,7 +155,9 @@ class EngineProcessor(ABC):
|
||||
try:
|
||||
init_ok = self.engine.init(eng_setting)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.exception("Init method of engine %s failed due to an exception.", self.engine.name)
|
||||
logger.exception(
|
||||
f"(PID {os.getpid()}) 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,11 +152,13 @@ class OnlineProcessor(EngineProcessor):
|
||||
# add Accept-Language header
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language
|
||||
|
||||
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"
|
||||
if self.engine.send_accept_language_header:
|
||||
if 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
|
||||
|
||||
+194
-40
@@ -154,14 +154,13 @@ ui:
|
||||
# URL formatting: pretty, full or host
|
||||
url_formatting: pretty
|
||||
|
||||
# Lock arbitrary settings on the preferences page.
|
||||
#
|
||||
# preferences:
|
||||
# lock:
|
||||
preferences:
|
||||
# Lock arbitrary settings on the preferences page.
|
||||
lock: []
|
||||
# - categories
|
||||
# - language
|
||||
# - autocomplete
|
||||
# - favicon
|
||||
# - favicon_resolver
|
||||
# - safesearch
|
||||
# - method
|
||||
# - doi_resolver
|
||||
@@ -804,10 +803,45 @@ 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]
|
||||
@@ -890,6 +924,27 @@ 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
|
||||
@@ -965,6 +1020,22 @@ 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
|
||||
@@ -1116,6 +1187,22 @@ 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
|
||||
@@ -1207,41 +1294,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:
|
||||
@@ -1974,6 +2061,14 @@ 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
|
||||
@@ -2068,6 +2163,22 @@ 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
|
||||
@@ -2525,6 +2636,38 @@ 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
|
||||
@@ -2549,12 +2692,6 @@ engines:
|
||||
results: HTML
|
||||
language: de
|
||||
|
||||
- name: svgrepo
|
||||
engine: svgrepo
|
||||
shortcut: svg
|
||||
timeout: 10.0
|
||||
disabled: true
|
||||
|
||||
- name: tootfinder
|
||||
engine: tootfinder
|
||||
shortcut: toot
|
||||
@@ -2600,6 +2737,23 @@ 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,6 +15,7 @@ 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__))
|
||||
|
||||
@@ -146,6 +147,8 @@ 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:
|
||||
@@ -236,16 +239,13 @@ 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': {
|
||||
'lock': SettingsValue(list, []),
|
||||
},
|
||||
"preferences": SettingsPref,
|
||||
'outgoing': {
|
||||
'useragent_suffix': SettingsValue(str, ''),
|
||||
'request_timeout': SettingsValue(numbers.Real, 3.0),
|
||||
|
||||
+16
-7
@@ -121,8 +121,8 @@ class SQLiteAppl(abc.ABC):
|
||||
|
||||
.. _WAL: https://sqlite.org/wal.html
|
||||
"""
|
||||
SQLITE_CONNECT_ARGS: dict[str,str|int|bool|None] = {
|
||||
# "timeout": 5.0,
|
||||
SQLITE_CONNECT_ARGS: dict[str, str | float | int | bool | None] = {
|
||||
"timeout": 3.0, # default is 5sec
|
||||
# "detect_types": 0,
|
||||
"check_same_thread": bool(SQLITE_THREADING_MODE != "serialized"),
|
||||
"cached_statements": 0, # https://github.com/python/cpython/issues/118172
|
||||
@@ -195,6 +195,7 @@ 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)
|
||||
|
||||
@@ -209,7 +210,7 @@ class SQLiteAppl(abc.ABC):
|
||||
def _compatibility(self):
|
||||
|
||||
if self.SQLITE_THREADING_MODE == "serialized":
|
||||
self._DB: sqlite3.Connection | None = None
|
||||
self._DB = None
|
||||
else:
|
||||
msg = (
|
||||
f"SQLite library is compiled with {self.SQLITE_THREADING_MODE} mode,"
|
||||
@@ -228,7 +229,13 @@ class SQLiteAppl(abc.ABC):
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
conn = sqlite3.Connection(self.db_url, **self.SQLITE_CONNECT_ARGS) # type: ignore
|
||||
conn.execute(f"PRAGMA journal_mode={self.SQLITE_JOURNAL_MODE}")
|
||||
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
|
||||
|
||||
@@ -312,7 +319,8 @@ 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.
|
||||
self.init(conn)
|
||||
with conn:
|
||||
self.init(conn)
|
||||
|
||||
return conn
|
||||
|
||||
@@ -330,7 +338,8 @@ class SQLiteAppl(abc.ABC):
|
||||
self._init_done = True
|
||||
|
||||
logger.debug("init DB: %s", self.db_url)
|
||||
self.properties.init(conn)
|
||||
with conn:
|
||||
self.properties.init(conn)
|
||||
|
||||
ver = self.properties("DB_SCHEMA")
|
||||
if ver is None:
|
||||
@@ -409,7 +418,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 be created
|
||||
if res.fetchone() is None: # DB schema needs to 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"./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
|
||||
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
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"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"}
|
||||
{"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"}
|
||||
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"./DH1EQbEY.min.js";
|
||||
import{i as e,n as t,r as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.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"./DH1EQ
|
||||
* @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=DGJ63wI6.min.js.map
|
||||
//# sourceMappingURL=B8prKeWj.min.js.map
|
||||
+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
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
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
||||
var e=e=>{if(!e)throw Error(`DOM element not found`)};export{e as t};
|
||||
//# sourceMappingURL=DH1EQbEY.min.js.map
|
||||
//# sourceMappingURL=DK4yUVpy.min.js.map
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"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"}
|
||||
{"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"}
|
||||
@@ -0,0 +1,2 @@
|
||||
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
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"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"}
|
||||
{"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"}
|
||||
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"./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
|
||||
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
|
||||
+1
-1
File diff suppressed because one or more lines are too long
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"./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
|
||||
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
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
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
|
||||
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"./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
|
||||
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
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"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"}
|
||||
{"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"}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"_DH1EQbEY.min.js": {
|
||||
"file": "chunk/DH1EQbEY.min.js",
|
||||
"_DK4yUVpy.min.js": {
|
||||
"file": "chunk/DK4yUVpy.min.js",
|
||||
"name": "assertelement"
|
||||
},
|
||||
"_chlzpS6K.min.js": {
|
||||
"file": "chunk/chlzpS6K.min.js",
|
||||
"_DcK-mo-Y.min.js": {
|
||||
"file": "chunk/DcK-mo-Y.min.js",
|
||||
"name": "getelement",
|
||||
"imports": [
|
||||
"_DH1EQbEY.min.js"
|
||||
"_DK4yUVpy.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/index.ts": {
|
||||
@@ -27,78 +27,78 @@
|
||||
]
|
||||
},
|
||||
"src/js/main/autocomplete.ts": {
|
||||
"file": "chunk/CQ8vfMdp.min.js",
|
||||
"file": "chunk/DvCYLbJr.min.js",
|
||||
"name": "autocomplete",
|
||||
"src": "src/js/main/autocomplete.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DH1EQbEY.min.js"
|
||||
"_DK4yUVpy.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/main/keyboard.ts": {
|
||||
"file": "chunk/aUw47Wy0.min.js",
|
||||
"file": "chunk/C93hSkpT.min.js",
|
||||
"name": "keyboard",
|
||||
"src": "src/js/main/keyboard.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DH1EQbEY.min.js"
|
||||
"_DK4yUVpy.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/main/preferences.ts": {
|
||||
"file": "chunk/DZidprJh.min.js",
|
||||
"file": "chunk/e2-9fzwE.min.js",
|
||||
"name": "preferences",
|
||||
"src": "src/js/main/preferences.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DH1EQbEY.min.js"
|
||||
"_DK4yUVpy.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/main/results.ts": {
|
||||
"file": "chunk/DGJ63wI6.min.js",
|
||||
"file": "chunk/B8prKeWj.min.js",
|
||||
"name": "results",
|
||||
"src": "src/js/main/results.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DH1EQbEY.min.js"
|
||||
"_DK4yUVpy.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/main/search.ts": {
|
||||
"file": "chunk/BnP4vIuG.min.js",
|
||||
"file": "chunk/5Ako-qGW.min.js",
|
||||
"name": "search",
|
||||
"src": "src/js/main/search.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_chlzpS6K.min.js"
|
||||
"_DcK-mo-Y.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/plugin/Calculator.ts": {
|
||||
"file": "chunk/DyePpW7L.min.js",
|
||||
"file": "chunk/DDL5uWMz.min.js",
|
||||
"name": "calculator",
|
||||
"src": "src/js/plugin/Calculator.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_chlzpS6K.min.js"
|
||||
"_DcK-mo-Y.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/plugin/InfiniteScroll.ts": {
|
||||
"file": "chunk/Cx4rGXMm.min.js",
|
||||
"file": "chunk/DpvWr1cn.min.js",
|
||||
"name": "infinitescroll",
|
||||
"src": "src/js/plugin/InfiniteScroll.ts",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"src/js/index.ts",
|
||||
"_DH1EQbEY.min.js",
|
||||
"_chlzpS6K.min.js"
|
||||
"_DK4yUVpy.min.js",
|
||||
"_DcK-mo-Y.min.js"
|
||||
]
|
||||
},
|
||||
"src/js/plugin/MapView.ts": {
|
||||
"file": "chunk/DwAGgYJF.min.js",
|
||||
"file": "chunk/U6YV4Y8e.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/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};
|
||||
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};
|
||||
//# 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' not in locked_preferences -%}
|
||||
{%- if 'favicon_resolver' not in locked_preferences -%}
|
||||
{%- include 'simple/preferences/favicon.html' -%}
|
||||
{%- endif -%}
|
||||
{% if 'safesearch' not in locked_preferences %}
|
||||
|
||||
Binary file not shown.
@@ -17,20 +17,22 @@
|
||||
# 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-19 12:07+0000\n"
|
||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\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"
|
||||
"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']
|
||||
@@ -647,7 +649,7 @@ msgstr "인수들의 {func}를 계산하세요"
|
||||
#: searx/engines/boardreader.py:107
|
||||
#, python-brace-format
|
||||
msgid "Posted by {author}"
|
||||
msgstr ""
|
||||
msgstr "작성자: {author}"
|
||||
|
||||
#: searx/engines/openstreetmap.py:155
|
||||
msgid "Show route in map .."
|
||||
@@ -730,11 +732,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"
|
||||
@@ -970,14 +972,16 @@ 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"
|
||||
@@ -2192,4 +2196,3 @@ msgstr "비디오 숨기기"
|
||||
|
||||
#~ msgid "Number of results"
|
||||
#~ msgstr "결과 수"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Utility functions for the engines"""
|
||||
|
||||
import time
|
||||
|
||||
import re
|
||||
import importlib
|
||||
@@ -809,3 +810,12 @@ 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, is_locked
|
||||
from searx.preferences import Preferences
|
||||
|
||||
|
||||
# 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 is_locked('language'):
|
||||
if "language" in preferences.cfg.lock:
|
||||
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 is_locked('safesearch'):
|
||||
if "safesearch" in preferences.cfg.lock:
|
||||
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 is_locked('categories') and form is not None:
|
||||
if not "categories" in preferences.cfg.lock 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 is_locked('categories'):
|
||||
if not "categories" in preferences.cfg.lock:
|
||||
# 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 is_locked('categories') and raw_text_query.specific:
|
||||
if not "categories" in preferences.cfg.lock and raw_text_query.specific:
|
||||
# if engines are calculated from query,
|
||||
# set categories by using that information
|
||||
query_engineref_list = raw_text_query.enginerefs
|
||||
|
||||
+15
-3
@@ -377,7 +377,6 @@ 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'),
|
||||
@@ -977,7 +976,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
|
||||
)
|
||||
@@ -1349,6 +1348,8 @@ def run():
|
||||
|
||||
def init():
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
|
||||
if searx.sxng_debug or app.debug:
|
||||
app.debug = True
|
||||
searx.sxng_debug = True
|
||||
@@ -1359,6 +1360,18 @@ 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)
|
||||
@@ -1367,7 +1380,6 @@ 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