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' python-version: '3.9'
- name: Install tools (search engine) - name: Install tools (search engine)
run: pip install bandit isort mypy pycodestyle pyflakes pyright working-directory: src/searchengine/nova3
- name: Gather files (search engine)
run: | run: |
export "PY_FILES=$(find . -type f -name '*.py' -path '*searchengine*' ! -name 'socks.py' -printf '%p ')" pip install uv
echo $PY_FILES uv sync
echo "PY_FILES=$PY_FILES" >> "$GITHUB_ENV"
- name: Check typings (search engine) - name: Check typings (search engine)
run: | working-directory: src/searchengine/nova3
curl \ run: uv run just check
-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
- name: Lint code (search engine) - name: Lint code (search engine)
run: | working-directory: src/searchengine/nova3
pyflakes $PY_FILES run: uv run just lint
bandit --skip B110,B310,B314,B405 $PY_FILES
- name: Format code (search engine) - name: Format code (search engine)
working-directory: src/searchengine/nova3
run: | run: |
pycodestyle \ uv run just format
--ignore=E265,E402 \ git diff --exit-code
--max-line-length=1000 \
--statistics \
$PY_FILES
isort \
--check \
--diff \
$PY_FILES
- name: Build code (search engine) - name: Build code (search engine)
run: | working-directory: src/searchengine/nova3
python -m compileall $PY_FILES run: uv run just build

2
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.vscode/ .vscode/
src/gui/geoip/GeoIP.dat src/gui/geoip/GeoIP.dat
src/gui/geoip/GeoIP.dat.gz src/gui/geoip/GeoIP.dat.gz
src/qbittorrent src/qbittorrent
@@ -10,7 +11,6 @@ CMakeLists.txt.user*
qbittorrent.pro.user* qbittorrent.pro.user*
conf.pri conf.pri
Makefile* Makefile*
*.pyc
*.log *.log
# Compiled object files # 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"], "curly": ["error", "multi-or-nest", "consistent"],
"eqeqeq": "error", "eqeqeq": "error",
"guard-for-in": "error", "guard-for-in": "error",
"no-implicit-coercion": "error",
"no-undef": "off", "no-undef": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
"no-var": "error", "no-var": "error",
@@ -73,7 +74,10 @@ export default [
"Unicorn/no-array-for-each": "error", "Unicorn/no-array-for-each": "error",
"Unicorn/no-for-loop": "error", "Unicorn/no-for-loop": "error",
"Unicorn/no-zero-fractions": "error", "Unicorn/no-zero-fractions": "error",
"Unicorn/prefer-classlist-toggle": "error",
"Unicorn/prefer-native-coercion-functions": "error",
"Unicorn/prefer-number-properties": "error", "Unicorn/prefer-number-properties": "error",
"Unicorn/prefer-single-call": "error",
"Unicorn/switch-case-braces": ["error", "avoid"] "Unicorn/switch-case-braces": ["error", "avoid"]
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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