Compare commits

...

2 Commits

Author SHA1 Message Date
Chocobo1
c075097acd Allow to run CI checks locally
Now the developer is able to run the checks easily and locally (by
following the instructions in nova3/README.md).

PR #23262.
2025-09-14 22:17:23 +08:00
Chocobo1
b0148ef36c WebUI: enforce coding style
* WebUI: prefer `classList.toggle()` over other pattern
  Addresses: https://github.com/qbittorrent/qBittorrent/pull/23231#discussion_r2328647152
* WebUI: prefer using built-in objects Constructor
* WebUI: combine function calls

PR #23261.
2025-09-14 22:11:23 +08:00
12 changed files with 156 additions and 54 deletions

View File

@@ -59,44 +59,25 @@ jobs:
python-version: '3.9'
- name: Install tools (search engine)
run: pip install bandit isort mypy pycodestyle pyflakes pyright
- name: Gather files (search engine)
working-directory: src/searchengine/nova3
run: |
export "PY_FILES=$(find . -type f -name '*.py' -path '*searchengine*' ! -name 'socks.py' -printf '%p ')"
echo $PY_FILES
echo "PY_FILES=$PY_FILES" >> "$GITHUB_ENV"
pip install uv
uv sync
- name: Check typings (search engine)
run: |
curl \
-L \
-o src/searchengine/nova3/socks.pyi "https://github.com/python/typeshed/raw/refs/heads/main/stubs/PySocks/socks.pyi"
MYPYPATH="src/searchengine/nova3" \
mypy \
--explicit-package-bases \
--strict \
$PY_FILES
pyright \
$PY_FILES
working-directory: src/searchengine/nova3
run: uv run just check
- name: Lint code (search engine)
run: |
pyflakes $PY_FILES
bandit --skip B110,B310,B314,B405 $PY_FILES
working-directory: src/searchengine/nova3
run: uv run just lint
- name: Format code (search engine)
working-directory: src/searchengine/nova3
run: |
pycodestyle \
--ignore=E265,E402 \
--max-line-length=1000 \
--statistics \
$PY_FILES
isort \
--check \
--diff \
$PY_FILES
uv run just format
git diff --exit-code
- name: Build code (search engine)
run: |
python -m compileall $PY_FILES
working-directory: src/searchengine/nova3
run: uv run just build

2
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.vscode/
src/gui/geoip/GeoIP.dat
src/gui/geoip/GeoIP.dat.gz
src/qbittorrent
@@ -10,7 +11,6 @@ CMakeLists.txt.user*
qbittorrent.pro.user*
conf.pri
Makefile*
*.pyc
*.log
# Compiled object files

4
src/searchengine/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*.egg-info
*.lock
*.pyc
*.pyi

View File

