Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02a54c1e06 | ||
|
|
c360868e5b | ||
|
|
b5ed73fcb6 | ||
|
|
28423d332d | ||
|
|
ef7bae9ddb | ||
|
|
cb74e7f810 | ||
|
|
54ca1d9cfb | ||
|
|
ef0ec03da0 | ||
|
|
629368e40c | ||
|
|
68647d0806 | ||
|
|
81371861f6 | ||
|
|
d47cb96bfb | ||
|
|
62366b05c0 | ||
|
|
1c10abe2d3 | ||
|
|
3afd949268 | ||
|
|
2a6fc07be3 | ||
|
|
decdfa6f7d | ||
|
|
8252c0bf6d | ||
|
|
47aa11475b | ||
|
|
0ce72dd2cc | ||
|
|
c6d7fe91d3 | ||
|
|
7847767832 | ||
|
|
d2209bfbb0 | ||
|
|
ca8e1582be | ||
|
|
243d3a9df2 | ||
|
|
ff8f5ddcc9 | ||
|
|
b00212a836 | ||
|
|
22c64b8d39 | ||
|
|
ad46ebd7d8 | ||
|
|
f4822b3fc4 | ||
|
|
9f042d2f67 | ||
|
|
815f58994a | ||
|
|
6a0ac6a391 | ||
|
|
bba4f64e8a | ||
|
|
619696aa6c | ||
|
|
2dc2b14628 | ||
|
|
6671d89bed | ||
|
|
d48c4b5554 | ||
|
|
44b98e6102 | ||
|
|
7a3e41d420 | ||
|
|
ce913b8934 | ||
|
|
19ffbcd306 | ||
|
|
e652ca7d29 | ||
|
|
26b90797e0 | ||
|
|
a5659a5364 | ||
|
|
a9d9900334 | ||
|
|
4e2c1ea7f4 | ||
|
|
f5035873ad | ||
|
|
c11cbda117 | ||
|
|
bad451b3da | ||
|
|
896ec8d3af | ||
|
|
12f8b6515c | ||
|
|
d8ae3abd59 | ||
|
|
487f519227 | ||
|
|
3cd69d30e2 | ||
|
|
783a094625 | ||
|
|
dcf1d477ae | ||
|
|
d63bbef7a0 | ||
|
|
4d9c982cda | ||
|
|
2ed96e6fcf | ||
|
|
019460e07d | ||
|
|
8a8b4eaec0 | ||
|
|
b3c3895294 | ||
|
|
df67eb6c26 | ||
|
|
f372096fb1 | ||
|
|
52284fd320 | ||
|
|
49064747a2 | ||
|
|
591ad564a9 | ||
|
|
2e624bed40 | ||
|
|
2e41cf7407 | ||
|
|
ce48993997 | ||
|
|
e40de2d992 | ||
|
|
fdd8525b1a | ||
|
|
bcd2b709fe | ||
|
|
e75fd22d09 | ||
|
|
e831fc2a1c | ||
|
|
c0014f015e | ||
|
|
fe5bdd1bf2 | ||
|
|
367fb6537c | ||
|
|
c0042add30 | ||
|
|
c0ec29fdc3 | ||
|
|
579809a053 | ||
|
|
9cde918dd2 | ||
|
|
63b827edc7 | ||
|
|
74ffd01152 | ||
|
|
280aceb1ae | ||
|
|
1f7711ef1f | ||
|
|
12df7aed60 | ||
|
|
96c35f1ed3 | ||
|
|
0d6910ae5a | ||
|
|
087ec6fbda | ||
|
|
fc028154a4 | ||
|
|
274b63b677 | ||
|
|
3169aafd28 | ||
|
|
c497719921 | ||
|
|
e57ebb0aa5 | ||
|
|
77d07c8743 | ||
|
|
9aab16aef7 | ||
|
|
3accbaca79 | ||
|
|
9d82be9d89 | ||
|
|
461f174b09 | ||
|
|
1354f3952b | ||
|
|
f9c9c35546 | ||
|
|
71a2424c24 | ||
|
|
ce19e9fae3 | ||
|
|
f725cc7936 | ||
|
|
ca49650407 | ||
|
|
94218a3ac6 | ||
|
|
beb324c0f5 | ||
|
|
604ee1698c | ||
|
|
ef05645f09 | ||
|
|
d4ce87c234 | ||
|
|
ab5dd7599e | ||
|
|
230c3632d9 | ||
|
|
42829cecd6 | ||
|
|
3b80090654 | ||
|
|
e61d097562 | ||
|
|
32f2da4ef0 | ||
|
|
96479fff65 | ||
|
|
d4f00d15d4 | ||
|
|
a667e8c905 | ||
|
|
0a94da4368 | ||
|
|
56b1f64541 | ||
|
|
923a307454 | ||
|
|
87bf8c86ed | ||
|
|
61d660276f | ||
|
|
6a27c21008 | ||
|
|
ffe96f8a6f | ||
|
|
931fd9787b | ||
|
|
42e1d61296 | ||
|
|
765a9999df | ||
|
|
ba055b3e09 | ||
|
|
3fdc6d753a | ||
|
|
3e454637fb | ||
|
|
c7f3080aac | ||
|
|
072311b5e0 | ||
|
|
c06e9f0889 | ||
|
|
4781754dc4 | ||
|
|
28b61729c7 | ||
|
|
14a9f84c6c | ||
|
|
8b01679e8f | ||
|
|
eaf1fcb349 | ||
|
|
e20e370353 | ||
|
|
3605a2d58b | ||
|
|
aef258321c | ||
|
|
a303e9c0ca | ||
|
|
ccffbfc164 | ||
|
|
242dc6e398 | ||
|
|
22056605a6 | ||
|
|
23e7e4da00 | ||
|
|
15a91992e4 | ||
|
|
03c439a5b9 | ||
|
|
be836e614a | ||
|
|
15b0c8ef3a | ||
|
|
a1144dda3e | ||
|
|
86008c9dd6 | ||
|
|
8f452ee892 | ||
|
|
745d5b6fc5 | ||
|
|
05cd77f71b | ||
|
|
b4f616fed7 | ||
|
|
fd29de6c55 | ||
|
|
eca1af4be1 | ||
|
|
7a01f5830c | ||
|
|
18af21159b | ||
|
|
248e37991c | ||
|
|
ff424745fd | ||
|
|
79c8ffe0da | ||
|
|
2b1c88c54c |
@@ -1,50 +0,0 @@
|
||||
---
|
||||
name: "Bug report"
|
||||
about: Report a bug in SearXNG"
|
||||
labels: ["bug"]
|
||||
type: "bug"
|
||||
---
|
||||
|
||||
_Replace this placeholder with a meaningful and precise description of the bug._
|
||||
|
||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
||||
|
||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
||||
-->
|
||||
|
||||
### How To Reproduce?
|
||||
|
||||
<!-- How can we reproduce this issue? (as minimally and as precisely as
|
||||
possible) -->
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
### Screenshots & Logs
|
||||
|
||||
<!-- If applicable, add screenshots, logs to help explain your problem. -->
|
||||
|
||||
### Version of SearXNG
|
||||
|
||||
<!-- Commit number if you are using on master branch and stipulate if you forked
|
||||
SearXNG -->
|
||||
|
||||
<!-- Look at the bottom of the SearXNG page and check for the version after
|
||||
"Powered by SearXNG" If you are using a forked version of SearXNG include a
|
||||
link to the fork source code. -->
|
||||
|
||||
### How did you install SearXNG?
|
||||
|
||||
<!-- Did you install SearXNG using the official documentation or using
|
||||
searxng-docker? -->
|
||||
|
||||
### Additional context
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
||||
@@ -0,0 +1,62 @@
|
||||
name: Bug report
|
||||
description: Report a bug in SearXNG
|
||||
labels: ["bug"]
|
||||
type: bug
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A meaningful and precise description of the bug.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: How can we reproduce this issue? As minimally and as precisely as possible.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Screenshots & Logs
|
||||
description: If applicable, add screenshots or logs to help explain your problem.
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version of SearXNG
|
||||
description: >
|
||||
Look at the bottom of the SearXNG page after "Powered by SearXNG".
|
||||
If you are on master, use the commit. If you forked SearXNG, include a link to the fork.
|
||||
placeholder: e.g. 2026.9.13+96479fff6
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: install
|
||||
attributes:
|
||||
label: How did you install SearXNG?
|
||||
description: https://docs.searxng.org/admin/installation.html
|
||||
options:
|
||||
- Public instance
|
||||
- Installation script
|
||||
- Docker Compose
|
||||
- Docker (docker run)
|
||||
- Podman
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: ai-policy
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
options:
|
||||
- label: >
|
||||
I read the [AI Policy](https://github.com/searxng/searxng/blob/master/AI_POLICY.rst)
|
||||
and hereby confirm that this issue conforms with the policy.
|
||||
required: true
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
name: Engine request"
|
||||
about: Request a new engine in SearXNG"
|
||||
labels: ["engine request"]
|
||||
type: "feature"
|
||||
---
|
||||
|
||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
||||
|
||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
||||
-->
|
||||
|
||||
### Working URL to the engine
|
||||
|
||||
<!-- Please check if the engine is responding correctly before submitting -->
|
||||
|
||||
### Why do you want to add this engine?
|
||||
|
||||
<!-- What's special about this engine? -->
|
||||
|
||||
### Features of this engine
|
||||
|
||||
<!-- Features of this engine: Serves special content, is fast, is easy to
|
||||
integrate, ... ? -->
|
||||
|
||||
### How can SearXNG fetch results from this engine?
|
||||
|
||||
<!-- List API URL, example code and more that could be useful for the developers
|
||||
in order to implement this engine. If you don't know what to write, let
|
||||
this part blank. -->
|
||||
|
||||
### Applicable category of this engine
|
||||
|
||||
<!-- Where should this new engine fit in SearXNG? Current categories in
|
||||
SearXNG: general, files, images, it, map, music, news, science, social
|
||||
media and videos. -->
|
||||
|
||||
### Additional context
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
||||
@@ -0,0 +1,56 @@
|
||||
name: Engine request
|
||||
description: Request a new engine in SearXNG
|
||||
labels: ["engine request"]
|
||||
type: feature
|
||||
body:
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: Working URL to the engine
|
||||
description: Please check if the engine is responding correctly before submitting.
|
||||
placeholder: https://example.com/search?q=foobar
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: why
|
||||
attributes:
|
||||
label: Why do you want to add this engine?
|
||||
description: What's special about this engine?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: Category
|
||||
description: Where should this new engine fit in SearXNG?
|
||||
options:
|
||||
- general
|
||||
- files
|
||||
- images
|
||||
- it
|
||||
- map
|
||||
- music
|
||||
- news
|
||||
- science
|
||||
- social media
|
||||
- videos
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: fetch
|
||||
attributes:
|
||||
label: How can SearXNG fetch results?
|
||||
description: API URL, example code, etc... anything that you think could be useful. Leave blank if you don't know.
|
||||
|
||||
- type: checkboxes
|
||||
id: ai-policy
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
options:
|
||||
- label: >
|
||||
I read the [AI Policy](https://github.com/searxng/searxng/blob/master/AI_POLICY.rst)
|
||||
and hereby confirm that this issue conforms with the policy.
|
||||
required: true
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
name: "Feature request"
|
||||
about: "Request a new feature in SearXNG"
|
||||
labels: ["new feature"]
|
||||
type: "feature"
|
||||
---
|
||||
|
||||
_Replace this placeholder with a concise description of the feature._
|
||||
|
||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
||||
|
||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
||||
-->
|
||||
|
||||
### Is your feature request related to a problem?
|
||||
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...] -->
|
||||
|
||||
### Describe the solution you'd like
|
||||
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
### Describe alternatives you've considered
|
||||
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Feature request
|
||||
description: Request a new feature in SearXNG
|
||||
labels: ["new feature"]
|
||||
type: feature
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A concise description of the feature.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Is your feature request related to a problem?
|
||||
description: A clear and concise description of what the problem is.
|
||||
placeholder: I'm always frustrated when [...]
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
placeholder: I want to be able to [...]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
placeholder: I've considered [...]
|
||||
|
||||
- type: checkboxes
|
||||
id: ai-policy
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
options:
|
||||
- label: >
|
||||
I read the [AI Policy](https://github.com/searxng/searxng/blob/master/AI_POLICY.rst)
|
||||
and hereby confirm that this issue conforms with the policy.
|
||||
required: true
|
||||
@@ -0,0 +1,41 @@
|
||||
// @ts-check
|
||||
// Closes issues and prs whose authors/agents don't accept the ai policy
|
||||
// https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
const boxCocRegex = /\[\s*[Xx]\s*\].*AI Policy/;
|
||||
const refCocRegex = /\[AI Policy\](?::\s*|\()https:\/\/github\.com\/searxng\/searxng\/.*AI_POLICY/;
|
||||
|
||||
export default async ({ github, context }) => {
|
||||
const item = context.payload.issue ?? context.payload.pull_request;
|
||||
if (!item?.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (boxCocRegex.test(item.body) && refCocRegex.test(item.body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = context.payload.issue ? "issue" : "pull request";
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: item.number,
|
||||
body: `Hello! Thank you for your contribution.
|
||||
|
||||
Unfortunately your ${type} was closed as the AI Policy has not been accepted.
|
||||
Please open a new ${type} after confirming your contribution aligns with our AI Policy.`,
|
||||
...context.repo
|
||||
});
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: item.number,
|
||||
labels: ["invalid:slop"],
|
||||
...context.repo
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
issue_number: item.number,
|
||||
state: "closed",
|
||||
state_reason: "not_planned",
|
||||
...context.repo
|
||||
});
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
// Closes issues and prs whose authors/agents don't accept the ai policy
|
||||
// https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
module.exports = async ({ github, context }) => {
|
||||
const item = context.payload.pull_request || context.payload.issue;
|
||||
const body = item.body || '';
|
||||
const kind = context.payload.pull_request ? 'pull request' : 'issue';
|
||||
|
||||
// https://github.com/searxng/searxng/pull/6476#discussion_r3683782481
|
||||
const hasBox = /\[[Xx]\].*AI Policy/.test(body);
|
||||
const hasRef = /\[AI Policy\]:\s*https:\/\/github\.com\/searxng\/searxng\/.*AI_POLICY/.test(body);
|
||||
if (hasBox && hasRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
body:
|
||||
'Hello! Thank you for your contribution.\n\n' +
|
||||
`Unfortunately your ${kind} was closed as the AI Policy has not been accepted.\n\n` +
|
||||
`Please open a new ${kind} after confirming your contribution aligns with our AI Policy.`,
|
||||
});
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
labels: ['invalid:slop'],
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned',
|
||||
});
|
||||
};
|
||||
@@ -18,7 +18,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check AI Policy
|
||||
name: Check
|
||||
# for issues with an author who has not contributed before
|
||||
if: >-
|
||||
github.event.sender.type != 'Bot' &&
|
||||
@@ -34,5 +34,5 @@ jobs:
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/ai_policy.cjs');
|
||||
await script({ github, context });
|
||||
const script = await import("${{ github.workspace }}/.github/scripts/aiPolicy.mjs")
|
||||
await script.default({ github, context });
|
||||
|
||||
@@ -41,28 +41,17 @@ jobs:
|
||||
- runner: ubuntu-26.04-arm
|
||||
arch: armv7
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
outputs:
|
||||
docker_tag: ${{ steps.build.outputs.docker_tag }}
|
||||
git_url: ${{ steps.build.outputs.git_url }}
|
||||
|
||||
steps:
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: "${{ github.repository_owner }}"
|
||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -93,6 +82,14 @@ jobs:
|
||||
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
||||
run: make container.build
|
||||
|
||||
- name: Upload container image
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: container-image-${{ matrix.arch }}
|
||||
path: "/var/tmp/searxng-podman/*.tar"
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
test:
|
||||
name: Test (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
@@ -105,19 +102,12 @@ jobs:
|
||||
arch: amd64
|
||||
- runner: ubuntu-26.04-arm
|
||||
arch: arm64
|
||||
- runner: ubuntu-26.04-arm
|
||||
- runner: ubuntu-26.04
|
||||
arch: armv7
|
||||
|
||||
steps:
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: "${{ github.repository_owner }}"
|
||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -125,10 +115,16 @@ jobs:
|
||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Download container image
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
pattern: container-image-${{ matrix.arch }}
|
||||
path: "/var/tmp/searxng-podman"
|
||||
merge-multiple: true
|
||||
|
||||
- name: Test
|
||||
env:
|
||||
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
||||
GIT_URL: "${{ needs.build.outputs.git_url }}"
|
||||
run: make container.test
|
||||
|
||||
release:
|
||||
@@ -163,8 +159,14 @@ jobs:
|
||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Download container images
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
pattern: container-image-*
|
||||
path: "/var/tmp/searxng-podman"
|
||||
merge-multiple: true
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
GIT_URL: "${{ needs.build.outputs.git_url }}"
|
||||
DOCKER_TAG: "${{ needs.build.outputs.docker_tag }}"
|
||||
run: make container.push
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- update_external_bangs.py
|
||||
- update_firefox_version.py
|
||||
- update_engine_traits.py
|
||||
- update_wikidata_units.py
|
||||
- update_wikidata.py
|
||||
- update_engine_descriptions.py
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "26"
|
||||
check-latest: "true"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
# yamllint disable rule:line-length
|
||||
name: Sync
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
codeberg:
|
||||
if: github.repository_owner == 'searxng'
|
||||
name: Codeberg
|
||||
runs-on: ubuntu-26.04-arm
|
||||
steps:
|
||||
- name: Sync
|
||||
env:
|
||||
ORIGIN: "github.com/searxng/searxng"
|
||||
DESTINATION: "codeberg.org/searxng/searxng"
|
||||
DESTINATION_USER: "${{ secrets.CODEBERG_USER }}"
|
||||
DESTINATION_TOKEN: "${{ secrets.CODEBERG_TOKEN }}"
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
git init --quiet --bare .sync
|
||||
cd .sync
|
||||
|
||||
git fetch --no-tags "https://${ORIGIN}.git" \
|
||||
'+refs/heads/master:refs/heads/master'
|
||||
|
||||
git push --force "https://${DESTINATION_USER}:${DESTINATION_TOKEN}@${DESTINATION}.git" \
|
||||
'refs/heads/master:refs/heads/master'
|
||||
@@ -25,25 +25,25 @@
|
||||
"dependencies": {
|
||||
"ionicons": "^8.1.0",
|
||||
"normalize.css": "8.0.1",
|
||||
"ol": "^10.10.0",
|
||||
"ol": "^10.11.0",
|
||||
"swiped-events": "1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.9",
|
||||
"@types/node": "^26.2.0",
|
||||
"browserslist": "^4.28.8",
|
||||
"@biomejs/biome": "2.5.15",
|
||||
"@types/node": "^26.6.5",
|
||||
"browserslist": "^4.29.4",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"edge.js": "^6.5.1",
|
||||
"less": "^4.9.0",
|
||||
"mathjs": "^15.2.0",
|
||||
"sharp": "~0.35.3",
|
||||
"sort-package-json": "^4.0.0",
|
||||
"stylelint": "^17.14.1",
|
||||
"sharp": "~0.35.5",
|
||||
"sort-package-json": "^4.1.0",
|
||||
"stylelint": "^17.16.0",
|
||||
"stylelint-config-standard-less": "^4.1.0",
|
||||
"stylelint-prettier": "^5.0.3",
|
||||
"svgo": "^4.0.2",
|
||||
"svgo": "^4.1.0",
|
||||
"typescript": "~7.0.2",
|
||||
"vite": "^8.2.1",
|
||||
"vite": "^8.3.3",
|
||||
"vite-bundle-analyzer": "^1.3.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,67 +8,68 @@ export type KeyBindingLayout = "default" | "vim";
|
||||
type KeyBinding = {
|
||||
key: string;
|
||||
fun: (event: KeyboardEvent) => void;
|
||||
des: string;
|
||||
cat: string;
|
||||
des?: string;
|
||||
cat?: string;
|
||||
};
|
||||
|
||||
type HighlightResultElement = "down" | "up" | "visible" | "bottom" | "top";
|
||||
|
||||
/* common base for layouts */
|
||||
|
||||
const baseKeyBinding: Record<string, KeyBinding> = {
|
||||
Escape: {
|
||||
key: "ESC",
|
||||
fun: (event: KeyboardEvent) => removeFocus(event),
|
||||
des: "remove focus from the focused input",
|
||||
cat: "Control"
|
||||
des: settings.translations?.hotkeys_remove_focus,
|
||||
cat: settings.translations?.hotkeys_category_control
|
||||
},
|
||||
c: {
|
||||
key: "c",
|
||||
fun: () => copyURLToClipboard(),
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
des: settings.translations?.hotkeys_copy_url,
|
||||
cat: settings.translations?.hotkeys_category_results
|
||||
},
|
||||
h: {
|
||||
key: "h",
|
||||
fun: () => toggleHelp(keyBindings),
|
||||
des: "toggle help window",
|
||||
cat: "Other"
|
||||
des: settings.translations?.hotkeys_toggle_help,
|
||||
cat: settings.translations?.hotkeys_category_other
|
||||
},
|
||||
i: {
|
||||
key: "i",
|
||||
fun: () => searchInputFocus(),
|
||||
des: "focus on the search input",
|
||||
cat: "Control"
|
||||
des: settings.translations?.hotkeys_focus_search,
|
||||
cat: settings.translations?.hotkeys_category_control
|
||||
},
|
||||
n: {
|
||||
key: "n",
|
||||
fun: () => GoToNextPage(),
|
||||
des: "go to next page",
|
||||
cat: "Results"
|
||||
des: settings.translations?.hotkeys_next_page,
|
||||
cat: settings.translations?.hotkeys_category_results
|
||||
},
|
||||
o: {
|
||||
key: "o",
|
||||
fun: () => openResult(false),
|
||||
des: "open search result",
|
||||
cat: "Results"
|
||||
des: settings.translations?.hotkeys_open_result,
|
||||
cat: settings.translations?.hotkeys_category_results
|
||||
},
|
||||
p: {
|
||||
key: "p",
|
||||
fun: () => GoToPreviousPage(),
|
||||
des: "go to previous page",
|
||||
cat: "Results"
|
||||
des: settings.translations?.hotkeys_previous_page,
|
||||
cat: settings.translations?.hotkeys_category_results
|
||||
},
|
||||
r: {
|
||||
key: "r",
|
||||
fun: () => reloadPage(),
|
||||
des: "reload page from the server",
|
||||
cat: "Control"
|
||||
des: settings.translations?.hotkeys_reload_page,
|
||||
cat: settings.translations?.hotkeys_category_control
|
||||
},
|
||||
t: {
|
||||
key: "t",
|
||||
fun: () => openResult(true),
|
||||
des: "open the result in a new tab",
|
||||
cat: "Results"
|
||||
des: settings.translations?.hotkeys_open_new_tab,
|
||||
cat: settings.translations?.hotkeys_category_results
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,14 +79,14 @@ const keyBindingLayouts: Record<KeyBindingLayout, Record<string, KeyBinding>> =
|
||||
ArrowLeft: {
|
||||
key: "←",
|
||||
fun: () => highlightResult("up")(),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
des: settings.translations?.hotkeys_select_previous,
|
||||
cat: settings.translations?.hotkeys_category_results
|
||||
},
|
||||
ArrowRight: {
|
||||
key: "→",
|
||||
fun: () => highlightResult("down")(),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
des: settings.translations?.hotkeys_select_next,
|
||||
cat: settings.translations?.hotkeys_category_results
|
||||
},
|
||||
...baseKeyBinding
|
||||
},
|
||||
@@ -95,56 +96,56 @@ const keyBindingLayouts: Record<KeyBindingLayout, Record<string, KeyBinding>> =
|
||||
b: {
|
||||
key: "b",
|
||||
fun: () => scrollPage(-window.innerHeight),
|
||||
des: "scroll one page up",
|
||||
cat: "Navigation"
|
||||
des: settings.translations?.hotkeys_page_up,
|
||||
cat: settings.translations?.hotkeys_category_navigation
|
||||
},
|
||||
d: {
|
||||
key: "d",
|
||||
fun: () => scrollPage(window.innerHeight / 2),
|
||||
des: "scroll half a page down",
|
||||
cat: "Navigation"
|
||||
des: settings.translations?.hotkeys_half_page_down,
|
||||
cat: settings.translations?.hotkeys_category_navigation
|
||||
},
|
||||
f: {
|
||||
key: "f",
|
||||
fun: () => scrollPage(window.innerHeight),
|
||||
des: "scroll one page down",
|
||||
cat: "Navigation"
|
||||
des: settings.translations?.hotkeys_page_down,
|
||||
cat: settings.translations?.hotkeys_category_navigation
|
||||
},
|
||||
g: {
|
||||
key: "g",
|
||||
fun: () => scrollPageTo(-document.body.scrollHeight, "top"),
|
||||
des: "scroll to the top of the page",
|
||||
cat: "Navigation"
|
||||
des: settings.translations?.hotkeys_scroll_top,
|
||||
cat: settings.translations?.hotkeys_category_navigation
|
||||
},
|
||||
j: {
|
||||
key: "j",
|
||||
fun: () => highlightResult("down")(),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
des: settings.translations?.hotkeys_select_next,
|
||||
cat: settings.translations?.hotkeys_category_results
|
||||
},
|
||||
k: {
|
||||
key: "k",
|
||||
fun: () => highlightResult("up")(),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
des: settings.translations?.hotkeys_select_previous,
|
||||
cat: settings.translations?.hotkeys_category_results
|
||||
},
|
||||
u: {
|
||||
key: "u",
|
||||
fun: () => scrollPage(-window.innerHeight / 2),
|
||||
des: "scroll half a page up",
|
||||
cat: "Navigation"
|
||||
des: settings.translations?.hotkeys_half_page_up,
|
||||
cat: settings.translations?.hotkeys_category_navigation
|
||||
},
|
||||
v: {
|
||||
key: "v",
|
||||
fun: () => scrollPageTo(document.body.scrollHeight, "bottom"),
|
||||
des: "scroll to the bottom of the page",
|
||||
cat: "Navigation"
|
||||
des: settings.translations?.hotkeys_scroll_bottom,
|
||||
cat: settings.translations?.hotkeys_category_navigation
|
||||
},
|
||||
y: {
|
||||
key: "y",
|
||||
fun: () => copyURLToClipboard(),
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
des: settings.translations?.hotkeys_copy_url,
|
||||
cat: settings.translations?.hotkeys_category_results
|
||||
},
|
||||
...baseKeyBinding
|
||||
}
|
||||
@@ -344,6 +345,7 @@ const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBin
|
||||
|
||||
for (const binding of Object.values(keyBindings)) {
|
||||
const { cat } = binding;
|
||||
if (!cat) continue;
|
||||
categories[cat] ??= [];
|
||||
categories[cat].push(binding);
|
||||
}
|
||||
@@ -352,8 +354,8 @@ const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBin
|
||||
(a, b) => (categories[b]?.length ?? 0) - (categories[a]?.length ?? 0)
|
||||
);
|
||||
|
||||
let html = '<a href="#" class="close" aria-label="close" title="close">×</a>';
|
||||
html += "<h3>How to navigate SearXNG with hotkeys</h3>";
|
||||
let html = `<a href="#" class="close" aria-label="${settings.translations?.close}" title="${settings.translations?.close}">×</a>`;
|
||||
html += `<h3>${settings.translations?.hotkeys_navigation_title}</h3>`;
|
||||
html += "<table>";
|
||||
|
||||
for (const [i, categoryKey] of sortedCategoryKeys.entries()) {
|
||||
@@ -372,6 +374,7 @@ const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBin
|
||||
html += '<ul class="list-unstyled">';
|
||||
|
||||
for (const binding of bindings) {
|
||||
if (!binding.des) continue;
|
||||
html += `<li><kbd>${binding.key}</kbd> ${binding.des}</li>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { listen, mutable, settings } from "../toolkit.ts";
|
||||
import { assertElement } from "../util/assertElement.ts";
|
||||
|
||||
let imgTimeoutID: number;
|
||||
const loadErrorSrc = `${settings.theme_static_path}/img/img_load_error.svg`;
|
||||
|
||||
const imageLoader = (resultElement: HTMLElement): void => {
|
||||
if (imgTimeoutID) clearTimeout(imgTimeoutID);
|
||||
@@ -15,7 +16,7 @@ const imageLoader = (resultElement: HTMLElement): void => {
|
||||
// use thumbnail until full image loads
|
||||
const thumbnail = resultElement.querySelector<HTMLImageElement>(".image_thumbnail");
|
||||
if (thumbnail) {
|
||||
if (thumbnail.src === `${settings.theme_static_path}/img/img_load_error.svg`) return;
|
||||
if (thumbnail.src === loadErrorSrc) return;
|
||||
|
||||
imgElement.onerror = (): void => {
|
||||
imgElement.src = thumbnail.src;
|
||||
@@ -35,17 +36,27 @@ const imageLoader = (resultElement: HTMLElement): void => {
|
||||
}, 1000) as unknown as number;
|
||||
};
|
||||
|
||||
const imageThumbnails: NodeListOf<HTMLImageElement> = document.querySelectorAll<HTMLImageElement>(
|
||||
"#urls img.image_thumbnail, img.thumbnail"
|
||||
);
|
||||
for (const thumbnail of imageThumbnails) {
|
||||
if (thumbnail.complete && thumbnail.naturalWidth === 0) {
|
||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
||||
const hideBrokenThumbnail = (img: HTMLImageElement): void => {
|
||||
if (img.classList.contains("thumbnail")) {
|
||||
img.closest("a.thumbnail_link")?.remove();
|
||||
return;
|
||||
}
|
||||
if (img.classList.contains("image_thumbnail") && img.src !== loadErrorSrc) {
|
||||
img.src = loadErrorSrc;
|
||||
}
|
||||
};
|
||||
|
||||
thumbnail.onerror = (): void => {
|
||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
||||
};
|
||||
document.addEventListener(
|
||||
"error",
|
||||
(event: Event) => {
|
||||
const img = event.target;
|
||||
if (img instanceof HTMLImageElement) hideBrokenThumbnail(img);
|
||||
},
|
||||
true
|
||||
);
|
||||
// some images may have already failed
|
||||
for (const img of document.querySelectorAll<HTMLImageElement>("img.thumbnail, #urls img.image_thumbnail")) {
|
||||
if (img.complete && img.naturalWidth === 0) hideBrokenThumbnail(img);
|
||||
}
|
||||
|
||||
const copyUrlButton: HTMLButtonElement | null =
|
||||
|
||||
@@ -14,10 +14,6 @@ const categoryButtons: HTMLButtonElement[] = Array.from(
|
||||
document.querySelectorAll<HTMLButtonElement>("#categories_container button.category")
|
||||
);
|
||||
|
||||
if (searchInput.value.length === 0) {
|
||||
searchReset.classList.add("empty");
|
||||
}
|
||||
|
||||
// focus search input on large screens
|
||||
if (!(isMobile || isResultsPage)) {
|
||||
searchInput.focus();
|
||||
@@ -35,15 +31,10 @@ if (isMobile) {
|
||||
});
|
||||
}
|
||||
|
||||
listen("input", searchInput, () => {
|
||||
searchReset.classList.toggle("empty", searchInput.value.length === 0);
|
||||
});
|
||||
|
||||
listen("click", searchReset, (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
searchInput.value = "";
|
||||
searchInput.focus();
|
||||
searchReset.classList.add("empty");
|
||||
});
|
||||
|
||||
for (const button of categoryButtons) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.hide_if_nojs {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.show_if_nojs {
|
||||
display: inline !important;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
Layout of the Video result class
|
||||
*/
|
||||
|
||||
.result-videos {
|
||||
a.thumbnail_link {
|
||||
.ltr-float-left();
|
||||
.ltr-margin-right(1rem);
|
||||
.ltr-margin-left(0);
|
||||
width: 10rem !important;
|
||||
aspect-ratio: 16 / 9 !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.embedded-video iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
}
|
||||
@@ -164,14 +164,14 @@ button.category_button {
|
||||
&:hover {
|
||||
color: var(--color-search-background-hover);
|
||||
}
|
||||
|
||||
&.empty * {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
html.no-js #clear_search.hide_if_nojs {
|
||||
display: none;
|
||||
#q:placeholder-shown ~ #clear_search {
|
||||
pointer-events: none;
|
||||
|
||||
* {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#q,
|
||||
@@ -204,12 +204,6 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
}
|
||||
}
|
||||
|
||||
.no-js #clear_search,
|
||||
.no-js #send_search {
|
||||
width: auto !important;
|
||||
.ltr-border-left(1px solid var(--color-search-border));
|
||||
}
|
||||
|
||||
.search_filters {
|
||||
margin-top: 0.6rem;
|
||||
.ltr-margin-right(0);
|
||||
|
||||
@@ -255,6 +255,8 @@ article[data-vim-selected].category-social {
|
||||
font-size: 1rem;
|
||||
color: var(--color-result-url-font);
|
||||
flex-flow: row nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -305,22 +307,44 @@ article[data-vim-selected].category-social {
|
||||
|
||||
a.thumbnail_link {
|
||||
position: relative;
|
||||
margin-top: 0.6rem;
|
||||
.ltr-margin-right(1rem);
|
||||
.ltr-float-left();
|
||||
display: block;
|
||||
.ltr-float-right();
|
||||
.ltr-margin-left(0.5rem);
|
||||
width: 8rem;
|
||||
aspect-ratio: 4 / 3;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
background: var(--color-result-border);
|
||||
|
||||
img.thumbnail_bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
filter: blur(2rem) brightness(0.5);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
img.thumbnail {
|
||||
width: 7rem;
|
||||
height: unset; // remove height value that was needed for lazy loading
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumbnail_length {
|
||||
.image-label-bottom-right();
|
||||
right: 6px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.result_inner {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.break {
|
||||
clear: both;
|
||||
}
|
||||
@@ -407,32 +431,10 @@ article[data-vim-selected].category-social {
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.result-videos {
|
||||
a.thumbnail_link img.thumbnail {
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.embedded-video iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@supports not (aspect-ratio: 1 / 1) {
|
||||
// support older browsers which do not have aspect-ratio
|
||||
// https://caniuse.com/?search=aspect-ratio
|
||||
.result-videos .embedded-video iframe {
|
||||
height: calc(@results-width * 9 / 16);
|
||||
}
|
||||
}
|
||||
|
||||
.engines {
|
||||
.ltr-float-right();
|
||||
clear: both;
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
@@ -471,6 +473,8 @@ article[data-vim-selected].category-social {
|
||||
& > a {
|
||||
position: relative;
|
||||
outline: none;
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -930,10 +934,6 @@ summary.title {
|
||||
}
|
||||
|
||||
.result {
|
||||
.thumbnail {
|
||||
max-width: 98%;
|
||||
}
|
||||
|
||||
.url {
|
||||
span.url {
|
||||
display: block;
|
||||
@@ -1144,13 +1144,11 @@ summary.title {
|
||||
*/
|
||||
|
||||
@media screen and (max-width: @small-phone) {
|
||||
.result-videos {
|
||||
img.thumbnail {
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: inherit;
|
||||
.result {
|
||||
a.thumbnail_link {
|
||||
aspect-ratio: 1 / 1;
|
||||
width: 7rem;
|
||||
border-radius: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1165,3 +1163,4 @@ pre code {
|
||||
@import "result_types/paper.less";
|
||||
@import "result_types/file.less";
|
||||
@import "result_types/image.less";
|
||||
@import "result_types/video.less";
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
|
||||
// stylelint-disable no-descending-specificity
|
||||
|
||||
html.no-js .hide_if_nojs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.js .show_if_nojs {
|
||||
.show_if_nojs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="ionicon">
|
||||
<path
|
||||
d="M430.11 347.9c-6.6-6.1-16.3-7.6-24.6-9-11.5-1.9-15.9-4-22.6-10-14.3-12.7-14.3-31.1 0-43.8l30.3-26.9c46.4-41 46.4-108.2 0-149.2-34.2-30.1-80.1-45-127.8-45-55.7 0-113.9 20.3-158.8 60.1-83.5 73.8-83.5 194.7 0 268.5 41.5 36.7 97.5 55 152.9 55.4h1.7c55.4 0 110-17.9 148.8-52.4 14.4-12.7 11.99-36.6.1-47.7Z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="32px"
|
||||
/>
|
||||
<circle
|
||||
fill="currentColor"
|
||||
cx="144"
|
||||
cy="208"
|
||||
r="32"
|
||||
/>
|
||||
<circle
|
||||
fill="currentColor"
|
||||
cx="152"
|
||||
cy="311"
|
||||
r="32"
|
||||
/>
|
||||
<circle
|
||||
fill="currentColor"
|
||||
cx="224"
|
||||
cy="144"
|
||||
r="32"
|
||||
/>
|
||||
<circle
|
||||
fill="currentColor"
|
||||
cx="256"
|
||||
cy="367"
|
||||
r="48"
|
||||
/>
|
||||
<circle
|
||||
fill="currentColor"
|
||||
cx="328"
|
||||
cy="144"
|
||||
r="32"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,6 +1,31 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M248 64C146.39 64 64 146.39 64 248s82.39 184 184 184 184-82.39 184-184S349.61 64 248 64z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/>
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M220 220h32v116"/>
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M208 340h88"/>
|
||||
<path d="M248 130a26 26 0 1026 26 26 26 0 00-26-26z" fill="currentColor" stroke="currentColor" stroke-miterlimit="10" stroke-width="1"/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="32"
|
||||
d="M248 64C146.39 64 64 146.39 64 248s82.39 184 184 184 184-82.39 184-184S349.61 64 248 64z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="32"
|
||||
d="M220 220h32v116"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="32" d="M208 340h88"
|
||||
/>
|
||||
<path
|
||||
d="M248 130a26 26 0 1026 26 26 26 0 00-26-26z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 648 B After Width: | Height: | Size: 852 B |
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="ionicon">
|
||||
<path
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
d="M108.56 342.78a60.34 60.34 0 1 0 60.56 60.44 60.63 60.63 0 0 0-60.56-60.44"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
d="M48 186.67v86.55c52 0 101.94 15.39 138.67 52.11s52 86.56 52 138.67h86.66c0-151.56-125.66-277.33-277.33-277.33"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
d="M48 48v86.56c185.25 0 329.22 144.08 329.22 329.44H464C464 234.66 277.67 48 48 48"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 625 B |
@@ -1,6 +1,31 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M368 415.86V72a24.07 24.07 0 00-24-24H72a24.07 24.07 0 00-24 24v352a40.12 40.12 0 0040 40h328" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
|
||||
<path d="M416 464h0a48 48 0 01-48-48V128h72a24 24 0 0124 24v264a48 48 0 01-48 48z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
|
||||
<path d="M240 128h64M240 192h64M112 256h192M112 320h192M112 384h192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/>
|
||||
<path d="M176 208h-64a16 16 0 01-16-16v-64a16 16 0 0116-16h64a16 16 0 0116 16v64a16 16 0 01-16 16z" fill="currentColor" stroke="currentColor" stroke-linejoin="round" stroke-width="1" />
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="32"
|
||||
d="M368 415.86V72a24.07 24.07 0 00-24-24H72a24.07 24.07 0 00-24 24v352a40.12 40.12 0 0040 40h328"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="32"
|
||||
d="M416 464h0a48 48 0 01-48-48V128h72a24 24 0 0124 24v264a48 48 0 01-48 48z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="32"
|
||||
d="M240 128h64M240 192h64M112 256h192M112 320h192M112 384h192"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1"
|
||||
d="M176 208h-64a16 16 0 01-16-16v-64a16 16 0 0116-16h64a16 16 0 0116 16v64a16 16 0 01-16 16z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 787 B After Width: | Height: | Size: 988 B |
@@ -0,0 +1,7 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M450-154v-309L180-619v309l270 156Zm60 0 270-156v-310L510-463v309Zm-60 69L150-258q-14-8-22-22t-8-30v-340q0-16 8-30t22-22l300-173q14-8 30-8t30 8l300 173q14 8 22 22t8 30v340q0 16-8 30t-22 22L510-85q-14 8-30 8t-30-8Zm194-525 102-59-266-154-102 59 266 154Zm-164 96 104-61-267-154-104 60 267 155Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 415 B |
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="ionicon">
|
||||
<circle
|
||||
fill="currentColor"
|
||||
cx="256"
|
||||
cy="256.02"
|
||||
r="32"
|
||||
/>
|
||||
<path
|
||||
d="M184.25 192.25a96 96 0 0 0 0 127.52M327.77 319.77a96 96 0 0 0 0-127.52M133.28 141.28a168 168 0 0 0 0 229.44M378.72 370.72a168 168 0 0 0 0-229.44" fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="32px"
|
||||
/>
|
||||
<path
|
||||
d="M435 416a240.34 240.34 0 0 0 0-320M77 96a240.34 240.34 0 0 0 0 320"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="32px"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 741 B |
@@ -0,0 +1,11 @@
|
||||
<svg
|
||||
height="512"
|
||||
viewBox="0 -960 20480 20480"
|
||||
width="512"
|
||||
version="1.1"
|
||||
>
|
||||
<path
|
||||
d="M 19480,18100 V 2980 q 0,-693 -493.5,-1186.5 Q 18493,1300 17800,1300 H 8560 q -693,0 -1186.5,493.5 Q 6880,2287 6880,2980 v 357 q 504,231 924,567 420,336 756,756 V 2980 h 9240 v 11067 l -987,-987 H 8560 V 9700 q -336,420 -756,756 -420,336 -924,567 v 2037 q 0,693 493.5,1186.5 Q 7867,14740 8560,14740 h 7560 z M 16120,11380 H 12760 V 9700 h 3360 z M 5200,9700 q 1050,0 1785,-735 735,-735 735,-1785 0,-1050 -735,-1785 -735,-735 -1785,-735 -231,0 -441,42 -210,42 -399,105 V 460 H 1000 v 1680 h 1680 v 5040 q 0,1050 735,1785 735,735 1785,735 z M 16120,8860 H 10240 V 7180 h 5880 z m 0,-2520 H 10240 V 4660 h 5880 z m 1680,6720 V 2980 Z"
|
||||
stroke="currentColor"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 808 B |
@@ -0,0 +1,26 @@
|
||||
<svg
|
||||
viewBox="0 0 512 512"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="34.6274"
|
||||
d="M 256,469.53574 C 373.93232,469.53574 469.53574,373.93232 469.53574,256 469.53574,138.06479 373.93232,42.464261 256,42.464261 138.06479,42.464261 42.464263,138.06479 42.464263,256 42.464263,373.93232 138.06479,469.53574 256,469.53574 Z"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="34.6274"
|
||||
d="m 256,111.7191 a 144.28091,144.28091 0 1 1 0,288.56181"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="34.6274"
|
||||
d="m 256,180.97393 a 75.034727,75.034727 0 0 1 69.31543,46.31417 75.060698,75.060698 0 0 1 0,57.4238 75.092439,75.092439 0 0 1 -16.26334,24.34019 A 75.069355,75.069355 0 0 1 256,331.02607"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 979 B |
@@ -39,14 +39,17 @@ const simple_icons: IconSet[] = [
|
||||
alert: "alert-outline.svg",
|
||||
appstore: "apps-outline.svg",
|
||||
book: "book-outline.svg",
|
||||
camera: "camera-outline.svg",
|
||||
close: "close-outline.svg",
|
||||
download: "download-outline.svg",
|
||||
"ellipsis-vertical": "ellipsis-vertical-outline.svg",
|
||||
"file-tray-full": "file-tray-full-outline.svg",
|
||||
"file-tray-stacked": "file-tray-stacked-outline.svg",
|
||||
film: "film-outline.svg",
|
||||
globe: "globe-outline.svg",
|
||||
heart: "heart-outline.svg",
|
||||
image: "image-outline.svg",
|
||||
language: "language-outline.svg",
|
||||
layers: "layers-outline.svg",
|
||||
leecher: "arrow-down.svg",
|
||||
location: "location-outline.svg",
|
||||
@@ -58,13 +61,14 @@ const simple_icons: IconSet[] = [
|
||||
"navigate-up": "chevron-up-outline.svg",
|
||||
people: "people-outline.svg",
|
||||
play: "play-outline.svg",
|
||||
radio: "radio-outline.svg",
|
||||
rainy: "rainy-outline.svg",
|
||||
save: "save-outline.svg",
|
||||
school: "school-outline.svg",
|
||||
search: "search-outline.svg",
|
||||
seeder: "swap-vertical.svg",
|
||||
settings: "settings-outline.svg",
|
||||
tv: "tv-outline.svg"
|
||||
tv: "tv-outline.svg",
|
||||
videocam: "videocam-outline.svg"
|
||||
},
|
||||
svgo_opts: sxng_icon_opts
|
||||
},
|
||||
@@ -75,7 +79,13 @@ const simple_icons: IconSet[] = [
|
||||
base: resolve(HERE, "src/svg/ionicons"),
|
||||
set: {
|
||||
"information-circle": "information-circle-outline.svg",
|
||||
newspaper: "newspaper-outline.svg"
|
||||
lyrics: "song-lyric-music.svg",
|
||||
newspaper: "newspaper-outline.svg",
|
||||
palette: "color-palette-outline.svg",
|
||||
package: "package_2.svg",
|
||||
radio: "radio-outline.svg",
|
||||
rss: "logo-rss.svg",
|
||||
tor: "tor-browser.svg"
|
||||
},
|
||||
svgo_opts: sxng_icon_opts
|
||||
}
|
||||
|
||||
@@ -7,9 +7,18 @@ for the backend. Example output of this EDGE template:
|
||||
--}}
|
||||
{#
|
||||
Catalog of SVG symbols that can be inserted into the HTML output of a Jinja
|
||||
template. This file from:
|
||||
template. This file is generated by:
|
||||
|
||||
make static.build.commit
|
||||
|
||||
from template:
|
||||
|
||||
client/simple/tools/jinja_svg_catalog.html.edge
|
||||
|
||||
To modify the icon set, edit the script:
|
||||
|
||||
client/simple/theme_icons.ts
|
||||
|
||||
client/simple/tools/icon_catalog.edge.html
|
||||
#}
|
||||
|
||||
{%-
|
||||
|
||||
@@ -52,7 +52,8 @@ export default {
|
||||
// stylesheets
|
||||
ltr: `${PATH.src}/less/style-ltr.less`,
|
||||
rtl: `${PATH.src}/less/style-rtl.less`,
|
||||
rss: `${PATH.src}/less/rss.less`
|
||||
rss: `${PATH.src}/less/rss.less`,
|
||||
noscript: `${PATH.src}/less/noscript.less`
|
||||
},
|
||||
|
||||
// file naming conventions / pathnames are relative to outDir (PATH.dist)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Read the documentation before using the `docker-compose.yml` file:
|
||||
# https://docs.searxng.org/admin/installation-docker.html
|
||||
#
|
||||
# Additional ENVs:
|
||||
# Additional variables:
|
||||
# https://docs.searxng.org/admin/settings/settings_general.html#settings-general
|
||||
# https://docs.searxng.org/admin/settings/settings_server.html#settings-server
|
||||
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
ARG CONTAINER_IMAGE_ORGANIZATION="searxng"
|
||||
ARG CONTAINER_IMAGE_NAME="searxng"
|
||||
FROM docker.io/searxng/base:searxng-builder AS builder
|
||||
|
||||
COPY ./requirements.txt ./requirements-server.txt ./
|
||||
|
||||
ENV UV_NO_MANAGED_PYTHON="true"
|
||||
|
||||
RUN --mount=type=cache,id=uv,target=/root/.cache/uv set -eux -o pipefail; \
|
||||
uv venv; \
|
||||
uv pip install --requirements ./requirements.txt --requirements ./requirements-server.txt; \
|
||||
uv cache prune --ci; \
|
||||
find ./.venv/lib/ -type f -exec strip --strip-unneeded {} + || true; \
|
||||
find ./.venv/lib/ -type d -name "__pycache__" -exec rm -rf {} +; \
|
||||
find ./.venv/lib/ -type f -name "*.pyc" -delete; \
|
||||
python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./.venv/lib/
|
||||
|
||||
COPY --exclude=./searx/version_frozen.py ./searx/ ./searx/
|
||||
|
||||
RUN set -eux -o pipefail; \
|
||||
python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./searx/; \
|
||||
find ./searx/static/ -type f \
|
||||
\( -name "*.html" -o -name "*.css" -o -name "*.js" -o -name "*.svg" \) \
|
||||
-exec gzip -9 -k {} + \
|
||||
-exec brotli -9 -k {} + \
|
||||
-exec gzip --test {}.gz + \
|
||||
-exec brotli --test {}.br +
|
||||
|
||||
FROM localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder AS builder
|
||||
FROM docker.io/searxng/base:searxng AS dist
|
||||
|
||||
COPY --chown=977:977 --from=builder /usr/local/searxng/.venv/ ./.venv/
|
||||
@@ -24,6 +46,8 @@ LABEL org.opencontainers.image.created="$CREATED" \
|
||||
org.opencontainers.image.url="https://searxng.org" \
|
||||
org.opencontainers.image.version="$VERSION"
|
||||
|
||||
# Extended from:
|
||||
# https://codeberg.org/searxng/base/src/branch/main/Dockerfile
|
||||
ENV __SEARXNG_VERSION="$VERSION" \
|
||||
__SEARXNG_SETTINGS_PATH="$__SEARXNG_CONFIG_PATH/settings.yml" \
|
||||
GRANIAN_PROCESS_NAME="searxng" \
|
||||
@@ -35,7 +59,6 @@ ENV __SEARXNG_VERSION="$VERSION" \
|
||||
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
|
||||
GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m"
|
||||
|
||||
# "*_PATH" ENVs are defined in base images
|
||||
VOLUME $__SEARXNG_CONFIG_PATH
|
||||
VOLUME $__SEARXNG_DATA_PATH
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
FROM docker.io/searxng/base:searxng-builder AS builder
|
||||
|
||||
COPY ./requirements.txt ./requirements-server.txt ./
|
||||
|
||||
ENV UV_NO_MANAGED_PYTHON="true"
|
||||
ENV UV_NATIVE_TLS="true"
|
||||
|
||||
ARG TIMESTAMP_VENV="0"
|
||||
|
||||
RUN --mount=type=cache,id=uv,target=/root/.cache/uv set -eux -o pipefail; \
|
||||
export SOURCE_DATE_EPOCH="$TIMESTAMP_VENV"; \
|
||||
uv venv; \
|
||||
uv pip install --requirements ./requirements.txt --requirements ./requirements-server.txt; \
|
||||
uv cache prune --ci; \
|
||||
find ./.venv/lib/ -type f -exec strip --strip-unneeded {} + || true; \
|
||||
find ./.venv/lib/ -type d -name "__pycache__" -exec rm -rf {} +; \
|
||||
find ./.venv/lib/ -type f -name "*.pyc" -delete; \
|
||||
python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./.venv/lib/; \
|
||||
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" {} +
|
||||
|
||||
COPY --exclude=./searx/version_frozen.py ./searx/ ./searx/
|
||||
|
||||
RUN set -eux -o pipefail; \
|
||||
python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./searx/; \
|
||||
find ./searx/static/ -type f \
|
||||
\( -name "*.html" -o -name "*.css" -o -name "*.js" -o -name "*.svg" \) \
|
||||
-exec gzip -9 -k {} + \
|
||||
-exec brotli -9 -k {} + \
|
||||
-exec gzip --test {}.gz + \
|
||||
-exec brotli --test {}.br +
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
valkey:
|
||||
container_name: searxng-valkey
|
||||
image: docker.io/valkey/valkey:9-alpine
|
||||
command: valkey-server --save 30 1 --loglevel warning
|
||||
command: valkey-server --save 300 1 --save 60 1000 --loglevel warning
|
||||
restart: always
|
||||
volumes:
|
||||
- valkey-data:/data/
|
||||
|
||||
@@ -13,7 +13,7 @@ check_file() {
|
||||
!!! "$target" is not a valid file, exiting...
|
||||
!!!
|
||||
EOF
|
||||
exit 127
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ check_directory() {
|
||||
!!! "$target" is not a valid directory, exiting...
|
||||
!!!
|
||||
EOF
|
||||
exit 127
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -283,12 +283,12 @@ container images are not officially supported):
|
||||
$ make container
|
||||
|
||||
$ docker images
|
||||
REPOSITORY TAG IMAGE ID SIZE
|
||||
localhost/searxng/searxng 2026.6.19-93f66bfb4 ... 265 MB
|
||||
localhost/searxng/searxng latest ... 265 MB
|
||||
localhost/searxng/searxng builder ... 687 MB
|
||||
docker.io/searxng/base searxng-builder ... 565 MB
|
||||
docker.io/searxng/base searxng ... 143 MB
|
||||
REPOSITORY TAG IMAGE ID SIZE
|
||||
localhost/searxng/searxng latest ... 271 MB
|
||||
localhost/searxng/searxng 2026.10.17-* ... 271 MB
|
||||
<none> <none> ... 677 MB
|
||||
docker.io/searxng/base searxng ... 144 MB
|
||||
docker.io/searxng/base searxng-builder ... 550 MB
|
||||
|
||||
Migrate from ``searxng-docker``
|
||||
===============================
|
||||
|
||||
@@ -58,10 +58,9 @@ engine is shown. Most of the options have a default value or even are optional.
|
||||
|
||||
# overwrite values from section 'outgoing:'
|
||||
enable_http2: false
|
||||
enable_http3: false
|
||||
retries: 1
|
||||
max_connections: 100
|
||||
max_keepalive_connections: 10
|
||||
keepalive_expiry: 5.0
|
||||
using_tor_proxy: false
|
||||
proxies:
|
||||
http:
|
||||
@@ -163,6 +162,16 @@ engine is shown. Most of the options have a default value or even are optional.
|
||||
``enable_http`` : optional
|
||||
Enable HTTP for this engine (by default only HTTPS is enabled).
|
||||
|
||||
``enable_http3`` : optional
|
||||
Use HTTP/3 (falls back to HTTP/2). Default ``false``.
|
||||
Ignored when a proxy is set.
|
||||
|
||||
.. hint::
|
||||
|
||||
HTTP/3 places demands on the IP infrastructure that are not met in every
|
||||
environment. Enable this option only if you are aware of these requirements
|
||||
and the extent to which they are met.
|
||||
|
||||
``retry_on_http_error`` : optional
|
||||
Retry request on some HTTP status code.
|
||||
|
||||
@@ -179,20 +188,12 @@ engine is shown. Most of the options have a default value or even are optional.
|
||||
Using tor proxy (``true``) or not (``false``) for this engine. The default is
|
||||
taken from ``using_tor_proxy`` of the :ref:`settings outgoing`.
|
||||
|
||||
.. _Pool limit configuration: https://www.python-httpx.org/advanced/#pool-limit-configuration
|
||||
|
||||
``max_keepalive_connection#s`` :
|
||||
`Pool limit configuration`_, overwrites value ``pool_maxsize`` from
|
||||
:ref:`settings outgoing` for this engine.
|
||||
.. _Pool limit configuration: https://curl-cffi.readthedocs.io/en/latest/api.html#sessions
|
||||
|
||||
``max_connections`` :
|
||||
`Pool limit configuration`_, overwrites value ``pool_connections`` from
|
||||
:ref:`settings outgoing` for this engine.
|
||||
|
||||
``keepalive_expiry`` :
|
||||
`Pool limit configuration`_, overwrites value ``keepalive_expiry`` from
|
||||
:ref:`settings outgoing` for this engine.
|
||||
|
||||
|
||||
.. _private engines:
|
||||
|
||||
|
||||
@@ -12,20 +12,12 @@ Communication with search engines.
|
||||
request_timeout: 2.0 # default timeout in seconds, can be override by engine
|
||||
max_request_timeout: 10.0 # the maximum timeout in seconds
|
||||
useragent_suffix: "" # information like an email address to the administrator
|
||||
pool_connections: 100 # Maximum number of allowable connections, or null
|
||||
# for no limits. The default is 100.
|
||||
pool_maxsize: 10 # Number of allowable keep-alive connections, or null
|
||||
# to always allow. The default is 10.
|
||||
enable_http2: true # See https://www.python-httpx.org/http2/
|
||||
pool_connections: 100 # Maximum number of concurrent connections (default: 100)
|
||||
enable_http2: true # Enables the use of HTTP2
|
||||
# uncomment below section if you want to use a custom server certificate
|
||||
# see https://www.python-httpx.org/advanced/#changing-the-verification-defaults
|
||||
# and https://www.python-httpx.org/compatibility/#ssl-configuration
|
||||
# verify: ~/.mitmproxy/mitmproxy-ca-cert.cer
|
||||
#
|
||||
# uncomment below section if you want to use a proxyq see: SOCKS proxies
|
||||
# https://2.python-requests.org/en/latest/user/advanced/#proxies
|
||||
# are also supported: see
|
||||
# https://2.python-requests.org/en/latest/user/advanced/#socks
|
||||
# uncomment below section if you want to use a proxy
|
||||
#
|
||||
# proxies:
|
||||
# all://:
|
||||
@@ -46,30 +38,26 @@ Communication with search engines.
|
||||
timeout to load). Can be override by ``timeout`` in the :ref:`settings engines`.
|
||||
|
||||
``useragent_suffix`` :
|
||||
Suffix to the user-agent SearXNG uses to send requests to others engines. If an
|
||||
engine wish to block you, a contact info here may be useful to avoid that.
|
||||
Suffix to add when an engine's User-Agent is set via searxng_useragent().
|
||||
Contact info here may be useful to avoid an engine blocking you.
|
||||
|
||||
.. _Pool limit configuration: https://www.python-httpx.org/advanced/#pool-limit-configuration
|
||||
|
||||
``pool_maxsize``:
|
||||
Number of allowable keep-alive connections, or ``null`` to always allow. The
|
||||
default is 10. See ``max_keepalive_connections`` `Pool limit configuration`_.
|
||||
.. _Pool limit configuration: https://curl-cffi.readthedocs.io/en/latest/api.html#sessions
|
||||
|
||||
``pool_connections`` :
|
||||
Maximum number of allowable connections, or ``null`` # for no limits. The
|
||||
default is 100. See ``max_connections`` `Pool limit configuration`_.
|
||||
Maximum number of concurrent connections. The default is 100.
|
||||
See ``max_clients`` `Pool limit configuration`_.
|
||||
|
||||
``keepalive_expiry`` :
|
||||
Number of seconds to keep a connection in the pool. By default 5.0 seconds.
|
||||
See ``keepalive_expiry`` `Pool limit configuration`_.
|
||||
|
||||
.. _httpx proxies: https://www.python-httpx.org/advanced/#http-proxying
|
||||
.. _curl_cffi proxies: https://curl-cffi.readthedocs.io/en/latest/quick_start.html
|
||||
|
||||
``proxies`` :
|
||||
Define one or more proxies you wish to use, see `httpx proxies`_.
|
||||
Define one or more proxies you wish to use, see `curl_cffi proxies`_.
|
||||
If there are more than one proxy for one protocol (http, https),
|
||||
requests to the engines are distributed in a round-robin fashion.
|
||||
|
||||
HTTP, HTTPS, SOCKS4, SOCKS5 and SOCKS5h proxies are supported
|
||||
(``http://``, ``https://``, ``socks4://``, ``socks5://``, ``socks5h://``). You should
|
||||
use ``socks5h://`` when using Tor so hostnames are resolved by the proxy.
|
||||
|
||||
``source_ips`` :
|
||||
If you use multiple network interfaces, define from which IP the requests must
|
||||
be made. Example:
|
||||
@@ -87,18 +75,15 @@ Communication with search engines.
|
||||
different proxy and source ip.
|
||||
|
||||
``enable_http2`` :
|
||||
Enable by default. Set to ``false`` to disable HTTP/2.
|
||||
|
||||
.. _httpx verification defaults: https://www.python-httpx.org/advanced/#changing-the-verification-defaults
|
||||
.. _httpx ssl configuration: https://www.python-httpx.org/compatibility/#ssl-configuration
|
||||
Enable by default (HTTP/2). Set to ``false`` to force HTTP/1.1.
|
||||
HTTP/3 is opt-in per engine (``enable_http3``).
|
||||
|
||||
``verify``: : ``$SSL_CERT_FILE``, ``$SSL_CERT_DIR``
|
||||
Allow to specify a path to certificate.
|
||||
see `httpx verification defaults`_.
|
||||
HTTPS verification uses the OS's trust store by default.
|
||||
Set a path to use a custom CA file.
|
||||
|
||||
In addition to ``verify``, SearXNG supports the ``$SSL_CERT_FILE`` (for a file) and
|
||||
``$SSL_CERT_DIR`` (for a directory) OpenSSL variables.
|
||||
see `httpx ssl configuration`_.
|
||||
|
||||
``max_redirects`` :
|
||||
30 by default. Maximum redirect before it is an error.
|
||||
|
||||
@@ -143,7 +143,7 @@ parameters with default value can be redefined for special purposes.
|
||||
data dict ``{}``
|
||||
cookies dict ``{}``
|
||||
verify bool ``True``
|
||||
headers.User-Agent str a random User-Agent
|
||||
headers.User-Agent str ``''``
|
||||
category str current category, like ``'general'``
|
||||
safesearch int ``0``, between ``0`` and ``2`` (normal, moderate, strict)
|
||||
time_range Optional[str] ``None``, can be ``day``, ``week``, ``month``, ``year``
|
||||
@@ -229,6 +229,8 @@ following parameters can be used to specify a search request:
|
||||
max_redirects int maximum redirects, hard limit
|
||||
soft_max_redirects int maximum redirects, soft limit. Record an error but don't stop the engine
|
||||
raise_for_httperror bool True by default: raise an exception if the HTTP code of response is >= 300
|
||||
impersonate str curl_cffi impersonate target (default: chrome, none to disable)
|
||||
curl_options dict Any extra libcurl options for the request
|
||||
=================== =========== ==========================================================================
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.. _adobe stock engine:
|
||||
|
||||
===========
|
||||
Adobe Stock
|
||||
===========
|
||||
|
||||
.. automodule:: searx.engines.adobe_stock
|
||||
:members:
|
||||
@@ -1,8 +0,0 @@
|
||||
.. _cara engine:
|
||||
|
||||
===========
|
||||
Cara Images
|
||||
===========
|
||||
|
||||
.. automodule:: searx.engines.cara
|
||||
:members:
|
||||
@@ -0,0 +1,8 @@
|
||||
.. _europepmc engine:
|
||||
|
||||
==========
|
||||
Europe PMC
|
||||
==========
|
||||
|
||||
.. automodule:: searx.engines.europepmc
|
||||
:members:
|
||||
@@ -1,8 +0,0 @@
|
||||
.. _loc engine:
|
||||
|
||||
===================
|
||||
Library of Congress
|
||||
===================
|
||||
|
||||
.. automodule:: searx.engines.loc
|
||||
:members:
|
||||
@@ -80,8 +80,8 @@ same environment, here are a few examples::
|
||||
# to test one of the update scripts
|
||||
(dev.env)$ searxng_extra/update/update_engine_traits.py --help
|
||||
|
||||
# to test the update of the wikidata units
|
||||
(dev.env)$ searxng_extra/update/update_wikidata_units.py
|
||||
# to test the update of the wikidata units and property names
|
||||
(dev.env)$ searxng_extra/update/update_wikidata.py
|
||||
|
||||
|
||||
.. sidebar:: further read
|
||||
|
||||
@@ -286,7 +286,7 @@ content becomes smart.
|
||||
files & folders origin :origin:`docs/dev/reST.rst` ``:origin:`docs/dev/reST.rst```
|
||||
pull request :pull:`4` ``:pull:`4```
|
||||
patch :patch:`af2cae6` ``:patch:`af2cae6```
|
||||
PyPi package :pypi:`httpx` ``:pypi:`httpx```
|
||||
PyPi package :pypi:`curl_cffi` ``:pypi:`curl_cffi```
|
||||
manual page man :man:`bash` ``:man:`bash```
|
||||
intersphinx_
|
||||
--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.. _result_types.video:
|
||||
|
||||
=============
|
||||
Video Results
|
||||
=============
|
||||
|
||||
.. automodule:: searx.result_types.video
|
||||
@@ -19,13 +19,13 @@ following types have been implemented so far ..
|
||||
main/paper
|
||||
main/file
|
||||
main/image
|
||||
main/video
|
||||
|
||||
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
|
||||
final typing is complete.
|
||||
|
||||
- :ref:`template default` / :py:obj:`Result`
|
||||
- :ref:`template videos`
|
||||
- :ref:`template torrent`
|
||||
- :ref:`template map`
|
||||
- :ref:`template packages`
|
||||
|
||||
@@ -90,10 +90,10 @@ Scripts to update static data in :origin:`searx/data/`
|
||||
:members:
|
||||
|
||||
|
||||
``update_wikidata_units.py``
|
||||
``update_wikidata.py``
|
||||
============================
|
||||
|
||||
:origin:`[source] <searxng_extra/update/update_wikidata_units.py>`
|
||||
:origin:`[source] <searxng_extra/update/update_wikidata.py>`
|
||||
|
||||
.. automodule:: searxng_extra.update.update_wikidata_units
|
||||
.. automodule:: searxng_extra.update.update_wikidata
|
||||
:members:
|
||||
|
||||
@@ -129,29 +129,6 @@ audio_src : uri,
|
||||
URL of an embedded ``<audio controls>``.
|
||||
|
||||
|
||||
.. _template videos:
|
||||
|
||||
``videos.html``
|
||||
---------------
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header` and
|
||||
- :ref:`macro result_sub_header`
|
||||
|
||||
Additional fields used in the :origin:`videos.html
|
||||
<searx/templates/simple/result_templates/videos.html>`:
|
||||
|
||||
iframe_src : :py:class:`str`
|
||||
URL of an embedded ``<iframe>`` / the frame is collapsible.
|
||||
|
||||
The videos are displayed as small thumbnails in the main results list, there
|
||||
is an additional button to collaps/open the embeded video.
|
||||
|
||||
content : :py:class:`str`
|
||||
Description of the code fragment.
|
||||
|
||||
|
||||
.. _template torrent:
|
||||
|
||||
``torrent.html``
|
||||
|
||||
@@ -48,7 +48,7 @@ PATH="${PY_ENV}/bin:${REPO_ROOT}/node_modules/.bin:${GOROOT}/bin:${GOPATH}/bin:$
|
||||
|
||||
PYOBJECTS="searx"
|
||||
PY_SETUP_EXTRAS='[test]'
|
||||
GECKODRIVER_VERSION="v0.37.0"
|
||||
GECKODRIVER_VERSION="v0.37.1"
|
||||
# SPHINXOPTS=
|
||||
BLACK_OPTIONS=("--target-version" "py311" "--line-length" "120" "--skip-string-normalization")
|
||||
BLACK_TARGETS=("--exclude" "(searx/static|searx/languages.py)" "--include" 'searxng.msg|\.pyi?$' "searx" "searxng_extra" "tests")
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
mock==5.2.0
|
||||
nose2[coverage_plugin]==0.16.0
|
||||
cov-core==1.15.0
|
||||
black==25.9.0
|
||||
pylint==4.0.7
|
||||
black==26.10.1
|
||||
pylint==4.1.3
|
||||
splinter==0.21.0
|
||||
selenium==4.47.0
|
||||
selenium==4.51.0
|
||||
Sphinx==8.2.3;python_version <= "3.11"
|
||||
Sphinx==9.1.0; python_version > "3.11"
|
||||
sphinx-issues==6.0.0
|
||||
@@ -18,11 +18,11 @@ myst-parser==5.0.0
|
||||
linuxdoc==20260504
|
||||
aiounittest==1.5.0
|
||||
yamllint==1.38.0
|
||||
wlc==2.1.1
|
||||
wlc==2.2.1
|
||||
coloredlogs==15.0.1
|
||||
docutils>=0.21.2;python_version <= "3.11"
|
||||
docutils>=0.22.4; python_version > "3.11"
|
||||
parameterized==0.9.0
|
||||
granian[reload]==2.8.2
|
||||
basedpyright==1.39.10
|
||||
granian[reload]==2.8.5
|
||||
basedpyright==1.40.2
|
||||
types-lxml==2026.2.16
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
granian==2.8.2
|
||||
granian[pname]==2.8.2
|
||||
granian==2.8.5
|
||||
granian[pname]==2.8.5
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
certifi==2026.7.22
|
||||
certifi==2026.10.23
|
||||
babel==2.18.0
|
||||
flask-babel==4.0.0
|
||||
flask==3.1.3
|
||||
jinja2==3.1.6
|
||||
lxml==6.1.2
|
||||
lxml==6.1.3
|
||||
pygments==2.21.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dateutil==3.0.0
|
||||
pyyaml==6.0.3
|
||||
httpx[http2]==0.28.1
|
||||
httpx-socks[asyncio]==0.13.1
|
||||
sniffio==1.3.1
|
||||
curl_cffi==0.17.0
|
||||
valkey==6.1.1
|
||||
markdown-it-py==4.2.0
|
||||
msgspec==0.21.1
|
||||
typer==0.27.1
|
||||
msgspec==0.22.0
|
||||
typer==0.27.3
|
||||
isodate==0.7.2
|
||||
whitenoise==6.12.0
|
||||
typing-extensions==4.16.0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Implementation of the :py:obj:`preference <searx.preference>` settings."""
|
||||
|
||||
# pylint: disable = too-few-public-methods
|
||||
|
||||
import typing as t
|
||||
|
||||
@@ -38,7 +38,6 @@ area:
|
||||
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ["AnswererInfo", "Answerer", "AnswerStorage"]
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from dataclasses import dataclass
|
||||
from searx.utils import load_module
|
||||
from searx.result_types.answer import BaseAnswer
|
||||
|
||||
|
||||
_default = pathlib.Path(__file__).parent
|
||||
log: logging.Logger = logging.getLogger("searx.answerers")
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from urllib.parse import urlencode
|
||||
|
||||
import lxml.etree
|
||||
import lxml.html
|
||||
from httpx import HTTPError
|
||||
from curl_cffi.requests.exceptions import RequestException
|
||||
|
||||
from searx import settings
|
||||
from searx.engines import (
|
||||
@@ -28,19 +28,19 @@ if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
|
||||
|
||||
def update_kwargs(**kwargs) -> None: # type: ignore
|
||||
def update_kwargs(kwargs) -> None: # type: ignore
|
||||
if 'timeout' not in kwargs:
|
||||
kwargs['timeout'] = settings['outgoing']['request_timeout']
|
||||
kwargs['raise_for_httperror'] = True
|
||||
|
||||
|
||||
def get(*args, **kwargs) -> "SXNG_Response": # type: ignore
|
||||
update_kwargs(**kwargs) # pyright: ignore[reportUnknownArgumentType]
|
||||
update_kwargs(kwargs)
|
||||
return http_get(*args, **kwargs) # pyright: ignore[reportUnknownArgumentType]
|
||||
|
||||
|
||||
def post(*args, **kwargs) -> "SXNG_Response": # type: ignore
|
||||
update_kwargs(**kwargs) # pyright: ignore[reportUnknownArgumentType]
|
||||
update_kwargs(kwargs)
|
||||
return http_post(*args, **kwargs) # pyright: ignore[reportUnknownArgumentType]
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ def bing(query: str, _sxng_locale: str) -> list[str]:
|
||||
base_url = "https://www.bing.com/AS/Suggestions?"
|
||||
# cvid has to be a 32 character long string consisting of numbers and uppsercase characters
|
||||
cvid = ''.join(random.choices(string.ascii_uppercase + string.digits, k=32))
|
||||
response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid}))
|
||||
response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid}), enable_http3=True)
|
||||
results: list[str] = []
|
||||
|
||||
if response.ok:
|
||||
@@ -83,7 +83,7 @@ def brave(query: str, _sxng_locale: str) -> list[str]:
|
||||
url = 'https://search.brave.com/api/suggest?'
|
||||
url += urlencode({'q': query})
|
||||
country = 'all'
|
||||
kwargs = {'cookies': {'country': country}}
|
||||
kwargs = {'cookies': {'country': country}, 'enable_http3': True}
|
||||
resp = get(url, **kwargs)
|
||||
results: list[str] = []
|
||||
|
||||
@@ -147,7 +147,7 @@ def google_complete(query: str, sxng_locale: str) -> list[str]:
|
||||
)
|
||||
results: list[str] = []
|
||||
|
||||
resp = get('https://www.google.com/complete/search?' + args)
|
||||
resp = get('https://www.google.com/complete/search?' + args, enable_http3=True)
|
||||
if resp and resp.ok:
|
||||
json_txt = resp.text[resp.text.find('[') : resp.text.find(']', -3) + 1]
|
||||
data = json.loads(json_txt)
|
||||
@@ -418,5 +418,5 @@ def search_autocomplete(backend_name: str, query: str, sxng_locale: str) -> list
|
||||
return []
|
||||
try:
|
||||
return backend(query, sxng_locale)
|
||||
except (HTTPError, SearxEngineResponseException):
|
||||
except (RequestException, SearxEngineResponseException):
|
||||
return []
|
||||
|
||||
@@ -5,7 +5,6 @@ Implementations used for bot detection.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ["init", "dump_request", "get_network", "too_many_requests", "ProxyFix"]
|
||||
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ class Config:
|
||||
if default is UNSET:
|
||||
raise KeyError(name)
|
||||
return default
|
||||
(modulename, name) = str(fqn).rsplit('.', 1)
|
||||
modulename, name = str(fqn).rsplit('.', 1)
|
||||
m = __import__(modulename, {}, {}, [name], 0)
|
||||
return getattr(m, name)
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ Accept_ header ..
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from ipaddress import (
|
||||
IPv4Network,
|
||||
IPv6Network,
|
||||
|
||||
@@ -14,7 +14,6 @@ bot if the Accept-Encoding_ header ..
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from ipaddress import (
|
||||
IPv4Network,
|
||||
IPv6Network,
|
||||
|
||||
@@ -11,7 +11,6 @@ if the Accept-Language_ header is unset.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from ipaddress import (
|
||||
IPv4Network,
|
||||
IPv6Network,
|
||||
|
||||
@@ -11,7 +11,6 @@ the Connection_ header is set to ``close``.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from ipaddress import (
|
||||
IPv4Network,
|
||||
IPv6Network,
|
||||
|
||||
@@ -20,6 +20,7 @@ Metadata`_. A request is filtered out in case of:
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ the User-Agent_ header is unset or matches the regular expression
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import re
|
||||
from ipaddress import (
|
||||
IPv4Network,
|
||||
@@ -25,7 +24,6 @@ import flask
|
||||
from . import config
|
||||
from ._helpers import too_many_requests
|
||||
|
||||
|
||||
USER_AGENT = (
|
||||
r'('
|
||||
+ r'unknown'
|
||||
|
||||
@@ -55,7 +55,6 @@ from ._helpers import (
|
||||
logger,
|
||||
)
|
||||
|
||||
|
||||
logger = logger.getChild('ip_limit')
|
||||
|
||||
BURST_WINDOW = 20
|
||||
|
||||
@@ -23,6 +23,7 @@ The ``ip_lists`` method implements :py:obj:`block-list <block_ip>` and
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Implementation of a middleware to determine the real IP of an HTTP request
|
||||
(:py:obj:`flask.request.remote_addr`) behind a proxy chain."""
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Providing a Valkey database for the botdetection methods."""
|
||||
|
||||
|
||||
import valkey
|
||||
|
||||
__all__ = ["set_valkey_client", "get_valkey_client"]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -465,7 +465,7 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
|
||||
|
||||
# Check if value is expired. It's possible that it's expired but has not
|
||||
# yet been automatically deleted by the periodic maintenance
|
||||
(value, expire) = row
|
||||
value, expire = row
|
||||
now = time.time()
|
||||
if expire < now:
|
||||
# The record is deleted during the maintenance interval. Deleting
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import warnings
|
||||
|
||||
|
||||
# limiter backward compatibility
|
||||
# ------------------------------
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
make data.all
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
__all__ = ["ahmia_blacklist_loader", "data_dir", "get_cache"]
|
||||
@@ -32,6 +33,13 @@ class WikiDataUnitType(t.TypedDict):
|
||||
to_si_factor: float
|
||||
|
||||
|
||||
WikiDataPropertyNameType = str | dict[str, str]
|
||||
"""Name of a Wikidata property. Can be either the plain name or a dictionary of
|
||||
language code to property name, e.g. ``{"en": "Date of birth"}``."""
|
||||
WikiDataPropertiesType = dict[str, WikiDataPropertyNameType]
|
||||
"""Dictionary from wikidata property ID to property name."""
|
||||
|
||||
|
||||
class LocalesType(t.TypedDict):
|
||||
"""Data structure of an item in ``locales.json``"""
|
||||
|
||||
@@ -41,6 +49,7 @@ class LocalesType(t.TypedDict):
|
||||
|
||||
USER_AGENTS: UserAgentType
|
||||
WIKIDATA_UNITS: dict[str, WikiDataUnitType]
|
||||
WIKIDATA_PROPERTIES: WikiDataPropertiesType
|
||||
TRACKER_PATTERNS: TrackerPatternsDB
|
||||
LOCALES: LocalesType
|
||||
CURRENCIES: CurrenciesDB
|
||||
@@ -52,11 +61,12 @@ ENGINE_DESCRIPTIONS: dict[str, dict[str, t.Any]]
|
||||
ENGINE_TRAITS: dict[str, dict[str, t.Any]]
|
||||
|
||||
|
||||
lazy_globals = {
|
||||
lazy_globals: dict[str, t.Any] = {
|
||||
"CURRENCIES": CurrenciesDB(),
|
||||
"USER_AGENTS": None,
|
||||
"EXTERNAL_URLS": None,
|
||||
"WIKIDATA_UNITS": None,
|
||||
"WIKIDATA_PROPERTIES": None,
|
||||
"EXTERNAL_BANGS": None,
|
||||
"OSM_KEYS_TAGS": None,
|
||||
"ENGINE_DESCRIPTIONS": None,
|
||||
@@ -69,6 +79,7 @@ data_json_files = {
|
||||
"USER_AGENTS": "useragents.json",
|
||||
"EXTERNAL_URLS": "external_urls.json",
|
||||
"WIKIDATA_UNITS": "wikidata_units.json",
|
||||
"WIKIDATA_PROPERTIES": "wikidata_properties.json",
|
||||
"EXTERNAL_BANGS": "external_bangs.json",
|
||||
"OSM_KEYS_TAGS": "osm_keys_tags.json",
|
||||
"ENGINE_DESCRIPTIONS": "engine_descriptions.json",
|
||||
|
||||
@@ -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
|
||||
@@ -10,7 +11,7 @@ import re
|
||||
from collections.abc import Iterator
|
||||
from urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
|
||||
|
||||
from httpx import HTTPError
|
||||
from curl_cffi.requests.exceptions import RequestException
|
||||
|
||||
from searx.data.core import get_cache, log
|
||||
from searx.network import get as http_get
|
||||
@@ -28,11 +29,11 @@ class TrackerPatternsDB:
|
||||
|
||||
ctx_name = "data_tracker_patterns"
|
||||
|
||||
# ClearURL rule lists, the first one that responds HTTP 200 is used
|
||||
CLEAR_LIST_URL = [
|
||||
# ClearURL rule lists, the first one that responds HTTP 200 is used
|
||||
"https://rules1.clearurls.xyz/data.minify.json",
|
||||
"https://cdn.jsdelivr.net/gh/clearurls/rules@refs/heads/gh-pages/data.minify.json",
|
||||
"https://rules2.clearurls.xyz/data.minify.json",
|
||||
"https://raw.githubusercontent.com/ClearURLs/Rules/refs/heads/master/data.min.json",
|
||||
"https://rules1.clearurls.xyz/data.minify.json",
|
||||
]
|
||||
|
||||
class Fields:
|
||||
@@ -87,12 +88,12 @@ class TrackerPatternsDB:
|
||||
try:
|
||||
resp = http_get(url, timeout=3)
|
||||
|
||||
except HTTPError as exc:
|
||||
log.warning("TRACKER_PATTERNS: HTTPError (%s) occured while fetching %s", url, exc)
|
||||
except RequestException as exc:
|
||||
log.warning("TRACKER_PATTERNS: RequestException while fetching %s: %s", url, exc)
|
||||
continue
|
||||
|
||||
if resp.status_code != 200:
|
||||
log.warning(f"TRACKER_PATTERNS: ClearURL ignore HTTP {resp.status_code} {url}")
|
||||
log.warning("TRACKER_PATTERNS: ClearURL ignore HTTP %s %s", resp.status_code, url)
|
||||
continue
|
||||
|
||||
break
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
],
|
||||
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
|
||||
"versions": [
|
||||
"154.0",
|
||||
"153.0"
|
||||
"157.0",
|
||||
"156.0"
|
||||
]
|
||||
}
|
||||
@@ -3474,11 +3474,6 @@
|
||||
"symbol": "mm⁻²",
|
||||
"to_si_factor": 1e-06
|
||||
},
|
||||
"Q136039973": {
|
||||
"si_name": "Q6137407",
|
||||
"symbol": "FPS",
|
||||
"to_si_factor": 1.0
|
||||
},
|
||||
"Q1361854": {
|
||||
"si_name": "Q11570",
|
||||
"symbol": "dwt",
|
||||
@@ -5254,6 +5249,11 @@
|
||||
"symbol": "μA",
|
||||
"to_si_factor": 1e-06
|
||||
},
|
||||
"Q31274648": {
|
||||
"si_name": "Q6137407",
|
||||
"symbol": "FPS",
|
||||
"to_si_factor": 1.0
|
||||
},
|
||||
"Q3186734": {
|
||||
"si_name": "Q3186734",
|
||||
"symbol": "J/(m³ K)",
|
||||
|
||||
@@ -305,7 +305,7 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
|
||||
|
||||
region: str = ""
|
||||
"""For an engine, when there is ``region: ...`` in the YAML settings the engine
|
||||
does support only this one region::
|
||||
does support only this one region:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
@@ -317,6 +317,9 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
|
||||
enable_http: bool
|
||||
"""Enable HTTP (by default only HTTPS is enabled)."""
|
||||
|
||||
enable_http3: bool = False
|
||||
"""Enables the use of HTTP/3 if available"""
|
||||
|
||||
shortcut: str
|
||||
"""Code used to execute bang requests (``!foo``)"""
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"""1337x"""
|
||||
|
||||
from urllib.parse import quote, urljoin
|
||||
from lxml import html
|
||||
from searx.utils import extract_text, eval_xpath, eval_xpath_list, eval_xpath_getindex
|
||||
|
||||
# about
|
||||
@@ -31,7 +30,7 @@ def request(query, params):
|
||||
def response(resp):
|
||||
results = []
|
||||
|
||||
dom = html.fromstring(resp.text)
|
||||
dom = resp.html()
|
||||
|
||||
for result in eval_xpath_list(dom, '//table[contains(@class, "table-list")]/tbody//tr'):
|
||||
href = urljoin(url, eval_xpath_getindex(result, './td[contains(@class, "name")]/a[2]/@href', 0))
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import typing as t
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
|
||||
from searx import logger
|
||||
from searx.enginelib import EngineCache
|
||||
@@ -88,7 +87,7 @@ def response(resp):
|
||||
if not resp.text or not resp.text.strip():
|
||||
return []
|
||||
|
||||
dom = html.fromstring(resp.text)
|
||||
dom = resp.html()
|
||||
results = []
|
||||
|
||||
for item in dom.xpath('//li[contains(@class, "res-list")]'):
|
||||
|
||||
@@ -6,7 +6,8 @@ from urllib.parse import urlencode
|
||||
from datetime import datetime
|
||||
|
||||
from searx.exceptions import SearxEngineAPIException
|
||||
from searx.utils import html_to_text, get_embeded_stream_url
|
||||
from searx.result_types import EngineResults
|
||||
from searx.utils import html_to_text
|
||||
|
||||
about = {
|
||||
"website": "https://tv.360kan.com/",
|
||||
@@ -29,12 +30,12 @@ def request(query, params):
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
def response(resp) -> EngineResults:
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
raise SearxEngineAPIException(f"Invalid response: {e}") from e
|
||||
results = []
|
||||
res = EngineResults()
|
||||
|
||||
if "data" not in data or "result" not in data["data"]:
|
||||
raise SearxEngineAPIException("Invalid response")
|
||||
@@ -50,16 +51,15 @@ def response(resp):
|
||||
except (ValueError, TypeError):
|
||||
published_date = None
|
||||
|
||||
results.append(
|
||||
{
|
||||
'url': entry["play_url"],
|
||||
'title': html_to_text(entry["title"]),
|
||||
'content': html_to_text(entry["description"]),
|
||||
'template': 'videos.html',
|
||||
'publishedDate': published_date,
|
||||
'thumbnail': entry["cover_img"],
|
||||
"iframe_src": get_embeded_stream_url(entry["play_url"]),
|
||||
}
|
||||
res.add(
|
||||
res.types.Video(
|
||||
url=entry["play_url"],
|
||||
title=html_to_text(entry["title"]),
|
||||
content=html_to_text(entry["description"]),
|
||||
template='videos.html',
|
||||
publishedDate=published_date,
|
||||
thumbnail=entry["cover_img"],
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
return res
|
||||
|
||||
@@ -12,8 +12,6 @@ through our exclusive distribution partners.
|
||||
import typing as t
|
||||
|
||||
import codecs
|
||||
import random
|
||||
import string
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
@@ -33,11 +31,11 @@ about = {
|
||||
}
|
||||
|
||||
base_url = "https://500px.com"
|
||||
api_url = "https://api.500px.com"
|
||||
|
||||
categories = ["images"]
|
||||
api_url = "https://api-neo.500px.com"
|
||||
|
||||
categories = ["stock images"]
|
||||
paging = True
|
||||
|
||||
results_per_page = 30
|
||||
"""Number of results to return in the request.
|
||||
|
||||
@@ -46,47 +44,71 @@ The default was taken from the WEB UI, where the GraphQL query sets the value to
|
||||
"""
|
||||
|
||||
|
||||
SXNG_query = """query PhotoSearchPaginationContainerQuery(
|
||||
$first: Int, $cursor: String, $search: String!, $sort: PhotoSort, $filters: [PhotoSearchFilter!], $nlp: Boolean
|
||||
SXNG_query = """
|
||||
query searchResource(
|
||||
$keyword: String!
|
||||
$type: SearchType = TEXT
|
||||
$first: Int!
|
||||
$after: String
|
||||
$resourceTypes: [SearchResourceType!]
|
||||
$categories: [String!]
|
||||
$equipments: [String!]
|
||||
$downloadable: PhotoDownloadable
|
||||
$sort: SearchSortOption = RELEVANCE
|
||||
$excludeNsfw: Boolean
|
||||
) {
|
||||
...SXNG_query
|
||||
}
|
||||
|
||||
fragment SXNG_query on Query {
|
||||
photoSearch(sort: $sort, first: $first, after: $cursor, search: $search, filters: $filters, nlp: $nlp) {
|
||||
searchResource(
|
||||
keyword: $keyword
|
||||
type: $type
|
||||
first: $first
|
||||
after: $after
|
||||
resourceTypes: $resourceTypes
|
||||
categories: $categories
|
||||
equipments: $equipments
|
||||
downloadable: $downloadable
|
||||
sort: $sort
|
||||
excludeNsfw: $excludeNsfw
|
||||
) {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
canonicalPath
|
||||
name
|
||||
description
|
||||
width
|
||||
height
|
||||
photographer: uploader {
|
||||
displayName
|
||||
}
|
||||
images(sizes: [35, 33]) {
|
||||
size
|
||||
url
|
||||
jpegUrl
|
||||
webpUrl
|
||||
__typename
|
||||
... on Photo {
|
||||
id
|
||||
title
|
||||
description
|
||||
licensing {
|
||||
status
|
||||
__typename
|
||||
}
|
||||
urls {
|
||||
size_600
|
||||
size_1024
|
||||
size_2048
|
||||
size_4k
|
||||
__typename
|
||||
}
|
||||
uploader {
|
||||
displayName
|
||||
__typename
|
||||
}
|
||||
isNsfw
|
||||
width
|
||||
height
|
||||
dominantColorLight
|
||||
dominantColorDark
|
||||
uploadedAt
|
||||
__typename
|
||||
}
|
||||
}
|
||||
cursor
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def setup(_) -> bool:
|
||||
global SXNG_query # pylint: disable=global-statement
|
||||
rand_str: str = "".join(random.choices(string.ascii_letters, k=5))
|
||||
SXNG_query = SXNG_query.replace("SXNG_query", "PhotoSearchPaginationContainer_query_1" + rand_str)
|
||||
return True
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
# cursor is the base64 hash of the string "pos-<offset-1>", e.g. "pos-29" -> "cG9zLTI5"
|
||||
offset = ((params["pageno"] - 1) * results_per_page) - 1
|
||||
@@ -95,14 +117,14 @@ def request(query: str, params: "OnlineParams") -> None:
|
||||
params["url"] = f"{api_url}/graphql"
|
||||
params["method"] = "POST"
|
||||
params["json"] = {
|
||||
"operationName": "PhotoSearchPaginationContainerQuery",
|
||||
"operationName": "searchResource",
|
||||
"variables": {
|
||||
"after": cursor,
|
||||
"first": results_per_page,
|
||||
"cursor": cursor,
|
||||
"search": query,
|
||||
"keyword": query,
|
||||
"resourceTypes": ["PHOTO"],
|
||||
"sort": "RELEVANCE",
|
||||
"filters": [],
|
||||
"nlp": False,
|
||||
"type": "TEXT",
|
||||
},
|
||||
"query": SXNG_query,
|
||||
}
|
||||
@@ -110,27 +132,20 @@ def request(query: str, params: "OnlineParams") -> None:
|
||||
|
||||
def response(resp: "SXNG_Response"):
|
||||
res = EngineResults()
|
||||
json_data = resp.json()["data"]["photoSearch"]
|
||||
json_data = resp.json()["data"]["searchResource"]
|
||||
|
||||
for edge in json_data["edges"]:
|
||||
node = edge["node"] # pyright: ignore[reportAny]
|
||||
if not node["images"]:
|
||||
continue
|
||||
images: list[dict[str, str]] = sorted(node["images"], key=lambda i: i["size"])
|
||||
thumbnail_src = images[0]["url"]
|
||||
img_src = images[-1]["url"]
|
||||
image_urls = [url for (resolution, url) in node["urls"].items() if resolution.startswith("size_")]
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
{
|
||||
"template": "images.html",
|
||||
"url": base_url + node["canonicalPath"],
|
||||
"thumbnail_src": thumbnail_src,
|
||||
"img_src": img_src,
|
||||
"title": node["name"],
|
||||
"content": node["description"],
|
||||
"author": node["photographer"]["displayName"],
|
||||
"resolution": f"{node['width']}x{node['height']}",
|
||||
}
|
||||
res.types.Image(
|
||||
url=f"{base_url}/photo/{node['id']}",
|
||||
thumbnail_src=image_urls[0],
|
||||
img_src=image_urls[-1],
|
||||
title=node["title"],
|
||||
content=node["description"] or "",
|
||||
author=node["uploader"]["displayName"],
|
||||
resolution=f"{node['width']}x{node['height']}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ categories: list[str]
|
||||
disabled: bool
|
||||
display_error_messages: bool
|
||||
enable_http: bool
|
||||
enable_http3: bool
|
||||
engine_type: str
|
||||
inactive: bool
|
||||
max_page: int
|
||||
|
||||
@@ -115,26 +115,26 @@ def load_engine(engine_data: dict[str, t.Any]) -> "Engine | types.ModuleType | N
|
||||
logger.error('An engine does not have a "name" field')
|
||||
return None
|
||||
if '_' in engine_name:
|
||||
logger.error('Engine name contains underscore: "{}"'.format(engine_name))
|
||||
logger.error('Engine name contains underscore: "%s"', engine_name)
|
||||
return None
|
||||
|
||||
if engine_name.lower() != engine_name:
|
||||
logger.warning('Engine name is not lowercase: "{}", converting to lowercase'.format(engine_name))
|
||||
logger.warning('Engine name is not lowercase: "%s", converting to lowercase', engine_name)
|
||||
engine_name = engine_name.lower()
|
||||
engine_data['name'] = engine_name
|
||||
|
||||
# load_module
|
||||
module_name = engine_data.get('engine')
|
||||
if module_name is None:
|
||||
logger.error('The "engine" field is missing for the engine named "{}"'.format(engine_name))
|
||||
logger.error('The "engine" field is missing for the engine named "%s"', engine_name)
|
||||
return None
|
||||
try:
|
||||
engine = load_module(module_name + '.py', ENGINE_DIR)
|
||||
except (SyntaxError, KeyboardInterrupt, SystemExit, SystemError, ImportError, RuntimeError):
|
||||
logger.exception('Fatal exception in engine "{}"'.format(module_name))
|
||||
logger.exception('Fatal exception in engine "%s"', module_name)
|
||||
sys.exit(1)
|
||||
except BaseException:
|
||||
logger.exception('Cannot load engine "{}"'.format(module_name))
|
||||
logger.exception('Cannot load engine "%s"', module_name)
|
||||
return None
|
||||
|
||||
check_engine_module(engine)
|
||||
@@ -187,8 +187,9 @@ def set_loggers(engine: "Engine|types.ModuleType", engine_name: str):
|
||||
def update_engine_attributes(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]):
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
# set engine attributes from engine_data
|
||||
# set / update engine attributes from engine_data
|
||||
kvargs: dict[str, t.Any]
|
||||
engine.about = getattr(engine, "about", EngineAbout())
|
||||
if isinstance(engine.about, EngineAbout):
|
||||
kvargs = {**msgspec.to_builtins(engine.about), **engine_data.get("about", {})}
|
||||
else:
|
||||
@@ -246,7 +247,7 @@ def is_missing_required_attributes(engine: "Engine | types.ModuleType"):
|
||||
missing = False
|
||||
for engine_attr in dir(engine):
|
||||
if not engine_attr.startswith('_') and getattr(engine, engine_attr) is None:
|
||||
logger.error('Missing engine config attribute: "{0}.{1}"'.format(engine.name, engine_attr))
|
||||
logger.error('Missing engine config attribute: "%s.%s"', engine.name, engine_attr)
|
||||
missing = True
|
||||
return missing
|
||||
|
||||
@@ -276,12 +277,12 @@ def call_engine_setup(engine: "Engine | types.ModuleType", engine_data: dict[str
|
||||
if setup_func is None:
|
||||
setup_ok = True
|
||||
elif not callable(setup_func):
|
||||
logger.error(f"engine's setup method isn't a callable (is of type: {type(setup_func)})")
|
||||
logger.error("engine's setup method isn't a callable (is of type: %s)", type(setup_func))
|
||||
else:
|
||||
try:
|
||||
setup_ok = engine.setup(engine_data)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.exception(f"(PID {os.getpid()}) {engine.name}: engine SETUP failed, exception: {e}")
|
||||
logger.exception("(PID %s) %s: engine SETUP failed, exception: %s", os.getpid(), engine.name, e)
|
||||
setup_ok = False
|
||||
|
||||
# The evaluation of the return value is analogous to Engine.init
|
||||
@@ -289,18 +290,18 @@ def call_engine_setup(engine: "Engine | types.ModuleType", engine_data: dict[str
|
||||
setup_ok = True
|
||||
|
||||
if not setup_ok:
|
||||
logger.error(f"(PID {os.getpid()}) {engine.name}: engine setup was not successful")
|
||||
logger.error("(PID %s) %s: engine setup was not successful", os.getpid(), engine.name)
|
||||
return setup_ok
|
||||
|
||||
|
||||
def register_engine(engine: "Engine | types.ModuleType"):
|
||||
if engine.name in engines:
|
||||
logger.error('Engine config error: ambiguous name: {0}'.format(engine.name))
|
||||
logger.error('Engine config error: ambiguous name: %s', engine.name)
|
||||
sys.exit(1)
|
||||
engines[engine.name] = engine
|
||||
|
||||
if engine.shortcut in engine_shortcuts:
|
||||
logger.error('Engine config error: ambiguous shortcut: {0}'.format(engine.shortcut))
|
||||
logger.error('Engine config error: ambiguous shortcut: %s', engine.shortcut)
|
||||
sys.exit(1)
|
||||
engine_shortcuts[engine.shortcut] = engine.name
|
||||
|
||||
@@ -326,7 +327,9 @@ def load_engines(engine_list: list[dict[str, t.Any]]):
|
||||
# if an engine can't be loaded (if for example the engine is missing
|
||||
# tor or some other requirements) its set to inactive!
|
||||
logger.error(
|
||||
f"(PID {os.getpid()}) {engine_data.get('name', '???')}: can't register engine (loading engine failed)"
|
||||
"(PID %s) %s: can't register engine (loading engine failed)",
|
||||
os.getpid(),
|
||||
engine_data.get("name", "???"),
|
||||
)
|
||||
engine_data["inactive"] = True
|
||||
return engines
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""`Adobe Stock`_ is a service that gives access to millions of royalty-free
|
||||
assets. Assets types include photos, vectors, illustrations, templates, 3D
|
||||
assets, videos, motion graphics templates and audio tracks.
|
||||
|
||||
.. Adobe Stock: https://stock.adobe.com/
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
The engine has the following mandatory setting:
|
||||
|
||||
- SearXNG's :ref:`engine categories`
|
||||
- Adobe-Stock's :py:obj:`adobe_order`
|
||||
- Adobe-Stock's :py:obj:`adobe_content_types`
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: adobe stock
|
||||
engine: adobe_stock
|
||||
shortcut: asi
|
||||
categories: [images]
|
||||
adobe_order: relevance
|
||||
adobe_content_types: ["photo", "illustration", "zip_vector", "template", "3d", "image"]
|
||||
|
||||
- name: adobe stock video
|
||||
engine: adobe_stock
|
||||
network: adobe stock
|
||||
shortcut: asi
|
||||
categories: [videos]
|
||||
adobe_order: relevance
|
||||
adobe_content_types: ["video"]
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import isodate
|
||||
|
||||
about = {
|
||||
"website": "https://stock.adobe.com/",
|
||||
"wikidata_id": "Q5977430",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
categories = []
|
||||
paging = True
|
||||
results_per_page = 10
|
||||
|
||||
base_url = "https://stock.adobe.com"
|
||||
|
||||
adobe_order: str = ""
|
||||
"""Sort order, can be one of:
|
||||
|
||||
- ``relevance`` or
|
||||
- ``featured`` or
|
||||
- ``creation`` (most recent) or
|
||||
- ``nb_downloads`` (number of downloads)
|
||||
"""
|
||||
|
||||
ADOBE_VALID_TYPES = ["photo", "illustration", "zip_vector", "video", "template", "3d", "audio", "image"]
|
||||
adobe_content_types: list = []
|
||||
"""A list of of content types. The following content types are offered:
|
||||
|
||||
- Images: ``image``
|
||||
- Videos: ``video``
|
||||
- Templates: ``template``
|
||||
- 3D: ``3d``
|
||||
- Audio ``audio``
|
||||
|
||||
Additional subcategories:
|
||||
|
||||
- Photos: ``photo``
|
||||
- Illustrations: ``illustration``
|
||||
- Vectors: ``zip_vector`` (Vectors),
|
||||
"""
|
||||
|
||||
# Do we need support for "free_collection" and "include_stock_enterprise"?
|
||||
|
||||
|
||||
def init(_):
|
||||
if not categories:
|
||||
raise ValueError("adobe_stock engine: categories is unset")
|
||||
|
||||
# adobe_order
|
||||
if not adobe_order:
|
||||
raise ValueError("adobe_stock engine: adobe_order is unset")
|
||||
if adobe_order not in ["relevance", "featured", "creation", "nb_downloads"]:
|
||||
raise ValueError(f"unsupported adobe_order: {adobe_order}")
|
||||
|
||||
# adobe_content_types
|
||||
if not adobe_content_types:
|
||||
raise ValueError("adobe_stock engine: adobe_content_types is unset")
|
||||
|
||||
if isinstance(adobe_content_types, list):
|
||||
for t in adobe_content_types:
|
||||
if t not in ADOBE_VALID_TYPES:
|
||||
raise ValueError("adobe_stock engine: adobe_content_types: '%s' is invalid" % t)
|
||||
else:
|
||||
raise ValueError(
|
||||
"adobe_stock engine: adobe_content_types must be a list of strings not %s" % type(adobe_content_types)
|
||||
)
|
||||
|
||||
|
||||
def request(query, params):
|
||||
|
||||
args = {
|
||||
"k": query,
|
||||
"limit": results_per_page,
|
||||
"order": adobe_order,
|
||||
"search_page": params["pageno"],
|
||||
"search_type": "pagination",
|
||||
}
|
||||
|
||||
for content_type in ADOBE_VALID_TYPES:
|
||||
args[f"filters[content_type:{content_type}]"] = 1 if content_type in adobe_content_types else 0
|
||||
|
||||
params["url"] = f"{base_url}/de/Ajax/Search?{urlencode(args)}"
|
||||
|
||||
# headers required to bypass bot-detection
|
||||
if params["searxng_locale"] == "all":
|
||||
params["headers"]["Accept-Language"] = "en-US,en;q=0.5"
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def parse_image_item(item):
|
||||
return {
|
||||
"template": "images.html",
|
||||
"url": item["content_url"],
|
||||
"title": item["title"],
|
||||
"content": item["asset_type"],
|
||||
"img_src": item["content_thumb_extra_large_url"],
|
||||
"thumbnail_src": item["thumbnail_url"],
|
||||
"resolution": f"{item['content_original_width']}x{item['content_original_height']}",
|
||||
"img_format": item["format"],
|
||||
"author": item["author"],
|
||||
}
|
||||
|
||||
|
||||
def parse_video_item(item):
|
||||
|
||||
# in video items, the title is more or less a "content description", we try
|
||||
# to reduce the length of the title ..
|
||||
|
||||
title = item["title"]
|
||||
content = ""
|
||||
if "." in title.strip()[:-1]:
|
||||
content = title
|
||||
title = title.split(".", 1)[0]
|
||||
elif "," in title:
|
||||
content = title
|
||||
title = title.split(",", 1)[0]
|
||||
elif len(title) > 50:
|
||||
content = title
|
||||
title = ""
|
||||
for w in content.split(" "):
|
||||
title += f" {w}"
|
||||
if len(title) > 50:
|
||||
title = title.strip() + "\u2026"
|
||||
break
|
||||
|
||||
return {
|
||||
"template": "videos.html",
|
||||
"url": item["content_url"],
|
||||
"title": title,
|
||||
"content": content,
|
||||
# https://en.wikipedia.org/wiki/ISO_8601#Durations
|
||||
"length": isodate.parse_duration(item["time_duration"]),
|
||||
"publishedDate": datetime.fromisoformat(item["creation_date"]),
|
||||
"thumbnail": item["thumbnail_url"],
|
||||
"iframe_src": item["video_small_preview_url"],
|
||||
"metadata": item["asset_type"],
|
||||
}
|
||||
|
||||
|
||||
def parse_audio_item(item):
|
||||
audio_data = item["audio_data"]
|
||||
content = audio_data.get("description") or ""
|
||||
if audio_data.get("album"):
|
||||
content = audio_data["album"] + " - " + content
|
||||
|
||||
return {
|
||||
"url": item["content_url"],
|
||||
"title": item["title"],
|
||||
"content": content,
|
||||
# "thumbnail": base_url + item["thumbnail_url"],
|
||||
"iframe_src": audio_data["preview"]["url"],
|
||||
"publishedDate": datetime.fromisoformat(audio_data["release_date"]) if audio_data["release_date"] else None,
|
||||
"length": timedelta(seconds=round(audio_data["duration"] / 1000)) if audio_data["duration"] else None,
|
||||
"author": item.get("artist_name"),
|
||||
}
|
||||
|
||||
|
||||
def response(resp):
|
||||
results = []
|
||||
|
||||
json_resp = resp.json()
|
||||
|
||||
if isinstance(json_resp["items"], list):
|
||||
return None
|
||||
for item in json_resp["items"].values():
|
||||
if item["asset_type"].lower() in ["image", "premium-image", "illustration", "vector"]:
|
||||
result = parse_image_item(item)
|
||||
elif item["asset_type"].lower() == "video":
|
||||
result = parse_video_item(item)
|
||||
elif item["asset_type"].lower() == "audio":
|
||||
result = parse_audio_item(item)
|
||||
else:
|
||||
logger.error("no handle for %s --> %s", item["asset_type"], item)
|
||||
continue
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
@@ -12,7 +12,6 @@ servers and for Docker images.
|
||||
import re
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
from dateutil import parser
|
||||
|
||||
from searx.utils import eval_xpath, eval_xpath_list, extract_text
|
||||
@@ -57,7 +56,7 @@ def request(query, params):
|
||||
def response(resp):
|
||||
results = []
|
||||
|
||||
doc = html.fromstring(resp.text)
|
||||
doc = resp.html()
|
||||
for result in eval_xpath_list(doc, "//table/tbody/tr"):
|
||||
|
||||
if len(result.xpath("./td")) < 9:
|
||||
|
||||
@@ -39,7 +39,6 @@ import random
|
||||
import typing as t
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from lxml import html
|
||||
from lxml.etree import ElementBase
|
||||
|
||||
from searx.data import ENGINE_TRAITS
|
||||
@@ -143,7 +142,7 @@ def request(query: str, params: "OnlineParams") -> None:
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
res = EngineResults()
|
||||
dom = html.fromstring(resp.text)
|
||||
dom = resp.html()
|
||||
|
||||
# Each result is a div with class "flex" inside "js-aarecord-list-outer"
|
||||
# container. The "flex" filter excludes non-result div such as section
|
||||
@@ -255,7 +254,7 @@ def fetch_traits(engine_traits: EngineTraits) -> None:
|
||||
if not resp.ok:
|
||||
raise RuntimeError("Response from Anna's Archive is not OK.")
|
||||
|
||||
dom = html.fromstring(resp.text)
|
||||
dom = resp.html()
|
||||
|
||||
# supported language codes
|
||||
lang_map: dict[str, str] = {}
|
||||
|
||||
@@ -14,7 +14,6 @@ list in ``settings.yml``:
|
||||
"""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
from searx.result_types import EngineResults, MainResult
|
||||
from searx.utils import eval_xpath, eval_xpath_list, extract_text
|
||||
|
||||
@@ -62,7 +61,7 @@ def request(query, params):
|
||||
|
||||
def response(resp) -> EngineResults:
|
||||
res = EngineResults()
|
||||
doc = html.fromstring(resp.text)
|
||||
doc = resp.html()
|
||||
|
||||
for result in eval_xpath_list(doc, "//div[@class='article']"):
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
|
||||
from searx.utils import (
|
||||
eval_xpath_list,
|
||||
@@ -22,7 +21,7 @@ about = {
|
||||
}
|
||||
|
||||
# engine dependent config
|
||||
categories = ['files', 'apps']
|
||||
categories = ['it', 'apps']
|
||||
paging = True
|
||||
time_range_support = False
|
||||
|
||||
@@ -43,7 +42,7 @@ def request(query, params):
|
||||
def response(resp):
|
||||
results = []
|
||||
|
||||
dom = html.fromstring(resp.text)
|
||||
dom = resp.html()
|
||||
|
||||
# parse results
|
||||
for result in eval_xpath_list(dom, "//div[@id='content']//div[@class='listWidget']/div/div[@class='appRow']"):
|
||||
|
||||
@@ -17,7 +17,7 @@ about = {
|
||||
"results": 'JSON',
|
||||
}
|
||||
|
||||
categories = ['files', 'apps']
|
||||
categories = ['it', 'apps']
|
||||
safesearch = True
|
||||
|
||||
search_url = 'https://itunes.apple.com/search?{query}'
|
||||
|
||||
@@ -37,8 +37,7 @@ def obtain_token():
|
||||
)
|
||||
token['value'] = loads(actual_token.text)['authInfo']['access_token']
|
||||
token['last_updated'] = update_time
|
||||
# pylint: disable=bare-except
|
||||
except:
|
||||
except: # pylint: disable=bare-except
|
||||
pass
|
||||
return token
|
||||
|
||||
|
||||