Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] d8dd88add9 [upd] pypi: Update docutils requirement from >=0.21.2 to >=0.23
Updates the requirements on [docutils](https://github.com/rtfd/recommonmark) to permit the latest version.
- [Changelog](https://github.com/readthedocs/recommonmark/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rtfd/recommonmark/commits)

---
updated-dependencies:
- dependency-name: docutils
  dependency-version: '0.23'
  dependency-type: direct:development
...

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

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

+21 -21
View File
@@ -1,13 +1,13 @@
{
"_DK4yUVpy.min.js": {
"file": "chunk/DK4yUVpy.min.js",
"_DH1EQbEY.min.js": {
"file": "chunk/DH1EQbEY.min.js",
"name": "assertelement"
},
"_DcK-mo-Y.min.js": {
"file": "chunk/DcK-mo-Y.min.js",
"_chlzpS6K.min.js": {
"file": "chunk/chlzpS6K.min.js",
"name": "getelement",
"imports": [
"_DK4yUVpy.min.js"
"_DH1EQbEY.min.js"
]
},
"src/js/index.ts": {
@@ -27,78 +27,78 @@
]
},
"src/js/main/autocomplete.ts": {
"file": "chunk/DvCYLbJr.min.js",
"file": "chunk/CQ8vfMdp.min.js",
"name": "autocomplete",
"src": "src/js/main/autocomplete.ts",
"isDynamicEntry": true,
"imports": [
"src/js/index.ts",
"_DK4yUVpy.min.js"
"_DH1EQbEY.min.js"
]
},
"src/js/main/keyboard.ts": {
"file": "chunk/C93hSkpT.min.js",
"file": "chunk/aUw47Wy0.min.js",
"name": "keyboard",
"src": "src/js/main/keyboard.ts",
"isDynamicEntry": true,
"imports": [
"src/js/index.ts",
"_DK4yUVpy.min.js"
"_DH1EQbEY.min.js"
]
},
"src/js/main/preferences.ts": {
"file": "chunk/e2-9fzwE.min.js",
"file": "chunk/DZidprJh.min.js",
"name": "preferences",
"src": "src/js/main/preferences.ts",
"isDynamicEntry": true,
"imports": [
"src/js/index.ts",
"_DK4yUVpy.min.js"
"_DH1EQbEY.min.js"
]
},
"src/js/main/results.ts": {
"file": "chunk/B8prKeWj.min.js",
"file": "chunk/DGJ63wI6.min.js",
"name": "results",
"src": "src/js/main/results.ts",
"isDynamicEntry": true,
"imports": [
"src/js/index.ts",
"_DK4yUVpy.min.js"
"_DH1EQbEY.min.js"
]
},
"src/js/main/search.ts": {
"file": "chunk/5Ako-qGW.min.js",
"file": "chunk/BnP4vIuG.min.js",
"name": "search",
"src": "src/js/main/search.ts",
"isDynamicEntry": true,
"imports": [
"src/js/index.ts",
"_DcK-mo-Y.min.js"
"_chlzpS6K.min.js"
]
},
"src/js/plugin/Calculator.ts": {
"file": "chunk/DDL5uWMz.min.js",
"file": "chunk/DyePpW7L.min.js",
"name": "calculator",
"src": "src/js/plugin/Calculator.ts",
"isDynamicEntry": true,
"imports": [
"src/js/index.ts",
"_DcK-mo-Y.min.js"
"_chlzpS6K.min.js"
]
},
"src/js/plugin/InfiniteScroll.ts": {
"file": "chunk/DpvWr1cn.min.js",
"file": "chunk/Cx4rGXMm.min.js",
"name": "infinitescroll",
"src": "src/js/plugin/InfiniteScroll.ts",
"isDynamicEntry": true,
"imports": [
"src/js/index.ts",
"_DK4yUVpy.min.js",
"_DcK-mo-Y.min.js"
"_DH1EQbEY.min.js",
"_chlzpS6K.min.js"
]
},
"src/js/plugin/MapView.ts": {
"file": "chunk/U6YV4Y8e.min.js",
"file": "chunk/DwAGgYJF.min.js",
"name": "mapview",
"src": "src/js/plugin/MapView.ts",
"isDynamicEntry": true,
+2 -2
View File
@@ -1,3 +1,3 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./chunk/U6YV4Y8e.min.js","./sxng-mapview.min.css","./chunk/DpvWr1cn.min.js","./chunk/DK4yUVpy.min.js","./chunk/DcK-mo-Y.min.js","./chunk/DDL5uWMz.min.js","./chunk/C93hSkpT.min.js","./chunk/5Ako-qGW.min.js","./chunk/DvCYLbJr.min.js","./chunk/B8prKeWj.min.js","./chunk/e2-9fzwE.min.js"])))=>i.map(i=>d[i]);
var e=class{id;constructor(e){this.id=e,queueMicrotask(()=>this.invoke())}async invoke(){try{console.debug(`[PLUGIN] ${this.id}: Running...`);let e=await this.run();if(!e)return;console.debug(`[PLUGIN] ${this.id}: Running post-exec...`),await this.post(e)}catch(e){console.error(`[PLUGIN] ${this.id}:`,e)}finally{console.debug(`[PLUGIN] ${this.id}: Done.`)}}},t={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},n={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},r=()=>{let e=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return e&&e in t?e:t.unknown},i=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},a=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},o=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},s=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?o(`DOMContentLoaded`,document,e,{once:!0}):e()},c=r(),l=i(),u=(e,t)=>{d(t)&&e()},d=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(c)}},f=`modulepreload`,p=function(e,t){return new URL(e,t).href},m={},h=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=p(t,n),t in m)return;m[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:f,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};s(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),o(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),o(`click`,`.searxng_init_map`,async function(e){e.preventDefault(),this.classList.remove(`searxng_init_map`),u(()=>h(async()=>{let{default:e}=await import(`./chunk/U6YV4Y8e.min.js`);return{default:e}},__vite__mapDeps([0,1]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[t.results]})}),l.plugins?.includes(`infiniteScroll`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DpvWr1cn.min.js`);return{default:e}},__vite__mapDeps([2,3,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]}),l.plugins?.includes(`calculator`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DDL5uWMz.min.js`);return{default:e}},__vite__mapDeps([5,4,3]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]})}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.index]}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/B8prKeWj.min.js`),__vite__mapDeps([9,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.results]}),s(()=>{h(()=>import(`./chunk/e2-9fzwE.min.js`),__vite__mapDeps([10,3]),import.meta.url)},{on:[c===t.preferences]});export{e as a,l as i,o as n,n as r,a as t};
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./chunk/DwAGgYJF.min.js","./sxng-mapview.min.css","./chunk/Cx4rGXMm.min.js","./chunk/DH1EQbEY.min.js","./chunk/chlzpS6K.min.js","./chunk/DyePpW7L.min.js","./chunk/aUw47Wy0.min.js","./chunk/BnP4vIuG.min.js","./chunk/CQ8vfMdp.min.js","./chunk/DGJ63wI6.min.js","./chunk/DZidprJh.min.js"])))=>i.map(i=>d[i]);
var e=class{id;constructor(e){this.id=e,queueMicrotask(()=>this.invoke())}async invoke(){try{console.debug(`[PLUGIN] ${this.id}: Running...`);let e=await this.run();if(!e)return;console.debug(`[PLUGIN] ${this.id}: Running post-exec...`),await this.post(e)}catch(e){console.error(`[PLUGIN] ${this.id}:`,e)}finally{console.debug(`[PLUGIN] ${this.id}: Done.`)}}},t={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},n={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},r=()=>{let e=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return e&&e in t?e:t.unknown},i=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},a=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},o=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},s=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?o(`DOMContentLoaded`,document,e,{once:!0}):e()},c=r(),l=i(),u=(e,t)=>{d(t)&&e()},d=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(c)}},f=`modulepreload`,p=function(e,t){return new URL(e,t).href},m={},h=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=p(t,n),t in m)return;m[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:f,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};s(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),o(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),o(`click`,`.searxng_init_map`,async function(e){e.preventDefault(),this.classList.remove(`searxng_init_map`),u(()=>h(async()=>{let{default:e}=await import(`./chunk/DwAGgYJF.min.js`);return{default:e}},__vite__mapDeps([0,1]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[t.results]})}),l.plugins?.includes(`infiniteScroll`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/Cx4rGXMm.min.js`);return{default:e}},__vite__mapDeps([2,3,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]}),l.plugins?.includes(`calculator`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DyePpW7L.min.js`);return{default:e}},__vite__mapDeps([5,4,3]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]})}),s(()=>{h(()=>import(`./chunk/aUw47Wy0.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/BnP4vIuG.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/CQ8vfMdp.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.index]}),s(()=>{h(()=>import(`./chunk/aUw47Wy0.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/DGJ63wI6.min.js`),__vite__mapDeps([9,3]),import.meta.url),h(()=>import(`./chunk/BnP4vIuG.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/CQ8vfMdp.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.results]}),s(()=>{h(()=>import(`./chunk/DZidprJh.min.js`),__vite__mapDeps([10,3]),import.meta.url)},{on:[c===t.preferences]});export{e as a,l as i,o as n,n as r,a as t};
//# sourceMappingURL=sxng-core.min.js.map
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -180,7 +180,7 @@
{%- if 'autocomplete' not in locked_preferences -%}
{%- include 'simple/preferences/autocomplete.html' -%}
{%- endif -%}
{%- if 'favicon_resolver' not in locked_preferences -%}
{%- if 'favicon' not in locked_preferences -%}
{%- include 'simple/preferences/favicon.html' -%}
{%- endif -%}
{% if 'safesearch' not in locked_preferences %}
Binary file not shown.
+11 -14
View File
@@ -17,22 +17,20 @@
# yoonhahwang <yoonhahwang@noreply.codeberg.org>, 2025.
# choonarine <choonarine@noreply.codeberg.org>, 2025.
# pywc <pywc@noreply.codeberg.org>, 2025.
# daemul72 <daemul72@noreply.codeberg.org>, 2026.
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-05-25 10:44+0000\n"
"PO-Revision-Date: 2026-05-31 08:03+0000\n"
"Last-Translator: daemul72 <daemul72@noreply.codeberg.org>\n"
"Language-Team: Korean <https://translate.codeberg.org/projects/searxng/"
"searxng/ko/>\n"
"PO-Revision-Date: 2026-05-19 12:07+0000\n"
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
"Language: ko\n"
"Language-Team: Korean "
"<https://translate.codeberg.org/projects/searxng/searxng/ko/>\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.5\n"
"Generated-By: Babel 2.18.0\n"
#. CONSTANT_NAMES['NO_SUBGROUPING']
@@ -649,7 +647,7 @@ msgstr "인수들의 {func}를 계산하세요"
#: searx/engines/boardreader.py:107
#, python-brace-format
msgid "Posted by {author}"
msgstr "작성자: {author}"
msgstr ""
#: searx/engines/openstreetmap.py:155
msgid "Show route in map .."
@@ -732,11 +730,11 @@ msgstr "Onion 검색 결과중 Ahmia 블랙리스트에 포함된 페이지를
#: searx/plugins/calculator.py:25
msgid "Calculator"
msgstr "계산기"
msgstr ""
#: searx/plugins/calculator.py:26
msgid "Parses and solves mathematical expressions."
msgstr "수식들을 분석하고 풀이합니다."
msgstr ""
#: searx/plugins/hash_plugin.py:33
msgid "Hash plugin"
@@ -972,16 +970,14 @@ msgid ""
"This is a preview of the settings used by the 'Search URL' you used to "
"get here."
msgstr ""
"이는 사용자가 이 페이지에 접속하기 위해 사용한 '검색 URL'에서 사용되는 "
"설정의 미리보기입니다."
#: searx/templates/simple/preferences.html:158
msgid "Press save to copy these preferences to your browser."
msgstr "이 설정을 브라우저에 복사하려면 '저장'을 누르세요."
msgstr ""
#: searx/templates/simple/preferences.html:159
msgid "Click here to view your browser preferences instead:"
msgstr "브라우저 설정을 확인하려면 여기를 클릭하세요:"
msgstr ""
#: searx/templates/simple/preferences.html:169
msgid "General"
@@ -2196,3 +2192,4 @@ msgstr "비디오 숨기기"
#~ msgid "Number of results"
#~ msgstr "결과 수"
-10
View File
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Utility functions for the engines"""
import time
import re
import importlib
@@ -810,12 +809,3 @@ def parse_duration_string(duration_str: str) -> timedelta | None:
pass
return None
# Format the video duration
def format_duration(duration: str | int) -> str:
seconds = int(duration)
length = time.gmtime(seconds)
if length.tm_hour:
return time.strftime("%H:%M:%S", length)
return time.strftime("%M:%S", length)
+6 -6
View File
@@ -8,7 +8,7 @@ from searx.webutils import VALID_LANGUAGE_CODE
from searx.query import RawTextQuery
from searx.engines import categories, engines
from searx.search.models import SearchQuery, EngineRef
from searx.preferences import Preferences
from searx.preferences import Preferences, is_locked
# remove duplicate queries.
@@ -53,7 +53,7 @@ def parse_pageno(form: Dict[str, str]) -> int:
def parse_lang(preferences: Preferences, form: Dict[str, str], raw_text_query: RawTextQuery) -> str:
if "language" in preferences.cfg.lock:
if is_locked('language'):
return preferences.get_value('language')
# get language
# set specific language if set on request, query or preferences
@@ -73,7 +73,7 @@ def parse_lang(preferences: Preferences, form: Dict[str, str], raw_text_query: R
def parse_safesearch(preferences: Preferences, form: Dict[str, str]) -> int:
if "safesearch" in preferences.cfg.lock:
if is_locked('safesearch'):
return preferences.get_value('safesearch')
if 'safesearch' in form:
@@ -135,7 +135,7 @@ def parse_category_form(query_categories: List[str], name: str, value: str) -> N
def get_selected_categories(preferences: Preferences, form: Optional[Dict[str, str]]) -> List[str]:
selected_categories = []
if not "categories" in preferences.cfg.lock and form is not None:
if not is_locked('categories') and form is not None:
for name, value in form.items():
parse_category_form(selected_categories, name, value)
@@ -175,7 +175,7 @@ def parse_generic(preferences: Preferences, form: Dict[str, str], disabled_engin
# set categories/engines
explicit_engine_list = False
if not "categories" in preferences.cfg.lock:
if not is_locked('categories'):
# parse the form only if the categories are not locked
for pd_name, pd in form.items(): # pylint: disable=invalid-name
if pd_name == 'engines':
@@ -266,7 +266,7 @@ def get_search_query_from_webapp(
if query_lang == 'auto':
query_lang = preferences.client.locale_tag or 'all'
if not "categories" in preferences.cfg.lock and raw_text_query.specific:
if not is_locked('categories') and raw_text_query.specific:
# if engines are calculated from query,
# set categories by using that information
query_engineref_list = raw_text_query.enginerefs
+3 -15
View File
@@ -377,6 +377,7 @@ def get_client_settings():
'theme_static_path': custom_url_for('static', filename='themes/simple'),
'results_on_new_tab': req_pref.get_value('results_on_new_tab'),
'favicon_resolver': req_pref.get_value('favicon_resolver'),
'advanced_search': req_pref.get_value('advanced_search'),
'query_in_title': req_pref.get_value('query_in_title'),
'safesearch': req_pref.get_value('safesearch'),
'theme': req_pref.get_value('theme'),
@@ -976,7 +977,7 @@ def preferences():
current_doi_resolver = get_doi_resolver(),
allowed_plugins = allowed_plugins,
preferences_url_params = sxng_request.preferences.get_as_url_params(),
locked_preferences = get_setting("preferences").lock,
locked_preferences = get_setting("preferences.lock", []),
doi_resolvers = get_setting("doi_resolvers", {}),
# fmt: on
)
@@ -1348,8 +1349,6 @@ def run():
def init():
# pylint: disable=import-outside-toplevel
if searx.sxng_debug or app.debug:
app.debug = True
searx.sxng_debug = True
@@ -1360,18 +1359,6 @@ def init():
logger.error("server.secret_key is not changed. Please use something else instead of ultrasecretkey.")
sys.exit(1)
# init database schema first / DB schema is created with the first connect
from searx.data import get_cache
from searx.enginelib import ENGINES_CACHE
conn = get_cache().connect()
conn.close()
conn = ENGINES_CACHE.connect()
conn.close()
favicons.init()
# init application
locales_initialize()
valkey_initialize()
searx.plugins.initialize(app)
@@ -1380,6 +1367,7 @@ def init():
searx.search.initialize(check_network=True, enable_metrics=metrics)
limiter.initialize(app, settings)
favicons.init()
def static_headers(headers: Headers, _path: str, _url: str) -> None: