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:
|
jobs:
|
||||||
check:
|
check:
|
||||||
name: Check AI Policy
|
name: Check
|
||||||
# for issues with an author who has not contributed before
|
# for issues with an author who has not contributed before
|
||||||
if: >-
|
if: >-
|
||||||
github.event.sender.type != 'Bot' &&
|
github.event.sender.type != 'Bot' &&
|
||||||
@@ -34,5 +34,5 @@ jobs:
|
|||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const script = require('./.github/scripts/ai_policy.cjs');
|
const script = await import("${{ github.workspace }}/.github/scripts/aiPolicy.mjs")
|
||||||
await script({ github, context });
|
await script.default({ github, context });
|
||||||
|
|||||||
@@ -41,28 +41,17 @@ jobs:
|
|||||||
- runner: ubuntu-26.04-arm
|
- runner: ubuntu-26.04-arm
|
||||||
arch: armv7
|
arch: armv7
|
||||||
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
docker_tag: ${{ steps.build.outputs.docker_tag }}
|
docker_tag: ${{ steps.build.outputs.docker_tag }}
|
||||||
git_url: ${{ steps.build.outputs.git_url }}
|
|
||||||
|
|
||||||
steps:
|
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
|
- name: Setup Python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
python-version: "${{ env.PYTHON_VERSION }}"
|
||||||
|
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
@@ -93,6 +82,14 @@ jobs:
|
|||||||
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
||||||
run: make container.build
|
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:
|
test:
|
||||||
name: Test (${{ matrix.arch }})
|
name: Test (${{ matrix.arch }})
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
@@ -105,19 +102,12 @@ jobs:
|
|||||||
arch: amd64
|
arch: amd64
|
||||||
- runner: ubuntu-26.04-arm
|
- runner: ubuntu-26.04-arm
|
||||||
arch: arm64
|
arch: arm64
|
||||||
- runner: ubuntu-26.04-arm
|
- runner: ubuntu-26.04
|
||||||
arch: armv7
|
arch: armv7
|
||||||
|
|
||||||
steps:
|
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
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
@@ -125,10 +115,16 @@ jobs:
|
|||||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||||
persist-credentials: "false"
|
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
|
- name: Test
|
||||||
env:
|
env:
|
||||||
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
||||||
GIT_URL: "${{ needs.build.outputs.git_url }}"
|
|
||||||
run: make container.test
|
run: make container.test
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@@ -163,8 +159,14 @@ jobs:
|
|||||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||||
persist-credentials: "false"
|
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
|
- name: Release
|
||||||
env:
|
env:
|
||||||
GIT_URL: "${{ needs.build.outputs.git_url }}"
|
|
||||||
DOCKER_TAG: "${{ needs.build.outputs.docker_tag }}"
|
DOCKER_TAG: "${{ needs.build.outputs.docker_tag }}"
|
||||||
run: make container.push
|
run: make container.push
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
- update_external_bangs.py
|
- update_external_bangs.py
|
||||||
- update_firefox_version.py
|
- update_firefox_version.py
|
||||||
- update_engine_traits.py
|
- update_engine_traits.py
|
||||||
- update_wikidata_units.py
|
- update_wikidata.py
|
||||||
- update_engine_descriptions.py
|
- update_engine_descriptions.py
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
python-version: "${{ env.PYTHON_VERSION }}"
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||||
with:
|
with:
|
||||||
node-version: "26"
|
node-version: "26"
|
||||||
check-latest: "true"
|
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": {
|
"dependencies": {
|
||||||
"ionicons": "^8.1.0",
|
"ionicons": "^8.1.0",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"ol": "^10.10.0",
|
"ol": "^10.11.0",
|
||||||
"swiped-events": "1.2.0"
|
"swiped-events": "1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.5.9",
|
"@biomejs/biome": "2.5.15",
|
||||||
"@types/node": "^26.2.0",
|
"@types/node": "^26.6.5",
|
||||||
"browserslist": "^4.28.8",
|
"browserslist": "^4.29.4",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"edge.js": "^6.5.1",
|
"edge.js": "^6.5.1",
|
||||||
"less": "^4.9.0",
|
"less": "^4.9.0",
|
||||||
"mathjs": "^15.2.0",
|
"mathjs": "^15.2.0",
|
||||||
"sharp": "~0.35.3",
|
"sharp": "~0.35.5",
|
||||||
"sort-package-json": "^4.0.0",
|
"sort-package-json": "^4.1.0",
|
||||||
"stylelint": "^17.14.1",
|
"stylelint": "^17.16.0",
|
||||||
"stylelint-config-standard-less": "^4.1.0",
|
"stylelint-config-standard-less": "^4.1.0",
|
||||||
"stylelint-prettier": "^5.0.3",
|
"stylelint-prettier": "^5.0.3",
|
||||||
"svgo": "^4.0.2",
|
"svgo": "^4.1.0",
|
||||||
"typescript": "~7.0.2",
|
"typescript": "~7.0.2",
|
||||||
"vite": "^8.2.1",
|
"vite": "^8.3.3",
|
||||||
"vite-bundle-analyzer": "^1.3.9"
|
"vite-bundle-analyzer": "^1.3.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,67 +8,68 @@ export type KeyBindingLayout = "default" | "vim";
|
|||||||
type KeyBinding = {
|
type KeyBinding = {
|
||||||
key: string;
|
key: string;
|
||||||
fun: (event: KeyboardEvent) => void;
|
fun: (event: KeyboardEvent) => void;
|
||||||
des: string;
|
des?: string;
|
||||||
cat: string;
|
cat?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HighlightResultElement = "down" | "up" | "visible" | "bottom" | "top";
|
type HighlightResultElement = "down" | "up" | "visible" | "bottom" | "top";
|
||||||
|
|
||||||
/* common base for layouts */
|
/* common base for layouts */
|
||||||
|
|
||||||
const baseKeyBinding: Record<string, KeyBinding> = {
|
const baseKeyBinding: Record<string, KeyBinding> = {
|
||||||
Escape: {
|
Escape: {
|
||||||
key: "ESC",
|
key: "ESC",
|
||||||
fun: (event: KeyboardEvent) => removeFocus(event),
|
fun: (event: KeyboardEvent) => removeFocus(event),
|
||||||
des: "remove focus from the focused input",
|
des: settings.translations?.hotkeys_remove_focus,
|
||||||
cat: "Control"
|
cat: settings.translations?.hotkeys_category_control
|
||||||
},
|
},
|
||||||
c: {
|
c: {
|
||||||
key: "c",
|
key: "c",
|
||||||
fun: () => copyURLToClipboard(),
|
fun: () => copyURLToClipboard(),
|
||||||
des: "copy url of the selected result to the clipboard",
|
des: settings.translations?.hotkeys_copy_url,
|
||||||
cat: "Results"
|
cat: settings.translations?.hotkeys_category_results
|
||||||
},
|
},
|
||||||
h: {
|
h: {
|
||||||
key: "h",
|
key: "h",
|
||||||
fun: () => toggleHelp(keyBindings),
|
fun: () => toggleHelp(keyBindings),
|
||||||
des: "toggle help window",
|
des: settings.translations?.hotkeys_toggle_help,
|
||||||
cat: "Other"
|
cat: settings.translations?.hotkeys_category_other
|
||||||
},
|
},
|
||||||
i: {
|
i: {
|
||||||
key: "i",
|
key: "i",
|
||||||
fun: () => searchInputFocus(),
|
fun: () => searchInputFocus(),
|
||||||
des: "focus on the search input",
|
des: settings.translations?.hotkeys_focus_search,
|
||||||
cat: "Control"
|
cat: settings.translations?.hotkeys_category_control
|
||||||
},
|
},
|
||||||
n: {
|
n: {
|
||||||
key: "n",
|
key: "n",
|
||||||
fun: () => GoToNextPage(),
|
fun: () => GoToNextPage(),
|
||||||
des: "go to next page",
|
des: settings.translations?.hotkeys_next_page,
|
||||||
cat: "Results"
|
cat: settings.translations?.hotkeys_category_results
|
||||||
},
|
},
|
||||||
o: {
|
o: {
|
||||||
key: "o",
|
key: "o",
|
||||||
fun: () => openResult(false),
|
fun: () => openResult(false),
|
||||||
des: "open search result",
|
des: settings.translations?.hotkeys_open_result,
|
||||||
cat: "Results"
|
cat: settings.translations?.hotkeys_category_results
|
||||||
},
|
},
|
||||||
p: {
|
p: {
|
||||||
key: "p",
|
key: "p",
|
||||||
fun: () => GoToPreviousPage(),
|
fun: () => GoToPreviousPage(),
|
||||||
des: "go to previous page",
|
des: settings.translations?.hotkeys_previous_page,
|
||||||
cat: "Results"
|
cat: settings.translations?.hotkeys_category_results
|
||||||
},
|
},
|
||||||
r: {
|
r: {
|
||||||
key: "r",
|
key: "r",
|
||||||
fun: () => reloadPage(),
|
fun: () => reloadPage(),
|
||||||
des: "reload page from the server",
|
des: settings.translations?.hotkeys_reload_page,
|
||||||
cat: "Control"
|
cat: settings.translations?.hotkeys_category_control
|
||||||
},
|
},
|
||||||
t: {
|
t: {
|
||||||
key: "t",
|
key: "t",
|
||||||
fun: () => openResult(true),
|
fun: () => openResult(true),
|
||||||
des: "open the result in a new tab",
|
des: settings.translations?.hotkeys_open_new_tab,
|
||||||
cat: "Results"
|
cat: settings.translations?.hotkeys_category_results
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,14 +79,14 @@ const keyBindingLayouts: Record<KeyBindingLayout, Record<string, KeyBinding>> =
|
|||||||
ArrowLeft: {
|
ArrowLeft: {
|
||||||
key: "←",
|
key: "←",
|
||||||
fun: () => highlightResult("up")(),
|
fun: () => highlightResult("up")(),
|
||||||
des: "select previous search result",
|
des: settings.translations?.hotkeys_select_previous,
|
||||||
cat: "Results"
|
cat: settings.translations?.hotkeys_category_results
|
||||||
},
|
},
|
||||||
ArrowRight: {
|
ArrowRight: {
|
||||||
key: "→",
|
key: "→",
|
||||||
fun: () => highlightResult("down")(),
|
fun: () => highlightResult("down")(),
|
||||||
des: "select next search result",
|
des: settings.translations?.hotkeys_select_next,
|
||||||
cat: "Results"
|
cat: settings.translations?.hotkeys_category_results
|
||||||
},
|
},
|
||||||
...baseKeyBinding
|
...baseKeyBinding
|
||||||
},
|
},
|
||||||
@@ -95,56 +96,56 @@ const keyBindingLayouts: Record<KeyBindingLayout, Record<string, KeyBinding>> =
|
|||||||
b: {
|
b: {
|
||||||
key: "b",
|
key: "b",
|
||||||
fun: () => scrollPage(-window.innerHeight),
|
fun: () => scrollPage(-window.innerHeight),
|
||||||
des: "scroll one page up",
|
des: settings.translations?.hotkeys_page_up,
|
||||||
cat: "Navigation"
|
cat: settings.translations?.hotkeys_category_navigation
|
||||||
},
|
},
|
||||||
d: {
|
d: {
|
||||||
key: "d",
|
key: "d",
|
||||||
fun: () => scrollPage(window.innerHeight / 2),
|
fun: () => scrollPage(window.innerHeight / 2),
|
||||||
des: "scroll half a page down",
|
des: settings.translations?.hotkeys_half_page_down,
|
||||||
cat: "Navigation"
|
cat: settings.translations?.hotkeys_category_navigation
|
||||||
},
|
},
|
||||||
f: {
|
f: {
|
||||||
key: "f",
|
key: "f",
|
||||||
fun: () => scrollPage(window.innerHeight),
|
fun: () => scrollPage(window.innerHeight),
|
||||||
des: "scroll one page down",
|
des: settings.translations?.hotkeys_page_down,
|
||||||
cat: "Navigation"
|
cat: settings.translations?.hotkeys_category_navigation
|
||||||
},
|
},
|
||||||
g: {
|
g: {
|
||||||
key: "g",
|
key: "g",
|
||||||
fun: () => scrollPageTo(-document.body.scrollHeight, "top"),
|
fun: () => scrollPageTo(-document.body.scrollHeight, "top"),
|
||||||
des: "scroll to the top of the page",
|
des: settings.translations?.hotkeys_scroll_top,
|
||||||
cat: "Navigation"
|
cat: settings.translations?.hotkeys_category_navigation
|
||||||
},
|
},
|
||||||
j: {
|
j: {
|
||||||
key: "j",
|
key: "j",
|
||||||
fun: () => highlightResult("down")(),
|
fun: () => highlightResult("down")(),
|
||||||
des: "select next search result",
|
des: settings.translations?.hotkeys_select_next,
|
||||||
cat: "Results"
|
cat: settings.translations?.hotkeys_category_results
|
||||||
},
|
},
|
||||||
k: {
|
k: {
|
||||||
key: "k",
|
key: "k",
|
||||||
fun: () => highlightResult("up")(),
|
fun: () => highlightResult("up")(),
|
||||||
des: "select previous search result",
|
des: settings.translations?.hotkeys_select_previous,
|
||||||
cat: "Results"
|
cat: settings.translations?.hotkeys_category_results
|
||||||
},
|
},
|
||||||
u: {
|
u: {
|
||||||
key: "u",
|
key: "u",
|
||||||
fun: () => scrollPage(-window.innerHeight / 2),
|
fun: () => scrollPage(-window.innerHeight / 2),
|
||||||
des: "scroll half a page up",
|
des: settings.translations?.hotkeys_half_page_up,
|
||||||
cat: "Navigation"
|
cat: settings.translations?.hotkeys_category_navigation
|
||||||
},
|
},
|
||||||
v: {
|
v: {
|
||||||
key: "v",
|
key: "v",
|
||||||
fun: () => scrollPageTo(document.body.scrollHeight, "bottom"),
|
fun: () => scrollPageTo(document.body.scrollHeight, "bottom"),
|
||||||
des: "scroll to the bottom of the page",
|
des: settings.translations?.hotkeys_scroll_bottom,
|
||||||
cat: "Navigation"
|
cat: settings.translations?.hotkeys_category_navigation
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
key: "y",
|
key: "y",
|
||||||
fun: () => copyURLToClipboard(),
|
fun: () => copyURLToClipboard(),
|
||||||
des: "copy url of the selected result to the clipboard",
|
des: settings.translations?.hotkeys_copy_url,
|
||||||
cat: "Results"
|
cat: settings.translations?.hotkeys_category_results
|
||||||
},
|
},
|
||||||
...baseKeyBinding
|
...baseKeyBinding
|
||||||
}
|
}
|
||||||
@@ -344,6 +345,7 @@ const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBin
|
|||||||
|
|
||||||
for (const binding of Object.values(keyBindings)) {
|
for (const binding of Object.values(keyBindings)) {
|
||||||
const { cat } = binding;
|
const { cat } = binding;
|
||||||
|
if (!cat) continue;
|
||||||
categories[cat] ??= [];
|
categories[cat] ??= [];
|
||||||
categories[cat].push(binding);
|
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)
|
(a, b) => (categories[b]?.length ?? 0) - (categories[a]?.length ?? 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
let html = '<a href="#" class="close" aria-label="close" title="close">×</a>';
|
let html = `<a href="#" class="close" aria-label="${settings.translations?.close}" title="${settings.translations?.close}">×</a>`;
|
||||||
html += "<h3>How to navigate SearXNG with hotkeys</h3>";
|
html += `<h3>${settings.translations?.hotkeys_navigation_title}</h3>`;
|
||||||
html += "<table>";
|
html += "<table>";
|
||||||
|
|
||||||
for (const [i, categoryKey] of sortedCategoryKeys.entries()) {
|
for (const [i, categoryKey] of sortedCategoryKeys.entries()) {
|
||||||
@@ -372,6 +374,7 @@ const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBin
|
|||||||
html += '<ul class="list-unstyled">';
|
html += '<ul class="list-unstyled">';
|
||||||
|
|
||||||
for (const binding of bindings) {
|
for (const binding of bindings) {
|
||||||
|
if (!binding.des) continue;
|
||||||
html += `<li><kbd>${binding.key}</kbd> ${binding.des}</li>`;
|
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";
|
import { assertElement } from "../util/assertElement.ts";
|
||||||
|
|
||||||
let imgTimeoutID: number;
|
let imgTimeoutID: number;
|
||||||
|
const loadErrorSrc = `${settings.theme_static_path}/img/img_load_error.svg`;
|
||||||
|
|
||||||
const imageLoader = (resultElement: HTMLElement): void => {
|
const imageLoader = (resultElement: HTMLElement): void => {
|
||||||
if (imgTimeoutID) clearTimeout(imgTimeoutID);
|
if (imgTimeoutID) clearTimeout(imgTimeoutID);
|
||||||
@@ -15,7 +16,7 @@ const imageLoader = (resultElement: HTMLElement): void => {
|
|||||||
// use thumbnail until full image loads
|
// use thumbnail until full image loads
|
||||||
const thumbnail = resultElement.querySelector<HTMLImageElement>(".image_thumbnail");
|
const thumbnail = resultElement.querySelector<HTMLImageElement>(".image_thumbnail");
|
||||||
if (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.onerror = (): void => {
|
||||||
imgElement.src = thumbnail.src;
|
imgElement.src = thumbnail.src;
|
||||||
@@ -35,17 +36,27 @@ const imageLoader = (resultElement: HTMLElement): void => {
|
|||||||
}, 1000) as unknown as number;
|
}, 1000) as unknown as number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageThumbnails: NodeListOf<HTMLImageElement> = document.querySelectorAll<HTMLImageElement>(
|
const hideBrokenThumbnail = (img: HTMLImageElement): void => {
|
||||||
"#urls img.image_thumbnail, img.thumbnail"
|
if (img.classList.contains("thumbnail")) {
|
||||||
);
|
img.closest("a.thumbnail_link")?.remove();
|
||||||
for (const thumbnail of imageThumbnails) {
|
return;
|
||||||
if (thumbnail.complete && thumbnail.naturalWidth === 0) {
|
|
||||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
|
||||||
}
|
}
|
||||||
|
if (img.classList.contains("image_thumbnail") && img.src !== loadErrorSrc) {
|
||||||
|
img.src = loadErrorSrc;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
thumbnail.onerror = (): void => {
|
document.addEventListener(
|
||||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
"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 =
|
const copyUrlButton: HTMLButtonElement | null =
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ const categoryButtons: HTMLButtonElement[] = Array.from(
|
|||||||
document.querySelectorAll<HTMLButtonElement>("#categories_container button.category")
|
document.querySelectorAll<HTMLButtonElement>("#categories_container button.category")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (searchInput.value.length === 0) {
|
|
||||||
searchReset.classList.add("empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
// focus search input on large screens
|
// focus search input on large screens
|
||||||
if (!(isMobile || isResultsPage)) {
|
if (!(isMobile || isResultsPage)) {
|
||||||
searchInput.focus();
|
searchInput.focus();
|
||||||
@@ -35,15 +31,10 @@ if (isMobile) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
listen("input", searchInput, () => {
|
|
||||||
searchReset.classList.toggle("empty", searchInput.value.length === 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("click", searchReset, (event: MouseEvent) => {
|
listen("click", searchReset, (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
searchInput.value = "";
|
searchInput.value = "";
|
||||||
searchInput.focus();
|
searchInput.focus();
|
||||||
searchReset.classList.add("empty");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const button of categoryButtons) {
|
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 {
|
&:hover {
|
||||||
color: var(--color-search-background-hover);
|
color: var(--color-search-background-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.empty * {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html.no-js #clear_search.hide_if_nojs {
|
#q:placeholder-shown ~ #clear_search {
|
||||||
display: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
* {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#q,
|
#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 {
|
.search_filters {
|
||||||
margin-top: 0.6rem;
|
margin-top: 0.6rem;
|
||||||
.ltr-margin-right(0);
|
.ltr-margin-right(0);
|
||||||
|
|||||||
@@ -255,6 +255,8 @@ article[data-vim-selected].category-social {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--color-result-url-font);
|
color: var(--color-result-url-font);
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -305,22 +307,44 @@ article[data-vim-selected].category-social {
|
|||||||
|
|
||||||
a.thumbnail_link {
|
a.thumbnail_link {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: 0.6rem;
|
display: block;
|
||||||
.ltr-margin-right(1rem);
|
.ltr-float-right();
|
||||||
.ltr-float-left();
|
.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 {
|
img.thumbnail {
|
||||||
width: 7rem;
|
position: relative;
|
||||||
height: unset; // remove height value that was needed for lazy loading
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail_length {
|
.thumbnail_length {
|
||||||
.image-label-bottom-right();
|
.image-label-bottom-right();
|
||||||
right: 6px;
|
right: 6px;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result_inner {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.break {
|
.break {
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
@@ -407,32 +431,10 @@ article[data-vim-selected].category-social {
|
|||||||
padding: 10px 0 0 0;
|
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 {
|
.engines {
|
||||||
.ltr-float-right();
|
.ltr-float-right();
|
||||||
|
clear: both;
|
||||||
|
margin-top: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -471,6 +473,8 @@ article[data-vim-selected].category-social {
|
|||||||
& > a {
|
& > a {
|
||||||
position: relative;
|
position: relative;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@@ -930,10 +934,6 @@ summary.title {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.result {
|
.result {
|
||||||
.thumbnail {
|
|
||||||
max-width: 98%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url {
|
.url {
|
||||||
span.url {
|
span.url {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -1144,13 +1144,11 @@ summary.title {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@media screen and (max-width: @small-phone) {
|
@media screen and (max-width: @small-phone) {
|
||||||
.result-videos {
|
.result {
|
||||||
img.thumbnail {
|
a.thumbnail_link {
|
||||||
float: none !important;
|
aspect-ratio: 1 / 1;
|
||||||
}
|
width: 7rem;
|
||||||
|
border-radius: 1.25rem;
|
||||||
.content {
|
|
||||||
overflow: inherit;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1165,3 +1163,4 @@ pre code {
|
|||||||
@import "result_types/paper.less";
|
@import "result_types/paper.less";
|
||||||
@import "result_types/file.less";
|
@import "result_types/file.less";
|
||||||
@import "result_types/image.less";
|
@import "result_types/image.less";
|
||||||
|
@import "result_types/video.less";
|
||||||
|
|||||||
@@ -4,11 +4,7 @@
|
|||||||
|
|
||||||
// stylelint-disable no-descending-specificity
|
// stylelint-disable no-descending-specificity
|
||||||
|
|
||||||
html.no-js .hide_if_nojs {
|
.show_if_nojs {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.js .show_if_nojs {
|
|
||||||
display: none;
|
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">
|
<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
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M220 220h32v116"/>
|
fill="none"
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M208 340h88"/>
|
stroke="currentColor"
|
||||||
<path d="M248 130a26 26 0 1026 26 26 26 0 00-26-26z" fill="currentColor" stroke="currentColor" stroke-miterlimit="10" stroke-width="1"/>
|
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>
|
</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">
|
<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
|
||||||
<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"/>
|
fill="none"
|
||||||
<path d="M240 128h64M240 192h64M112 256h192M112 320h192M112 384h192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/>
|
stroke="currentColor"
|
||||||
<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" />
|
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>
|
</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",
|
alert: "alert-outline.svg",
|
||||||
appstore: "apps-outline.svg",
|
appstore: "apps-outline.svg",
|
||||||
book: "book-outline.svg",
|
book: "book-outline.svg",
|
||||||
|
camera: "camera-outline.svg",
|
||||||
close: "close-outline.svg",
|
close: "close-outline.svg",
|
||||||
download: "download-outline.svg",
|
download: "download-outline.svg",
|
||||||
"ellipsis-vertical": "ellipsis-vertical-outline.svg",
|
"ellipsis-vertical": "ellipsis-vertical-outline.svg",
|
||||||
"file-tray-full": "file-tray-full-outline.svg",
|
"file-tray-full": "file-tray-full-outline.svg",
|
||||||
|
"file-tray-stacked": "file-tray-stacked-outline.svg",
|
||||||
film: "film-outline.svg",
|
film: "film-outline.svg",
|
||||||
globe: "globe-outline.svg",
|
globe: "globe-outline.svg",
|
||||||
heart: "heart-outline.svg",
|
heart: "heart-outline.svg",
|
||||||
image: "image-outline.svg",
|
image: "image-outline.svg",
|
||||||
|
language: "language-outline.svg",
|
||||||
layers: "layers-outline.svg",
|
layers: "layers-outline.svg",
|
||||||
leecher: "arrow-down.svg",
|
leecher: "arrow-down.svg",
|
||||||
location: "location-outline.svg",
|
location: "location-outline.svg",
|
||||||
@@ -58,13 +61,14 @@ const simple_icons: IconSet[] = [
|
|||||||
"navigate-up": "chevron-up-outline.svg",
|
"navigate-up": "chevron-up-outline.svg",
|
||||||
people: "people-outline.svg",
|
people: "people-outline.svg",
|
||||||
play: "play-outline.svg",
|
play: "play-outline.svg",
|
||||||
radio: "radio-outline.svg",
|
rainy: "rainy-outline.svg",
|
||||||
save: "save-outline.svg",
|
save: "save-outline.svg",
|
||||||
school: "school-outline.svg",
|
school: "school-outline.svg",
|
||||||
search: "search-outline.svg",
|
search: "search-outline.svg",
|
||||||
seeder: "swap-vertical.svg",
|
seeder: "swap-vertical.svg",
|
||||||
settings: "settings-outline.svg",
|
settings: "settings-outline.svg",
|
||||||
tv: "tv-outline.svg"
|
tv: "tv-outline.svg",
|
||||||
|
videocam: "videocam-outline.svg"
|
||||||
},
|
},
|
||||||
svgo_opts: sxng_icon_opts
|
svgo_opts: sxng_icon_opts
|
||||||
},
|
},
|
||||||
@@ -75,7 +79,13 @@ const simple_icons: IconSet[] = [
|
|||||||
base: resolve(HERE, "src/svg/ionicons"),
|
base: resolve(HERE, "src/svg/ionicons"),
|
||||||
set: {
|
set: {
|
||||||
"information-circle": "information-circle-outline.svg",
|
"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
|
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
|
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
|
// stylesheets
|
||||||
ltr: `${PATH.src}/less/style-ltr.less`,
|
ltr: `${PATH.src}/less/style-ltr.less`,
|
||||||
rtl: `${PATH.src}/less/style-rtl.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)
|
// file naming conventions / pathnames are relative to outDir (PATH.dist)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Read the documentation before using the `docker-compose.yml` file:
|
# Read the documentation before using the `docker-compose.yml` file:
|
||||||
# https://docs.searxng.org/admin/installation-docker.html
|
# 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_general.html#settings-general
|
||||||
# https://docs.searxng.org/admin/settings/settings_server.html#settings-server
|
# https://docs.searxng.org/admin/settings/settings_server.html#settings-server
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,29 @@
|
|||||||
ARG CONTAINER_IMAGE_ORGANIZATION="searxng"
|
FROM docker.io/searxng/base:searxng-builder AS builder
|
||||||
ARG CONTAINER_IMAGE_NAME="searxng"
|
|
||||||
|
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
|
FROM docker.io/searxng/base:searxng AS dist
|
||||||
|
|
||||||
COPY --chown=977:977 --from=builder /usr/local/searxng/.venv/ ./.venv/
|
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.url="https://searxng.org" \
|
||||||
org.opencontainers.image.version="$VERSION"
|
org.opencontainers.image.version="$VERSION"
|
||||||
|
|
||||||
|
# Extended from:
|
||||||
|
# https://codeberg.org/searxng/base/src/branch/main/Dockerfile
|
||||||
ENV __SEARXNG_VERSION="$VERSION" \
|
ENV __SEARXNG_VERSION="$VERSION" \
|
||||||
__SEARXNG_SETTINGS_PATH="$__SEARXNG_CONFIG_PATH/settings.yml" \
|
__SEARXNG_SETTINGS_PATH="$__SEARXNG_CONFIG_PATH/settings.yml" \
|
||||||
GRANIAN_PROCESS_NAME="searxng" \
|
GRANIAN_PROCESS_NAME="searxng" \
|
||||||
@@ -35,7 +59,6 @@ ENV __SEARXNG_VERSION="$VERSION" \
|
|||||||
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
|
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
|
||||||
GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m"
|
GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m"
|
||||||
|
|
||||||
# "*_PATH" ENVs are defined in base images
|
|
||||||
VOLUME $__SEARXNG_CONFIG_PATH
|
VOLUME $__SEARXNG_CONFIG_PATH
|
||||||
VOLUME $__SEARXNG_DATA_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:
|
valkey:
|
||||||
container_name: searxng-valkey
|
container_name: searxng-valkey
|
||||||
image: docker.io/valkey/valkey:9-alpine
|
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
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- valkey-data:/data/
|
- valkey-data:/data/
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ check_file() {
|
|||||||
!!! "$target" is not a valid file, exiting...
|
!!! "$target" is not a valid file, exiting...
|
||||||
!!!
|
!!!
|
||||||
EOF
|
EOF
|
||||||
exit 127
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ check_directory() {
|
|||||||
!!! "$target" is not a valid directory, exiting...
|
!!! "$target" is not a valid directory, exiting...
|
||||||
!!!
|
!!!
|
||||||
EOF
|
EOF
|
||||||
exit 127
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -283,12 +283,12 @@ container images are not officially supported):
|
|||||||
$ make container
|
$ make container
|
||||||
|
|
||||||
$ docker images
|
$ docker images
|
||||||
REPOSITORY TAG IMAGE ID SIZE
|
REPOSITORY TAG IMAGE ID SIZE
|
||||||
localhost/searxng/searxng 2026.6.19-93f66bfb4 ... 265 MB
|
localhost/searxng/searxng latest ... 271 MB
|
||||||
localhost/searxng/searxng latest ... 265 MB
|
localhost/searxng/searxng 2026.10.17-* ... 271 MB
|
||||||
localhost/searxng/searxng builder ... 687 MB
|
<none> <none> ... 677 MB
|
||||||
docker.io/searxng/base searxng-builder ... 565 MB
|
docker.io/searxng/base searxng ... 144 MB
|
||||||
docker.io/searxng/base searxng ... 143 MB
|
docker.io/searxng/base searxng-builder ... 550 MB
|
||||||
|
|
||||||
Migrate from ``searxng-docker``
|
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:'
|
# overwrite values from section 'outgoing:'
|
||||||
enable_http2: false
|
enable_http2: false
|
||||||
|
enable_http3: false
|
||||||
retries: 1
|
retries: 1
|
||||||
max_connections: 100
|
max_connections: 100
|
||||||
max_keepalive_connections: 10
|
|
||||||
keepalive_expiry: 5.0
|
|
||||||
using_tor_proxy: false
|
using_tor_proxy: false
|
||||||
proxies:
|
proxies:
|
||||||
http:
|
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`` : optional
|
||||||
Enable HTTP for this engine (by default only HTTPS is enabled).
|
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_on_http_error`` : optional
|
||||||
Retry request on some HTTP status code.
|
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
|
Using tor proxy (``true``) or not (``false``) for this engine. The default is
|
||||||
taken from ``using_tor_proxy`` of the :ref:`settings outgoing`.
|
taken from ``using_tor_proxy`` of the :ref:`settings outgoing`.
|
||||||
|
|
||||||
.. _Pool limit configuration: https://www.python-httpx.org/advanced/#pool-limit-configuration
|
.. _Pool limit configuration: https://curl-cffi.readthedocs.io/en/latest/api.html#sessions
|
||||||
|
|
||||||
``max_keepalive_connection#s`` :
|
|
||||||
`Pool limit configuration`_, overwrites value ``pool_maxsize`` from
|
|
||||||
:ref:`settings outgoing` for this engine.
|
|
||||||
|
|
||||||
``max_connections`` :
|
``max_connections`` :
|
||||||
`Pool limit configuration`_, overwrites value ``pool_connections`` from
|
`Pool limit configuration`_, overwrites value ``pool_connections`` from
|
||||||
:ref:`settings outgoing` for this engine.
|
:ref:`settings outgoing` for this engine.
|
||||||
|
|
||||||
``keepalive_expiry`` :
|
|
||||||
`Pool limit configuration`_, overwrites value ``keepalive_expiry`` from
|
|
||||||
:ref:`settings outgoing` for this engine.
|
|
||||||
|
|
||||||
|
|
||||||
.. _private engines:
|
.. _private engines:
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,12 @@ Communication with search engines.
|
|||||||
request_timeout: 2.0 # default timeout in seconds, can be override by engine
|
request_timeout: 2.0 # default timeout in seconds, can be override by engine
|
||||||
max_request_timeout: 10.0 # the maximum timeout in seconds
|
max_request_timeout: 10.0 # the maximum timeout in seconds
|
||||||
useragent_suffix: "" # information like an email address to the administrator
|
useragent_suffix: "" # information like an email address to the administrator
|
||||||
pool_connections: 100 # Maximum number of allowable connections, or null
|
pool_connections: 100 # Maximum number of concurrent connections (default: 100)
|
||||||
# for no limits. The default is 100.
|
enable_http2: true # Enables the use of HTTP2
|
||||||
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/
|
|
||||||
# uncomment below section if you want to use a custom server certificate
|
# 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
|
# verify: ~/.mitmproxy/mitmproxy-ca-cert.cer
|
||||||
#
|
#
|
||||||
# uncomment below section if you want to use a proxyq see: SOCKS proxies
|
# uncomment below section if you want to use a proxy
|
||||||
# https://2.python-requests.org/en/latest/user/advanced/#proxies
|
|
||||||
# are also supported: see
|
|
||||||
# https://2.python-requests.org/en/latest/user/advanced/#socks
|
|
||||||
#
|
#
|
||||||
# proxies:
|
# proxies:
|
||||||
# all://:
|
# all://:
|
||||||
@@ -46,30 +38,26 @@ Communication with search engines.
|
|||||||
timeout to load). Can be override by ``timeout`` in the :ref:`settings engines`.
|
timeout to load). Can be override by ``timeout`` in the :ref:`settings engines`.
|
||||||
|
|
||||||
``useragent_suffix`` :
|
``useragent_suffix`` :
|
||||||
Suffix to the user-agent SearXNG uses to send requests to others engines. If an
|
Suffix to add when an engine's User-Agent is set via searxng_useragent().
|
||||||
engine wish to block you, a contact info here may be useful to avoid that.
|
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 limit configuration: https://curl-cffi.readthedocs.io/en/latest/api.html#sessions
|
||||||
|
|
||||||
``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_connections`` :
|
``pool_connections`` :
|
||||||
Maximum number of allowable connections, or ``null`` # for no limits. The
|
Maximum number of concurrent connections. The default is 100.
|
||||||
default is 100. See ``max_connections`` `Pool limit configuration`_.
|
See ``max_clients`` `Pool limit configuration`_.
|
||||||
|
|
||||||
``keepalive_expiry`` :
|
.. _curl_cffi proxies: https://curl-cffi.readthedocs.io/en/latest/quick_start.html
|
||||||
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
|
|
||||||
|
|
||||||
``proxies`` :
|
``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),
|
If there are more than one proxy for one protocol (http, https),
|
||||||
requests to the engines are distributed in a round-robin fashion.
|
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`` :
|
``source_ips`` :
|
||||||
If you use multiple network interfaces, define from which IP the requests must
|
If you use multiple network interfaces, define from which IP the requests must
|
||||||
be made. Example:
|
be made. Example:
|
||||||
@@ -87,18 +75,15 @@ Communication with search engines.
|
|||||||
different proxy and source ip.
|
different proxy and source ip.
|
||||||
|
|
||||||
``enable_http2`` :
|
``enable_http2`` :
|
||||||
Enable by default. Set to ``false`` to disable HTTP/2.
|
Enable by default (HTTP/2). Set to ``false`` to force HTTP/1.1.
|
||||||
|
HTTP/3 is opt-in per engine (``enable_http3``).
|
||||||
.. _httpx verification defaults: https://www.python-httpx.org/advanced/#changing-the-verification-defaults
|
|
||||||
.. _httpx ssl configuration: https://www.python-httpx.org/compatibility/#ssl-configuration
|
|
||||||
|
|
||||||
``verify``: : ``$SSL_CERT_FILE``, ``$SSL_CERT_DIR``
|
``verify``: : ``$SSL_CERT_FILE``, ``$SSL_CERT_DIR``
|
||||||
Allow to specify a path to certificate.
|
HTTPS verification uses the OS's trust store by default.
|
||||||
see `httpx verification defaults`_.
|
Set a path to use a custom CA file.
|
||||||
|
|
||||||
In addition to ``verify``, SearXNG supports the ``$SSL_CERT_FILE`` (for a file) and
|
In addition to ``verify``, SearXNG supports the ``$SSL_CERT_FILE`` (for a file) and
|
||||||
``$SSL_CERT_DIR`` (for a directory) OpenSSL variables.
|
``$SSL_CERT_DIR`` (for a directory) OpenSSL variables.
|
||||||
see `httpx ssl configuration`_.
|
|
||||||
|
|
||||||
``max_redirects`` :
|
``max_redirects`` :
|
||||||
30 by default. Maximum redirect before it is an error.
|
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 ``{}``
|
data dict ``{}``
|
||||||
cookies dict ``{}``
|
cookies dict ``{}``
|
||||||
verify bool ``True``
|
verify bool ``True``
|
||||||
headers.User-Agent str a random User-Agent
|
headers.User-Agent str ``''``
|
||||||
category str current category, like ``'general'``
|
category str current category, like ``'general'``
|
||||||
safesearch int ``0``, between ``0`` and ``2`` (normal, moderate, strict)
|
safesearch int ``0``, between ``0`` and ``2`` (normal, moderate, strict)
|
||||||
time_range Optional[str] ``None``, can be ``day``, ``week``, ``month``, ``year``
|
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
|
max_redirects int maximum redirects, hard limit
|
||||||
soft_max_redirects int maximum redirects, soft limit. Record an error but don't stop the engine
|
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
|
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
|
# to test one of the update scripts
|
||||||
(dev.env)$ searxng_extra/update/update_engine_traits.py --help
|
(dev.env)$ searxng_extra/update/update_engine_traits.py --help
|
||||||
|
|
||||||
# to test the update of the wikidata units
|
# to test the update of the wikidata units and property names
|
||||||
(dev.env)$ searxng_extra/update/update_wikidata_units.py
|
(dev.env)$ searxng_extra/update/update_wikidata.py
|
||||||
|
|
||||||
|
|
||||||
.. sidebar:: further read
|
.. sidebar:: further read
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ content becomes smart.
|
|||||||
files & folders origin :origin:`docs/dev/reST.rst` ``:origin:`docs/dev/reST.rst```
|
files & folders origin :origin:`docs/dev/reST.rst` ``:origin:`docs/dev/reST.rst```
|
||||||
pull request :pull:`4` ``:pull:`4```
|
pull request :pull:`4` ``:pull:`4```
|
||||||
patch :patch:`af2cae6` ``:patch:`af2cae6```
|
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```
|
manual page man :man:`bash` ``:man:`bash```
|
||||||
intersphinx_
|
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/paper
|
||||||
main/file
|
main/file
|
||||||
main/image
|
main/image
|
||||||
|
main/video
|
||||||
|
|
||||||
The :ref:`LegacyResult <LegacyResult>` is used internally for the results that
|
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
|
have not yet been typed. The templates can be used as orientation until the
|
||||||
final typing is complete.
|
final typing is complete.
|
||||||
|
|
||||||
- :ref:`template default` / :py:obj:`Result`
|
- :ref:`template default` / :py:obj:`Result`
|
||||||
- :ref:`template videos`
|
|
||||||
- :ref:`template torrent`
|
- :ref:`template torrent`
|
||||||
- :ref:`template map`
|
- :ref:`template map`
|
||||||
- :ref:`template packages`
|
- :ref:`template packages`
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ Scripts to update static data in :origin:`searx/data/`
|
|||||||
:members:
|
: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:
|
:members:
|
||||||
|
|||||||
@@ -129,29 +129,6 @@ audio_src : uri,
|
|||||||
URL of an embedded ``<audio controls>``.
|
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:
|
.. _template torrent:
|
||||||
|
|
||||||
``torrent.html``
|
``torrent.html``
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ PATH="${PY_ENV}/bin:${REPO_ROOT}/node_modules/.bin:${GOROOT}/bin:${GOPATH}/bin:$
|
|||||||
|
|
||||||
PYOBJECTS="searx"
|
PYOBJECTS="searx"
|
||||||
PY_SETUP_EXTRAS='[test]'
|
PY_SETUP_EXTRAS='[test]'
|
||||||
GECKODRIVER_VERSION="v0.37.0"
|
GECKODRIVER_VERSION="v0.37.1"
|
||||||
# SPHINXOPTS=
|
# SPHINXOPTS=
|
||||||
BLACK_OPTIONS=("--target-version" "py311" "--line-length" "120" "--skip-string-normalization")
|
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")
|
BLACK_TARGETS=("--exclude" "(searx/static|searx/languages.py)" "--include" 'searxng.msg|\.pyi?$' "searx" "searxng_extra" "tests")
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
mock==5.2.0
|
mock==5.2.0
|
||||||
nose2[coverage_plugin]==0.16.0
|
nose2[coverage_plugin]==0.16.0
|
||||||
cov-core==1.15.0
|
cov-core==1.15.0
|
||||||
black==25.9.0
|
black==26.10.1
|
||||||
pylint==4.0.7
|
pylint==4.1.3
|
||||||
splinter==0.21.0
|
splinter==0.21.0
|
||||||
selenium==4.47.0
|
selenium==4.51.0
|
||||||
Sphinx==8.2.3;python_version <= "3.11"
|
Sphinx==8.2.3;python_version <= "3.11"
|
||||||
Sphinx==9.1.0; python_version > "3.11"
|
Sphinx==9.1.0; python_version > "3.11"
|
||||||
sphinx-issues==6.0.0
|
sphinx-issues==6.0.0
|
||||||
@@ -18,11 +18,11 @@ myst-parser==5.0.0
|
|||||||
linuxdoc==20260504
|
linuxdoc==20260504
|
||||||
aiounittest==1.5.0
|
aiounittest==1.5.0
|
||||||
yamllint==1.38.0
|
yamllint==1.38.0
|
||||||
wlc==2.1.1
|
wlc==2.2.1
|
||||||
coloredlogs==15.0.1
|
coloredlogs==15.0.1
|
||||||
docutils>=0.21.2;python_version <= "3.11"
|
docutils>=0.21.2;python_version <= "3.11"
|
||||||
docutils>=0.22.4; python_version > "3.11"
|
docutils>=0.22.4; python_version > "3.11"
|
||||||
parameterized==0.9.0
|
parameterized==0.9.0
|
||||||
granian[reload]==2.8.2
|
granian[reload]==2.8.5
|
||||||
basedpyright==1.39.10
|
basedpyright==1.40.2
|
||||||
types-lxml==2026.2.16
|
types-lxml==2026.2.16
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
granian==2.8.2
|
granian==2.8.5
|
||||||
granian[pname]==2.8.2
|
granian[pname]==2.8.5
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
certifi==2026.7.22
|
certifi==2026.10.23
|
||||||
babel==2.18.0
|
babel==2.18.0
|
||||||
flask-babel==4.0.0
|
flask-babel==4.0.0
|
||||||
flask==3.1.3
|
flask==3.1.3
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
lxml==6.1.2
|
lxml==6.1.3
|
||||||
pygments==2.21.0
|
pygments==2.21.0
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==3.0.0
|
||||||
pyyaml==6.0.3
|
pyyaml==6.0.3
|
||||||
httpx[http2]==0.28.1
|
curl_cffi==0.17.0
|
||||||
httpx-socks[asyncio]==0.13.1
|
|
||||||
sniffio==1.3.1
|
|
||||||
valkey==6.1.1
|
valkey==6.1.1
|
||||||
markdown-it-py==4.2.0
|
markdown-it-py==4.2.0
|
||||||
msgspec==0.21.1
|
msgspec==0.22.0
|
||||||
typer==0.27.1
|
typer==0.27.3
|
||||||
isodate==0.7.2
|
isodate==0.7.2
|
||||||
whitenoise==6.12.0
|
whitenoise==6.12.0
|
||||||
typing-extensions==4.16.0
|
typing-extensions==4.16.0
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Implementation of the :py:obj:`preference <searx.preference>` settings."""
|
"""Implementation of the :py:obj:`preference <searx.preference>` settings."""
|
||||||
|
|
||||||
# pylint: disable = too-few-public-methods
|
# pylint: disable = too-few-public-methods
|
||||||
|
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ area:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AnswererInfo", "Answerer", "AnswerStorage"]
|
__all__ = ["AnswererInfo", "Answerer", "AnswerStorage"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from dataclasses import dataclass
|
|||||||
from searx.utils import load_module
|
from searx.utils import load_module
|
||||||
from searx.result_types.answer import BaseAnswer
|
from searx.result_types.answer import BaseAnswer
|
||||||
|
|
||||||
|
|
||||||
_default = pathlib.Path(__file__).parent
|
_default = pathlib.Path(__file__).parent
|
||||||
log: logging.Logger = logging.getLogger("searx.answerers")
|
log: logging.Logger = logging.getLogger("searx.answerers")
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from urllib.parse import urlencode
|
|||||||
|
|
||||||
import lxml.etree
|
import lxml.etree
|
||||||
import lxml.html
|
import lxml.html
|
||||||
from httpx import HTTPError
|
from curl_cffi.requests.exceptions import RequestException
|
||||||
|
|
||||||
from searx import settings
|
from searx import settings
|
||||||
from searx.engines import (
|
from searx.engines import (
|
||||||
@@ -28,19 +28,19 @@ if t.TYPE_CHECKING:
|
|||||||
from searx.extended_types import SXNG_Response
|
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:
|
if 'timeout' not in kwargs:
|
||||||
kwargs['timeout'] = settings['outgoing']['request_timeout']
|
kwargs['timeout'] = settings['outgoing']['request_timeout']
|
||||||
kwargs['raise_for_httperror'] = True
|
kwargs['raise_for_httperror'] = True
|
||||||
|
|
||||||
|
|
||||||
def get(*args, **kwargs) -> "SXNG_Response": # type: ignore
|
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]
|
return http_get(*args, **kwargs) # pyright: ignore[reportUnknownArgumentType]
|
||||||
|
|
||||||
|
|
||||||
def post(*args, **kwargs) -> "SXNG_Response": # type: ignore
|
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]
|
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?"
|
base_url = "https://www.bing.com/AS/Suggestions?"
|
||||||
# cvid has to be a 32 character long string consisting of numbers and uppsercase characters
|
# 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))
|
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] = []
|
results: list[str] = []
|
||||||
|
|
||||||
if response.ok:
|
if response.ok:
|
||||||
@@ -83,7 +83,7 @@ def brave(query: str, _sxng_locale: str) -> list[str]:
|
|||||||
url = 'https://search.brave.com/api/suggest?'
|
url = 'https://search.brave.com/api/suggest?'
|
||||||
url += urlencode({'q': query})
|
url += urlencode({'q': query})
|
||||||
country = 'all'
|
country = 'all'
|
||||||
kwargs = {'cookies': {'country': country}}
|
kwargs = {'cookies': {'country': country}, 'enable_http3': True}
|
||||||
resp = get(url, **kwargs)
|
resp = get(url, **kwargs)
|
||||||
results: list[str] = []
|
results: list[str] = []
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ def google_complete(query: str, sxng_locale: str) -> list[str]:
|
|||||||
)
|
)
|
||||||
results: 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:
|
if resp and resp.ok:
|
||||||
json_txt = resp.text[resp.text.find('[') : resp.text.find(']', -3) + 1]
|
json_txt = resp.text[resp.text.find('[') : resp.text.find(']', -3) + 1]
|
||||||
data = json.loads(json_txt)
|
data = json.loads(json_txt)
|
||||||
@@ -418,5 +418,5 @@ def search_autocomplete(backend_name: str, query: str, sxng_locale: str) -> list
|
|||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
return backend(query, sxng_locale)
|
return backend(query, sxng_locale)
|
||||||
except (HTTPError, SearxEngineResponseException):
|
except (RequestException, SearxEngineResponseException):
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ Implementations used for bot detection.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["init", "dump_request", "get_network", "too_many_requests", "ProxyFix"]
|
__all__ = ["init", "dump_request", "get_network", "too_many_requests", "ProxyFix"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ class Config:
|
|||||||
if default is UNSET:
|
if default is UNSET:
|
||||||
raise KeyError(name)
|
raise KeyError(name)
|
||||||
return default
|
return default
|
||||||
(modulename, name) = str(fqn).rsplit('.', 1)
|
modulename, name = str(fqn).rsplit('.', 1)
|
||||||
m = __import__(modulename, {}, {}, [name], 0)
|
m = __import__(modulename, {}, {}, [name], 0)
|
||||||
return getattr(m, name)
|
return getattr(m, name)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ Accept_ header ..
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from ipaddress import (
|
from ipaddress import (
|
||||||
IPv4Network,
|
IPv4Network,
|
||||||
IPv6Network,
|
IPv6Network,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ bot if the Accept-Encoding_ header ..
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from ipaddress import (
|
from ipaddress import (
|
||||||
IPv4Network,
|
IPv4Network,
|
||||||
IPv6Network,
|
IPv6Network,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ if the Accept-Language_ header is unset.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from ipaddress import (
|
from ipaddress import (
|
||||||
IPv4Network,
|
IPv4Network,
|
||||||
IPv6Network,
|
IPv6Network,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ the Connection_ header is set to ``close``.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from ipaddress import (
|
from ipaddress import (
|
||||||
IPv4Network,
|
IPv4Network,
|
||||||
IPv6Network,
|
IPv6Network,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Metadata`_. A request is filtered out in case of:
|
|||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ the User-Agent_ header is unset or matches the regular expression
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from ipaddress import (
|
from ipaddress import (
|
||||||
IPv4Network,
|
IPv4Network,
|
||||||
@@ -25,7 +24,6 @@ import flask
|
|||||||
from . import config
|
from . import config
|
||||||
from ._helpers import too_many_requests
|
from ._helpers import too_many_requests
|
||||||
|
|
||||||
|
|
||||||
USER_AGENT = (
|
USER_AGENT = (
|
||||||
r'('
|
r'('
|
||||||
+ r'unknown'
|
+ r'unknown'
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ from ._helpers import (
|
|||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
logger = logger.getChild('ip_limit')
|
logger = logger.getChild('ip_limit')
|
||||||
|
|
||||||
BURST_WINDOW = 20
|
BURST_WINDOW = 20
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ The ``ip_lists`` method implements :py:obj:`block-list <block_ip>` and
|
|||||||
]
|
]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Implementation of a middleware to determine the real IP of an HTTP request
|
"""Implementation of a middleware to determine the real IP of an HTTP request
|
||||||
(:py:obj:`flask.request.remote_addr`) behind a proxy chain."""
|
(:py:obj:`flask.request.remote_addr`) behind a proxy chain."""
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Providing a Valkey database for the botdetection methods."""
|
"""Providing a Valkey database for the botdetection methods."""
|
||||||
|
|
||||||
|
|
||||||
import valkey
|
import valkey
|
||||||
|
|
||||||
__all__ = ["set_valkey_client", "get_valkey_client"]
|
__all__ = ["set_valkey_client", "get_valkey_client"]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Implementations needed for a branding of SearXNG."""
|
"""Implementations needed for a branding of SearXNG."""
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
# Struct fields aren't discovered in Python 3.14
|
# 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
|
# Check if value is expired. It's possible that it's expired but has not
|
||||||
# yet been automatically deleted by the periodic maintenance
|
# yet been automatically deleted by the periodic maintenance
|
||||||
(value, expire) = row
|
value, expire = row
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if expire < now:
|
if expire < now:
|
||||||
# The record is deleted during the maintenance interval. Deleting
|
# The record is deleted during the maintenance interval. Deleting
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
# limiter backward compatibility
|
# limiter backward compatibility
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
make data.all
|
make data.all
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
__all__ = ["ahmia_blacklist_loader", "data_dir", "get_cache"]
|
__all__ = ["ahmia_blacklist_loader", "data_dir", "get_cache"]
|
||||||
@@ -32,6 +33,13 @@ class WikiDataUnitType(t.TypedDict):
|
|||||||
to_si_factor: float
|
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):
|
class LocalesType(t.TypedDict):
|
||||||
"""Data structure of an item in ``locales.json``"""
|
"""Data structure of an item in ``locales.json``"""
|
||||||
|
|
||||||
@@ -41,6 +49,7 @@ class LocalesType(t.TypedDict):
|
|||||||
|
|
||||||
USER_AGENTS: UserAgentType
|
USER_AGENTS: UserAgentType
|
||||||
WIKIDATA_UNITS: dict[str, WikiDataUnitType]
|
WIKIDATA_UNITS: dict[str, WikiDataUnitType]
|
||||||
|
WIKIDATA_PROPERTIES: WikiDataPropertiesType
|
||||||
TRACKER_PATTERNS: TrackerPatternsDB
|
TRACKER_PATTERNS: TrackerPatternsDB
|
||||||
LOCALES: LocalesType
|
LOCALES: LocalesType
|
||||||
CURRENCIES: CurrenciesDB
|
CURRENCIES: CurrenciesDB
|
||||||
@@ -52,11 +61,12 @@ ENGINE_DESCRIPTIONS: dict[str, dict[str, t.Any]]
|
|||||||
ENGINE_TRAITS: dict[str, dict[str, t.Any]]
|
ENGINE_TRAITS: dict[str, dict[str, t.Any]]
|
||||||
|
|
||||||
|
|
||||||
lazy_globals = {
|
lazy_globals: dict[str, t.Any] = {
|
||||||
"CURRENCIES": CurrenciesDB(),
|
"CURRENCIES": CurrenciesDB(),
|
||||||
"USER_AGENTS": None,
|
"USER_AGENTS": None,
|
||||||
"EXTERNAL_URLS": None,
|
"EXTERNAL_URLS": None,
|
||||||
"WIKIDATA_UNITS": None,
|
"WIKIDATA_UNITS": None,
|
||||||
|
"WIKIDATA_PROPERTIES": None,
|
||||||
"EXTERNAL_BANGS": None,
|
"EXTERNAL_BANGS": None,
|
||||||
"OSM_KEYS_TAGS": None,
|
"OSM_KEYS_TAGS": None,
|
||||||
"ENGINE_DESCRIPTIONS": None,
|
"ENGINE_DESCRIPTIONS": None,
|
||||||
@@ -69,6 +79,7 @@ data_json_files = {
|
|||||||
"USER_AGENTS": "useragents.json",
|
"USER_AGENTS": "useragents.json",
|
||||||
"EXTERNAL_URLS": "external_urls.json",
|
"EXTERNAL_URLS": "external_urls.json",
|
||||||
"WIKIDATA_UNITS": "wikidata_units.json",
|
"WIKIDATA_UNITS": "wikidata_units.json",
|
||||||
|
"WIKIDATA_PROPERTIES": "wikidata_properties.json",
|
||||||
"EXTERNAL_BANGS": "external_bangs.json",
|
"EXTERNAL_BANGS": "external_bangs.json",
|
||||||
"OSM_KEYS_TAGS": "osm_keys_tags.json",
|
"OSM_KEYS_TAGS": "osm_keys_tags.json",
|
||||||
"ENGINE_DESCRIPTIONS": "engine_descriptions.json",
|
"ENGINE_DESCRIPTIONS": "engine_descriptions.json",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Simple implementation to store TrackerPatterns data in a SQL database."""
|
"""Simple implementation to store TrackerPatterns data in a SQL database."""
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
|
|
||||||
import typing as t
|
import typing as t
|
||||||
@@ -10,7 +11,7 @@ import re
|
|||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
|
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.data.core import get_cache, log
|
||||||
from searx.network import get as http_get
|
from searx.network import get as http_get
|
||||||
@@ -28,11 +29,11 @@ class TrackerPatternsDB:
|
|||||||
|
|
||||||
ctx_name = "data_tracker_patterns"
|
ctx_name = "data_tracker_patterns"
|
||||||
|
|
||||||
|
# ClearURL rule lists, the first one that responds HTTP 200 is used
|
||||||
CLEAR_LIST_URL = [
|
CLEAR_LIST_URL = [
|
||||||
# ClearURL rule lists, the first one that responds HTTP 200 is used
|
"https://cdn.jsdelivr.net/gh/clearurls/rules@refs/heads/gh-pages/data.minify.json",
|
||||||
"https://rules1.clearurls.xyz/data.minify.json",
|
|
||||||
"https://rules2.clearurls.xyz/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:
|
class Fields:
|
||||||
@@ -87,12 +88,12 @@ class TrackerPatternsDB:
|
|||||||
try:
|
try:
|
||||||
resp = http_get(url, timeout=3)
|
resp = http_get(url, timeout=3)
|
||||||
|
|
||||||
except HTTPError as exc:
|
except RequestException as exc:
|
||||||
log.warning("TRACKER_PATTERNS: HTTPError (%s) occured while fetching %s", url, exc)
|
log.warning("TRACKER_PATTERNS: RequestException while fetching %s: %s", url, exc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if resp.status_code != 200:
|
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
|
continue
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
],
|
],
|
||||||
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
|
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
|
||||||
"versions": [
|
"versions": [
|
||||||
"154.0",
|
"157.0",
|
||||||
"153.0"
|
"156.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -3474,11 +3474,6 @@
|
|||||||
"symbol": "mm⁻²",
|
"symbol": "mm⁻²",
|
||||||
"to_si_factor": 1e-06
|
"to_si_factor": 1e-06
|
||||||
},
|
},
|
||||||
"Q136039973": {
|
|
||||||
"si_name": "Q6137407",
|
|
||||||
"symbol": "FPS",
|
|
||||||
"to_si_factor": 1.0
|
|
||||||
},
|
|
||||||
"Q1361854": {
|
"Q1361854": {
|
||||||
"si_name": "Q11570",
|
"si_name": "Q11570",
|
||||||
"symbol": "dwt",
|
"symbol": "dwt",
|
||||||
@@ -5254,6 +5249,11 @@
|
|||||||
"symbol": "μA",
|
"symbol": "μA",
|
||||||
"to_si_factor": 1e-06
|
"to_si_factor": 1e-06
|
||||||
},
|
},
|
||||||
|
"Q31274648": {
|
||||||
|
"si_name": "Q6137407",
|
||||||
|
"symbol": "FPS",
|
||||||
|
"to_si_factor": 1.0
|
||||||
|
},
|
||||||
"Q3186734": {
|
"Q3186734": {
|
||||||
"si_name": "Q3186734",
|
"si_name": "Q3186734",
|
||||||
"symbol": "J/(m³ K)",
|
"symbol": "J/(m³ K)",
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
|
|||||||
|
|
||||||
region: str = ""
|
region: str = ""
|
||||||
"""For an engine, when there is ``region: ...`` in the YAML settings the engine
|
"""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
|
.. code:: yaml
|
||||||
|
|
||||||
@@ -317,6 +317,9 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
|
|||||||
enable_http: bool
|
enable_http: bool
|
||||||
"""Enable HTTP (by default only HTTPS is enabled)."""
|
"""Enable HTTP (by default only HTTPS is enabled)."""
|
||||||
|
|
||||||
|
enable_http3: bool = False
|
||||||
|
"""Enables the use of HTTP/3 if available"""
|
||||||
|
|
||||||
shortcut: str
|
shortcut: str
|
||||||
"""Code used to execute bang requests (``!foo``)"""
|
"""Code used to execute bang requests (``!foo``)"""
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"""1337x"""
|
"""1337x"""
|
||||||
|
|
||||||
from urllib.parse import quote, urljoin
|
from urllib.parse import quote, urljoin
|
||||||
from lxml import html
|
|
||||||
from searx.utils import extract_text, eval_xpath, eval_xpath_list, eval_xpath_getindex
|
from searx.utils import extract_text, eval_xpath, eval_xpath_list, eval_xpath_getindex
|
||||||
|
|
||||||
# about
|
# about
|
||||||
@@ -31,7 +30,7 @@ def request(query, params):
|
|||||||
def response(resp):
|
def response(resp):
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
dom = html.fromstring(resp.text)
|
dom = resp.html()
|
||||||
|
|
||||||
for result in eval_xpath_list(dom, '//table[contains(@class, "table-list")]/tbody//tr'):
|
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))
|
href = urljoin(url, eval_xpath_getindex(result, './td[contains(@class, "name")]/a[2]/@href', 0))
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from lxml import html
|
|
||||||
|
|
||||||
from searx import logger
|
from searx import logger
|
||||||
from searx.enginelib import EngineCache
|
from searx.enginelib import EngineCache
|
||||||
@@ -88,7 +87,7 @@ def response(resp):
|
|||||||
if not resp.text or not resp.text.strip():
|
if not resp.text or not resp.text.strip():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
dom = html.fromstring(resp.text)
|
dom = resp.html()
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for item in dom.xpath('//li[contains(@class, "res-list")]'):
|
for item in dom.xpath('//li[contains(@class, "res-list")]'):
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ from urllib.parse import urlencode
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from searx.exceptions import SearxEngineAPIException
|
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 = {
|
about = {
|
||||||
"website": "https://tv.360kan.com/",
|
"website": "https://tv.360kan.com/",
|
||||||
@@ -29,12 +30,12 @@ def request(query, params):
|
|||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
def response(resp):
|
def response(resp) -> EngineResults:
|
||||||
try:
|
try:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise SearxEngineAPIException(f"Invalid response: {e}") from e
|
raise SearxEngineAPIException(f"Invalid response: {e}") from e
|
||||||
results = []
|
res = EngineResults()
|
||||||
|
|
||||||
if "data" not in data or "result" not in data["data"]:
|
if "data" not in data or "result" not in data["data"]:
|
||||||
raise SearxEngineAPIException("Invalid response")
|
raise SearxEngineAPIException("Invalid response")
|
||||||
@@ -50,16 +51,15 @@ def response(resp):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
published_date = None
|
published_date = None
|
||||||
|
|
||||||
results.append(
|
res.add(
|
||||||
{
|
res.types.Video(
|
||||||
'url': entry["play_url"],
|
url=entry["play_url"],
|
||||||
'title': html_to_text(entry["title"]),
|
title=html_to_text(entry["title"]),
|
||||||
'content': html_to_text(entry["description"]),
|
content=html_to_text(entry["description"]),
|
||||||
'template': 'videos.html',
|
template='videos.html',
|
||||||
'publishedDate': published_date,
|
publishedDate=published_date,
|
||||||
'thumbnail': entry["cover_img"],
|
thumbnail=entry["cover_img"],
|
||||||
"iframe_src": get_embeded_stream_url(entry["play_url"]),
|
)
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return res
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ through our exclusive distribution partners.
|
|||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
import codecs
|
import codecs
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
@@ -33,11 +31,11 @@ about = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
base_url = "https://500px.com"
|
base_url = "https://500px.com"
|
||||||
api_url = "https://api.500px.com"
|
api_url = "https://api-neo.500px.com"
|
||||||
|
|
||||||
categories = ["images"]
|
|
||||||
|
|
||||||
|
categories = ["stock images"]
|
||||||
paging = True
|
paging = True
|
||||||
|
|
||||||
results_per_page = 30
|
results_per_page = 30
|
||||||
"""Number of results to return in the request.
|
"""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(
|
SXNG_query = """
|
||||||
$first: Int, $cursor: String, $search: String!, $sort: PhotoSort, $filters: [PhotoSearchFilter!], $nlp: Boolean
|
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
|
searchResource(
|
||||||
}
|
keyword: $keyword
|
||||||
|
type: $type
|
||||||
fragment SXNG_query on Query {
|
first: $first
|
||||||
photoSearch(sort: $sort, first: $first, after: $cursor, search: $search, filters: $filters, nlp: $nlp) {
|
after: $after
|
||||||
|
resourceTypes: $resourceTypes
|
||||||
|
categories: $categories
|
||||||
|
equipments: $equipments
|
||||||
|
downloadable: $downloadable
|
||||||
|
sort: $sort
|
||||||
|
excludeNsfw: $excludeNsfw
|
||||||
|
) {
|
||||||
edges {
|
edges {
|
||||||
|
cursor
|
||||||
node {
|
node {
|
||||||
id
|
__typename
|
||||||
canonicalPath
|
... on Photo {
|
||||||
name
|
|
||||||
description
|
|
||||||
width
|
|
||||||
height
|
|
||||||
photographer: uploader {
|
|
||||||
displayName
|
|
||||||
}
|
|
||||||
images(sizes: [35, 33]) {
|
|
||||||
size
|
|
||||||
url
|
|
||||||
jpegUrl
|
|
||||||
webpUrl
|
|
||||||
id
|
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:
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
# cursor is the base64 hash of the string "pos-<offset-1>", e.g. "pos-29" -> "cG9zLTI5"
|
# cursor is the base64 hash of the string "pos-<offset-1>", e.g. "pos-29" -> "cG9zLTI5"
|
||||||
offset = ((params["pageno"] - 1) * results_per_page) - 1
|
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["url"] = f"{api_url}/graphql"
|
||||||
params["method"] = "POST"
|
params["method"] = "POST"
|
||||||
params["json"] = {
|
params["json"] = {
|
||||||
"operationName": "PhotoSearchPaginationContainerQuery",
|
"operationName": "searchResource",
|
||||||
"variables": {
|
"variables": {
|
||||||
|
"after": cursor,
|
||||||
"first": results_per_page,
|
"first": results_per_page,
|
||||||
"cursor": cursor,
|
"keyword": query,
|
||||||
"search": query,
|
"resourceTypes": ["PHOTO"],
|
||||||
"sort": "RELEVANCE",
|
"sort": "RELEVANCE",
|
||||||
"filters": [],
|
"type": "TEXT",
|
||||||
"nlp": False,
|
|
||||||
},
|
},
|
||||||
"query": SXNG_query,
|
"query": SXNG_query,
|
||||||
}
|
}
|
||||||
@@ -110,27 +132,20 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
|
|
||||||
def response(resp: "SXNG_Response"):
|
def response(resp: "SXNG_Response"):
|
||||||
res = EngineResults()
|
res = EngineResults()
|
||||||
json_data = resp.json()["data"]["photoSearch"]
|
json_data = resp.json()["data"]["searchResource"]
|
||||||
|
|
||||||
for edge in json_data["edges"]:
|
for edge in json_data["edges"]:
|
||||||
node = edge["node"] # pyright: ignore[reportAny]
|
node = edge["node"] # pyright: ignore[reportAny]
|
||||||
if not node["images"]:
|
image_urls = [url for (resolution, url) in node["urls"].items() if resolution.startswith("size_")]
|
||||||
continue
|
|
||||||
images: list[dict[str, str]] = sorted(node["images"], key=lambda i: i["size"])
|
|
||||||
thumbnail_src = images[0]["url"]
|
|
||||||
img_src = images[-1]["url"]
|
|
||||||
res.add(
|
res.add(
|
||||||
res.types.LegacyResult(
|
res.types.Image(
|
||||||
{
|
url=f"{base_url}/photo/{node['id']}",
|
||||||
"template": "images.html",
|
thumbnail_src=image_urls[0],
|
||||||
"url": base_url + node["canonicalPath"],
|
img_src=image_urls[-1],
|
||||||
"thumbnail_src": thumbnail_src,
|
title=node["title"],
|
||||||
"img_src": img_src,
|
content=node["description"] or "",
|
||||||
"title": node["name"],
|
author=node["uploader"]["displayName"],
|
||||||
"content": node["description"],
|
resolution=f"{node['width']}x{node['height']}",
|
||||||
"author": node["photographer"]["displayName"],
|
|
||||||
"resolution": f"{node['width']}x{node['height']}",
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ categories: list[str]
|
|||||||
disabled: bool
|
disabled: bool
|
||||||
display_error_messages: bool
|
display_error_messages: bool
|
||||||
enable_http: bool
|
enable_http: bool
|
||||||
|
enable_http3: bool
|
||||||
engine_type: str
|
engine_type: str
|
||||||
inactive: bool
|
inactive: bool
|
||||||
max_page: int
|
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')
|
logger.error('An engine does not have a "name" field')
|
||||||
return None
|
return None
|
||||||
if '_' in engine_name:
|
if '_' in engine_name:
|
||||||
logger.error('Engine name contains underscore: "{}"'.format(engine_name))
|
logger.error('Engine name contains underscore: "%s"', engine_name)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if engine_name.lower() != engine_name:
|
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_name = engine_name.lower()
|
||||||
engine_data['name'] = engine_name
|
engine_data['name'] = engine_name
|
||||||
|
|
||||||
# load_module
|
# load_module
|
||||||
module_name = engine_data.get('engine')
|
module_name = engine_data.get('engine')
|
||||||
if module_name is None:
|
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
|
return None
|
||||||
try:
|
try:
|
||||||
engine = load_module(module_name + '.py', ENGINE_DIR)
|
engine = load_module(module_name + '.py', ENGINE_DIR)
|
||||||
except (SyntaxError, KeyboardInterrupt, SystemExit, SystemError, ImportError, RuntimeError):
|
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)
|
sys.exit(1)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.exception('Cannot load engine "{}"'.format(module_name))
|
logger.exception('Cannot load engine "%s"', module_name)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
check_engine_module(engine)
|
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]):
|
def update_engine_attributes(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]):
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
|
|
||||||
# set engine attributes from engine_data
|
# set / update engine attributes from engine_data
|
||||||
kvargs: dict[str, t.Any]
|
kvargs: dict[str, t.Any]
|
||||||
|
engine.about = getattr(engine, "about", EngineAbout())
|
||||||
if isinstance(engine.about, EngineAbout):
|
if isinstance(engine.about, EngineAbout):
|
||||||
kvargs = {**msgspec.to_builtins(engine.about), **engine_data.get("about", {})}
|
kvargs = {**msgspec.to_builtins(engine.about), **engine_data.get("about", {})}
|
||||||
else:
|
else:
|
||||||
@@ -246,7 +247,7 @@ def is_missing_required_attributes(engine: "Engine | types.ModuleType"):
|
|||||||
missing = False
|
missing = False
|
||||||
for engine_attr in dir(engine):
|
for engine_attr in dir(engine):
|
||||||
if not engine_attr.startswith('_') and getattr(engine, engine_attr) is None:
|
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
|
missing = True
|
||||||
return missing
|
return missing
|
||||||
|
|
||||||
@@ -276,12 +277,12 @@ def call_engine_setup(engine: "Engine | types.ModuleType", engine_data: dict[str
|
|||||||
if setup_func is None:
|
if setup_func is None:
|
||||||
setup_ok = True
|
setup_ok = True
|
||||||
elif not callable(setup_func):
|
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:
|
else:
|
||||||
try:
|
try:
|
||||||
setup_ok = engine.setup(engine_data)
|
setup_ok = engine.setup(engine_data)
|
||||||
except Exception as e: # pylint: disable=broad-except
|
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
|
setup_ok = False
|
||||||
|
|
||||||
# The evaluation of the return value is analogous to Engine.init
|
# 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
|
setup_ok = True
|
||||||
|
|
||||||
if not setup_ok:
|
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
|
return setup_ok
|
||||||
|
|
||||||
|
|
||||||
def register_engine(engine: "Engine | types.ModuleType"):
|
def register_engine(engine: "Engine | types.ModuleType"):
|
||||||
if engine.name in engines:
|
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)
|
sys.exit(1)
|
||||||
engines[engine.name] = engine
|
engines[engine.name] = engine
|
||||||
|
|
||||||
if engine.shortcut in engine_shortcuts:
|
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)
|
sys.exit(1)
|
||||||
engine_shortcuts[engine.shortcut] = engine.name
|
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
|
# if an engine can't be loaded (if for example the engine is missing
|
||||||
# tor or some other requirements) its set to inactive!
|
# tor or some other requirements) its set to inactive!
|
||||||
logger.error(
|
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
|
engine_data["inactive"] = True
|
||||||
return engines
|
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
|
import re
|
||||||
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from lxml import html
|
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
|
||||||
from searx.utils import eval_xpath, eval_xpath_list, extract_text
|
from searx.utils import eval_xpath, eval_xpath_list, extract_text
|
||||||
@@ -57,7 +56,7 @@ def request(query, params):
|
|||||||
def response(resp):
|
def response(resp):
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
doc = html.fromstring(resp.text)
|
doc = resp.html()
|
||||||
for result in eval_xpath_list(doc, "//table/tbody/tr"):
|
for result in eval_xpath_list(doc, "//table/tbody/tr"):
|
||||||
|
|
||||||
if len(result.xpath("./td")) < 9:
|
if len(result.xpath("./td")) < 9:
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import random
|
|||||||
import typing as t
|
import typing as t
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from lxml import html
|
|
||||||
from lxml.etree import ElementBase
|
from lxml.etree import ElementBase
|
||||||
|
|
||||||
from searx.data import ENGINE_TRAITS
|
from searx.data import ENGINE_TRAITS
|
||||||
@@ -143,7 +142,7 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
|
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
def response(resp: "SXNG_Response") -> EngineResults:
|
||||||
res = EngineResults()
|
res = EngineResults()
|
||||||
dom = html.fromstring(resp.text)
|
dom = resp.html()
|
||||||
|
|
||||||
# Each result is a div with class "flex" inside "js-aarecord-list-outer"
|
# Each result is a div with class "flex" inside "js-aarecord-list-outer"
|
||||||
# container. The "flex" filter excludes non-result div such as section
|
# 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:
|
if not resp.ok:
|
||||||
raise RuntimeError("Response from Anna's Archive is not OK.")
|
raise RuntimeError("Response from Anna's Archive is not OK.")
|
||||||
|
|
||||||
dom = html.fromstring(resp.text)
|
dom = resp.html()
|
||||||
|
|
||||||
# supported language codes
|
# supported language codes
|
||||||
lang_map: dict[str, str] = {}
|
lang_map: dict[str, str] = {}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ list in ``settings.yml``:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from lxml import html
|
|
||||||
from searx.result_types import EngineResults, MainResult
|
from searx.result_types import EngineResults, MainResult
|
||||||
from searx.utils import eval_xpath, eval_xpath_list, extract_text
|
from searx.utils import eval_xpath, eval_xpath_list, extract_text
|
||||||
|
|
||||||
@@ -62,7 +61,7 @@ def request(query, params):
|
|||||||
|
|
||||||
def response(resp) -> EngineResults:
|
def response(resp) -> EngineResults:
|
||||||
res = EngineResults()
|
res = EngineResults()
|
||||||
doc = html.fromstring(resp.text)
|
doc = resp.html()
|
||||||
|
|
||||||
for result in eval_xpath_list(doc, "//div[@class='article']"):
|
for result in eval_xpath_list(doc, "//div[@class='article']"):
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from lxml import html
|
|
||||||
|
|
||||||
from searx.utils import (
|
from searx.utils import (
|
||||||
eval_xpath_list,
|
eval_xpath_list,
|
||||||
@@ -22,7 +21,7 @@ about = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ['files', 'apps']
|
categories = ['it', 'apps']
|
||||||
paging = True
|
paging = True
|
||||||
time_range_support = False
|
time_range_support = False
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ def request(query, params):
|
|||||||
def response(resp):
|
def response(resp):
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
dom = html.fromstring(resp.text)
|
dom = resp.html()
|
||||||
|
|
||||||
# parse results
|
# parse results
|
||||||
for result in eval_xpath_list(dom, "//div[@id='content']//div[@class='listWidget']/div/div[@class='appRow']"):
|
for result in eval_xpath_list(dom, "//div[@id='content']//div[@class='listWidget']/div/div[@class='appRow']"):
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ about = {
|
|||||||
"results": 'JSON',
|
"results": 'JSON',
|
||||||
}
|
}
|
||||||
|
|
||||||
categories = ['files', 'apps']
|
categories = ['it', 'apps']
|
||||||
safesearch = True
|
safesearch = True
|
||||||
|
|
||||||
search_url = 'https://itunes.apple.com/search?{query}'
|
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['value'] = loads(actual_token.text)['authInfo']['access_token']
|
||||||
token['last_updated'] = update_time
|
token['last_updated'] = update_time
|
||||||
# pylint: disable=bare-except
|
except: # pylint: disable=bare-except
|
||||||
except:
|
|
||||||
pass
|
pass
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|||||||