@@ -0,0 +1,46 @@
nova3 Engine
===
## Development Workflow
0. Prerequisite
* A Linux-like environment
* [Python](https://www.python.org/) installed
* [uv](https://docs.astral.sh/uv/) installed
1. Setup development environment
1. Setup virtual environment and dependencies
```shell
uv sync
```
2. Activate virtual environment
```shell
source .venv/bin/activate
```
2. Run type check
```shell
just check
```
3. Run static analyzer
```shell
just lint
```
4. Apply formatting
```shell
just format
```
## References
* [How to write a search plugin](https://github.com/qbittorrent/search-plugins/wiki/How-to-write-a-search-plugin)
* [just - Command runner](https://just.systems/man/en/)
* [uv - Python package and project manager](https://docs.astral.sh/uv/)

View File

@@ -0,0 +1,54 @@
PY_FILES := `find . -maxdepth 1 -type f -name '*.py' ! -name 'socks.py' -printf '%P '`
# Show available recipes to run
default:
just --list
# Run type check
check files=PY_FILES: fetch_aux
mypy \
{{ files }}
pyright \
{{ files }}
# Byte-compile files
build files=PY_FILES:
python \
-m compileall \
{{ files }}
# Fetch auxiliary files
[private]
fetch_aux:
#!/usr/bin/sh
if [ ! -f 'socks.pyi' ]; then
curl -L -o socks.pyi "https://github.com/python/typeshed/raw/refs/heads/main/stubs/PySocks/socks.pyi"
fi
# Apply formatting
format files=PY_FILES:
pycodestyle \
--ignore=E265,E402 \
--max-line-length=1000 \
--statistics \
{{ files }}
isort \
--line-length 1000 \
{{ files }}
just \
--fmt \
--unstable
# Run static analyzer
lint files=PY_FILES:
pyflakes \
{{ files }}
bandit \
--skip B110,B310,B314,B405 \
{{ files }}
# Run tests
test files='tests/*.py': fetch_aux
pytest \
--showlocals \
{{ files }}

View File

@@ -0,0 +1,25 @@
[project]
name = "qBittorrent-search-engine"
description = "Search engine for qBittorrent search feature"
readme = "README.md"
requires-python = ">=3.9"
dynamic = ["version"]
[dependency-groups]
dev = [
"bandit",
"isort",
"mypy",
"pycodestyle",
"pyflakes",
"pyright",
"pytest",
"rust-just",
]
[tool.mypy]
explicit_package_bases = true
strict = true
[tool.setuptools.packages.find]
where = ["./"]

View File

@@ -36,6 +36,7 @@ export default [
"curly": ["error", "multi-or-nest", "consistent"],
"eqeqeq": "error",
"guard-for-in": "error",
"no-implicit-coercion": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-var": "error",
@@ -73,7 +74,10 @@ export default [
"Unicorn/no-array-for-each": "error",
"Unicorn/no-for-loop": "error",
"Unicorn/no-zero-fractions": "error",
"Unicorn/prefer-classlist-toggle": "error",
"Unicorn/prefer-native-coercion-functions": "error",
"Unicorn/prefer-number-properties": "error",
"Unicorn/prefer-single-call": "error",
"Unicorn/switch-case-braces": ["error", "avoid"]
}
}

View File

@@ -710,10 +710,8 @@ window.qBittorrent.DynamicTable ??= (() => {
colElem.classList.toggle("reverse", isReverse);
}
const oldColElem = getCol(this.dynamicTableFixedHeaderDivId, oldColumn);
if (oldColElem !== null) {
oldColElem.classList.remove("sorted");
oldColElem.classList.remove("reverse");
}
if (oldColElem !== null)
oldColElem.classList.remove("sorted", "reverse");
}
getSelectedRowId() {

View File

@@ -730,9 +730,7 @@ window.qBittorrent.Search ??= (() => {
for (const plugin of responseJSON)
searchPlugins.push(plugin);
const pluginOptions = [];
pluginOptions.push(createOption("QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]", "enabled"));
pluginOptions.push(createOption("QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]", "all"));
const pluginOptions = [createOption("QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]", "enabled"), createOption("QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]", "all")];
const searchPluginsEmpty = (searchPlugins.length === 0);
if (!searchPluginsEmpty) {

View File

@@ -17,12 +17,8 @@
MochaUI.initializeTabs("aboutTabs");
const showContent = (element) => {
for (const content of document.querySelectorAll(".aboutTabContent")) {
if (content === element)
content.classList.remove("invisible");
else
content.classList.add("invisible");
}
for (const content of document.querySelectorAll(".aboutTabContent"))
content.classList.toggle("invisible", (content !== element));
};
document.getElementById("aboutAboutLink").addEventListener("click", (event) => {

View File

@@ -36,12 +36,8 @@
MochaUI.initializeTabs("preferencesTabs");
const showTab = (element) => {
for (const tab of document.querySelectorAll(".PrefTab")) {
if (tab === element)
tab.classList.remove("invisible");
else
tab.classList.add("invisible");
}
for (const tab of document.querySelectorAll(".PrefTab"))
tab.classList.toggle("invisible", (tab !== element));
};
document.getElementById("PrefBehaviorLink").addEventListener("click", (e) => {

View File

@@ -620,8 +620,8 @@
if (articlesDiffer) {
// update unread count
const oldUnread = feedData[r.uid].map((art) => !art.isRead).filter((v) => v).length;
const newUnread = r.articles.map((art) => !art.isRead).filter((v) => v).length;
const oldUnread = feedData[r.uid].map((art) => !art.isRead).filter(Boolean).length;
const newUnread = r.articles.map((art) => !art.isRead).filter(Boolean).length;
const unreadDifference = newUnread - oldUnread;
// find all parents (and self) and add unread difference
@@ -733,7 +733,7 @@
});
// calculate number of unread
const numberOfUnread = dataEntry.articles.map((art) => !art.isRead).filter((v) => v).length;
const numberOfUnread = dataEntry.articles.map((art) => !art.isRead).filter(Boolean).length;
// find all items that contain this rss feed and add unread count
for (const row of rssFeedTable.getRowValues()) {
if (dataEntry.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)