mirror of
https://github.com/searxng/searxng.git
synced 2025-12-23 20:20:05 +00:00
Compare commits
84 Commits
dependabot
...
b299386d3e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b299386d3e | ||
|
|
21a4622f23 | ||
|
|
041f457dfa | ||
|
|
af111e413c | ||
|
|
431bf5d235 | ||
|
|
576c8ca99c | ||
|
|
45a4b8ad1c | ||
|
|
d14d695966 | ||
|
|
a2a47337cb | ||
|
|
ba98030438 | ||
|
|
1e200a1107 | ||
|
|
7a1b959646 | ||
|
|
b9b46431be | ||
|
|
3f18c0f40f | ||
|
|
1cfbd32a1d | ||
|
|
a15b594003 | ||
|
|
24d27a7a21 | ||
|
|
7af922c9df | ||
|
|
b1918dd121 | ||
|
|
1be19f8b58 | ||
|
|
3763b4bff4 | ||
|
|
52ffc4c7f4 | ||
|
|
0245327fc5 | ||
|
|
b155e66fe5 | ||
|
|
5712827703 | ||
|
|
7ba53d302d | ||
|
|
b8e4ebdc0c | ||
|
|
b37d09557a | ||
|
|
aa28af772c | ||
|
|
9c2b8f2f93 | ||
|
|
c48993452f | ||
|
|
6a2196c03d | ||
|
|
dce383881d | ||
|
|
1ebedcbc17 | ||
|
|
5d99877d8d | ||
|
|
adc1a2a1ea | ||
|
|
43065c5026 | ||
|
|
ea4a55fa57 | ||
|
|
d514dea5cc | ||
|
|
22e1d30017 | ||
|
|
4ca75a0450 | ||
|
|
50a4c653dc | ||
|
|
b7f9b489c9 | ||
|
|
2cdbbb249a | ||
|
|
edfa71cdea | ||
|
|
8dacbbbb15 | ||
|
|
b770a46e1f | ||
|
|
2c880f6084 | ||
|
|
c41b769f97 | ||
|
|
e363db970c | ||
|
|
16293132e3 | ||
|
|
f70120b0b9 | ||
|
|
a8f3644cdc | ||
|
|
4295e758c0 | ||
|
|
33e798b01b | ||
|
|
d84ae96cf9 | ||
|
|
9371658531 | ||
|
|
ee6d4f322f | ||
|
|
3725aef6f3 | ||
|
|
e840e3f960 | ||
|
|
a6bb1ecf87 | ||
|
|
636738779e | ||
|
|
1d138c5968 | ||
|
|
3e7e404fda | ||
|
|
602a73df9a | ||
|
|
57622793bf | ||
|
|
080f3a5f87 | ||
|
|
f54cf643b2 | ||
|
|
dd82d785ce | ||
|
|
f6cdd16449 | ||
|
|
576d30ffcd | ||
|
|
c34bb61284 | ||
|
|
8baefcc21e | ||
|
|
fc7d8b8be2 | ||
|
|
5492de15bb | ||
|
|
ced08e12aa | ||
|
|
613c1aa8eb | ||
|
|
899cf7e08a | ||
|
|
362cc13aeb | ||
|
|
d28a1c434f | ||
|
|
21d0428cf2 | ||
|
|
f0dfe3cc0e | ||
|
|
0559b9bfcf | ||
|
|
37f7960266 |
2
.github/workflows/checker.yml
vendored
2
.github/workflows/checker.yml
vendored
@@ -15,7 +15,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
|
||||
jobs:
|
||||
search:
|
||||
|
||||
123
.github/workflows/container.yml
vendored
123
.github/workflows/container.yml
vendored
@@ -18,106 +18,34 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Organization GHCR
|
||||
packages: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
|
||||
jobs:
|
||||
build-base:
|
||||
if: |
|
||||
(github.repository_owner == 'searxng' && github.event.workflow_run.conclusion == 'success')
|
||||
|| github.event_name == 'workflow_dispatch'
|
||||
name: Build base
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
# Organization GHCR
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- if: github.repository_owner == 'searxng'
|
||||
name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
- if: github.repository_owner == 'searxng'
|
||||
name: Get date
|
||||
id: date
|
||||
run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT
|
||||
|
||||
- if: github.repository_owner == 'searxng'
|
||||
name: Check cache apko
|
||||
id: cache-apko
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
# yamllint disable-line rule:line-length
|
||||
key: "apko-${{ steps.date.outputs.date }}-${{ hashFiles('./container/base.yml', './container/base-builder.yml') }}"
|
||||
path: "/tmp/.apko/"
|
||||
lookup-only: true
|
||||
|
||||
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
|
||||
name: Setup cache apko
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
# yamllint disable-line rule:line-length
|
||||
key: "apko-${{ steps.date.outputs.date }}-${{ hashFiles('./container/base.yml', './container/base-builder.yml') }}"
|
||||
restore-keys: "apko-${{ steps.date.outputs.date }}-"
|
||||
path: "/tmp/.apko/"
|
||||
|
||||
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
|
||||
name: Setup apko
|
||||
run: |
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
|
||||
brew install apko
|
||||
|
||||
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
|
||||
name: Login to GHCR
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: "${{ github.repository_owner }}"
|
||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
|
||||
name: Build
|
||||
run: |
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
|
||||
|
||||
apko publish ./container/base.yml ghcr.io/${{ github.repository_owner }}/base:searxng \
|
||||
--cache-dir=/tmp/.apko/ \
|
||||
--sbom=false \
|
||||
--vcs=false \
|
||||
--log-level=debug
|
||||
|
||||
apko publish ./container/base-builder.yml ghcr.io/${{ github.repository_owner }}/base:searxng-builder \
|
||||
--cache-dir=/tmp/.apko/ \
|
||||
--sbom=false \
|
||||
--vcs=false \
|
||||
--log-level=debug
|
||||
|
||||
build:
|
||||
if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
|
||||
name: Build (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: build-base
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
march: amd64
|
||||
os: ubuntu-24.04
|
||||
emulation: false
|
||||
- arch: arm64
|
||||
march: arm64
|
||||
os: ubuntu-24.04-arm
|
||||
emulation: false
|
||||
- arch: armv7
|
||||
march: arm64
|
||||
os: ubuntu-24.04-arm
|
||||
emulation: true
|
||||
|
||||
permissions:
|
||||
# Organization GHCR
|
||||
packages: write
|
||||
|
||||
outputs:
|
||||
@@ -125,6 +53,30 @@ jobs:
|
||||
git_url: ${{ steps.build.outputs.git_url }}
|
||||
|
||||
steps:
|
||||
# yamllint disable rule:line-length
|
||||
- name: Setup podman
|
||||
env:
|
||||
PODMAN_VERSION: "v5.6.2"
|
||||
run: |
|
||||
# dpkg man-db trigger is very slow on GHA runners
|
||||
# https://github.com/actions/runner-images/issues/10977
|
||||
# https://github.com/actions/runner/issues/4030
|
||||
sudo rm -f /var/lib/man-db/auto-update
|
||||
|
||||
sudo apt-get purge -y podman runc crun conmon
|
||||
|
||||
curl -fsSLO "https://github.com/mgoltzsche/podman-static/releases/download/${{ env.PODMAN_VERSION }}/podman-linux-${{ matrix.march }}.tar.gz"
|
||||
curl -fsSLO "https://github.com/mgoltzsche/podman-static/releases/download/${{ env.PODMAN_VERSION }}/podman-linux-${{ matrix.march }}.tar.gz.asc"
|
||||
gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 0CCF102C4F95D89E583FF1D4F8B5AF50344BB503
|
||||
gpg --batch --verify "podman-linux-${{ matrix.march }}.tar.gz.asc" "podman-linux-${{ matrix.march }}.tar.gz"
|
||||
|
||||
tar -xzf "podman-linux-${{ matrix.march }}.tar.gz"
|
||||
sudo cp -rfv ./podman-linux-${{ matrix.march }}/etc/. /etc/
|
||||
sudo cp -rfv ./podman-linux-${{ matrix.march }}/usr/. /usr/
|
||||
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||
# yamllint enable rule:line-length
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
@@ -143,16 +95,22 @@ jobs:
|
||||
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup cache container uv
|
||||
- name: Get date
|
||||
id: date
|
||||
run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT
|
||||
|
||||
- name: Setup cache container
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
key: "container-uv-${{ matrix.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: "container-uv-${{ matrix.arch }}-"
|
||||
path: "/var/tmp/buildah-cache-1001/uv/"
|
||||
key: "container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: |
|
||||
"container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-"
|
||||
"container-${{ matrix.arch }}-"
|
||||
path: "/var/tmp/buildah-cache-*/*"
|
||||
|
||||
- if: ${{ matrix.emulation }}
|
||||
name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
@@ -193,7 +151,7 @@ jobs:
|
||||
|
||||
- if: ${{ matrix.emulation }}
|
||||
name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
@@ -217,7 +175,6 @@ jobs:
|
||||
- test
|
||||
|
||||
permissions:
|
||||
# Organization GHCR
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
|
||||
2
.github/workflows/data-update.yml
vendored
2
.github/workflows/data-update.yml
vendored
@@ -15,7 +15,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
|
||||
jobs:
|
||||
data:
|
||||
|
||||
4
.github/workflows/documentation.yml
vendored
4
.github/workflows/documentation.yml
vendored
@@ -19,7 +19,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
- if: github.ref_name == 'master'
|
||||
name: Release
|
||||
uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 # v4.7.3
|
||||
uses: JamesIves/github-pages-deploy-action@4a3abc783e1a24aeb44c16e869ad83caf6b4cc23 # v4.7.4
|
||||
with:
|
||||
folder: "dist/docs"
|
||||
branch: "gh-pages"
|
||||
|
||||
4
.github/workflows/integration.yml
vendored
4
.github/workflows/integration.yml
vendored
@@ -18,7 +18,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version-file: "./.nvmrc"
|
||||
|
||||
|
||||
2
.github/workflows/l10n.yml
vendored
2
.github/workflows/l10n.yml
vendored
@@ -22,7 +22,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
|
||||
2
.github/workflows/security.yml
vendored
2
.github/workflows/security.yml
vendored
@@ -41,6 +41,6 @@ jobs:
|
||||
write-comment: "false"
|
||||
|
||||
- name: Upload SARIFs
|
||||
uses: github/codeql-action/upload-sarif@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
with:
|
||||
sarif_file: "./scout.sarif"
|
||||
|
||||
@@ -162,7 +162,7 @@ no-docstring-rgx=^_
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct variable names
|
||||
variable-rgx=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*)|([a-z]))$
|
||||
variable-rgx=([a-zA-Z0-9_]*)$
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": ["**", "!dist", "!node_modules"]
|
||||
"includes": ["**", "!node_modules"]
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
@@ -15,9 +15,9 @@
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
@@ -46,9 +46,13 @@
|
||||
"useSingleJsDocAsterisk": "error"
|
||||
},
|
||||
"nursery": {
|
||||
"noContinue": "warn",
|
||||
"noDeprecatedImports": "warn",
|
||||
"noFloatingPromises": "warn",
|
||||
"noImportCycles": "warn",
|
||||
"noIncrementDecrement": "warn",
|
||||
"noMisusedPromises": "warn",
|
||||
"noParametersOnlyUsedInRecursion": "warn",
|
||||
"noUselessCatchBinding": "warn",
|
||||
"noUselessUndefined": "warn",
|
||||
"useExhaustiveSwitchCases": "warn",
|
||||
@@ -65,6 +69,7 @@
|
||||
"style": {
|
||||
"noCommonJs": "error",
|
||||
"noEnum": "error",
|
||||
"noImplicitBoolean": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noNamespace": "error",
|
||||
"noNegationElse": "error",
|
||||
@@ -109,6 +114,12 @@
|
||||
"syntax": "explicit"
|
||||
}
|
||||
},
|
||||
"useConsistentTypeDefinitions": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"style": "type"
|
||||
}
|
||||
},
|
||||
"useDefaultSwitchClause": "error",
|
||||
"useExplicitLengthCheck": "error",
|
||||
"useForOf": "error",
|
||||
@@ -117,6 +128,7 @@
|
||||
"useNumericSeparators": "error",
|
||||
"useObjectSpread": "error",
|
||||
"useReadonlyClassProperties": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useShorthandAssign": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"useThrowNewError": "error",
|
||||
@@ -146,5 +158,15 @@
|
||||
"semicolons": "always",
|
||||
"trailingCommas": "none"
|
||||
}
|
||||
},
|
||||
"html": {
|
||||
"experimentalFullSupportEnabled": true,
|
||||
"formatter": {
|
||||
"attributePosition": "auto",
|
||||
"bracketSameLine": false,
|
||||
"indentScriptAndStyle": true,
|
||||
"selfCloseVoidElements": "always",
|
||||
"whitespaceSensitivity": "ignore"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
757
client/simple/package-lock.json
generated
757
client/simple/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,27 +25,27 @@
|
||||
"not dead"
|
||||
],
|
||||
"dependencies": {
|
||||
"ionicons": "~8.0.0",
|
||||
"ionicons": "~8.0.13",
|
||||
"normalize.css": "8.0.1",
|
||||
"ol": "~10.6.0",
|
||||
"ol": "~10.7.0",
|
||||
"swiped-events": "1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.5",
|
||||
"@types/node": "~24.6.2",
|
||||
"browserslist": "~4.26.3",
|
||||
"browserslist-to-esbuild": "~2.1.0",
|
||||
"@biomejs/biome": "2.3.5",
|
||||
"@types/node": "~24.10.1",
|
||||
"browserslist": "~4.28.0",
|
||||
"browserslist-to-esbuild": "~2.1.1",
|
||||
"edge.js": "~6.3.0",
|
||||
"less": "~4.4.1",
|
||||
"less": "~4.4.2",
|
||||
"lightningcss": "~1.30.2",
|
||||
"sharp": "~0.34.4",
|
||||
"sharp": "~0.34.5",
|
||||
"sort-package-json": "~3.4.0",
|
||||
"stylelint": "~16.24.0",
|
||||
"stylelint-config-standard-less": "~3.0.0",
|
||||
"stylelint-prettier": "~5.0.0",
|
||||
"stylelint": "~16.25.0",
|
||||
"stylelint-config-standard-less": "~3.0.1",
|
||||
"stylelint-prettier": "~5.0.3",
|
||||
"svgo": "~4.0.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "npm:rolldown-vite@7.1.15",
|
||||
"vite": "npm:rolldown-vite@7.2.2",
|
||||
"vite-bundle-analyzer": "~1.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Endpoints, endpoint, ready, settings } from "./toolkit.ts";
|
||||
|
||||
ready(
|
||||
() => {
|
||||
import("../main/keyboard.ts");
|
||||
import("../main/search.ts");
|
||||
void import("../main/keyboard.ts");
|
||||
void import("../main/search.ts");
|
||||
|
||||
if (settings.autocomplete) {
|
||||
import("../main/autocomplete.ts");
|
||||
void import("../main/autocomplete.ts");
|
||||
}
|
||||
},
|
||||
{ on: [endpoint === Endpoints.index] }
|
||||
@@ -16,17 +16,17 @@ ready(
|
||||
|
||||
ready(
|
||||
() => {
|
||||
import("../main/keyboard.ts");
|
||||
import("../main/mapresult.ts");
|
||||
import("../main/results.ts");
|
||||
import("../main/search.ts");
|
||||
void import("../main/keyboard.ts");
|
||||
void import("../main/mapresult.ts");
|
||||
void import("../main/results.ts");
|
||||
void import("../main/search.ts");
|
||||
|
||||
if (settings.infinite_scroll) {
|
||||
import("../main/infinite_scroll.ts");
|
||||
void import("../main/infinite_scroll.ts");
|
||||
}
|
||||
|
||||
if (settings.autocomplete) {
|
||||
import("../main/autocomplete.ts");
|
||||
void import("../main/autocomplete.ts");
|
||||
}
|
||||
},
|
||||
{ on: [endpoint === Endpoints.results] }
|
||||
@@ -34,7 +34,7 @@ ready(
|
||||
|
||||
ready(
|
||||
() => {
|
||||
import("../main/preferences.ts");
|
||||
void import("../main/preferences.ts");
|
||||
},
|
||||
{ on: [endpoint === Endpoints.preferences] }
|
||||
);
|
||||
|
||||
@@ -83,7 +83,7 @@ const observer: IntersectionObserver = new IntersectionObserver((entries: Inters
|
||||
if (paginationEntry?.isIntersecting) {
|
||||
observer.unobserve(paginationEntry.target);
|
||||
|
||||
loadNextPage(onlyImages, () => {
|
||||
void loadNextPage(onlyImages, () => {
|
||||
const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
|
||||
if (nextObservedElement) {
|
||||
observer.observe(nextObservedElement);
|
||||
|
||||
@@ -407,12 +407,31 @@ const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
|
||||
};
|
||||
|
||||
const copyURLToClipboard = async (): Promise<void> => {
|
||||
const currentUrlElement = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] h3 a");
|
||||
assertElement(currentUrlElement);
|
||||
const selectedResult = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
||||
if (!selectedResult) return;
|
||||
|
||||
const url = currentUrlElement.getAttribute("href");
|
||||
const resultAnchor = selectedResult.querySelector<HTMLAnchorElement>("a");
|
||||
assertElement(resultAnchor);
|
||||
|
||||
const url = resultAnchor.getAttribute("href");
|
||||
if (url) {
|
||||
await navigator.clipboard.writeText(url);
|
||||
if (window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(url);
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const node = document.createElement("span");
|
||||
node.textContent = url;
|
||||
resultAnchor.appendChild(node);
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand("copy");
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ listen("click", ".searxng_init_map", async function (this: HTMLElement, event: E
|
||||
Feature,
|
||||
Point
|
||||
} = await import("../pkg/ol.ts");
|
||||
import("ol/ol.css");
|
||||
void import("ol/ol.css");
|
||||
|
||||
const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { http, listen, settings } from "../core/toolkit.ts";
|
||||
import { assertElement, http, listen, settings } from "../core/toolkit.ts";
|
||||
|
||||
let engineDescriptions: Record<string, [string, string]> | undefined;
|
||||
|
||||
@@ -52,19 +52,25 @@ for (const engine of disableAllEngines) {
|
||||
listen("click", engine, () => toggleEngines(false, engineToggles));
|
||||
}
|
||||
|
||||
const copyHashButton: HTMLElement | null = document.querySelector<HTMLElement>("#copy-hash");
|
||||
if (copyHashButton) {
|
||||
listen("click", copyHashButton, async (event: Event) => {
|
||||
event.preventDefault();
|
||||
listen("click", "#copy-hash", async function (this: HTMLElement) {
|
||||
const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
|
||||
assertElement(target);
|
||||
|
||||
const { copiedText, hash } = copyHashButton.dataset;
|
||||
if (!(copiedText && hash)) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(hash);
|
||||
copyHashButton.innerText = copiedText;
|
||||
} catch (error) {
|
||||
console.error("Failed to copy hash:", error);
|
||||
if (window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(target.innerText);
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand("copy");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const copiedText = this.dataset.copiedText;
|
||||
if (copiedText) {
|
||||
this.innerText = copiedText;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -121,7 +121,19 @@ listen("click", "#copy_url", async function (this: HTMLElement) {
|
||||
const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
|
||||
assertElement(target);
|
||||
|
||||
await navigator.clipboard.writeText(target.innerText);
|
||||
if (window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(target.innerText);
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand("copy");
|
||||
}
|
||||
}
|
||||
|
||||
const copiedText = this.dataset.copiedText;
|
||||
if (copiedText) {
|
||||
this.innerText = copiedText;
|
||||
|
||||
22
client/simple/src/less/result_types/file.less
Normal file
22
client/simple/src/less/result_types/file.less
Normal file
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
Layout of the Files result class
|
||||
*/
|
||||
|
||||
#main_results .result-file {
|
||||
border: 1px solid var(--color-result-border);
|
||||
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
|
||||
.rounded-corners;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
|
||||
audio {
|
||||
width: 100%;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
}
|
||||
@@ -178,7 +178,6 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
#send_search {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0.8rem;
|
||||
background: none repeat scroll 0 0 var(--color-search-background);
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -196,6 +195,7 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
|
||||
#send_search {
|
||||
.ltr-rounded-right-corners(0.8rem);
|
||||
padding: 0.8rem;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -163,12 +163,22 @@ article[data-vim-selected].category-videos,
|
||||
article[data-vim-selected].category-news,
|
||||
article[data-vim-selected].category-map,
|
||||
article[data-vim-selected].category-music,
|
||||
article[data-vim-selected].category-files,
|
||||
article[data-vim-selected].category-social {
|
||||
border: 1px solid var(--color-result-vim-arrow);
|
||||
.rounded-corners;
|
||||
}
|
||||
|
||||
.image-label-bottom-right() {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--color-image-resolution-background);
|
||||
padding: 0.3rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-image-resolution-font);
|
||||
border-top-left-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin: @results-margin 0;
|
||||
padding: @result-padding;
|
||||
@@ -295,12 +305,22 @@ article[data-vim-selected].category-social {
|
||||
color: var(--color-result-description-highlight-font);
|
||||
}
|
||||
|
||||
img.thumbnail {
|
||||
a.thumbnail_link {
|
||||
position: relative;
|
||||
margin-top: 0.6rem;
|
||||
.ltr-margin-right(1rem);
|
||||
.ltr-float-left();
|
||||
padding-top: 0.6rem;
|
||||
.ltr-padding-right(1rem);
|
||||
width: 7rem;
|
||||
height: unset; // remove height value that was needed for lazy loading
|
||||
|
||||
img.thumbnail {
|
||||
width: 7rem;
|
||||
height: unset; // remove height value that was needed for lazy loading
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumbnail_length {
|
||||
.image-label-bottom-right();
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.break {
|
||||
@@ -366,7 +386,6 @@ article[data-vim-selected].category-social {
|
||||
.category-news,
|
||||
.category-map,
|
||||
.category-music,
|
||||
.category-files,
|
||||
.category-social {
|
||||
border: 1px solid var(--color-result-border);
|
||||
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
|
||||
@@ -391,23 +410,19 @@ article[data-vim-selected].category-social {
|
||||
}
|
||||
|
||||
.result-videos {
|
||||
img.thumbnail {
|
||||
.ltr-float-left();
|
||||
padding-top: 0.6rem;
|
||||
.ltr-padding-right(1rem);
|
||||
a.thumbnail_link img.thumbnail {
|
||||
width: 20rem;
|
||||
height: unset; // remove height value that was needed for lazy loading
|
||||
}
|
||||
}
|
||||
|
||||
.result-videos .content {
|
||||
overflow: hidden;
|
||||
}
|
||||
.content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-videos .embedded-video iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
.embedded-video iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@supports not (aspect-ratio: 1 / 1) {
|
||||
@@ -472,14 +487,7 @@ article[data-vim-selected].category-social {
|
||||
}
|
||||
|
||||
.image_resolution {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--color-image-resolution-background);
|
||||
padding: 0.3rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-image-resolution-font);
|
||||
border-top-left-radius: 0.3rem;
|
||||
.image-label-bottom-right();
|
||||
}
|
||||
|
||||
span.title,
|
||||
@@ -1158,3 +1166,4 @@ pre code {
|
||||
@import "result_types/keyvalue.less";
|
||||
@import "result_types/code.less";
|
||||
@import "result_types/paper.less";
|
||||
@import "result_types/file.less";
|
||||
|
||||
@@ -193,6 +193,15 @@ div.selectable_url {
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.dialog-warning-block {
|
||||
.dialog();
|
||||
|
||||
display: block;
|
||||
color: var(--color-warning);
|
||||
background: var(--color-warning-background);
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.dialog-modal {
|
||||
.dialog();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Custom vite plugins to build the web-client components of the simple theme.
|
||||
*
|
||||
* HINT:
|
||||
* This is an inital implementation for the migration of the build process
|
||||
* This is an initial implementation for the migration of the build process
|
||||
* from grunt to vite. For fully support (vite: build & serve) more work is
|
||||
* needed.
|
||||
*/
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
contents:
|
||||
repositories:
|
||||
- https://dl-cdn.alpinelinux.org/alpine/edge/main
|
||||
- https://dl-cdn.alpinelinux.org/alpine/edge/community
|
||||
packages:
|
||||
- alpine-base
|
||||
- build-base
|
||||
- python3-dev
|
||||
- uv
|
||||
- brotli
|
||||
|
||||
entrypoint:
|
||||
command: /bin/sh -l
|
||||
|
||||
work-dir: /usr/local/searxng/
|
||||
|
||||
environment:
|
||||
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
SSL_CERT_DIR: /etc/ssl/certs
|
||||
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
|
||||
HISTFILE: /dev/null
|
||||
|
||||
archs:
|
||||
- x86_64
|
||||
- aarch64
|
||||
- armv7
|
||||
@@ -1,62 +0,0 @@
|
||||
contents:
|
||||
repositories:
|
||||
- https://dl-cdn.alpinelinux.org/alpine/edge/main
|
||||
packages:
|
||||
- alpine-baselayout
|
||||
- ca-certificates
|
||||
- ca-certificates-bundle
|
||||
- musl-locales
|
||||
- musl-locales-lang
|
||||
- tzdata
|
||||
- busybox
|
||||
- python3
|
||||
- wget
|
||||
|
||||
entrypoint:
|
||||
command: /bin/sh -l
|
||||
|
||||
work-dir: /usr/local/searxng/
|
||||
|
||||
accounts:
|
||||
groups:
|
||||
- groupname: searxng
|
||||
gid: 977
|
||||
users:
|
||||
- username: searxng
|
||||
uid: 977
|
||||
shell: /bin/ash
|
||||
|
||||
environment:
|
||||
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
SSL_CERT_DIR: /etc/ssl/certs
|
||||
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
|
||||
HISTFILE: /dev/null
|
||||
CONFIG_PATH: /etc/searxng
|
||||
DATA_PATH: /var/cache/searxng
|
||||
|
||||
paths:
|
||||
# Workdir
|
||||
- path: /usr/local/searxng/
|
||||
type: directory
|
||||
uid: 977
|
||||
gid: 977
|
||||
permissions: 0o555
|
||||
|
||||
# Config volume
|
||||
- path: /etc/searxng/
|
||||
type: directory
|
||||
uid: 977
|
||||
gid: 977
|
||||
permissions: 0o755
|
||||
|
||||
# Data volume
|
||||
- path: /var/cache/searxng/
|
||||
type: directory
|
||||
uid: 977
|
||||
gid: 977
|
||||
permissions: 0o755
|
||||
|
||||
archs:
|
||||
- x86_64
|
||||
- aarch64
|
||||
- armv7
|
||||
@@ -19,8 +19,7 @@ RUN --mount=type=cache,id=uv,target=/root/.cache/uv set -eux -o pipefail; \
|
||||
find ./.venv/lib/python*/site-packages/*.dist-info/ -type f -name "RECORD" -exec sort -t, -k1,1 -o {} {} \;; \
|
||||
find ./.venv/ -exec touch -h --date="@$TIMESTAMP_VENV" {} +
|
||||
|
||||
# use "--exclude=./searx/version_frozen.py" when actions/runner-images updates to Podman 5.0+
|
||||
COPY ./searx/ ./searx/
|
||||
COPY --exclude=./searx/version_frozen.py ./searx/ ./searx/
|
||||
|
||||
ARG TIMESTAMP_SETTINGS="0"
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ ARG CONTAINER_IMAGE_NAME="searxng"
|
||||
FROM localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder AS builder
|
||||
FROM ghcr.io/searxng/base:searxng AS dist
|
||||
|
||||
COPY --chown=searxng:searxng --from=builder /usr/local/searxng/.venv/ ./.venv/
|
||||
COPY --chown=searxng:searxng --from=builder /usr/local/searxng/searx/ ./searx/
|
||||
COPY --chown=searxng:searxng ./container/ ./
|
||||
#COPY --chown=searxng:searxng ./searx/version_frozen.py ./searx/
|
||||
COPY --chown=977:977 --from=builder /usr/local/searxng/.venv/ ./.venv/
|
||||
COPY --chown=977:977 --from=builder /usr/local/searxng/searx/ ./searx/
|
||||
COPY --chown=977:977 ./container/ ./
|
||||
COPY --chown=977:977 ./searx/version_frozen.py ./searx/
|
||||
|
||||
ARG CREATED="0001-01-01T00:00:00Z"
|
||||
ARG VERSION="unknown"
|
||||
|
||||
@@ -48,7 +48,7 @@ solve the CAPTCHA from `qwant.com <https://www.qwant.com/>`__.
|
||||
|
||||
.. group-tab:: Firefox
|
||||
|
||||
.. kernel-figure:: answer-captcha/ffox-setting-proxy-socks.png
|
||||
.. kernel-figure:: /assets/answer-captcha/ffox-setting-proxy-socks.png
|
||||
:alt: FFox proxy on SOCKS5, 127.0.0.1:8080
|
||||
|
||||
Firefox's network settings
|
||||
@@ -66,4 +66,3 @@ solve the CAPTCHA from `qwant.com <https://www.qwant.com/>`__.
|
||||
|
||||
-N
|
||||
Do not execute a remote command. This is useful for just forwarding ports.
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ Basic container instancing example:
|
||||
$ cd ./searxng/
|
||||
|
||||
# Run the container
|
||||
$ docker run --name searxng --replace -d \
|
||||
$ docker run --name searxng -d \
|
||||
-p 8888:8080 \
|
||||
-v "./config/:/etc/searxng/" \
|
||||
-v "./data/:/var/cache/searxng/" \
|
||||
|
||||
@@ -4,22 +4,5 @@
|
||||
``brand:``
|
||||
==========
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
brand:
|
||||
issue_url: https://github.com/searxng/searxng/issues
|
||||
docs_url: https://docs.searxng.org
|
||||
public_instances: https://searx.space
|
||||
wiki_url: https://github.com/searxng/searxng/wiki
|
||||
|
||||
``issue_url`` :
|
||||
If you host your own issue tracker change this URL.
|
||||
|
||||
``docs_url`` :
|
||||
If you host your own documentation change this URL.
|
||||
|
||||
``public_instances`` :
|
||||
If you host your own https://searx.space change this URL.
|
||||
|
||||
``wiki_url`` :
|
||||
Link to your wiki (or ``false``)
|
||||
.. autoclass:: searx.brand.SettingsBrand
|
||||
:members:
|
||||
|
||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
1
docs/assets/sponsors/browserstack.svg
Normal file
1
docs/assets/sponsors/browserstack.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.2 KiB |
1
docs/assets/sponsors/docker.svg
Normal file
1
docs/assets/sponsors/docker.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.9 KiB |
1
docs/assets/sponsors/tuta.svg
Normal file
1
docs/assets/sponsors/tuta.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1024 384"><path fill="#410002" d="M479.178 119.294c-.533-.016-.998.357-1.218 1.078l-24.438 78.364.005-.004c-8.59 27.537 4.516 46.485 34.268 46.485 4.336 0 10.006-.357 11.776-.797.885-.264 1.33-.71 1.594-1.506l5.134-17.177c.264-.973-.09-1.77-1.507-1.683-4.517.445-8.588.797-12.308.797-14.964 0-21.075-7.968-16.646-22.224l10.446-33.47h30.373c.797 0 1.506-.445 1.858-1.418l5.401-17.355c.264-.973-.264-1.681-1.417-1.681H492.66l3.895-12.662c.265-.885.089-1.418-.62-2.034l-15.761-14.255c-.332-.3-.676-.45-.996-.459zm173.64 0c-.532-.016-.996.357-1.218 1.078l-24.436 78.364.005-.004c-8.59 27.537 4.517 46.485 34.268 46.485 4.342 0 10.004-.357 11.778-.797.884-.264 1.324-.71 1.593-1.506l5.133-17.177c.26-.973 0-1.77-1.508-1.683-4.517.445-8.59.797-12.307.797-14.966 0-21.077-7.968-16.646-22.224l10.445-33.47H690.3c.795 0 1.504-.445 1.854-1.418l5.402-17.355c.265-.973-.26-1.681-1.414-1.681H666.3l3.896-12.662c.265-.885.087-1.418-.618-2.034l-15.765-14.255c-.332-.3-.676-.45-.996-.459zm-48.32 29.404c-.974 0-1.503.444-1.862 1.417L590.502 188.9c-7.525 23.998-19.478 37.721-31.965 37.721-12.487 0-17.797-9.83-13.105-24.883l16.028-51.178c.351-1.149-.088-1.857-1.328-1.857H539.94c-.97 0-1.505.444-1.86 1.417l-15.497 49.233.008-.005c-8.765 27.982 5.315 46.31 27.452 46.31 12.747 0 22.756-6.111 29.93-16.118l-.176 12.838c0 1.241.621 1.593 1.681 1.593h14.17c1.064 0 1.504-.445 1.859-1.418l28.512-91.997c.35-1.15-.09-1.858-1.33-1.858zm147.96.005c-43.653 0-60.654 37.719-60.654 62.157-.09 21.339 13.282 34.798 31.08 34.798v.004c11.868 0 21.693-5.314 29.133-16.117v12.836c0 1.061.62 1.596 1.594 1.596h14.166c.974 0 1.505-.446 1.86-1.42l28.777-92.086c.265-.973-.266-1.768-1.24-1.768zm-.616 20.54h17.265l-6.197 19.57c-7.35 23.289-18.684 37.896-32.585 37.896-10.094 0-15.585-6.907-15.585-18.15 0-17.976 13.722-39.315 37.102-39.315z"/><path fill="#850122" d="M226.561 106.964c-.558.007-1.043.428-1.043 1.095V251.59c0 1.594 2.04 1.594 2.48 0L261.38 143.3c.445-1.241.446-2.039-.62-3.1l-33.204-32.762c-.299-.332-.66-.478-.996-.474zm55.983 41.739c-1.241 0-1.594.444-2.039 1.417l-43.919 142.203c-.176.797.177 1.594 1.242 1.594h145.747c1.418 0 2.04-.62 2.48-1.858l44.098-141.499c.445-1.417-.18-1.857-1.417-1.857zm-40.022-58.62c-1.418 0-1.594 1.242-.797 2.04l35.065 35.24c.796.798 1.594 1.061 2.836 1.061h149.467c1.065 0 1.68-1.24.62-2.214l-34.63-34.885c-.796-.796-1.592-1.242-3.274-1.242z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -120,6 +120,7 @@ ${fedora_build}
|
||||
pip install -U setuptools
|
||||
pip install -U wheel
|
||||
pip install -U pyyaml
|
||||
pip install -U msgspec
|
||||
|
||||
# jump to SearXNG's working tree and install SearXNG into virtualenv
|
||||
(${SERVICE_USER})$ cd \"$SEARXNG_SRC\"
|
||||
|
||||
8
docs/dev/engines/online/azure.rst
Normal file
8
docs/dev/engines/online/azure.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
.. _azure engine:
|
||||
|
||||
===============
|
||||
Azure Resources
|
||||
===============
|
||||
|
||||
.. automodule:: searx.engines.azure
|
||||
:members:
|
||||
8
docs/dev/engines/online/sourcehut.rst
Normal file
8
docs/dev/engines/online/sourcehut.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
.. _sourcehut engine:
|
||||
|
||||
=========
|
||||
Sourcehut
|
||||
=========
|
||||
|
||||
.. automodule:: searx.engines.sourcehut
|
||||
:members:
|
||||
7
docs/dev/result_types/main/file.rst
Normal file
7
docs/dev/result_types/main/file.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
.. _result_types.file:
|
||||
|
||||
============
|
||||
File Results
|
||||
============
|
||||
|
||||
.. automodule:: searx.result_types.file
|
||||
@@ -17,6 +17,7 @@ following types have been implemented so far ..
|
||||
main/keyvalue
|
||||
main/code
|
||||
main/paper
|
||||
main/file
|
||||
|
||||
The :ref:`LegacyResult <LegacyResult>` is used internally for the results that
|
||||
have not yet been typed. The templates can be used as orientation until the
|
||||
@@ -28,5 +29,4 @@ final typing is complete.
|
||||
- :ref:`template torrent`
|
||||
- :ref:`template map`
|
||||
- :ref:`template packages`
|
||||
- :ref:`template files`
|
||||
- :ref:`template products`
|
||||
|
||||
@@ -60,7 +60,7 @@ Fields used in the template :origin:`macro result_sub_header
|
||||
publishedDate : :py:obj:`datetime.datetime`
|
||||
The date on which the object was published.
|
||||
|
||||
length: :py:obj:`time.struct_time`
|
||||
length: :py:obj:`datetime.timedelta`
|
||||
Playing duration in seconds.
|
||||
|
||||
views: :py:class:`str`
|
||||
@@ -469,38 +469,6 @@ links : :py:class:`dict`
|
||||
Additional links in the form of ``{'link_name': 'http://example.com'}``
|
||||
|
||||
|
||||
.. _template files:
|
||||
|
||||
``files.html``
|
||||
--------------
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header` and
|
||||
- :ref:`macro result_sub_header`
|
||||
|
||||
Additional fields used in the :origin:`code.html
|
||||
<searx/templates/simple/result_templates/files.html>`:
|
||||
|
||||
filename, size, time: :py:class:`str`
|
||||
Filename, Filesize and Date of the file.
|
||||
|
||||
mtype : ``audio`` | ``video`` | :py:class:`str`
|
||||
Mimetype type of the file.
|
||||
|
||||
subtype : :py:class:`str`
|
||||
Mimetype / subtype of the file.
|
||||
|
||||
abstract : :py:class:`str`
|
||||
Abstract of the file.
|
||||
|
||||
author : :py:class:`str`
|
||||
Name of the author of the file
|
||||
|
||||
embedded : :py:class:`str`
|
||||
URL of an embedded media type (``audio`` or ``video``) / is collapsible.
|
||||
|
||||
|
||||
.. _template products:
|
||||
|
||||
``products.html``
|
||||
|
||||
@@ -56,4 +56,34 @@ If you don't trust anyone, you can set up your own, see :ref:`installation`.
|
||||
utils/index
|
||||
src/index
|
||||
|
||||
|
||||
----------------
|
||||
Acknowledgements
|
||||
----------------
|
||||
|
||||
The following organizations have provided SearXNG access to their paid plans at
|
||||
no cost:
|
||||
|
||||
.. flat-table::
|
||||
:widths: 1 1
|
||||
|
||||
* - .. image:: /assets/sponsors/docker.svg
|
||||
:target: https://docker.com
|
||||
:alt: Docker
|
||||
:align: center
|
||||
:height: 100 px
|
||||
|
||||
- .. image:: /assets/sponsors/tuta.svg
|
||||
:target: https://tuta.com
|
||||
:alt: Tuta
|
||||
:align: center
|
||||
:height: 100 px
|
||||
|
||||
* - .. image:: /assets/sponsors/browserstack.svg
|
||||
:target: https://browserstack.com
|
||||
:alt: BrowserStack
|
||||
:align: center
|
||||
:height: 100 px
|
||||
|
||||
|
||||
.. _searx.space: https://searx.space
|
||||
|
||||
2
manage
2
manage
@@ -117,7 +117,7 @@ EOF
|
||||
|
||||
dev.env() {
|
||||
go.env.dev
|
||||
nvm.env
|
||||
nvm.ensure
|
||||
node.env.dev
|
||||
|
||||
export GOENV
|
||||
|
||||
@@ -2,9 +2,9 @@ mock==5.2.0
|
||||
nose2[coverage_plugin]==0.15.1
|
||||
cov-core==1.15.0
|
||||
black==25.9.0
|
||||
pylint==3.3.9
|
||||
pylint==4.0.3
|
||||
splinter==0.21.0
|
||||
selenium==4.36.0
|
||||
selenium==4.38.0
|
||||
Pallets-Sphinx-Themes==2.3.0
|
||||
Sphinx==8.2.3 ; python_version >= '3.11'
|
||||
Sphinx==8.1.3 ; python_version < '3.11'
|
||||
@@ -23,6 +23,6 @@ wlc==1.16.1
|
||||
coloredlogs==15.0.1
|
||||
docutils>=0.21.2
|
||||
parameterized==0.9.0
|
||||
granian[reload]==2.5.5
|
||||
basedpyright==1.31.6
|
||||
granian[reload]==2.5.7
|
||||
basedpyright==1.33.0
|
||||
types-lxml==2025.8.25
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
granian==2.5.5
|
||||
granian==2.5.7
|
||||
granian[pname]==2.5.7
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
certifi==2025.10.5
|
||||
certifi==2025.11.12
|
||||
babel==2.17.0
|
||||
flask-babel==4.0.0
|
||||
flask==3.1.2
|
||||
@@ -9,14 +9,12 @@ python-dateutil==2.9.0.post0
|
||||
pyyaml==6.0.3
|
||||
httpx[http2]==0.28.1
|
||||
httpx-socks[asyncio]==0.10.0
|
||||
Brotli==1.1.0
|
||||
setproctitle==1.3.7
|
||||
valkey==6.1.1
|
||||
markdown-it-py==3.0.0
|
||||
fasttext-predict==0.9.2.4
|
||||
tomli==2.3.0; python_version < '3.11'
|
||||
msgspec==0.19.0
|
||||
typer-slim==0.19.2
|
||||
typer-slim==0.20.0
|
||||
isodate==0.7.2
|
||||
whitenoise==6.11.0
|
||||
typing-extensions==4.14.1
|
||||
typing-extensions==4.15.0
|
||||
|
||||
@@ -9,7 +9,7 @@ from os.path import dirname, abspath
|
||||
|
||||
import logging
|
||||
|
||||
import searx.unixthreadname # pylint: disable=unused-import
|
||||
import msgspec
|
||||
|
||||
# Debug
|
||||
LOG_FORMAT_DEBUG: str = '%(levelname)-7s %(name)-30.30s: %(message)s'
|
||||
@@ -76,20 +76,22 @@ def get_setting(name: str, default: t.Any = _unset) -> t.Any:
|
||||
settings and the ``default`` is unset, a :py:obj:`KeyError` is raised.
|
||||
|
||||
"""
|
||||
value: dict[str, t.Any] = settings
|
||||
value = settings
|
||||
for a in name.split('.'):
|
||||
if isinstance(value, dict):
|
||||
value = value.get(a, _unset)
|
||||
if isinstance(value, msgspec.Struct):
|
||||
value = getattr(value, a, _unset)
|
||||
elif isinstance(value, dict):
|
||||
value = value.get(a, _unset) # pyright: ignore
|
||||
else:
|
||||
value = _unset # type: ignore
|
||||
value = _unset
|
||||
|
||||
if value is _unset:
|
||||
if default is _unset:
|
||||
raise KeyError(name)
|
||||
value = default # type: ignore
|
||||
value = default
|
||||
break
|
||||
|
||||
return value
|
||||
return value # pyright: ignore
|
||||
|
||||
|
||||
def _is_color_terminal():
|
||||
|
||||
68
searx/brand.py
Normal file
68
searx/brand.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Implementations needed for a branding of SearXNG."""
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
# Struct fields aren't discovered in Python 3.14
|
||||
# - https://github.com/searxng/searxng/issues/5284
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["SettingsBrand"]
|
||||
|
||||
import msgspec
|
||||
|
||||
|
||||
class BrandCustom(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
|
||||
"""Custom settings in the brand section."""
|
||||
|
||||
links: dict[str, str] = {}
|
||||
"""Custom entries in the footer of the WEB page: ``[title]: [link]``"""
|
||||
|
||||
|
||||
class SettingsBrand(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
|
||||
"""Options for configuring brand properties.
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
brand:
|
||||
issue_url: https://github.com/searxng/searxng/issues
|
||||
docs_url: https://docs.searxng.org
|
||||
public_instances: https://searx.space
|
||||
wiki_url: https://github.com/searxng/searxng/wiki
|
||||
|
||||
custom:
|
||||
links:
|
||||
Uptime: https://uptime.searxng.org/history/example-org
|
||||
About: https://example.org/user/about.html
|
||||
"""
|
||||
|
||||
issue_url: str = "https://github.com/searxng/searxng/issues"
|
||||
"""If you host your own issue tracker change this URL."""
|
||||
|
||||
docs_url: str = "https://docs.searxng.org"
|
||||
"""If you host your own documentation change this URL."""
|
||||
|
||||
public_instances: str = "https://searx.space"
|
||||
"""If you host your own https://searx.space change this URL."""
|
||||
|
||||
wiki_url: str = "https://github.com/searxng/searxng/wiki"
|
||||
"""Link to your wiki (or ``false``)"""
|
||||
|
||||
custom: BrandCustom = msgspec.field(default_factory=BrandCustom)
|
||||
"""Optional customizing.
|
||||
|
||||
.. autoclass:: searx.brand.BrandCustom
|
||||
:members:
|
||||
"""
|
||||
|
||||
# new_issue_url is a hackish solution tailored for only one hoster (GH). As
|
||||
# long as we don't have a more general solution, we should support it in the
|
||||
# given function, but it should not be expanded further.
|
||||
|
||||
new_issue_url: str = "https://github.com/searxng/searxng/issues/new"
|
||||
"""If you host your own issue tracker not on GitHub, then unset this URL.
|
||||
|
||||
Note: This URL will create a pre-filled GitHub bug report form for an
|
||||
engine. Since this feature is implemented only for GH (and limited to
|
||||
engines), it will probably be replaced by another solution in the near
|
||||
future.
|
||||
"""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -848,6 +848,7 @@
|
||||
"ja": "レアル",
|
||||
"ko": "브라질 헤알",
|
||||
"lt": "Brazilijos realas",
|
||||
"lv": "Brazīlijas reāls",
|
||||
"ms": "Real Brazil",
|
||||
"nl": "Braziliaanse real",
|
||||
"oc": "Real",
|
||||
@@ -932,6 +933,7 @@
|
||||
"ja": "ニュルタム",
|
||||
"ko": "부탄 눌트럼",
|
||||
"lt": "Ngultrumas",
|
||||
"lv": "ngultrums",
|
||||
"ml": "ങൾട്രം",
|
||||
"ms": "Ngultrum Bhutan",
|
||||
"nl": "Bhutaanse ngultrum",
|
||||
@@ -1327,15 +1329,15 @@
|
||||
"cs": "Kolumbijské peso",
|
||||
"da": "Colombiansk peso",
|
||||
"de": "kolumbianischer Peso",
|
||||
"en": "Colombian peso",
|
||||
"en": "peso",
|
||||
"eo": "kolombia peso",
|
||||
"es": "peso colombiano",
|
||||
"es": "peso",
|
||||
"et": "Colombia peeso",
|
||||
"eu": "Peso kolonbiar",
|
||||
"fi": "Kolumbian peso",
|
||||
"fr": "peso colombien",
|
||||
"ga": "peso na Colóime",
|
||||
"gl": "Peso colombiano",
|
||||
"gl": "peso colombiano",
|
||||
"he": "פסו קולומביאני",
|
||||
"hr": "Kolumbijski pezo",
|
||||
"hu": "kolumbiai peso",
|
||||
@@ -1411,9 +1413,9 @@
|
||||
"cy": "peso (Ciwba)",
|
||||
"da": "Cubanske pesos",
|
||||
"de": "kubanischer Peso",
|
||||
"en": "Cuban peso",
|
||||
"en": "peso",
|
||||
"eo": "kuba peso",
|
||||
"es": "peso cubano",
|
||||
"es": "peso",
|
||||
"fi": "Kuuban peso",
|
||||
"fr": "peso cubain",
|
||||
"ga": "peso Chúba",
|
||||
@@ -1715,7 +1717,7 @@
|
||||
"nl": "Egyptisch pond",
|
||||
"oc": "Liura egipciana",
|
||||
"pa": "ਮਿਸਰੀ ਪਾਊਂਡ",
|
||||
"pl": "Funt egipski",
|
||||
"pl": "funt egipski",
|
||||
"pt": "libra egípcia",
|
||||
"ro": "Liră egipteană",
|
||||
"ru": "египетский фунт",
|
||||
@@ -1772,7 +1774,7 @@
|
||||
"de": "Äthiopischer Birr",
|
||||
"en": "bir",
|
||||
"eo": "etiopa birro",
|
||||
"es": "Birr etíope",
|
||||
"es": "bir etíope",
|
||||
"fi": "Etiopian birr",
|
||||
"fr": "Birr",
|
||||
"ga": "birr",
|
||||
@@ -2859,6 +2861,7 @@
|
||||
"sl": "kirgiški som",
|
||||
"sr": "киргиски сом",
|
||||
"sv": "Kirgizistansk som",
|
||||
"szl": "Sōm (waluta)",
|
||||
"tr": "Kırgızistan somu",
|
||||
"tt": "кыргыз сумы",
|
||||
"uk": "сом"
|
||||
@@ -3792,9 +3795,9 @@
|
||||
"cs": "Mexické peso",
|
||||
"cy": "peso (Mecsico)",
|
||||
"de": "Mexikanischer Peso",
|
||||
"en": "Mexican peso",
|
||||
"en": "peso",
|
||||
"eo": "meksika peso",
|
||||
"es": "peso mexicano",
|
||||
"es": "peso",
|
||||
"et": "Mehhiko peeso",
|
||||
"eu": "Mexikar peso",
|
||||
"fi": "Meksikon peso",
|
||||
@@ -4208,7 +4211,7 @@
|
||||
"fi": "Panaman balboa",
|
||||
"fr": "Balboa",
|
||||
"ga": "balboa Phanama",
|
||||
"gl": "Balboa",
|
||||
"gl": "balboa",
|
||||
"he": "בלבואה",
|
||||
"hr": "Panamska balboa",
|
||||
"hu": "panamai balboa",
|
||||
@@ -4269,7 +4272,7 @@
|
||||
"tr": "Nuevo Sol",
|
||||
"tt": "Перу яңа соле",
|
||||
"uk": "Новий соль",
|
||||
"vi": "Sol Peru"
|
||||
"vi": "Sol Perú"
|
||||
},
|
||||
"PGK": {
|
||||
"ar": "كينا بابوا غينيا الجديدة",
|
||||
@@ -4926,7 +4929,7 @@
|
||||
"ar": "دولار سنغافوري",
|
||||
"bg": "Сингапурски долар",
|
||||
"bn": "সিঙ্গাপুর ডলার",
|
||||
"ca": "dòlar de Singapur",
|
||||
"ca": "dòlar singapurès",
|
||||
"cs": "Singapurský dolar",
|
||||
"da": "singaporeansk dollar",
|
||||
"de": "Singapur-Dollar",
|
||||
@@ -5534,7 +5537,7 @@
|
||||
"af": "Nuwe Taiwannese dollar",
|
||||
"ar": "دولار تايواني جديد",
|
||||
"bg": "Нов тайвански долар",
|
||||
"ca": "nou dòlar de Taiwan",
|
||||
"ca": "Nou dòlar taiwanès",
|
||||
"cs": "Tchajwanský dolar",
|
||||
"cy": "Doler Newydd Taiwan",
|
||||
"da": "taiwan dollar",
|
||||
@@ -5808,6 +5811,7 @@
|
||||
"lt": "Uzbekijos sumas",
|
||||
"lv": "Uzbekistānas soms",
|
||||
"nl": "Oezbeekse sum",
|
||||
"oc": "som ozbèc",
|
||||
"pa": "ਉਜ਼ਬੇਕਿਸਤਾਨੀ ਸੋਮ",
|
||||
"pl": "Sum",
|
||||
"pt": "som usbeque",
|
||||
@@ -5948,6 +5952,7 @@
|
||||
"sk": "Tala",
|
||||
"sr": "самоанска тала",
|
||||
"sv": "Samoansk Tala",
|
||||
"tr": "Samoa talası",
|
||||
"tt": "самоа таласы",
|
||||
"uk": "Самоанська тала"
|
||||
},
|
||||
@@ -6095,12 +6100,14 @@
|
||||
"hu": "karibi forint",
|
||||
"it": "fiorino caraibico",
|
||||
"ja": "カリブ・ギルダー",
|
||||
"ko": "카리브 휠던",
|
||||
"nl": "Caribische gulden",
|
||||
"pap": "Florin karibense",
|
||||
"pl": "Gulden karaibski",
|
||||
"pt": "Florim do Caribe",
|
||||
"ro": "Gulden caraibian",
|
||||
"ru": "Карибский гульден",
|
||||
"sk": "Karibský gulden",
|
||||
"sl": "karibski goldinar"
|
||||
},
|
||||
"XDR": {
|
||||
@@ -7074,6 +7081,7 @@
|
||||
"brazilski real": "BRL",
|
||||
"brazilský real": "BRL",
|
||||
"brazílsky real": "BRL",
|
||||
"brazīlijas reāls": "BRL",
|
||||
"brezilya reali": "BRL",
|
||||
"brit font": "GBP",
|
||||
"brita pundo": "GBP",
|
||||
@@ -7147,6 +7155,7 @@
|
||||
"burundžio frankas": "BIF",
|
||||
"butana ngultrumo": "BTN",
|
||||
"butanski ngultrum": "BTN",
|
||||
"butānas ngultrums": "BTN",
|
||||
"butut": "GMD",
|
||||
"bututs": "GMD",
|
||||
"bwp": "BWP",
|
||||
@@ -7818,6 +7827,7 @@
|
||||
"dirrã marroquino": "MAD",
|
||||
"dírham de los emiratos árabes unidos": "AED",
|
||||
"dírham dels emirats àrabs units": "AED",
|
||||
"dírham emiratià": "AED",
|
||||
"dírham marroquí": "MAD",
|
||||
"djf": "DJF",
|
||||
"djiboeti frank": "DJF",
|
||||
@@ -8250,6 +8260,7 @@
|
||||
"dòlar namibià": "NAD",
|
||||
"dòlar neozelandès": "NZD",
|
||||
"dòlar salomonès": "SBD",
|
||||
"dòlar singapurès": "SGD",
|
||||
"dòlar surinamès": "SRD",
|
||||
"dòlar taiwanès": "TWD",
|
||||
"dòlars canadencs": "CAD",
|
||||
@@ -10513,6 +10524,7 @@
|
||||
"ngultrum na bútáine": "BTN",
|
||||
"ngultrumas": "BTN",
|
||||
"ngultrumo": "BTN",
|
||||
"ngultrums": "BTN",
|
||||
"ngwee": "ZMW",
|
||||
"nhân dân tệ": "CNY",
|
||||
"nhân dân tệ trung quốc": "CNY",
|
||||
@@ -11629,6 +11641,7 @@
|
||||
"samoa dolaro": "WST",
|
||||
"samoa tala": "WST",
|
||||
"samoa talao": "WST",
|
||||
"samoa talası": "WST",
|
||||
"samoaanse tala": "WST",
|
||||
"samoan tala": "WST",
|
||||
"samoan tālā": "WST",
|
||||
@@ -11839,10 +11852,10 @@
|
||||
"sol d'or": "PEN",
|
||||
"sol de oro": "PEN",
|
||||
"sol novo": "PEN",
|
||||
"sol peru": "PEN",
|
||||
"sol peruan": "PEN",
|
||||
"sol peruano": "PEN",
|
||||
"sol peruviano": "PEN",
|
||||
"sol perú": "PEN",
|
||||
"solomon adaları doları": "SBD",
|
||||
"solomon dollar": "SBD",
|
||||
"solomon islands dollar": "SBD",
|
||||
@@ -11868,6 +11881,7 @@
|
||||
"som kîrgîz": "KGS",
|
||||
"som na cirgeastáine": "KGS",
|
||||
"som na húisbéiceastáine": "UZS",
|
||||
"som ozbèc": "UZS",
|
||||
"som quirguiz": "KGS",
|
||||
"som usbeco": "UZS",
|
||||
"som usbeque": "UZS",
|
||||
@@ -11915,6 +11929,7 @@
|
||||
"sovjetisk rubel": "RUB",
|
||||
"soʻm": "UZS",
|
||||
"soʻm uzbekistan": "UZS",
|
||||
"sōm": "KGS",
|
||||
"söm": "UZS",
|
||||
"special drawing right": "XDR",
|
||||
"special drawing rights": "XDR",
|
||||
@@ -14440,6 +14455,7 @@
|
||||
"דולר פיג'י": "FJD",
|
||||
"דולר קיימני": "KYD",
|
||||
"דולר קנדי": "CAD",
|
||||
"דולר של איי קיימן": "KYD",
|
||||
"דונג וייטנאמי ": "VND",
|
||||
"דינר אלג'ירי": "DZD",
|
||||
"דינר בחרייני": "BHD",
|
||||
@@ -14647,6 +14663,7 @@
|
||||
"الجنيه الإسترليني": "GBP",
|
||||
"الجنيه السودانى": "SDG",
|
||||
"الجنيه المصري": "EGP",
|
||||
"الدولار الامريكي": "IQD",
|
||||
"الدولار البربادوسي": "BBD",
|
||||
"الدولار البهامي": "BSD",
|
||||
"الدولار الكندي": "CAD",
|
||||
@@ -14906,6 +14923,7 @@
|
||||
"شيلينغ كينيي": "KES",
|
||||
"عملة السعودية": "SAR",
|
||||
"عملة المملكة العربية السعودية": "SAR",
|
||||
"عملة ذهبيه": "IQD",
|
||||
"عملة قطر": "QAR",
|
||||
"غواراني": "PYG",
|
||||
"غواراني باراغواي": "PYG",
|
||||
@@ -15735,6 +15753,7 @@
|
||||
"203"
|
||||
],
|
||||
"칠레 페소": "CLP",
|
||||
"카리브 휠던": "XCG",
|
||||
"카보베르데 에스쿠도": "CVE",
|
||||
"카보베르데 이스쿠두": "CVE",
|
||||
"카보베르데에스쿠도": "CVE",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Simple implementation to store TrackerPatterns data in a SQL database."""
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
import typing as t
|
||||
|
||||
@@ -119,6 +120,12 @@ class TrackerPatternsDB:
|
||||
|
||||
for rule in self.rules():
|
||||
|
||||
query_str: str = parsed_new_url.query
|
||||
if not query_str:
|
||||
# There are no more query arguments in the parsed_new_url on
|
||||
# which rules can be applied, stop iterating over the rules.
|
||||
break
|
||||
|
||||
if not re.match(rule[self.Fields.url_regexp], new_url):
|
||||
# no match / ignore pattern
|
||||
continue
|
||||
@@ -136,18 +143,32 @@ class TrackerPatternsDB:
|
||||
# overlapping urlPattern like ".*"
|
||||
continue
|
||||
|
||||
# remove tracker arguments from the url-query part
|
||||
query_args: list[tuple[str, str]] = list(parse_qsl(parsed_new_url.query))
|
||||
if query_args:
|
||||
# remove tracker arguments from the url-query part
|
||||
for name, val in query_args.copy():
|
||||
# remove URL arguments
|
||||
for pattern in rule[self.Fields.del_args]:
|
||||
if re.match(pattern, name):
|
||||
log.debug(
|
||||
"TRACKER_PATTERNS: %s remove tracker arg: %s='%s'", parsed_new_url.netloc, name, val
|
||||
)
|
||||
query_args.remove((name, val))
|
||||
|
||||
for name, val in query_args.copy():
|
||||
# remove URL arguments
|
||||
parsed_new_url = parsed_new_url._replace(query=urlencode(query_args))
|
||||
new_url = urlunparse(parsed_new_url)
|
||||
|
||||
else:
|
||||
# The query argument for URLs like:
|
||||
# - 'http://example.org?q=' --> query_str is 'q=' and query_args is []
|
||||
# - 'http://example.org?/foo/bar' --> query_str is 'foo/bar' and query_args is []
|
||||
# is a simple string and not a key/value dict.
|
||||
for pattern in rule[self.Fields.del_args]:
|
||||
if re.match(pattern, name):
|
||||
log.debug("TRACKER_PATTERNS: %s remove tracker arg: %s='%s'", parsed_new_url.netloc, name, val)
|
||||
query_args.remove((name, val))
|
||||
|
||||
parsed_new_url = parsed_new_url._replace(query=urlencode(query_args))
|
||||
new_url = urlunparse(parsed_new_url)
|
||||
if re.match(pattern, query_str):
|
||||
log.debug("TRACKER_PATTERNS: %s remove tracker arg: '%s'", parsed_new_url.netloc, query_str)
|
||||
parsed_new_url = parsed_new_url._replace(query="")
|
||||
new_url = urlunparse(parsed_new_url)
|
||||
break
|
||||
|
||||
if new_url != url:
|
||||
return new_url
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
],
|
||||
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
|
||||
"versions": [
|
||||
"143.0",
|
||||
"142.0"
|
||||
"144.0",
|
||||
"143.0"
|
||||
]
|
||||
}
|
||||
@@ -3294,6 +3294,16 @@
|
||||
"symbol": "slug",
|
||||
"to_si_factor": 14.593903
|
||||
},
|
||||
"Q136416965": {
|
||||
"si_name": null,
|
||||
"symbol": "GT/S",
|
||||
"to_si_factor": null
|
||||
},
|
||||
"Q136417074": {
|
||||
"si_name": null,
|
||||
"symbol": "MT/S",
|
||||
"to_si_factor": null
|
||||
},
|
||||
"Q1374438": {
|
||||
"si_name": "Q11574",
|
||||
"symbol": "ks",
|
||||
@@ -6375,9 +6385,9 @@
|
||||
"to_si_factor": 86400.0
|
||||
},
|
||||
"Q577": {
|
||||
"si_name": null,
|
||||
"si_name": "Q11574",
|
||||
"symbol": "a",
|
||||
"to_si_factor": null
|
||||
"to_si_factor": 31557600.0
|
||||
},
|
||||
"Q57899268": {
|
||||
"si_name": "Q3332095",
|
||||
|
||||
@@ -12,7 +12,7 @@ from urllib.parse import urlencode, urljoin, urlparse
|
||||
import lxml
|
||||
import babel
|
||||
|
||||
from searx.utils import extract_text, eval_xpath_list, eval_xpath_getindex
|
||||
from searx.utils import extract_text, eval_xpath_list, eval_xpath_getindex, searxng_useragent
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.locales import language_tag
|
||||
|
||||
@@ -45,7 +45,7 @@ def request(query, params):
|
||||
query += ' (' + eng_lang + ')'
|
||||
# wiki.archlinux.org is protected by anubis
|
||||
# - https://github.com/searxng/searxng/issues/4646#issuecomment-2817848019
|
||||
params['headers']['User-Agent'] = "SearXNG"
|
||||
params['headers']['User-Agent'] = searxng_useragent()
|
||||
elif netloc == 'wiki.archlinuxcn.org':
|
||||
base_url = 'https://' + netloc + '/wzh/index.php?'
|
||||
|
||||
|
||||
190
searx/engines/azure.py
Normal file
190
searx/engines/azure.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Engine for Azure resources. This engine mimics the standard search bar in Azure
|
||||
Portal (for resources and resource groups).
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
You must `register an application in Microsoft Entra ID`_ and assign it the
|
||||
'Reader' role in your subscription.
|
||||
|
||||
To use this engine, add an entry similar to the following to your engine list in
|
||||
``settings.yml``:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: azure
|
||||
engine: azure
|
||||
...
|
||||
azure_tenant_id: "your_tenant_id"
|
||||
azure_client_id: "your_client_id"
|
||||
azure_client_secret: "your_client_secret"
|
||||
azure_token_expiration_seconds: 5000
|
||||
|
||||
.. _register an application in Microsoft Entra ID:
|
||||
https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app
|
||||
|
||||
"""
|
||||
import typing as t
|
||||
|
||||
from searx.enginelib import EngineCache
|
||||
from searx.network import post as http_post
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
engine_type = "online"
|
||||
categories = ["it", "cloud"]
|
||||
|
||||
# Default values, should be overridden in settings.yml
|
||||
azure_tenant_id = ""
|
||||
azure_client_id = ""
|
||||
azure_client_secret = ""
|
||||
azure_token_expiration_seconds = 5000
|
||||
"""Time for which an auth token is valid (sec.)"""
|
||||
azure_batch_endpoint = "https://management.azure.com/batch?api-version=2020-06-01"
|
||||
|
||||
about = {
|
||||
"website": "https://www.portal.azure.com",
|
||||
"wikidata_id": "Q725967",
|
||||
"official_api_documentation": "https://learn.microsoft.com/en-us/\
|
||||
rest/api/azure-resourcegraph/?view=rest-azureresourcegraph-resourcegraph-2024-04-01",
|
||||
"use_official_api": True,
|
||||
"require_api_key": True,
|
||||
"results": "JSON",
|
||||
"language": "en",
|
||||
}
|
||||
|
||||
CACHE: EngineCache
|
||||
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
|
||||
seconds."""
|
||||
|
||||
|
||||
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||
"""Initialization of the engine.
|
||||
|
||||
- Instantiate a cache for this engine (:py:obj:`CACHE`).
|
||||
- Checks whether the tenant_id, client_id and client_secret are set,
|
||||
otherwise the engine is inactive.
|
||||
|
||||
"""
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache(engine_settings["name"])
|
||||
|
||||
missing_opts: list[str] = []
|
||||
for opt in ("azure_tenant_id", "azure_client_id", "azure_client_secret"):
|
||||
if not engine_settings.get(opt, ""):
|
||||
missing_opts.append(opt)
|
||||
if missing_opts:
|
||||
logger.error("missing values for options: %s", ", ".join(missing_opts))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def authenticate(t_id: str, c_id: str, c_secret: str) -> str:
|
||||
"""Authenticates to Azure using Oauth2 Client Credentials Flow and returns
|
||||
an access token."""
|
||||
|
||||
url = f"https://login.microsoftonline.com/{t_id}/oauth2/v2.0/token"
|
||||
body = {
|
||||
"client_id": c_id,
|
||||
"client_secret": c_secret,
|
||||
"grant_type": "client_credentials",
|
||||
"scope": "https://management.azure.com/.default",
|
||||
}
|
||||
|
||||
resp: SXNG_Response = http_post(url, body)
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"Azure authentication failed (status {resp.status_code}): {resp.text}")
|
||||
return resp.json()["access_token"]
|
||||
|
||||
|
||||
def get_auth_token(t_id: str, c_id: str, c_secret: str) -> str:
|
||||
key = f"azure_tenant_id: {t_id:}, azure_client_id: {c_id}, azure_client_secret: {c_secret}"
|
||||
token: str | None = CACHE.get(key)
|
||||
if token:
|
||||
return token
|
||||
token = authenticate(t_id, c_id, c_secret)
|
||||
CACHE.set(key=key, value=token, expire=azure_token_expiration_seconds)
|
||||
return token
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
|
||||
token = get_auth_token(azure_tenant_id, azure_client_id, azure_client_secret)
|
||||
|
||||
params["url"] = azure_batch_endpoint
|
||||
params["method"] = "POST"
|
||||
params["headers"]["Authorization"] = f"Bearer {token}"
|
||||
params["headers"]["Content-Type"] = "application/json"
|
||||
params["json"] = {
|
||||
"requests": [
|
||||
{
|
||||
"url": "/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01",
|
||||
"httpMethod": "POST",
|
||||
"name": "resourceGroups",
|
||||
"requestHeaderDetails": {"commandName": "Microsoft.ResourceGraph"},
|
||||
"content": {
|
||||
"query": (
|
||||
f"ResourceContainers"
|
||||
f" | where (name contains ('{query}'))"
|
||||
f" | where (type =~ ('Microsoft.Resources/subscriptions/resourcegroups'))"
|
||||
f" | project id,name,type,kind,subscriptionId,resourceGroup"
|
||||
f" | extend matchscore = name startswith '{query}'"
|
||||
f" | extend normalizedName = tolower(tostring(name))"
|
||||
f" | sort by matchscore desc, normalizedName asc"
|
||||
f" | take 30"
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"url": "/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01",
|
||||
"httpMethod": "POST",
|
||||
"name": "resources",
|
||||
"requestHeaderDetails": {
|
||||
"commandName": "Microsoft.ResourceGraph",
|
||||
},
|
||||
"content": {
|
||||
"query": f"Resources | where name contains '{query}' | take 30",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
res = EngineResults()
|
||||
json_data = resp.json()
|
||||
|
||||
for result in json_data["responses"]:
|
||||
if result["name"] == "resourceGroups":
|
||||
for data in result["content"]["data"]:
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=(
|
||||
f"https://portal.azure.com/#@/resource"
|
||||
f"/subscriptions/{data['subscriptionId']}/resourceGroups/{data['name']}/overview"
|
||||
),
|
||||
title=data["name"],
|
||||
content=f"Resource Group in Subscription: {data['subscriptionId']}",
|
||||
)
|
||||
)
|
||||
elif result["name"] == "resources":
|
||||
for data in result["content"]["data"]:
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=(
|
||||
f"https://portal.azure.com/#@/resource"
|
||||
f"/subscriptions/{data['subscriptionId']}/resourceGroups/{data['resourceGroup']}"
|
||||
f"/providers/{data['type']}/{data['name']}/overview"
|
||||
),
|
||||
title=data["name"],
|
||||
content=(
|
||||
f"Resource of type {data['type']} in Subscription:"
|
||||
f" {data['subscriptionId']}, Resource Group: {data['resourceGroup']}"
|
||||
),
|
||||
)
|
||||
)
|
||||
return res
|
||||
@@ -108,6 +108,10 @@ def request(query, params):
|
||||
time_ranges = {'day': '1', 'week': '2', 'month': '3', 'year': f'5_{unix_day-365}_{unix_day}'}
|
||||
params['url'] += f'&filters=ex1:"ez{time_ranges[params["time_range"]]}"'
|
||||
|
||||
# in some regions where geoblocking is employed (e.g. China),
|
||||
# www.bing.com redirects to the regional version of Bing
|
||||
params['allow_redirects'] = True
|
||||
|
||||
return params
|
||||
|
||||
|
||||
@@ -197,7 +201,6 @@ def fetch_traits(engine_traits: EngineTraits):
|
||||
"User-Agent": gen_useragent(),
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||
"Accept-Language": "en-US;q=0.5,en;q=0.3",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"DNT": "1",
|
||||
"Connection": "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
|
||||
@@ -198,9 +198,6 @@ time_range_map: dict[str, str] = {
|
||||
|
||||
def request(query: str, params: dict[str, t.Any]) -> None:
|
||||
|
||||
# Don't accept br encoding / see https://github.com/searxng/searxng/pull/1787
|
||||
params['headers']['Accept-Encoding'] = 'gzip, deflate'
|
||||
|
||||
args: dict[str, t.Any] = {
|
||||
'q': query,
|
||||
'source': 'web',
|
||||
@@ -436,14 +433,11 @@ def fetch_traits(engine_traits: EngineTraits):
|
||||
|
||||
engine_traits.custom["ui_lang"] = {}
|
||||
|
||||
headers = {
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
}
|
||||
lang_map = {'no': 'nb'} # norway
|
||||
|
||||
# languages (UI)
|
||||
|
||||
resp = get('https://search.brave.com/settings', headers=headers)
|
||||
resp = get('https://search.brave.com/settings')
|
||||
|
||||
if not resp.ok: # type: ignore
|
||||
print("ERROR: response from Brave is not OK.")
|
||||
@@ -472,7 +466,7 @@ def fetch_traits(engine_traits: EngineTraits):
|
||||
|
||||
# search regions of brave
|
||||
|
||||
resp = get('https://cdn.search.brave.com/serp/v2/_app/immutable/chunks/parameters.734c106a.js', headers=headers)
|
||||
resp = get('https://cdn.search.brave.com/serp/v2/_app/immutable/chunks/parameters.734c106a.js')
|
||||
|
||||
if not resp.ok: # type: ignore
|
||||
print("ERROR: response from Brave is not OK.")
|
||||
|
||||
@@ -23,14 +23,14 @@ paging = True
|
||||
# search-url
|
||||
base_url = 'https://www.deviantart.com'
|
||||
|
||||
results_xpath = '//div[@class="_2pZkk"]/div/div/a'
|
||||
results_xpath = '//div[@class="V_S0t_"]/div/div/a'
|
||||
url_xpath = './@href'
|
||||
thumbnail_src_xpath = './div/img/@src'
|
||||
img_src_xpath = './div/img/@srcset'
|
||||
title_xpath = './@aria-label'
|
||||
premium_xpath = '../div/div/div/text()'
|
||||
premium_keytext = 'Watch the artist to view this deviation'
|
||||
cursor_xpath = '(//a[@class="_1OGeq"]/@href)[last()]'
|
||||
cursor_xpath = '(//a[@class="vQ2brP"]/@href)[last()]'
|
||||
|
||||
|
||||
def request(query, params):
|
||||
|
||||
63
searx/engines/devicons.py
Normal file
63
searx/engines/devicons.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Devicons (icons)"""
|
||||
|
||||
import typing as t
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from extended_types import SXNG_Response
|
||||
from search.processors.online import OnlineParams
|
||||
|
||||
|
||||
about = {
|
||||
"website": "https://devicon.dev/",
|
||||
"wikidata_id": None,
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": True,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
cdn_base_url = "https://cdn.jsdelivr.net/gh/devicons/devicon@latest"
|
||||
categories = ["images", "icons"]
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams"):
|
||||
params["url"] = f"{cdn_base_url}/devicon.json"
|
||||
params['query'] = query
|
||||
return params
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
res = EngineResults()
|
||||
query_parts = resp.search_params["query"].lower().split(" ")
|
||||
|
||||
def is_result_match(result: dict[str, t.Any]) -> bool:
|
||||
for part in query_parts:
|
||||
if part in result["name"]:
|
||||
return True
|
||||
|
||||
for tag in result["altnames"] + result["tags"]:
|
||||
if part in tag:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
filtered_results = filter(is_result_match, resp.json())
|
||||
for result in filtered_results:
|
||||
for image_type in result["versions"]["svg"]:
|
||||
img_src = f"{cdn_base_url}/icons/{result['name']}/{result['name']}-{image_type}.svg"
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
{
|
||||
"template": "images.html",
|
||||
"url": img_src,
|
||||
"title": result["name"],
|
||||
"content": f"Base color: {result['color']}",
|
||||
"img_src": img_src,
|
||||
"img_format": "SVG",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
@@ -6,6 +6,7 @@ from urllib.parse import urlencode
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from flask_babel import gettext
|
||||
from searx.utils import html_to_text
|
||||
|
||||
# Engine metadata
|
||||
about = {
|
||||
@@ -75,6 +76,7 @@ def response(resp):
|
||||
object_id = hit["objectID"]
|
||||
points = hit.get("points") or 0
|
||||
num_comments = hit.get("num_comments") or 0
|
||||
content = hit.get("url") or html_to_text(hit.get("comment_text")) or html_to_text(hit.get("story_text"))
|
||||
|
||||
metadata = ""
|
||||
if points != 0 or num_comments != 0:
|
||||
@@ -83,7 +85,7 @@ def response(resp):
|
||||
{
|
||||
"title": hit.get("title") or f"{gettext('author')}: {hit['author']}",
|
||||
"url": f"https://news.ycombinator.com/item?id={object_id}",
|
||||
"content": hit.get("url") or hit.get("comment_text") or hit.get("story_text") or "",
|
||||
"content": content,
|
||||
"metadata": metadata,
|
||||
"author": hit["author"],
|
||||
"publishedDate": datetime.fromtimestamp(hit["created_at_i"]),
|
||||
|
||||
@@ -43,9 +43,12 @@ import babel
|
||||
from httpx import Response
|
||||
from lxml import html
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.extended_types import SXNG_Response
|
||||
|
||||
from searx.locales import get_official_locales, language_tag, region_tag
|
||||
from searx.utils import eval_xpath_list
|
||||
from searx.result_types import EngineResults, MainResult
|
||||
from searx.network import raise_for_httperror
|
||||
|
||||
search_url = "https://leta.mullvad.net"
|
||||
|
||||
@@ -112,7 +115,8 @@ class DataNodeResultIndices(t.TypedDict):
|
||||
favicon: int
|
||||
|
||||
|
||||
def request(query: str, params: dict):
|
||||
def request(query: str, params: dict[str, t.Any]) -> None:
|
||||
params["raise_for_httperror"] = False
|
||||
params["method"] = "GET"
|
||||
args = {
|
||||
"q": query,
|
||||
@@ -136,10 +140,19 @@ def request(query: str, params: dict):
|
||||
|
||||
params["url"] = f"{search_url}/search/__data.json?{urlencode(args)}"
|
||||
|
||||
return params
|
||||
|
||||
def response(resp: SXNG_Response) -> EngineResults:
|
||||
results = EngineResults()
|
||||
|
||||
if resp.status_code in (403, 429):
|
||||
# It doesn't matter if you're using Mullvad's VPN and a proper browser,
|
||||
# you'll still get blocked for specific searches with a 403 or 429 HTTP
|
||||
# status code.
|
||||
# https://github.com/searxng/searxng/issues/5328#issue-3518337233
|
||||
return results
|
||||
# raise for other errors
|
||||
raise_for_httperror(resp)
|
||||
|
||||
def response(resp: Response) -> EngineResults:
|
||||
json_response = resp.json()
|
||||
|
||||
nodes = json_response["nodes"]
|
||||
@@ -159,7 +172,6 @@ def response(resp: Response) -> EngineResults:
|
||||
|
||||
query_items_indices = query_meta_data["items"]
|
||||
|
||||
results = EngineResults()
|
||||
for idx in data_nodes[query_items_indices]:
|
||||
query_item_indices: DataNodeResultIndices = data_nodes[idx]
|
||||
results.add(
|
||||
|
||||
@@ -55,15 +55,18 @@ def response(resp):
|
||||
if result['type'] == 'story':
|
||||
continue
|
||||
|
||||
main_image = result['images']['orig']
|
||||
results.append(
|
||||
{
|
||||
'template': 'images.html',
|
||||
'url': result['link'] or f"{base_url}/pin/{result['id']}/",
|
||||
'url': result.get('link') or f"{base_url}/pin/{result['id']}/",
|
||||
'title': result.get('title') or result.get('grid_title'),
|
||||
'content': (result.get('rich_summary') or {}).get('display_description') or "",
|
||||
'img_src': result['images']['orig']['url'],
|
||||
'img_src': main_image['url'],
|
||||
'thumbnail_src': result['images']['236x']['url'],
|
||||
'source': (result.get('rich_summary') or {}).get('site_name'),
|
||||
'resolution': f"{main_image['width']}x{main_image['height']}",
|
||||
'author': f"{result['pinner'].get('full_name')} ({result['pinner']['username']})",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -89,6 +89,9 @@ time_range_support = True
|
||||
send_accept_language_header = True
|
||||
categories = ["general", "web"] # general, images, videos, news
|
||||
|
||||
# HTTP2 requests immediately get blocked by a CAPTCHA
|
||||
enable_http2 = False
|
||||
|
||||
search_type = "search"
|
||||
"""must be any of ``search``, ``images``, ``videos``, ``news``"""
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ from searx.exceptions import (
|
||||
SearxEngineAPIException,
|
||||
SearxEngineTooManyRequestsException,
|
||||
SearxEngineCaptchaException,
|
||||
SearxEngineAccessDeniedException,
|
||||
)
|
||||
from searx.network import raise_for_httperror
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
@@ -81,6 +82,9 @@ max_page = 5
|
||||
"""5 pages maximum (``&p=5``): Trying to do more just results in an improper
|
||||
redirect"""
|
||||
|
||||
# Otherwise Qwant will return 403 if not set
|
||||
send_accept_language_header = True
|
||||
|
||||
qwant_categ = None
|
||||
"""One of ``web-lite`` (or ``web``), ``news``, ``images`` or ``videos``"""
|
||||
|
||||
@@ -130,17 +134,17 @@ def request(query, params):
|
||||
|
||||
elif qwant_categ == 'images':
|
||||
|
||||
args['count'] = 50
|
||||
args['locale'] = q_locale
|
||||
args['safesearch'] = params['safesearch']
|
||||
args['count'] = 50
|
||||
args['tgp'] = 3
|
||||
args['offset'] = (params['pageno'] - 1) * args['count']
|
||||
|
||||
else: # web, news, videos
|
||||
|
||||
args['count'] = 10
|
||||
args['locale'] = q_locale
|
||||
args['safesearch'] = params['safesearch']
|
||||
args['count'] = 10
|
||||
args['llm'] = 'false'
|
||||
args['tgp'] = 3
|
||||
args['offset'] = (params['pageno'] - 1) * args['count']
|
||||
@@ -184,8 +188,12 @@ def parse_web_api(resp):
|
||||
|
||||
results = []
|
||||
|
||||
# load JSON result
|
||||
search_results = loads(resp.text)
|
||||
# Try to load JSON result
|
||||
try:
|
||||
search_results = loads(resp.text)
|
||||
except ValueError:
|
||||
search_results = {}
|
||||
|
||||
data = search_results.get('data', {})
|
||||
|
||||
# check for an API error
|
||||
@@ -195,6 +203,8 @@ def parse_web_api(resp):
|
||||
raise SearxEngineTooManyRequestsException()
|
||||
if search_results.get("data", {}).get("error_data", {}).get("captchaUrl") is not None:
|
||||
raise SearxEngineCaptchaException()
|
||||
if resp.status_code == 403:
|
||||
raise SearxEngineAccessDeniedException()
|
||||
msg = ",".join(data.get('message', ['unknown']))
|
||||
raise SearxEngineAPIException(f"{msg} ({error_code})")
|
||||
|
||||
|
||||
@@ -13,23 +13,12 @@ Configuration
|
||||
|
||||
You must configure the following settings:
|
||||
|
||||
``base_url``:
|
||||
Location where recoll-webui can be reached.
|
||||
- :py:obj:`base_url`
|
||||
- :py:obj:`mount_prefix`
|
||||
- :py:obj:`dl_prefix`
|
||||
- :py:obj:`search_dir`
|
||||
|
||||
``mount_prefix``:
|
||||
Location where the file hierarchy is mounted on your *local* filesystem.
|
||||
|
||||
``dl_prefix``:
|
||||
Location where the file hierarchy as indexed by recoll can be reached.
|
||||
|
||||
``search_dir``:
|
||||
Part of the indexed file hierarchy to be search, if empty the full domain is
|
||||
searched.
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
Scenario:
|
||||
Example scenario:
|
||||
|
||||
#. Recoll indexes a local filesystem mounted in ``/export/documents/reference``,
|
||||
#. the Recoll search interface can be reached at https://recoll.example.org/ and
|
||||
@@ -37,107 +26,127 @@ Scenario:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
base_url: https://recoll.example.org/
|
||||
base_url: https://recoll.example.org
|
||||
mount_prefix: /export/documents
|
||||
dl_prefix: https://download.example.org
|
||||
search_dir: ''
|
||||
search_dir: ""
|
||||
|
||||
Implementations
|
||||
===============
|
||||
|
||||
"""
|
||||
import typing as t
|
||||
|
||||
from datetime import date, timedelta
|
||||
from json import loads
|
||||
from urllib.parse import urlencode, quote
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
|
||||
# about
|
||||
about = {
|
||||
"website": None,
|
||||
"wikidata_id": 'Q15735774',
|
||||
"official_api_documentation": 'https://www.lesbonscomptes.com/recoll/',
|
||||
"wikidata_id": "Q15735774",
|
||||
"official_api_documentation": "https://www.lesbonscomptes.com/recoll/",
|
||||
"use_official_api": True,
|
||||
"require_api_key": False,
|
||||
"results": 'JSON',
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
# engine dependent config
|
||||
paging = True
|
||||
time_range_support = True
|
||||
|
||||
# parameters from settings.yml
|
||||
base_url = None
|
||||
search_dir = ''
|
||||
mount_prefix = None
|
||||
dl_prefix = None
|
||||
base_url: str = ""
|
||||
"""Location where recoll-webui can be reached."""
|
||||
|
||||
# embedded
|
||||
embedded_url = '<{ttype} controls height="166px" ' + 'src="{url}" type="{mtype}"></{ttype}>'
|
||||
mount_prefix: str = ""
|
||||
"""Location where the file hierarchy is mounted on your *local* filesystem."""
|
||||
|
||||
dl_prefix: str = ""
|
||||
"""Location where the file hierarchy as indexed by recoll can be reached."""
|
||||
|
||||
search_dir: str = ""
|
||||
"""Part of the indexed file hierarchy to be search, if empty the full domain is
|
||||
searched."""
|
||||
|
||||
_s2i: dict[str | None, int] = {"day": 1, "week": 7, "month": 30, "year": 365}
|
||||
|
||||
|
||||
# helper functions
|
||||
def get_time_range(time_range):
|
||||
sw = {'day': 1, 'week': 7, 'month': 30, 'year': 365} # pylint: disable=invalid-name
|
||||
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||
"""Initialization of the Recoll engine, checks if the mandatory values are
|
||||
configured.
|
||||
"""
|
||||
missing: list[str] = []
|
||||
for cfg_name in ["base_url", "mount_prefix", "dl_prefix"]:
|
||||
if not engine_settings.get(cfg_name):
|
||||
missing.append(cfg_name)
|
||||
if missing:
|
||||
logger.error("missing recoll configuration: %s", missing)
|
||||
return False
|
||||
|
||||
offset = sw.get(time_range, 0)
|
||||
if engine_settings["base_url"].endswith("/"):
|
||||
engine_settings["base_url"] = engine_settings["base_url"][:-1]
|
||||
return True
|
||||
|
||||
|
||||
def search_after(time_range: str | None) -> str:
|
||||
offset = _s2i.get(time_range, 0)
|
||||
if not offset:
|
||||
return ''
|
||||
|
||||
return ""
|
||||
return (date.today() - timedelta(days=offset)).isoformat()
|
||||
|
||||
|
||||
# do search-request
|
||||
def request(query, params):
|
||||
search_after = get_time_range(params['time_range'])
|
||||
search_url = base_url + 'json?{query}&highlight=0'
|
||||
params['url'] = search_url.format(
|
||||
query=urlencode({'query': query, 'page': params['pageno'], 'after': search_after, 'dir': search_dir})
|
||||
)
|
||||
|
||||
return params
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
args = {
|
||||
"query": query,
|
||||
"page": params["pageno"],
|
||||
"after": search_after(params["time_range"]),
|
||||
"dir": search_dir,
|
||||
"highlight": 0,
|
||||
}
|
||||
params["url"] = f"{base_url}/json?{urlencode(args)}"
|
||||
|
||||
|
||||
# get response from search-request
|
||||
def response(resp):
|
||||
results = []
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
|
||||
response_json = loads(resp.text)
|
||||
res = EngineResults()
|
||||
json_data = resp.json()
|
||||
|
||||
if not response_json:
|
||||
return []
|
||||
if not json_data:
|
||||
return res
|
||||
|
||||
for result in response_json.get('results', []):
|
||||
title = result['label']
|
||||
url = result['url'].replace('file://' + mount_prefix, dl_prefix)
|
||||
content = '{}'.format(result['snippet'])
|
||||
for result in json_data.get("results", []):
|
||||
|
||||
# append result
|
||||
item = {'url': url, 'title': title, 'content': content, 'template': 'files.html'}
|
||||
url = result.get("url", "").replace("file://" + mount_prefix, dl_prefix)
|
||||
|
||||
if result['size']:
|
||||
item['size'] = int(result['size'])
|
||||
|
||||
for parameter in ['filename', 'abstract', 'author', 'mtype', 'time']:
|
||||
if result[parameter]:
|
||||
item[parameter] = result[parameter]
|
||||
mtype = subtype = result.get("mtype", "")
|
||||
if mtype:
|
||||
mtype, subtype = (mtype.split("/", 1) + [""])[:2]
|
||||
|
||||
# facilitate preview support for known mime types
|
||||
if 'mtype' in result and '/' in result['mtype']:
|
||||
(mtype, subtype) = result['mtype'].split('/')
|
||||
item['mtype'] = mtype
|
||||
item['subtype'] = subtype
|
||||
thumbnail = embedded = ""
|
||||
if mtype in ["audio", "video"]:
|
||||
embedded = url
|
||||
if mtype in ["image"] and subtype in ["bmp", "gif", "jpeg", "png"]:
|
||||
thumbnail = url
|
||||
|
||||
if mtype in ['audio', 'video']:
|
||||
item['embedded'] = embedded_url.format(
|
||||
ttype=mtype, url=quote(url.encode('utf8'), '/:'), mtype=result['mtype']
|
||||
)
|
||||
|
||||
if mtype in ['image'] and subtype in ['bmp', 'gif', 'jpeg', 'png']:
|
||||
item['thumbnail'] = url
|
||||
|
||||
results.append(item)
|
||||
|
||||
if 'nres' in response_json:
|
||||
results.append({'number_of_results': response_json['nres']})
|
||||
|
||||
return results
|
||||
res.add(
|
||||
res.types.File(
|
||||
title=result.get("label", ""),
|
||||
url=url,
|
||||
content=result.get("snippet", ""),
|
||||
size=result.get("size", ""),
|
||||
filename=result.get("filename", ""),
|
||||
abstract=result.get("abstract", ""),
|
||||
author=result.get("author", ""),
|
||||
mtype=mtype,
|
||||
subtype=subtype,
|
||||
time=result.get("time", ""),
|
||||
embedded=embedded,
|
||||
thumbnail=thumbnail,
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
||||
@@ -17,7 +17,6 @@ The engine has the following additional settings:
|
||||
shortcut: reu
|
||||
sort_order: "relevance"
|
||||
|
||||
|
||||
Implementations
|
||||
===============
|
||||
|
||||
@@ -26,6 +25,7 @@ Implementations
|
||||
from json import dumps
|
||||
from urllib.parse import quote_plus
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil import parser
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
@@ -76,15 +76,62 @@ def request(query, params):
|
||||
def response(resp) -> EngineResults:
|
||||
res = EngineResults()
|
||||
|
||||
for result in resp.json().get("result", {}).get("articles", []):
|
||||
resp_json = resp.json()
|
||||
if not resp_json.get("result"):
|
||||
return res
|
||||
|
||||
for result in resp_json["result"].get("articles", []):
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=base_url + result["canonical_url"],
|
||||
title=result["web"],
|
||||
content=result["description"],
|
||||
thumbnail=result.get("thumbnail", {}).get("url", ""),
|
||||
thumbnail=resize_url(result.get("thumbnail", {}), height=80),
|
||||
metadata=result.get("kicker", {}).get("name"),
|
||||
publishedDate=datetime.fromisoformat(result["display_time"]),
|
||||
publishedDate=parser.isoparse(result["display_time"]),
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
def resize_url(thumbnail: dict[str, str], width: int = 0, height: int = 0) -> str:
|
||||
"""Generates a URL for Reuter's thumbnail with the dimensions *width* and
|
||||
*height*. If no URL can be generated from the *thumbnail data*, an empty
|
||||
string will be returned.
|
||||
|
||||
width: default is *unset* (``0``)
|
||||
Image width in pixels (negative values are ignored). If only width is
|
||||
specified, the height matches the original aspect ratio.
|
||||
|
||||
height: default is *unset* (``0``)
|
||||
Image height in pixels (negative values are ignored). If only height is
|
||||
specified, the width matches the original aspect ratio.
|
||||
|
||||
The file size of a full-size image is usually several MB; when reduced to a
|
||||
height of, for example, 80 points, only a few KB remain!
|
||||
|
||||
Fields of the *thumbnail data* (``result.articles.[<int>].thumbnail``):
|
||||
|
||||
thumbnail.url:
|
||||
Is a full-size image (>MB).
|
||||
|
||||
thumbnail.width & .height:
|
||||
Dimensions of the full-size image.
|
||||
|
||||
thumbnail.resizer_url:
|
||||
Reuters has a *resizer* `REST-API for the images`_, this is the URL of the
|
||||
service. This URL includes the ``&auth`` argument, other arguments are
|
||||
``&width=<int>`` and ``&height=<int>``.
|
||||
|
||||
.. _REST-API for the images:
|
||||
https://dev.arcxp.com/photo-center/image-resizer/resizer-v2-how-to-transform-images/#query-parameters
|
||||
"""
|
||||
|
||||
url = thumbnail.get("resizer_url")
|
||||
if not url:
|
||||
return ""
|
||||
if int(width) > 0:
|
||||
url += f"&width={int(width)}"
|
||||
if int(height) > 0:
|
||||
url += f"&height={int(height)}"
|
||||
return url
|
||||
|
||||
90
searx/engines/sourcehut.py
Normal file
90
searx/engines/sourcehut.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Engine to search in the collaborative software platform SourceHut_.
|
||||
|
||||
.. _SourceHut: https://sourcehut.org/
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
You can configure the following setting:
|
||||
|
||||
- :py:obj:`sourcehut_sort_order`
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: sourcehut
|
||||
shortcut: srht
|
||||
engine: sourcehut
|
||||
# sourcehut_sort_order: longest-active
|
||||
|
||||
Implementations
|
||||
===============
|
||||
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
|
||||
from searx.utils import eval_xpath, eval_xpath_list, extract_text, searxng_useragent
|
||||
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://sourcehut.org",
|
||||
"wikidata_id": "Q78514485",
|
||||
"official_api_documentation": "https://man.sr.ht/",
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "HTML",
|
||||
}
|
||||
|
||||
categories = ["it", "repos"]
|
||||
paging = True
|
||||
|
||||
base_url: str = "https://sr.ht/projects"
|
||||
"""Browse public projects."""
|
||||
|
||||
|
||||
sourcehut_sort_order: str = "recently-updated"
|
||||
"""The sort order of the results. Possible values:
|
||||
|
||||
- ``recently-updated``
|
||||
- ``longest-active``
|
||||
"""
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
|
||||
args = {"search": query, "page": params["pageno"], "sort": sourcehut_sort_order}
|
||||
params["url"] = f"{base_url}?{urlencode(args)}"
|
||||
|
||||
# standard user agents are blocked by 'go-away', a foss bot detection tool
|
||||
params["headers"]["User-Agent"] = searxng_useragent()
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
|
||||
res = EngineResults()
|
||||
doc = html.fromstring(resp.text)
|
||||
|
||||
for item in eval_xpath_list(doc, "(//div[@class='event-list'])[1]/div[contains(@class, 'event')]"):
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
template="packages.html",
|
||||
url=base_url + (extract_text(eval_xpath(item, "./h4/a[2]/@href")) or ""),
|
||||
title=extract_text(eval_xpath(item, "./h4")),
|
||||
package_name=extract_text(eval_xpath(item, "./h4/a[2]")),
|
||||
content=extract_text(eval_xpath(item, "./p")),
|
||||
maintainer=(extract_text(eval_xpath(item, "./h4/a[1]")) or "").removeprefix("~"),
|
||||
tags=[
|
||||
tag.removeprefix("#") for tag in eval_xpath_list(item, "./div[contains(@class, 'tags')]/a/text()")
|
||||
],
|
||||
)
|
||||
)
|
||||
return res
|
||||
@@ -404,6 +404,10 @@ def _get_image_result(result) -> dict[str, t.Any] | None:
|
||||
def response(resp):
|
||||
categ = startpage_categ.capitalize()
|
||||
results_raw = '{' + extr(resp.text, f"React.createElement(UIStartpage.AppSerp{categ}, {{", '}})') + '}}'
|
||||
|
||||
if resp.headers.get('Location', '').startswith("https://www.startpage.com/sp/captcha"):
|
||||
raise SearxEngineCaptchaException()
|
||||
|
||||
results_json = loads(results_raw)
|
||||
results_obj = results_json.get('render', {}).get('presenter', {}).get('regions', {})
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ def request(query, params):
|
||||
params['headers'].update(
|
||||
{
|
||||
'Connection': 'keep-alive',
|
||||
'Accept-Encoding': 'gzip, defalte, br',
|
||||
'Host': 'tineye.com',
|
||||
'DNT': '1',
|
||||
'TE': 'trailers',
|
||||
|
||||
@@ -1,102 +1,208 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Wikimedia Commons (images)"""
|
||||
"""`Wikimedia Commons`_ is a collection of more than 120 millions freely usable
|
||||
media files to which anyone can contribute.
|
||||
|
||||
This engine uses the `MediaWiki query API`_, with which engines can be configured
|
||||
for searching images, videos, audio, and other files in the Wikimedia.
|
||||
|
||||
.. _MediaWiki query API: https://commons.wikimedia.org/w/api.php?action=help&modules=query
|
||||
.. _Wikimedia Commons: https://commons.wikimedia.org/
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
The engine has the following additional settings:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: wikicommons.images
|
||||
engine: wikicommons
|
||||
wc_search_type: image
|
||||
|
||||
- name: wikicommons.videos
|
||||
engine: wikicommons
|
||||
wc_search_type: video
|
||||
|
||||
- name: wikicommons.audio
|
||||
engine: wikicommons
|
||||
wc_search_type: audio
|
||||
|
||||
- name: wikicommons.files
|
||||
engine: wikicommons
|
||||
wc_search_type: file
|
||||
|
||||
|
||||
Implementations
|
||||
===============
|
||||
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
|
||||
import datetime
|
||||
|
||||
from urllib.parse import urlencode
|
||||
import pathlib
|
||||
from urllib.parse import urlencode, unquote
|
||||
|
||||
from searx.utils import html_to_text, humanize_bytes
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
# about
|
||||
about = {
|
||||
"website": 'https://commons.wikimedia.org/',
|
||||
"wikidata_id": 'Q565',
|
||||
"official_api_documentation": 'https://commons.wikimedia.org/w/api.php',
|
||||
"website": "https://commons.wikimedia.org/",
|
||||
"wikidata_id": "Q565",
|
||||
"official_api_documentation": "https://commons.wikimedia.org/w/api.php",
|
||||
"use_official_api": True,
|
||||
"require_api_key": False,
|
||||
"results": 'JSON',
|
||||
"results": "JSON",
|
||||
}
|
||||
categories = ['images']
|
||||
search_type = 'images'
|
||||
|
||||
base_url = "https://commons.wikimedia.org"
|
||||
search_prefix = (
|
||||
'?action=query'
|
||||
'&format=json'
|
||||
'&generator=search'
|
||||
'&gsrnamespace=6'
|
||||
'&gsrprop=snippet'
|
||||
'&prop=info|imageinfo'
|
||||
'&iiprop=url|size|mime'
|
||||
'&iiurlheight=180' # needed for the thumb url
|
||||
)
|
||||
categories: list[str] = []
|
||||
paging = True
|
||||
number_of_results = 10
|
||||
|
||||
search_types = {
|
||||
'images': 'bitmap|drawing',
|
||||
'videos': 'video',
|
||||
'audio': 'audio',
|
||||
'files': 'multimedia|office|archive|3d',
|
||||
wc_api_url = "https://commons.wikimedia.org/w/api.php"
|
||||
wc_search_type: str = ""
|
||||
|
||||
SEARCH_TYPES: dict[str, str] = {
|
||||
"image": "bitmap|drawing",
|
||||
"video": "video",
|
||||
"audio": "audio",
|
||||
"file": "multimedia|office|archive|3d",
|
||||
}
|
||||
# FileType = t.Literal["bitmap", "drawing", "video", "audio", "multimedia", "office", "archive", "3d"]
|
||||
# FILE_TYPES = list(t.get_args(FileType))
|
||||
|
||||
|
||||
def request(query, params):
|
||||
language = 'en'
|
||||
if params['language'] != 'all':
|
||||
language = params['language'].split('-')[0]
|
||||
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||
"""Initialization of the Wikimedia engine, checks if the value configured in
|
||||
:py:obj:`wc_search_type` is valid."""
|
||||
|
||||
if search_type not in search_types:
|
||||
raise ValueError(f"Unsupported search type: {search_type}")
|
||||
if engine_settings.get("wc_search_type") not in SEARCH_TYPES:
|
||||
logger.error(
|
||||
"wc_search_type: %s isn't a valid file type (%s)",
|
||||
engine_settings.get("wc_search_type"),
|
||||
",".join(SEARCH_TYPES.keys()),
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
filetype = search_types[search_type]
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
uselang: str = "en"
|
||||
if params["searxng_locale"] != "all":
|
||||
uselang = params["searxng_locale"].split("-")[0]
|
||||
filetype = SEARCH_TYPES[wc_search_type]
|
||||
args = {
|
||||
'uselang': language,
|
||||
'gsrlimit': number_of_results,
|
||||
'gsroffset': number_of_results * (params["pageno"] - 1),
|
||||
'gsrsearch': f"filetype:{filetype} {query}",
|
||||
# https://commons.wikimedia.org/w/api.php
|
||||
"format": "json",
|
||||
"uselang": uselang,
|
||||
"action": "query",
|
||||
# https://commons.wikimedia.org/w/api.php?action=help&modules=query
|
||||
"prop": "info|imageinfo",
|
||||
# generator (gsr optins) https://commons.wikimedia.org/w/api.php?action=help&modules=query%2Bsearch
|
||||
"generator": "search",
|
||||
"gsrnamespace": "6", # https://www.mediawiki.org/wiki/Help:Namespaces#Renaming_namespaces
|
||||
"gsrprop": "snippet",
|
||||
"gsrlimit": number_of_results,
|
||||
"gsroffset": number_of_results * (params["pageno"] - 1),
|
||||
"gsrsearch": f"filetype:{filetype} {query}",
|
||||
# imageinfo: https://commons.wikimedia.org/w/api.php?action=help&modules=query%2Bimageinfo
|
||||
"iiprop": "url|size|mime",
|
||||
"iiurlheight": "180", # needed for the thumb url
|
||||
}
|
||||
|
||||
params["url"] = f"{base_url}/w/api.php{search_prefix}&{urlencode(args, safe=':|')}"
|
||||
return params
|
||||
params["url"] = f"{wc_api_url}?{urlencode(args, safe=':|')}"
|
||||
|
||||
|
||||
def response(resp):
|
||||
results = []
|
||||
json = resp.json()
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
|
||||
if not json.get("query", {}).get("pages"):
|
||||
return results
|
||||
for item in json["query"]["pages"].values():
|
||||
res = EngineResults()
|
||||
json_data = resp.json()
|
||||
pages = json_data.get("query", {}).get("pages", {}).values()
|
||||
|
||||
for item in pages:
|
||||
|
||||
if not item.get("imageinfo", []):
|
||||
continue
|
||||
imageinfo = item["imageinfo"][0]
|
||||
title = item["title"].replace("File:", "").rsplit('.', 1)[0]
|
||||
result = {
|
||||
'url': imageinfo["descriptionurl"],
|
||||
'title': title,
|
||||
'content': html_to_text(item["snippet"]),
|
||||
}
|
||||
|
||||
if search_type == "images":
|
||||
result['template'] = 'images.html'
|
||||
result['img_src'] = imageinfo["url"]
|
||||
result['thumbnail_src'] = imageinfo["thumburl"]
|
||||
result['resolution'] = f'{imageinfo["width"]} x {imageinfo["height"]}'
|
||||
else:
|
||||
result['thumbnail'] = imageinfo["thumburl"]
|
||||
title: str = item["title"].replace("File:", "").rsplit(".", 1)[0]
|
||||
content = html_to_text(item["snippet"])
|
||||
|
||||
if search_type == "videos":
|
||||
result['template'] = 'videos.html'
|
||||
if imageinfo.get('duration'):
|
||||
result['length'] = datetime.timedelta(seconds=int(imageinfo['duration']))
|
||||
result['iframe_src'] = imageinfo['url']
|
||||
elif search_type == "files":
|
||||
result['template'] = 'files.html'
|
||||
result['metadata'] = imageinfo['mime']
|
||||
result['size'] = humanize_bytes(imageinfo['size'])
|
||||
elif search_type == "audio":
|
||||
result['iframe_src'] = imageinfo['url']
|
||||
url: str = imageinfo["descriptionurl"]
|
||||
media_url: str = imageinfo["url"]
|
||||
mimetype: str = imageinfo["mime"]
|
||||
thumbnail: str = imageinfo["thumburl"]
|
||||
size = imageinfo.get("size")
|
||||
if size:
|
||||
size = humanize_bytes(size)
|
||||
|
||||
results.append(result)
|
||||
duration = None
|
||||
seconds: str = imageinfo.get("duration")
|
||||
if seconds:
|
||||
try:
|
||||
duration = datetime.timedelta(seconds=int(seconds))
|
||||
except OverflowError:
|
||||
pass
|
||||
|
||||
return results
|
||||
if wc_search_type == "file":
|
||||
res.add(
|
||||
res.types.File(
|
||||
title=title,
|
||||
url=url,
|
||||
content=content,
|
||||
size=size,
|
||||
mimetype=mimetype,
|
||||
filename=unquote(pathlib.Path(media_url).name),
|
||||
embedded=media_url,
|
||||
thumbnail=thumbnail,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if wc_search_type == "image":
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
template="images.html",
|
||||
title=title,
|
||||
url=url,
|
||||
content=content,
|
||||
img_src=imageinfo["url"],
|
||||
thumbnail_src=thumbnail,
|
||||
resolution=f"{imageinfo['width']} x {imageinfo['height']}",
|
||||
img_format=imageinfo["mime"],
|
||||
filesize=size,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if wc_search_type == "video":
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
template="videos.html",
|
||||
title=title,
|
||||
url=url,
|
||||
content=content,
|
||||
iframe_src=media_url,
|
||||
length=duration,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if wc_search_type == "audio":
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
template="default.html",
|
||||
title=title,
|
||||
url=url,
|
||||
content=content,
|
||||
audio_src=media_url,
|
||||
length=duration,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
return res
|
||||
|
||||
@@ -35,7 +35,7 @@ content_xpath = './/div[@class="b-serp-item__content"]//div[@class="b-serp-item_
|
||||
|
||||
|
||||
def catch_bad_response(resp):
|
||||
if resp.url.path.startswith('/showcaptcha'):
|
||||
if resp.headers.get('x-yandex-captcha') == 'captcha':
|
||||
raise SearxEngineCaptchaException()
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import httpx
|
||||
if typing.TYPE_CHECKING:
|
||||
import searx.preferences
|
||||
import searx.results
|
||||
from searx.search.processors import ParamTypes
|
||||
from searx.search.processors import OnlineParamTypes
|
||||
|
||||
|
||||
class SXNG_Request(flask.Request):
|
||||
@@ -83,4 +83,4 @@ class SXNG_Response(httpx.Response):
|
||||
"""
|
||||
|
||||
ok: bool
|
||||
search_params: "ParamTypes"
|
||||
search_params: "OnlineParamTypes"
|
||||
|
||||
@@ -23,6 +23,7 @@ __all__ = [
|
||||
"WeatherAnswer",
|
||||
"Code",
|
||||
"Paper",
|
||||
"File",
|
||||
]
|
||||
|
||||
import typing as t
|
||||
@@ -33,6 +34,7 @@ from .answer import AnswerSet, Answer, Translations, WeatherAnswer
|
||||
from .keyvalue import KeyValue
|
||||
from .code import Code
|
||||
from .paper import Paper
|
||||
from .file import File
|
||||
|
||||
|
||||
class ResultList(list[Result | LegacyResult], abc.ABC):
|
||||
@@ -47,6 +49,7 @@ class ResultList(list[Result | LegacyResult], abc.ABC):
|
||||
KeyValue = KeyValue
|
||||
Code = Code
|
||||
Paper = Paper
|
||||
File = File
|
||||
MainResult = MainResult
|
||||
Result = Result
|
||||
Translations = Translations
|
||||
|
||||
@@ -27,7 +27,6 @@ import typing as t
|
||||
import re
|
||||
import urllib.parse
|
||||
import warnings
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from collections.abc import Callable
|
||||
@@ -236,13 +235,6 @@ class Result(msgspec.Struct, kw_only=True):
|
||||
url: str | None = None
|
||||
"""A link related to this *result*"""
|
||||
|
||||
template: str = "default.html"
|
||||
"""Name of the template used to render the result.
|
||||
|
||||
By default :origin:`result_templates/default.html
|
||||
<searx/templates/simple/result_templates/default.html>` is used.
|
||||
"""
|
||||
|
||||
engine: str | None = ""
|
||||
"""Name of the engine *this* result comes from. In case of *plugins* a
|
||||
prefix ``plugin:`` is set, in case of *answerer* prefix ``answerer:`` is
|
||||
@@ -350,6 +342,13 @@ class Result(msgspec.Struct, kw_only=True):
|
||||
class MainResult(Result): # pylint: disable=missing-class-docstring
|
||||
"""Base class of all result types displayed in :ref:`area main results`."""
|
||||
|
||||
template: str = "default.html"
|
||||
"""Name of the template used to render the result.
|
||||
|
||||
By default :origin:`result_templates/default.html
|
||||
<searx/templates/simple/result_templates/default.html>` is used.
|
||||
"""
|
||||
|
||||
title: str = ""
|
||||
"""Link title of the result item."""
|
||||
|
||||
@@ -359,6 +358,12 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
|
||||
img_src: str = ""
|
||||
"""URL of a image that is displayed in the result item."""
|
||||
|
||||
iframe_src: str = ""
|
||||
"""URL of an embedded ``<iframe>`` / the frame is collapsible."""
|
||||
|
||||
audio_src: str = ""
|
||||
"""URL of an embedded ``<audio controls>``."""
|
||||
|
||||
thumbnail: str = ""
|
||||
"""URL of a thumbnail that is displayed in the result item."""
|
||||
|
||||
@@ -372,7 +377,7 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
|
||||
completely eliminated.
|
||||
"""
|
||||
|
||||
length: time.struct_time | None = None
|
||||
length: datetime.timedelta | None = None
|
||||
"""Playing duration in seconds."""
|
||||
|
||||
views: str = ""
|
||||
|
||||
94
searx/result_types/file.py
Normal file
94
searx/result_types/file.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Typification of the *file* results. Results of this type are rendered in
|
||||
the :origin:`file.html <searx/templates/simple/result_templates/file.html>`
|
||||
template.
|
||||
|
||||
----
|
||||
|
||||
.. autoclass:: File
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
"""
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
|
||||
__all__ = ["File"]
|
||||
|
||||
import typing as t
|
||||
import mimetypes
|
||||
|
||||
from ._base import MainResult
|
||||
|
||||
|
||||
@t.final
|
||||
class File(MainResult, kw_only=True):
|
||||
"""Class for results of type *file*"""
|
||||
|
||||
template: str = "file.html"
|
||||
|
||||
filename: str = ""
|
||||
"""Name of the file."""
|
||||
|
||||
size: str = ""
|
||||
"""Size of bytes in human readable notation (``MB`` for 1024 * 1024 Bytes
|
||||
file size.)"""
|
||||
|
||||
time: str = ""
|
||||
"""Indication of a time, such as the date of the last modification or the
|
||||
date of creation. This is a simple string, the *date* of which can be freely
|
||||
chosen according to the context."""
|
||||
|
||||
mimetype: str = ""
|
||||
"""Mimetype/Subtype of the file. For ``audio`` and ``video``, a URL can be
|
||||
passed in the :py:obj:`File.embedded` field to embed the referenced media in
|
||||
the result. If no value is specified, the MIME type is determined from
|
||||
``self.filename`` or, alternatively, from ``self.embedded`` (if either of
|
||||
the two values is set)."""
|
||||
|
||||
abstract: str = ""
|
||||
"""Abstract of the file."""
|
||||
|
||||
author: str = ""
|
||||
"""Author of the file."""
|
||||
|
||||
embedded: str = ""
|
||||
"""URL of an embedded media type (audio or video) / is collapsible."""
|
||||
|
||||
mtype: str = ""
|
||||
"""Used for displaying :py:obj:`File.embedded`. Its value is automatically
|
||||
populated from the base type of :py:obj:`File.mimetype`, and can be
|
||||
explicitly set to enforce e.g. ``audio`` or ``video`` when mimetype is
|
||||
something like "application/ogg" but its know the content is for example a
|
||||
video."""
|
||||
|
||||
subtype: str = ""
|
||||
"""Used for displaying :py:obj:`File.embedded`. Its value is automatically
|
||||
populated from the subtype type of :py:obj:`File.mimetype`, and can be
|
||||
explicitly set to enforce a subtype for the :py:obj:`File.embedded`
|
||||
element."""
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
if not self.mtype or not self.subtype:
|
||||
|
||||
fn = self.filename or self.embedded
|
||||
if not self.mimetype and fn:
|
||||
self.mimetype = mimetypes.guess_type(fn, strict=False)[0] or ""
|
||||
|
||||
mtype, subtype = (self.mimetype.split("/", 1) + [""])[:2]
|
||||
|
||||
if not self.mtype:
|
||||
# I don't know why, but the ogg video stream is not displayed,
|
||||
# may https://github.com/videojs/video.js can help?
|
||||
if self.embedded.endswith(".ogv"):
|
||||
self.mtype = "video"
|
||||
elif self.embedded.endswith(".oga"):
|
||||
self.mtype = "audio"
|
||||
else:
|
||||
self.mtype = mtype
|
||||
|
||||
if not self.subtype:
|
||||
self.subtype = subtype
|
||||
@@ -82,7 +82,6 @@ def _download_and_check_if_image(image_url: str) -> bool:
|
||||
'User-Agent': gen_useragent(),
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US;q=0.5,en;q=0.3',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'DNT': '1',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
|
||||
@@ -35,7 +35,7 @@ class HTTPParams(t.TypedDict):
|
||||
headers: dict[str, str]
|
||||
"""HTTP header information."""
|
||||
|
||||
data: dict[str, str]
|
||||
data: dict[str, str | int | dict[str, str | int]]
|
||||
"""Sending `form encoded data`_.
|
||||
|
||||
.. _form encoded data:
|
||||
@@ -56,7 +56,7 @@ class HTTPParams(t.TypedDict):
|
||||
https://www.python-httpx.org/quickstart/#sending-json-encoded-data
|
||||
"""
|
||||
|
||||
url: str
|
||||
url: str | None
|
||||
"""Requested url."""
|
||||
|
||||
cookies: dict[str, str]
|
||||
@@ -200,7 +200,7 @@ class OnlineProcessor(EngineProcessor):
|
||||
request_args["content"] = params["content"]
|
||||
|
||||
# send the request
|
||||
response = req(params["url"], **request_args)
|
||||
response = req(params["url"], **request_args) # pyright: ignore[reportArgumentType]
|
||||
|
||||
# check soft limit of the redirect count
|
||||
if len(response.history) > soft_max_redirects:
|
||||
|
||||
@@ -24,7 +24,6 @@ brand:
|
||||
wiki_url: https://github.com/searxng/searxng/wiki
|
||||
issue_url: https://github.com/searxng/searxng/issues
|
||||
# custom:
|
||||
# maintainer: "Jon Doe"
|
||||
# # Custom entries in the footer: [title]: [link]
|
||||
# links:
|
||||
# Uptime: https://uptime.searxng.org/history/darmarit-org
|
||||
@@ -139,7 +138,7 @@ ui:
|
||||
# Open result links in a new tab by default
|
||||
# results_on_new_tab: false
|
||||
theme_args:
|
||||
# style of simple theme: auto, light, dark
|
||||
# style of simple theme: auto, light, dark, black
|
||||
simple_style: auto
|
||||
# Perform search immediately if a category selected.
|
||||
# Disable to select multiple categories at once and start the search manually.
|
||||
@@ -388,25 +387,6 @@ engines:
|
||||
timeout: 6
|
||||
disabled: true
|
||||
|
||||
- name: alexandria
|
||||
engine: json_engine
|
||||
shortcut: alx
|
||||
categories: general
|
||||
paging: true
|
||||
search_url: https://api.alexandria.org/?a=1&q={query}&p={pageno}
|
||||
results_query: results
|
||||
title_query: title
|
||||
url_query: url
|
||||
content_query: snippet
|
||||
timeout: 1.5
|
||||
disabled: true
|
||||
about:
|
||||
website: https://alexandria.org/
|
||||
official_api_documentation: https://github.com/alexandria-org/alexandria-api/raw/master/README.md
|
||||
use_official_api: true
|
||||
require_api_key: false
|
||||
results: JSON
|
||||
|
||||
- name: astrophysics data system
|
||||
engine: astrophysics_data_system
|
||||
shortcut: ads
|
||||
@@ -496,6 +476,15 @@ engines:
|
||||
shortcut: ask
|
||||
disabled: true
|
||||
|
||||
# - name: azure
|
||||
# engine: azure
|
||||
# shortcut: az
|
||||
# categories: [it, cloud]
|
||||
# azure_tenant_id: "your_tenant_id"
|
||||
# azure_client_id: "your_client_id"
|
||||
# azure_client_secret: "your_client_secret"
|
||||
# disabled: true
|
||||
|
||||
# tmp suspended: dh key too small
|
||||
# - name: base
|
||||
# engine: base
|
||||
@@ -728,6 +717,11 @@ engines:
|
||||
shortcut: da
|
||||
timeout: 3.0
|
||||
|
||||
- name: devicons
|
||||
engine: devicons
|
||||
shortcut: di
|
||||
timeout: 3.0
|
||||
|
||||
- name: ddg definitions
|
||||
engine: duckduckgo_definitions
|
||||
shortcut: ddd
|
||||
@@ -2290,31 +2284,27 @@ engines:
|
||||
|
||||
- name: wikicommons.images
|
||||
engine: wikicommons
|
||||
shortcut: wc
|
||||
shortcut: wci
|
||||
categories: images
|
||||
search_type: images
|
||||
number_of_results: 10
|
||||
wc_search_type: image
|
||||
|
||||
- name: wikicommons.videos
|
||||
engine: wikicommons
|
||||
shortcut: wcv
|
||||
categories: videos
|
||||
search_type: videos
|
||||
number_of_results: 10
|
||||
wc_search_type: video
|
||||
|
||||
- name: wikicommons.audio
|
||||
engine: wikicommons
|
||||
shortcut: wca
|
||||
categories: music
|
||||
search_type: audio
|
||||
number_of_results: 10
|
||||
wc_search_type: audio
|
||||
|
||||
- name: wikicommons.files
|
||||
engine: wikicommons
|
||||
shortcut: wcf
|
||||
categories: files
|
||||
search_type: files
|
||||
number_of_results: 10
|
||||
wc_search_type: file
|
||||
|
||||
- name: wolframalpha
|
||||
shortcut: wa
|
||||
@@ -2673,43 +2663,10 @@ engines:
|
||||
|
||||
- name: sourcehut
|
||||
shortcut: srht
|
||||
engine: xpath
|
||||
paging: true
|
||||
search_url: https://sr.ht/projects?page={pageno}&search={query}
|
||||
results_xpath: (//div[@class="event-list"])[1]/div[@class="event"]
|
||||
url_xpath: ./h4/a[2]/@href
|
||||
title_xpath: ./h4/a[2]
|
||||
content_xpath: ./p
|
||||
first_page_num: 1
|
||||
categories: [it, repos]
|
||||
engine: sourcehut
|
||||
# https://docs.searxng.org/dev/engines/online/sourcehut.html
|
||||
# sourcehut_sort_order: longest-active
|
||||
disabled: true
|
||||
about:
|
||||
website: https://sr.ht
|
||||
wikidata_id: Q78514485
|
||||
official_api_documentation: https://man.sr.ht/
|
||||
use_official_api: false
|
||||
require_api_key: false
|
||||
results: HTML
|
||||
|
||||
- name: goo
|
||||
shortcut: goo
|
||||
engine: xpath
|
||||
paging: true
|
||||
search_url: https://search.goo.ne.jp/web.jsp?MT={query}&FR={pageno}0
|
||||
url_xpath: //div[@class="result"]/p[@class='title fsL1']/a/@href
|
||||
title_xpath: //div[@class="result"]/p[@class='title fsL1']/a
|
||||
content_xpath: //p[contains(@class,'url fsM')]/following-sibling::p
|
||||
first_page_num: 0
|
||||
categories: [general, web]
|
||||
disabled: true
|
||||
timeout: 4.0
|
||||
about:
|
||||
website: https://search.goo.ne.jp
|
||||
wikidata_id: Q249044
|
||||
use_official_api: false
|
||||
require_api_key: false
|
||||
results: HTML
|
||||
language: ja
|
||||
|
||||
- name: bt4g
|
||||
engine: bt4g
|
||||
|
||||
@@ -10,7 +10,10 @@ import logging
|
||||
from base64 import b64decode
|
||||
from os.path import dirname, abspath
|
||||
|
||||
import msgspec
|
||||
|
||||
from typing_extensions import override
|
||||
from .brand import SettingsBrand
|
||||
from .sxng_locales import sxng_locales
|
||||
|
||||
searx_dir = abspath(dirname(__file__))
|
||||
@@ -138,19 +141,38 @@ class SettingsBytesValue(SettingsValue):
|
||||
def apply_schema(settings: dict[str, t.Any], schema: dict[str, t.Any], path_list: list[str]):
|
||||
error = False
|
||||
for key, value in schema.items():
|
||||
if isinstance(value, SettingsValue):
|
||||
if isinstance(value, type) and issubclass(value, msgspec.Struct):
|
||||
try:
|
||||
# Type Validation at runtime:
|
||||
# https://jcristharif.com/msgspec/structs.html#type-validation
|
||||
cfg_dict = settings.get(key)
|
||||
cfg_json = msgspec.json.encode(cfg_dict)
|
||||
settings[key] = msgspec.json.decode(cfg_json, type=value)
|
||||
except msgspec.ValidationError as e:
|
||||
# To get a more meaningful error message, we need to replace the
|
||||
# `$` by the (doted) name space. For example if ValidationError
|
||||
# was raised for the field `name` in structure at `foo.bar`:
|
||||
# Expected `str`, got `int` - at `$.name`
|
||||
# is converted to:
|
||||
# Expected `str`, got `int` - at `foo.bar.name`
|
||||
msg = str(e)
|
||||
msg = msg.replace("`$.", "`" + ".".join([*path_list, key]) + ".")
|
||||
logger.error(msg)
|
||||
error = True
|
||||
elif isinstance(value, SettingsValue):
|
||||
try:
|
||||
settings[key] = value(settings.get(key, _UNDEFINED))
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
# don't stop now: check other values
|
||||
logger.error('%s: %s', '.'.join([*path_list, key]), e)
|
||||
msg = ".".join([*path_list, key]) + f": {e}"
|
||||
logger.error(msg)
|
||||
error = True
|
||||
elif isinstance(value, dict):
|
||||
error = error or apply_schema(settings.setdefault(key, {}), schema[key], [*path_list, key])
|
||||
else:
|
||||
settings.setdefault(key, value)
|
||||
if len(path_list) == 0 and error:
|
||||
raise ValueError('Invalid settings.yml')
|
||||
raise ValueError("Invalid settings.yml")
|
||||
return error
|
||||
|
||||
|
||||
@@ -164,14 +186,7 @@ SCHEMA: dict[str, t.Any] = {
|
||||
'enable_metrics': SettingsValue(bool, True),
|
||||
'open_metrics': SettingsValue(str, ''),
|
||||
},
|
||||
'brand': {
|
||||
'issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues'),
|
||||
'new_issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues/new'),
|
||||
'docs_url': SettingsValue(str, 'https://docs.searxng.org'),
|
||||
'public_instances': SettingsValue((False, str), 'https://searx.space'),
|
||||
'wiki_url': SettingsValue((False, str), 'https://github.com/searxng/searxng/wiki'),
|
||||
'custom': SettingsValue(dict, {'links': {}}),
|
||||
},
|
||||
'brand': SettingsBrand,
|
||||
'search': {
|
||||
'safe_search': SettingsValue((0, 1, 2), 0),
|
||||
'autocomplete': SettingsValue(str, ''),
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
searx/static/themes/simple/js/chunk.min.js
vendored
Normal file
1
searx/static/themes/simple/js/chunk.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Object.create,Object.defineProperty,Object.getOwnPropertyDescriptor,Object.getOwnPropertyNames,Object.getPrototypeOf,Object.prototype.hasOwnProperty;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["css/ol.min.css"])))=>i.map(i=>d[i]);
|
||||
import{i as e,t}from"./searxng.core.min.js";e(`click`,`.searxng_init_map`,async function(e){e.preventDefault(),this.classList.remove(`searxng_init_map`);let{View:n,OlMap:r,TileLayer:i,VectorLayer:a,OSM:o,VectorSource:s,Style:c,Stroke:l,Fill:u,Circle:d,fromLonLat:f,GeoJSON:p,Feature:m,Point:h}=await t(async()=>{let{View:e,OlMap:t,TileLayer:n,VectorLayer:r,OSM:i,VectorSource:a,Style:o,Stroke:s,Fill:c,Circle:l,fromLonLat:u,GeoJSON:d,Feature:f,Point:p}=await import(`./ol.min.js`);return{View:e,OlMap:t,TileLayer:n,VectorLayer:r,OSM:i,VectorSource:a,Style:o,Stroke:s,Fill:c,Circle:l,fromLonLat:u,GeoJSON:d,Feature:f,Point:p}},[]);t(()=>Promise.resolve({}),__vite__mapDeps([0]));let{leafletTarget:g,mapLon:_,mapLat:v,mapGeojson:y}=this.dataset,b=Number.parseFloat(_||`0`),x=Number.parseFloat(v||`0`),S=new n({maxZoom:16,enableRotation:!1}),C=new r({target:g,layers:[new i({source:new o({maxZoom:16})})],view:S});try{let e=new s({features:[new m({geometry:new h(f([b,x]))})]}),t=new a({source:e,style:new c({image:new d({radius:6,fill:new u({color:`#3050ff`})})})});C.addLayer(t)}catch(e){console.error(`Failed to create marker layer:`,e)}if(y)try{let e=new s({features:new p().readFeatures(JSON.parse(y),{dataProjection:`EPSG:4326`,featureProjection:`EPSG:3857`})}),t=new a({source:e,style:new c({stroke:new l({color:`#3050ff`,width:2}),fill:new u({color:`#3050ff33`})})});C.addLayer(t),S.fit(e.getExtent(),{padding:[20,20,20,20]})}catch(e){console.error(`Failed to create GeoJSON layer:`,e)}});
|
||||
import{i as e,t}from"./searxng.core.min.js";e(`click`,`.searxng_init_map`,async function(e){e.preventDefault(),this.classList.remove(`searxng_init_map`);let{View:n,OlMap:r,TileLayer:i,VectorLayer:a,OSM:o,VectorSource:s,Style:c,Stroke:l,Fill:u,Circle:d,fromLonLat:f,GeoJSON:p,Feature:m,Point:h}=await t(async()=>{let{View:e,OlMap:t,TileLayer:n,VectorLayer:r,OSM:i,VectorSource:a,Style:o,Stroke:s,Fill:c,Circle:l,fromLonLat:u,GeoJSON:d,Feature:f,Point:p}=await import(`./ol.min.js`);return{View:e,OlMap:t,TileLayer:n,VectorLayer:r,OSM:i,VectorSource:a,Style:o,Stroke:s,Fill:c,Circle:l,fromLonLat:u,GeoJSON:d,Feature:f,Point:p}},[]);t(()=>Promise.resolve({}),__vite__mapDeps([0]));let{leafletTarget:g,mapLon:_,mapLat:v,mapGeojson:y}=this.dataset,b=Number.parseFloat(_||`0`),x=Number.parseFloat(v||`0`),S=new n({maxZoom:16,enableRotation:!1}),C=new r({target:g,layers:[new i({source:new o({maxZoom:16})})],view:S});try{let e=new a({source:new s({features:[new m({geometry:new h(f([b,x]))})]}),style:new c({image:new d({radius:6,fill:new u({color:`#3050ff`})})})});C.addLayer(e)}catch(e){console.error(`Failed to create marker layer:`,e)}if(y)try{let e=new s({features:new p().readFeatures(JSON.parse(y),{dataProjection:`EPSG:4326`,featureProjection:`EPSG:3857`})}),t=new a({source:e,style:new c({stroke:new l({color:`#3050ff`,width:2}),fill:new u({color:`#3050ff33`})})});C.addLayer(t),S.fit(e.getExtent(),{padding:[20,20,20,20]})}catch(e){console.error(`Failed to create GeoJSON layer:`,e)}});
|
||||
//# sourceMappingURL=mapresult.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"mappings":";4CAIA,EAAO,QAAS,oBAAqB,eAAmC,EAAc,CACpF,EAAM,gBAAgB,CACtB,KAAK,UAAU,OAAO,mBAAmB,CAEzC,GAAM,CACJ,OACA,QACA,YACA,cACA,MACA,eACA,QACA,SACA,OACA,SACA,aACA,UACA,UACA,8BAdI,CACJ,OACA,QACA,YACA,cACA,MACA,eACA,QACA,SACA,OACA,SACA,aACA,UACA,UACA,SACE,MAAM,OAAO,sBAdf,OACA,QACA,YACA,cACA,MACA,eACA,QACA,SACA,OACA,SACA,aACA,UACA,UACA,cAEF,yBAAO,uBAEP,GAAM,CAAE,cAAe,EAAQ,SAAQ,SAAQ,cAAe,KAAK,QAE7D,EAAM,OAAO,WAAW,GAAU,IAAI,CACtC,EAAM,OAAO,WAAW,GAAU,IAAI,CACtC,EAAO,IAAI,EAAK,CAAE,QAAS,GAAI,eAAgB,GAAO,CAAC,CACvD,EAAM,IAAI,EAAM,CACZ,SACR,OAAQ,CAAC,IAAI,EAAU,CAAE,OAAQ,IAAI,EAAI,CAAE,QAAS,GAAI,CAAC,CAAE,CAAC,CAAC,CACvD,OACP,CAAC,CAEF,GAAI,CACF,IAAM,EAAe,IAAI,EAAa,CACpC,SAAU,CACR,IAAI,EAAQ,CACV,SAAU,IAAI,EAAM,EAAW,CAAC,EAAK,EAAI,CAAC,CAAC,CAC5C,CAAC,CACH,CACF,CAAC,CAEI,EAAc,IAAI,EAAY,CAClC,OAAQ,EACR,MAAO,IAAI,EAAM,CACf,MAAO,IAAI,EAAO,CAChB,OAAQ,EACR,KAAM,IAAI,EAAK,CAAE,MAAO,UAAW,CAAC,CACrC,CAAC,CACH,CAAC,CACH,CAAC,CAEF,EAAI,SAAS,EAAY,OAClB,EAAO,CACd,QAAQ,MAAM,iCAAkC,EAAM,CAGxD,GAAI,EACF,GAAI,CACF,IAAM,EAAY,IAAI,EAAa,CACjC,SAAU,IAAI,GAAS,CAAC,aAAa,KAAK,MAAM,EAAW,CAAE,CAC3D,eAAgB,YAChB,kBAAmB,YACpB,CAAC,CACH,CAAC,CAEI,EAAW,IAAI,EAAY,CAC/B,OAAQ,EACR,MAAO,IAAI,EAAM,CACf,OAAQ,IAAI,EAAO,CAAE,MAAO,UAAW,MAAO,EAAG,CAAC,CAClD,KAAM,IAAI,EAAK,CAAE,MAAO,YAAa,CAAC,CACvC,CAAC,CACH,CAAC,CAEF,EAAI,SAAS,EAAS,CAEtB,EAAK,IAAI,EAAU,WAAW,CAAE,CAAE,QAAS,CAAC,GAAI,GAAI,GAAI,GAAG,CAAE,CAAC,OACvD,EAAO,CACd,QAAQ,MAAM,kCAAmC,EAAM,GAG3D","names":[],"ignoreList":[],"sources":["../../../../../client/simple/src/js/main/mapresult.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { listen } from \"../core/toolkit.ts\";\n\nlisten(\"click\", \".searxng_init_map\", async function (this: HTMLElement, event: Event) {\n event.preventDefault();\n this.classList.remove(\"searxng_init_map\");\n\n const {\n View,\n OlMap,\n TileLayer,\n VectorLayer,\n OSM,\n VectorSource,\n Style,\n Stroke,\n Fill,\n Circle,\n fromLonLat,\n GeoJSON,\n Feature,\n Point\n } = await import(\"../pkg/ol.ts\");\n import(\"ol/ol.css\");\n\n const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset;\n\n const lon = Number.parseFloat(mapLon || \"0\");\n const lat = Number.parseFloat(mapLat || \"0\");\n const view = new View({ maxZoom: 16, enableRotation: false });\n const map = new OlMap({\n target: target,\n layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],\n view: view\n });\n\n try {\n const markerSource = new VectorSource({\n features: [\n new Feature({\n geometry: new Point(fromLonLat([lon, lat]))\n })\n ]\n });\n\n const markerLayer = new VectorLayer({\n source: markerSource,\n style: new Style({\n image: new Circle({\n radius: 6,\n fill: new Fill({ color: \"#3050ff\" })\n })\n })\n });\n\n map.addLayer(markerLayer);\n } catch (error) {\n console.error(\"Failed to create marker layer:\", error);\n }\n\n if (mapGeojson) {\n try {\n const geoSource = new VectorSource({\n features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {\n dataProjection: \"EPSG:4326\",\n featureProjection: \"EPSG:3857\"\n })\n });\n\n const geoLayer = new VectorLayer({\n source: geoSource,\n style: new Style({\n stroke: new Stroke({ color: \"#3050ff\", width: 2 }),\n fill: new Fill({ color: \"#3050ff33\" })\n })\n });\n\n map.addLayer(geoLayer);\n\n view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] });\n } catch (error) {\n console.error(\"Failed to create GeoJSON layer:\", error);\n }\n }\n});\n"],"file":"js/mapresult.min.js"}
|
||||
{"version":3,"mappings":";4CAIA,EAAO,QAAS,oBAAqB,eAAmC,EAAc,CACpF,EAAM,gBAAgB,CACtB,KAAK,UAAU,OAAO,mBAAmB,CAEzC,GAAM,CACJ,OACA,QACA,YACA,cACA,MACA,eACA,QACA,SACA,OACA,SACA,aACA,UACA,UACA,8BAdI,CACJ,OACA,QACA,YACA,cACA,MACA,eACA,QACA,SACA,OACA,SACA,aACA,UACA,UACA,SACE,MAAM,OAAO,sBAdf,OACA,QACA,YACA,cACA,MACA,eACA,QACA,SACA,OACA,SACA,aACA,UACA,UACA,cAEF,MAAK,mBAAO,uBAEZ,GAAM,CAAE,cAAe,EAAQ,SAAQ,SAAQ,cAAe,KAAK,QAE7D,EAAM,OAAO,WAAW,GAAU,IAAI,CACtC,EAAM,OAAO,WAAW,GAAU,IAAI,CACtC,EAAO,IAAI,EAAK,CAAE,QAAS,GAAI,eAAgB,GAAO,CAAC,CACvD,EAAM,IAAI,EAAM,CACZ,SACR,OAAQ,CAAC,IAAI,EAAU,CAAE,OAAQ,IAAI,EAAI,CAAE,QAAS,GAAI,CAAC,CAAE,CAAC,CAAC,CACvD,OACP,CAAC,CAEF,GAAI,CASF,IAAM,EAAc,IAAI,EAAY,CAClC,OATmB,IAAI,EAAa,CACpC,SAAU,CACR,IAAI,EAAQ,CACV,SAAU,IAAI,EAAM,EAAW,CAAC,EAAK,EAAI,CAAC,CAAC,CAC5C,CAAC,CACH,CACF,CAAC,CAIA,MAAO,IAAI,EAAM,CACf,MAAO,IAAI,EAAO,CAChB,OAAQ,EACR,KAAM,IAAI,EAAK,CAAE,MAAO,UAAW,CAAC,CACrC,CAAC,CACH,CAAC,CACH,CAAC,CAEF,EAAI,SAAS,EAAY,OAClB,EAAO,CACd,QAAQ,MAAM,iCAAkC,EAAM,CAGxD,GAAI,EACF,GAAI,CACF,IAAM,EAAY,IAAI,EAAa,CACjC,SAAU,IAAI,GAAS,CAAC,aAAa,KAAK,MAAM,EAAW,CAAE,CAC3D,eAAgB,YAChB,kBAAmB,YACpB,CAAC,CACH,CAAC,CAEI,EAAW,IAAI,EAAY,CAC/B,OAAQ,EACR,MAAO,IAAI,EAAM,CACf,OAAQ,IAAI,EAAO,CAAE,MAAO,UAAW,MAAO,EAAG,CAAC,CAClD,KAAM,IAAI,EAAK,CAAE,MAAO,YAAa,CAAC,CACvC,CAAC,CACH,CAAC,CAEF,EAAI,SAAS,EAAS,CAEtB,EAAK,IAAI,EAAU,WAAW,CAAE,CAAE,QAAS,CAAC,GAAI,GAAI,GAAI,GAAG,CAAE,CAAC,OACvD,EAAO,CACd,QAAQ,MAAM,kCAAmC,EAAM,GAG3D","names":[],"ignoreList":[],"sources":["../../../../../client/simple/src/js/main/mapresult.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { listen } from \"../core/toolkit.ts\";\n\nlisten(\"click\", \".searxng_init_map\", async function (this: HTMLElement, event: Event) {\n event.preventDefault();\n this.classList.remove(\"searxng_init_map\");\n\n const {\n View,\n OlMap,\n TileLayer,\n VectorLayer,\n OSM,\n VectorSource,\n Style,\n Stroke,\n Fill,\n Circle,\n fromLonLat,\n GeoJSON,\n Feature,\n Point\n } = await import(\"../pkg/ol.ts\");\n void import(\"ol/ol.css\");\n\n const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset;\n\n const lon = Number.parseFloat(mapLon || \"0\");\n const lat = Number.parseFloat(mapLat || \"0\");\n const view = new View({ maxZoom: 16, enableRotation: false });\n const map = new OlMap({\n target: target,\n layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],\n view: view\n });\n\n try {\n const markerSource = new VectorSource({\n features: [\n new Feature({\n geometry: new Point(fromLonLat([lon, lat]))\n })\n ]\n });\n\n const markerLayer = new VectorLayer({\n source: markerSource,\n style: new Style({\n image: new Circle({\n radius: 6,\n fill: new Fill({ color: \"#3050ff\" })\n })\n })\n });\n\n map.addLayer(markerLayer);\n } catch (error) {\n console.error(\"Failed to create marker layer:\", error);\n }\n\n if (mapGeojson) {\n try {\n const geoSource = new VectorSource({\n features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {\n dataProjection: \"EPSG:4326\",\n featureProjection: \"EPSG:3857\"\n })\n });\n\n const geoLayer = new VectorLayer({\n source: geoSource,\n style: new Style({\n stroke: new Stroke({ color: \"#3050ff\", width: 2 }),\n fill: new Fill({ color: \"#3050ff33\" })\n })\n });\n\n map.addLayer(geoLayer);\n\n view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] });\n } catch (error) {\n console.error(\"Failed to create GeoJSON layer:\", error);\n }\n }\n});\n"],"file":"mapresult.min.js"}
|
||||
14
searx/static/themes/simple/js/ol.min.js
vendored
14
searx/static/themes/simple/js/ol.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
import{i as e,o as t,r as n}from"./searxng.core.min.js";var r,i=async()=>{if(!r){try{r=await(await n(`GET`,`engine_descriptions.json`)).json()}catch(e){console.error(`Error fetching engineDescriptions:`,e)}if(r)for(let[e,[n,i]]of Object.entries(r)){let r=document.querySelectorAll(`[data-engine-name="${e}"] .engine-description`),a=` (<i>${t.translations?.Source}: ${i}</i>)`;for(let e of r)e.innerHTML=n+a}}},a=(e,t)=>{for(let n of t)n.offsetParent&&(n.checked=!e)},o=document.querySelectorAll(`[data-engine-name]`);for(let t of o)e(`mouseenter`,t,i);var s=document.querySelectorAll(`tbody input[type=checkbox][class~=checkbox-onoff]`),c=document.querySelectorAll(`.enable-all-engines`);for(let t of c)e(`click`,t,()=>a(!0,s));var l=document.querySelectorAll(`.disable-all-engines`);for(let t of l)e(`click`,t,()=>a(!1,s));var u=document.querySelector(`#copy-hash`);u&&e(`click`,u,async e=>{e.preventDefault();let{copiedText:t,hash:n}=u.dataset;if(t&&n)try{await navigator.clipboard.writeText(n),u.innerText=t}catch(e){console.error(`Failed to copy hash:`,e)}});
|
||||
import{i as e,n as t,o as n,r}from"./searxng.core.min.js";var i,a=async()=>{if(!i){try{i=await(await r(`GET`,`engine_descriptions.json`)).json()}catch(e){console.error(`Error fetching engineDescriptions:`,e)}if(i)for(let[e,[t,r]]of Object.entries(i)){let i=document.querySelectorAll(`[data-engine-name="${e}"] .engine-description`),a=` (<i>${n.translations?.Source}: ${r}</i>)`;for(let e of i)e.innerHTML=t+a}}},o=(e,t)=>{for(let n of t)n.offsetParent&&(n.checked=!e)},s=document.querySelectorAll(`[data-engine-name]`);for(let t of s)e(`mouseenter`,t,a);var c=document.querySelectorAll(`tbody input[type=checkbox][class~=checkbox-onoff]`),l=document.querySelectorAll(`.enable-all-engines`);for(let t of l)e(`click`,t,()=>o(!0,c));var u=document.querySelectorAll(`.disable-all-engines`);for(let t of u)e(`click`,t,()=>o(!1,c));e(`click`,`#copy-hash`,async function(){let e=this.parentElement?.querySelector(`pre`);if(t(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`)}}let n=this.dataset.copiedText;n&&(this.innerText=n)});
|
||||
//# sourceMappingURL=preferences.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"preferences.min.js","names":["engineDescriptions: Record<string, [string, string]> | undefined","engineElements: NodeListOf<HTMLElement>","engineToggles: NodeListOf<HTMLInputElement>","enableAllEngines: NodeListOf<HTMLElement>","disableAllEngines: NodeListOf<HTMLElement>","copyHashButton: HTMLElement | null"],"sources":["../../../../../client/simple/src/js/main/preferences.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { http, listen, settings } from \"../core/toolkit.ts\";\n\nlet engineDescriptions: Record<string, [string, string]> | undefined;\n\nconst loadEngineDescriptions = async (): Promise<void> => {\n if (engineDescriptions) return;\n try {\n const res = await http(\"GET\", \"engine_descriptions.json\");\n engineDescriptions = await res.json();\n } catch (error) {\n console.error(\"Error fetching engineDescriptions:\", error);\n }\n if (!engineDescriptions) return;\n\n for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {\n const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name=\"${engine_name}\"] .engine-description`);\n const sourceText = ` (<i>${settings.translations?.Source}: ${source}</i>)`;\n\n for (const element of elements) {\n element.innerHTML = description + sourceText;\n }\n }\n};\n\nconst toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputElement>): void => {\n for (const engineToggle of engineToggles) {\n // check if element visible, so that only engines of the current category are modified\n if (engineToggle.offsetParent) {\n engineToggle.checked = !enable;\n }\n }\n};\n\nconst engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\"[data-engine-name]\");\nfor (const engineElement of engineElements) {\n listen(\"mouseenter\", engineElement, loadEngineDescriptions);\n}\n\nconst engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(\n \"tbody input[type=checkbox][class~=checkbox-onoff]\"\n);\n\nconst enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".enable-all-engines\");\nfor (const engine of enableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(true, engineToggles));\n}\n\nconst disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".disable-all-engines\");\nfor (const engine of disableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(false, engineToggles));\n}\n\nconst copyHashButton: HTMLElement | null = document.querySelector<HTMLElement>(\"#copy-hash\");\nif (copyHashButton) {\n listen(\"click\", copyHashButton, async (event: Event) => {\n event.preventDefault();\n\n const { copiedText, hash } = copyHashButton.dataset;\n if (!(copiedText && hash)) return;\n\n try {\n await navigator.clipboard.writeText(hash);\n copyHashButton.innerText = copiedText;\n } catch (error) {\n console.error(\"Failed to copy hash:\", error);\n }\n });\n}\n"],"mappings":"wDAIA,IAAIA,EAEE,EAAyB,SAA2B,CACpD,MACJ,IAAI,CAEF,EAAqB,MADT,MAAM,EAAK,MAAO,2BAA2B,EAC1B,MAAM,OAC9B,EAAO,CACd,QAAQ,MAAM,qCAAsC,EAAM,CAEvD,KAEL,IAAK,GAAM,CAAC,EAAa,CAAC,EAAa,MAAY,OAAO,QAAQ,EAAmB,CAAE,CACrF,IAAM,EAAW,SAAS,iBAA8B,sBAAsB,EAAY,wBAAwB,CAC5G,EAAa,QAAQ,EAAS,cAAc,OAAO,SAAS,EAAO,OAEzE,IAAK,IAAM,KAAW,EACpB,EAAQ,UAAY,EAAc,KAKlC,GAAiB,EAAiB,IAAsD,CAC5F,IAAK,IAAM,KAAgB,EAErB,EAAa,eACf,EAAa,QAAU,CAAC,IAKxBC,EAA0C,SAAS,iBAA8B,qBAAqB,CAC5G,IAAK,IAAM,KAAiB,EAC1B,EAAO,aAAc,EAAe,EAAuB,CAG7D,IAAMC,EAA8C,SAAS,iBAC3D,oDACD,CAEKC,EAA4C,SAAS,iBAA8B,sBAAsB,CAC/G,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAM,EAAc,CAAC,CAGnE,IAAMC,EAA6C,SAAS,iBAA8B,uBAAuB,CACjH,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAO,EAAc,CAAC,CAGpE,IAAMC,EAAqC,SAAS,cAA2B,aAAa,CACxF,GACF,EAAO,QAAS,EAAgB,KAAO,IAAiB,CACtD,EAAM,gBAAgB,CAEtB,GAAM,CAAE,aAAY,QAAS,EAAe,QACtC,MAAc,EAEpB,GAAI,CACF,MAAM,UAAU,UAAU,UAAU,EAAK,CACzC,EAAe,UAAY,QACpB,EAAO,CACd,QAAQ,MAAM,uBAAwB,EAAM,GAE9C"}
|
||||
{"version":3,"file":"preferences.min.js","names":["engineDescriptions: Record<string, [string, string]> | undefined","engineElements: NodeListOf<HTMLElement>","engineToggles: NodeListOf<HTMLInputElement>","enableAllEngines: NodeListOf<HTMLElement>","disableAllEngines: NodeListOf<HTMLElement>"],"sources":["../../../../../client/simple/src/js/main/preferences.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { assertElement, http, listen, settings } from \"../core/toolkit.ts\";\n\nlet engineDescriptions: Record<string, [string, string]> | undefined;\n\nconst loadEngineDescriptions = async (): Promise<void> => {\n if (engineDescriptions) return;\n try {\n const res = await http(\"GET\", \"engine_descriptions.json\");\n engineDescriptions = await res.json();\n } catch (error) {\n console.error(\"Error fetching engineDescriptions:\", error);\n }\n if (!engineDescriptions) return;\n\n for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {\n const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name=\"${engine_name}\"] .engine-description`);\n const sourceText = ` (<i>${settings.translations?.Source}: ${source}</i>)`;\n\n for (const element of elements) {\n element.innerHTML = description + sourceText;\n }\n }\n};\n\nconst toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputElement>): void => {\n for (const engineToggle of engineToggles) {\n // check if element visible, so that only engines of the current category are modified\n if (engineToggle.offsetParent) {\n engineToggle.checked = !enable;\n }\n }\n};\n\nconst engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\"[data-engine-name]\");\nfor (const engineElement of engineElements) {\n listen(\"mouseenter\", engineElement, loadEngineDescriptions);\n}\n\nconst engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(\n \"tbody input[type=checkbox][class~=checkbox-onoff]\"\n);\n\nconst enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".enable-all-engines\");\nfor (const engine of enableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(true, engineToggles));\n}\n\nconst disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".disable-all-engines\");\nfor (const engine of disableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(false, engineToggles));\n}\n\nlisten(\"click\", \"#copy-hash\", async function (this: HTMLElement) {\n const target = this.parentElement?.querySelector<HTMLPreElement>(\"pre\");\n assertElement(target);\n\n if (window.isSecureContext) {\n await navigator.clipboard.writeText(target.innerText);\n } else {\n const selection = window.getSelection();\n if (selection) {\n const range = document.createRange();\n range.selectNodeContents(target);\n selection.removeAllRanges();\n selection.addRange(range);\n document.execCommand(\"copy\");\n }\n }\n\n const copiedText = this.dataset.copiedText;\n if (copiedText) {\n this.innerText = copiedText;\n }\n});\n"],"mappings":"0DAIA,IAAIA,EAEE,EAAyB,SAA2B,CACpD,MACJ,IAAI,CAEF,EAAqB,MADT,MAAM,EAAK,MAAO,2BAA2B,EAC1B,MAAM,OAC9B,EAAO,CACd,QAAQ,MAAM,qCAAsC,EAAM,CAEvD,KAEL,IAAK,GAAM,CAAC,EAAa,CAAC,EAAa,MAAY,OAAO,QAAQ,EAAmB,CAAE,CACrF,IAAM,EAAW,SAAS,iBAA8B,sBAAsB,EAAY,wBAAwB,CAC5G,EAAa,QAAQ,EAAS,cAAc,OAAO,SAAS,EAAO,OAEzE,IAAK,IAAM,KAAW,EACpB,EAAQ,UAAY,EAAc,KAKlC,GAAiB,EAAiB,IAAsD,CAC5F,IAAK,IAAM,KAAgB,EAErB,EAAa,eACf,EAAa,QAAU,CAAC,IAKxBC,EAA0C,SAAS,iBAA8B,qBAAqB,CAC5G,IAAK,IAAM,KAAiB,EAC1B,EAAO,aAAc,EAAe,EAAuB,CAG7D,IAAMC,EAA8C,SAAS,iBAC3D,oDACD,CAEKC,EAA4C,SAAS,iBAA8B,sBAAsB,CAC/G,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAM,EAAc,CAAC,CAGnE,IAAMC,EAA6C,SAAS,iBAA8B,uBAAuB,CACjH,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAO,EAAc,CAAC,CAGpE,EAAO,QAAS,aAAc,gBAAmC,CAC/D,IAAM,EAAS,KAAK,eAAe,cAA8B,MAAM,CAGvE,GAFA,EAAc,EAAO,CAEjB,OAAO,gBACT,MAAM,UAAU,UAAU,UAAU,EAAO,UAAU,KAChD,CACL,IAAM,EAAY,OAAO,cAAc,CACvC,GAAI,EAAW,CACb,IAAM,EAAQ,SAAS,aAAa,CACpC,EAAM,mBAAmB,EAAO,CAChC,EAAU,iBAAiB,CAC3B,EAAU,SAAS,EAAM,CACzB,SAAS,YAAY,OAAO,EAIhC,IAAM,EAAa,KAAK,QAAQ,WAC5B,IACF,KAAK,UAAY,IAEnB"}
|
||||
11
searx/static/themes/simple/js/results.min.js
vendored
11
searx/static/themes/simple/js/results.min.js
vendored
@@ -1,11 +1,2 @@
|
||||
import{a as e,i as t,n,o as r}from"./searxng.core.min.js";
|
||||
/*!
|
||||
* swiped-events.js - v@version@
|
||||
* Pure JavaScript swipe events
|
||||
* https://github.com/john-doherty/swiped-events
|
||||
* @inspiration https://stackoverflow.com/questions/16348031/disable-scrolling-when-touch-moving-certain-element
|
||||
* @author John Doherty <www.johndoherty.info>
|
||||
* @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=e=>{i&&clearTimeout(i);let t=e.querySelector(`.result-images-source img`);if(!t)return;let n=e.querySelector(`.image_thumbnail`);if(n){if(n.src===`${r.theme_static_path}/img/img_load_error.svg`)return;t.onerror=()=>{t.src=n.src},t.src=n.src}let a=t.getAttribute(`data-src`);a&&(i=setTimeout(()=>{t.src=a,t.removeAttribute(`data-src`)},1e3))},o=document.querySelectorAll(`#urls img.image_thumbnail`);for(let e of o)e.complete&&e.naturalWidth===0&&(e.src=`${r.theme_static_path}/img/img_load_error.svg`),e.onerror=()=>{e.src=`${r.theme_static_path}/img/img_load_error.svg`};document.querySelector(`#search_url button#copy_url`)?.style.setProperty(`display`,`block`),e.selectImage=t=>{document.getElementById(`results`)?.classList.add(`image-detail-open`),window.location.hash=`#image-viewer`,e.scrollPageToSelected?.(),t&&a(t)},e.closeDetail=()=>{document.getElementById(`results`)?.classList.remove(`image-detail-open`),window.location.hash===`#image-viewer`&&window.history.back(),e.scrollPageToSelected?.()},t(`click`,`.btn-collapse`,function(){let e=this.getAttribute(`data-btn-text-collapsed`),t=this.getAttribute(`data-btn-text-not-collapsed`),r=this.getAttribute(`data-target`);if(!(r&&e&&t))return;let i=document.querySelector(r);n(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(n(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`);n(e),await navigator.clipboard.writeText(e.innerText);let t=this.dataset.copiedText;t&&(this.innerText=t)}),t(`click`,`.result-detail-close`,t=>{t.preventDefault(),e.closeDetail?.()}),t(`click`,`.result-detail-previous`,t=>{t.preventDefault(),e.selectPrevious?.(!1)}),t(`click`,`.result-detail-next`,t=>{t.preventDefault(),e.selectNext?.(!1)}),window.addEventListener(`hashchange`,()=>{window.location.hash!==`#image-viewer`&&e.closeDetail?.()});var s=document.querySelectorAll(`.swipe-horizontal`);for(let n of s)t(`swiped-left`,n,()=>{e.selectNext?.(!1)}),t(`swiped-right`,n,()=>{e.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);
|
||||
import{a as e,i as t,n,o as r}from"./searxng.core.min.js";(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=e=>{i&&clearTimeout(i);let t=e.querySelector(`.result-images-source img`);if(!t)return;let n=e.querySelector(`.image_thumbnail`);if(n){if(n.src===`${r.theme_static_path}/img/img_load_error.svg`)return;t.onerror=()=>{t.src=n.src},t.src=n.src}let a=t.getAttribute(`data-src`);a&&(i=setTimeout(()=>{t.src=a,t.removeAttribute(`data-src`)},1e3))},o=document.querySelectorAll(`#urls img.image_thumbnail`);for(let e of o)e.complete&&e.naturalWidth===0&&(e.src=`${r.theme_static_path}/img/img_load_error.svg`),e.onerror=()=>{e.src=`${r.theme_static_path}/img/img_load_error.svg`};document.querySelector(`#search_url button#copy_url`)?.style.setProperty(`display`,`block`),e.selectImage=t=>{document.getElementById(`results`)?.classList.add(`image-detail-open`),window.location.hash=`#image-viewer`,e.scrollPageToSelected?.(),t&&a(t)},e.closeDetail=()=>{document.getElementById(`results`)?.classList.remove(`image-detail-open`),window.location.hash===`#image-viewer`&&window.history.back(),e.scrollPageToSelected?.()},t(`click`,`.btn-collapse`,function(){let e=this.getAttribute(`data-btn-text-collapsed`),t=this.getAttribute(`data-btn-text-not-collapsed`),r=this.getAttribute(`data-target`);if(!(r&&e&&t))return;let i=document.querySelector(r);n(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(n(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(n(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`)}}let t=this.dataset.copiedText;t&&(this.innerText=t)}),t(`click`,`.result-detail-close`,t=>{t.preventDefault(),e.closeDetail?.()}),t(`click`,`.result-detail-previous`,t=>{t.preventDefault(),e.selectPrevious?.(!1)}),t(`click`,`.result-detail-next`,t=>{t.preventDefault(),e.selectNext?.(!1)}),window.addEventListener(`hashchange`,()=>{window.location.hash!==`#image-viewer`&&e.closeDetail?.()});var s=document.querySelectorAll(`.swipe-horizontal`);for(let n of s)t(`swiped-left`,n,()=>{e.selectNext?.(!1)}),t(`swiped-right`,n,()=>{e.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=results.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
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"_chunk.min.js": {
|
||||
"file": "js/chunk.min.js",
|
||||
"name": "chunk"
|
||||
},
|
||||
"node_modules/ol/ol.css": {
|
||||
"file": "css/ol.min.css",
|
||||
"name": "ol.css",
|
||||
"names": [
|
||||
"ol.css"
|
||||
],
|
||||
@@ -96,6 +101,7 @@
|
||||
},
|
||||
"src/less/rss.less": {
|
||||
"file": "css/rss.min.css",
|
||||
"name": "rss.css",
|
||||
"names": [
|
||||
"rss.css"
|
||||
],
|
||||
@@ -104,6 +110,7 @@
|
||||
},
|
||||
"src/less/style-ltr.less": {
|
||||
"file": "css/searxng-ltr.min.css",
|
||||
"name": "searxng-ltr.css",
|
||||
"names": [
|
||||
"searxng-ltr.css"
|
||||
],
|
||||
@@ -112,6 +119,7 @@
|
||||
},
|
||||
"src/less/style-rtl.less": {
|
||||
"file": "css/searxng-rtl.min.css",
|
||||
"name": "searxng-rtl.css",
|
||||
"names": [
|
||||
"searxng-rtl.css"
|
||||
],
|
||||
|
||||
@@ -51,7 +51,11 @@
|
||||
{%- endif -%}
|
||||
{%- endblock -%}
|
||||
{%- block linkto_preferences -%}
|
||||
<a href="{{ url_for('preferences') }}" class="link_on_top_preferences">{{ icon_big('settings') }}<span>{{ _('Preferences') }}</span></a>
|
||||
{%- if request.args.get('preferences') -%}
|
||||
<a href="{{ url_for('preferences') }}?preferences={{ request.args.get('preferences') }}&preferences_preview_only=true" class="link_on_top_preferences">{{ icon_big('settings') }}<span>{{ _('Preferences') }}</span></a>
|
||||
{%- else -%}
|
||||
<a href="{{ url_for('preferences') }}" class="link_on_top_preferences">{{ icon_big('settings') }}<span>{{ _('Preferences') }}</span></a>
|
||||
{%- endif -%}
|
||||
{%- endblock -%}
|
||||
</nav>
|
||||
{% block header %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div id="search_url" role="complementary" aria-labelledby="search_url-title">
|
||||
<details class="sidebar-collapsible">
|
||||
<summary class="title" id="search_url-title">{{ _('Search URL') }}</summary>
|
||||
<button id="copy_url" type="submit" data-copied-text="{{ _('Copied') }}">{{ _('Copy') }}</button>
|
||||
<button id="copy_url" type="button" class="button" data-copied-text="{{ _('Copied') }}">{{ _('Copy') }}</button>
|
||||
<div class="selectable_url">
|
||||
<pre>{{ url_for('search', _external=True) }}?q={{ q|urlencode }}&language={{ current_language }}&time_range={{ time_range }}&safesearch={{ safesearch }}
|
||||
{%- if pageno > 1 -%}
|
||||
|
||||
@@ -30,14 +30,15 @@
|
||||
{%- endfor %}
|
||||
</div>
|
||||
{{- result_close_link() -}}
|
||||
{%- if result.thumbnail %}{{ result_open_link(result.url) }}<img class="thumbnail" src="{{ image_proxify(result.thumbnail) }}" title="{{ result.title|striptags }}" loading="lazy">{{ result_close_link() }}{% endif -%}
|
||||
{%- if result.thumbnail %}{{ result_open_link(result.url, classes='thumbnail_link') }}<img class="thumbnail" src="{{ image_proxify(result.thumbnail) }}" title="{{ result.title|striptags }}" loading="lazy">{%- if result.length -%}<span class="thumbnail_length">{{ result.length }}</span>{%- endif -%}{{ result_close_link() }}{% endif -%}
|
||||
<h3>{{ result_link(result.url, result.title|safe) }}</h3>
|
||||
{%- endmacro -%}
|
||||
|
||||
<!-- Draw result sub header -->
|
||||
{%- macro result_sub_header(result) -%}
|
||||
{%- if result.publishedDate %}<time class="published_date" datetime="{{ result.pubdate }}" >{{ result.publishedDate }}</time>{% endif -%}
|
||||
{%- if result.length %}<div class="result_length">{{ _('Length') }}: {{ result.length }}</div>{% endif -%}
|
||||
<!-- Length is displayed inside the thumbnail if there's any, so don't display it here a second time -->
|
||||
{%- if result.length and not result.thumbnail %}<div class="result_length">{{ _('Length') }}: {{ result.length }}</div>{% endif -%}
|
||||
{%- if result.views %}<div class="result_views">{{ _('Views') }}: {{ result.views }}</div>{% endif -%}
|
||||
{%- if result.author %}<div class="result_author">{{ _('Author') }}: {{ result.author }}</div>{% endif -%}
|
||||
{%- if result.metadata %}<div class="highlight">{{ result.metadata|safe }}</div>{% endif -%}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{%- if pageno == 1 -%}
|
||||
<li>{{ _("Refresh the page.") }}</li>{{- '' -}}
|
||||
<li>{{ _("Search for another query or select another category (above).") }}</li>{{- '' -}}
|
||||
<li>{{ _("Change the search engine used in the preferences:") }} <a href="/preferences">/preferences</a></li>{{- '' -}}
|
||||
<li>{{ _("Change the search engine used in the preferences:") }} <a href="{{ url_for('preferences') }}">/preferences</a></li>{{- '' -}}
|
||||
<li>{{ _("Switch to another instance:") }} <a href="https://searx.space">https://searx.space</a></li>{{- '' -}}
|
||||
{%- else -%}
|
||||
<li>{{ _("Search for another query or select another category.") }}</li>{{- '' -}}
|
||||
|
||||
@@ -155,6 +155,16 @@
|
||||
|
||||
<h1>{{ _('Preferences') }}</h1>
|
||||
|
||||
{%- if request.args.get('preferences_preview_only') == 'true' -%}
|
||||
<div class="dialog-warning-block">
|
||||
<p>{{ _("This is a preview of the settings used by the 'Search URL' you used to get here.") }}</p>
|
||||
<ul>
|
||||
<li>{{ _('Press save to copy these preferences to your browser.') }}</li>
|
||||
<li>{{ _('Click here to view your browser preferences instead:') }} <a href="{{ url_for('preferences') }}">/preferences</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
|
||||
<form id="search_form" method="post" action="{{ url_for('preferences') }}" autocomplete="off">
|
||||
{{- tabs_open() -}}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
{{- preferences_url_params|e }}
|
||||
</pre>{{- '' -}}
|
||||
</div>
|
||||
<button id="copy-hash" class="button" data-hash="{{- preferences_url_params|e -}}" data-copied-text="{{- _('Copied') -}}">{{- _('Copy') -}}</button>
|
||||
<button id="copy-hash" type="button" class="button" data-hash="{{- preferences_url_params|e -}}" data-copied-text="{{- _('Copied') -}}">{{- _('Copy') -}}</button>
|
||||
</div>
|
||||
<h4>
|
||||
{{- _('Insert copied preferences hash (without URL) to restore') -}}:{{- '' -}}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user