mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2026-01-10 01:22:31 -06:00
Compare commits
167 Commits
release-5.
...
06756936f3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06756936f3 | ||
|
|
7ed026ef78 | ||
|
|
78fae0ae76 | ||
|
|
8aa1a96d71 | ||
|
|
4132173b30 | ||
|
|
8e2125ee72 | ||
|
|
05bcc4e175 | ||
|
|
5dfb51a8d2 | ||
|
|
526abdf7ce | ||
|
|
617b1da842 | ||
|
|
c59ac3b970 | ||
|
|
0c48b70e5b | ||
|
|
1cb6173ad1 | ||
|
|
2cbfb91b88 | ||
|
|
0729c9a2f8 | ||
|
|
7982f66fa6 | ||
|
|
50d60b9589 | ||
|
|
96f0eebc4e | ||
|
|
4b07597d54 | ||
|
|
a9213627a9 | ||
|
|
28c1ba869b | ||
|
|
054003970e | ||
|
|
44bb1ac7eb | ||
|
|
e309b17732 | ||
|
|
e10fb40a48 | ||
|
|
7648a2838d | ||
|
|
8cd1a80852 | ||
|
|
c79a9624af | ||
|
|
4e9c514c2f | ||
|
|
84ed24e257 | ||
|
|
48f7f6fb8c | ||
|
|
f47754838b | ||
|
|
a3d1ff0eb2 | ||
|
|
afcfea5b8f | ||
|
|
84cd8e1535 | ||
|
|
eb3718fc91 | ||
|
|
7e0247fefe | ||
|
|
f9f031cdb4 | ||
|
|
d56b353c52 | ||
|
|
83799f4f07 | ||
|
|
c7caae1150 | ||
|
|
1662a9deb2 | ||
|
|
6c310aa311 | ||
|
|
368748ac52 | ||
|
|
9c81e58de6 | ||
|
|
778a158597 | ||
|
|
09071d2b69 | ||
|
|
b79ac0d716 | ||
|
|
de767871f1 | ||
|
|
2477e13b3f | ||
|
|
eb82c9078d | ||
|
|
663da093bd | ||
|
|
13f9c20a69 | ||
|
|
86e4b662ce | ||
|
|
dcaf4b6d80 | ||
|
|
d29f47c36e | ||
|
|
ab04064adc | ||
|
|
9afbd47b52 | ||
|
|
582dc99cae | ||
|
|
3691eb948e | ||
|
|
76bb4e5f98 | ||
|
|
0262faa915 | ||
|
|
f1b7c4572b | ||
|
|
c494314a29 | ||
|
|
559f47ab0c | ||
|
|
380b25e22f | ||
|
|
7745ac19d8 | ||
|
|
6cd6267c26 | ||
|
|
e7dee969e1 | ||
|
|
bb68a39b53 | ||
|
|
4c91cd9372 | ||
|
|
0791828b84 | ||
|
|
c2f2a38582 | ||
|
|
ad4bdc0653 | ||
|
|
f3095935ea | ||
|
|
732b2bcbdb | ||
|
|
33aaa867b5 | ||
|
|
fdd17159eb | ||
|
|
e9fee414df | ||
|
|
1077cbba2b | ||
|
|
70dbe9468a | ||
|
|
45babc336d | ||
|
|
70822e8942 | ||
|
|
b5394e7939 | ||
|
|
411ca0f668 | ||
|
|
7b3aa51bb1 | ||
|
|
0b3bce8993 | ||
|
|
0160aa28b6 | ||
|
|
0187f19f60 | ||
|
|
e87dfe35f3 | ||
|
|
e51be45ce6 | ||
|
|
b4a16f6464 | ||
|
|
250fef4ee7 | ||
|
|
8fc5d0914d | ||
|
|
fc5daf6e1d | ||
|
|
c878a09d27 | ||
|
|
2aee875642 | ||
|
|
2785636d3f | ||
|
|
15069b2643 | ||
|
|
f0361f1bed | ||
|
|
110e6d32b4 | ||
|
|
3d73026ff2 | ||
|
|
abafbc0685 | ||
|
|
5465605377 | ||
|
|
9331580e86 | ||
|
|
795889c417 | ||
|
|
ff03eeab5b | ||
|
|
f0b9a17566 | ||
|
|
72e8b3272b | ||
|
|
6c36830e5e | ||
|
|
cdddaae939 | ||
|
|
f540381caf | ||
|
|
055d82bda4 | ||
|
|
0796f96ee4 | ||
|
|
841cffafa7 | ||
|
|
ade39432be | ||
|
|
830d2c207b | ||
|
|
97865545c3 | ||
|
|
3abdc3134b | ||
|
|
5a716a40fb | ||
|
|
943e403241 | ||
|
|
103ea813dc | ||
|
|
52b1f3588a | ||
|
|
4bd50672e8 | ||
|
|
8c8a0ac54c | ||
|
|
7b4a3fccc6 | ||
|
|
d21653e8cf | ||
|
|
627d89813c | ||
|
|
b28c229f85 | ||
|
|
8d0870c953 | ||
|
|
5a4b3b25d3 | ||
|
|
d174bc75e4 | ||
|
|
882da47609 | ||
|
|
b74b334e34 | ||
|
|
53f919aea8 | ||
|
|
62a7fd86d6 | ||
|
|
96295adc08 | ||
|
|
8f53fb8178 | ||
|
|
37eb80919c | ||
|
|
1b044d9476 | ||
|
|
83599f1f7b | ||
|
|
6e1b5ec18b | ||
|
|
249c80aaaf | ||
|
|
0ac47496d4 | ||
|
|
4ec80de268 | ||
|
|
f432c1e615 | ||
|
|
41d9ee91a1 | ||
|
|
ba3d89b674 | ||
|
|
1ca33d45ba | ||
|
|
a9b54d94a0 | ||
|
|
693390ff27 | ||
|
|
5ddc5a8b87 | ||
|
|
ad9100ac07 | ||
|
|
1043bea896 | ||
|
|
955688c125 | ||
|
|
8da43a4054 | ||
|
|
ddf6dd5fa2 | ||
|
|
8c02bbb4bc | ||
|
|
7e95375cec | ||
|
|
29201fa016 | ||
|
|
1a3d0f6fab | ||
|
|
f58d6ae984 | ||
|
|
7f0134108a | ||
|
|
d79dc86d00 | ||
|
|
38070c6eee | ||
|
|
c9eb1fbac8 | ||
|
|
7238bad5a6 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
||||
custom: "https://www.qbittorrent.org/donate.php"
|
||||
custom: "https://www.qbittorrent.org/donate"
|
||||
|
||||
10
.github/workflows/ci_file_health.yaml
vendored
10
.github/workflows/ci_file_health.yaml
vendored
@@ -52,13 +52,13 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
pip install zizmor
|
||||
IGNORE_RULEID='(.ruleId != "template-injection")
|
||||
and (.ruleId != "unpinned-uses")'
|
||||
IGNORE_ID='(.id != "template-injection")
|
||||
and (.id != "unpinned-uses")'
|
||||
IGNORE_RULEID='(.ruleId != "zizmor/template-injection")
|
||||
and (.ruleId != "zizmor/unpinned-uses")'
|
||||
IGNORE_ID='(.id != "zizmor/template-injection")
|
||||
and (.id != "zizmor/unpinned-uses")'
|
||||
zizmor \
|
||||
--format sarif \
|
||||
--pedantic \
|
||||
--persona auditor \
|
||||
./ \
|
||||
| jq "(.runs[].results |= map(select($IGNORE_RULEID)))
|
||||
| (.runs[].tool.driver.rules |= map(select($IGNORE_ID)))" \
|
||||
|
||||
11
.github/workflows/ci_macos.yaml
vendored
11
.github/workflows/ci_macos.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
matrix:
|
||||
libt_version: ["2.0.11", "1.2.20"]
|
||||
qbt_gui: ["GUI=ON", "GUI=OFF"]
|
||||
qt_version: ["6.7.0"]
|
||||
qt_version: ["6.9.0"]
|
||||
|
||||
env:
|
||||
boost_path: "${{ github.workspace }}/../boost"
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
store_cache: ${{ github.ref == 'refs/heads/master' }}
|
||||
update_packager_index: false
|
||||
ccache_options: |
|
||||
max_size=2G
|
||||
max_size=1G
|
||||
|
||||
- name: Install boost
|
||||
env:
|
||||
@@ -70,6 +70,9 @@ jobs:
|
||||
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?"
|
||||
fi
|
||||
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
|
||||
cd "${{ env.boost_path }}"
|
||||
./bootstrap.sh
|
||||
./b2 stage --stagedir=./ --with-headers
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v4
|
||||
@@ -95,7 +98,7 @@ jobs:
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_CXX_STANDARD=20 \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}" \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
|
||||
-Ddeprecated-functions=OFF
|
||||
cmake --build build
|
||||
sudo cmake --install build
|
||||
@@ -109,7 +112,7 @@ jobs:
|
||||
-G "Ninja" \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}" \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
|
||||
-DTESTING=ON \
|
||||
-DVERBOSE_CONFIGURE=ON \
|
||||
-D${{ matrix.qbt_gui }}
|
||||
|
||||
12
.github/workflows/ci_python.yaml
vendored
12
.github/workflows/ci_python.yaml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
python-version: '3' # use default version
|
||||
|
||||
- name: Install tools (auxiliary scripts)
|
||||
run: pip install bandit pycodestyle pyflakes
|
||||
run: pip install bandit isort pycodestyle pyflakes
|
||||
|
||||
- name: Gather files (auxiliary scripts)
|
||||
run: |
|
||||
@@ -44,6 +44,10 @@ jobs:
|
||||
--max-line-length=1000 \
|
||||
--statistics \
|
||||
$PY_FILES
|
||||
isort \
|
||||
--check \
|
||||
--diff \
|
||||
$PY_FILES
|
||||
|
||||
- name: Build code (auxiliary scripts)
|
||||
run: |
|
||||
@@ -55,7 +59,7 @@ jobs:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Install tools (search engine)
|
||||
run: pip install bandit mypy pycodestyle pyflakes pyright
|
||||
run: pip install bandit isort mypy pycodestyle pyflakes pyright
|
||||
|
||||
- name: Gather files (search engine)
|
||||
run: |
|
||||
@@ -85,6 +89,10 @@ jobs:
|
||||
--max-line-length=1000 \
|
||||
--statistics \
|
||||
$PY_FILES
|
||||
isort \
|
||||
--check \
|
||||
--diff \
|
||||
$PY_FILES
|
||||
|
||||
- name: Build code (search engine)
|
||||
run: |
|
||||
|
||||
25
.github/workflows/ci_ubuntu.yaml
vendored
25
.github/workflows/ci_ubuntu.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
matrix:
|
||||
libt_version: ["2.0.11", "1.2.20"]
|
||||
qbt_gui: ["GUI=ON", "GUI=OFF"]
|
||||
qt_version: ["6.5.2"]
|
||||
qt_version: ["6.6.3"]
|
||||
|
||||
env:
|
||||
boost_path: "${{ github.workspace }}/../boost"
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
store_cache: ${{ github.ref == 'refs/heads/master' }}
|
||||
update_packager_index: false
|
||||
ccache_options: |
|
||||
max_size=2G
|
||||
max_size=1G
|
||||
|
||||
- name: Install boost
|
||||
env:
|
||||
@@ -65,6 +65,9 @@ jobs:
|
||||
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?"
|
||||
fi
|
||||
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
|
||||
cd "${{ env.boost_path }}"
|
||||
./bootstrap.sh
|
||||
./b2 stage --stagedir=./ --with-headers
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v4
|
||||
@@ -90,7 +93,7 @@ jobs:
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_CXX_STANDARD=20 \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}" \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
|
||||
-Ddeprecated-functions=OFF
|
||||
cmake --build build
|
||||
sudo cmake --install build
|
||||
@@ -112,7 +115,7 @@ jobs:
|
||||
-G "Ninja" \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}" \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
|
||||
-DCMAKE_INSTALL_PREFIX="/usr" \
|
||||
-DTESTING=ON \
|
||||
-DVERBOSE_CONFIGURE=ON \
|
||||
@@ -138,16 +141,15 @@ jobs:
|
||||
|
||||
- name: Install AppImage
|
||||
run: |
|
||||
sudo apt install libfuse2
|
||||
curl \
|
||||
-L \
|
||||
-Z \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-static-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-static-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage
|
||||
chmod +x \
|
||||
linuxdeploy-static-x86_64.AppImage \
|
||||
linuxdeploy-plugin-qt-static-x86_64.AppImage \
|
||||
linuxdeploy-x86_64.AppImage \
|
||||
linuxdeploy-plugin-qt-x86_64.AppImage \
|
||||
linuxdeploy-plugin-appimage-x86_64.AppImage
|
||||
|
||||
- name: Prepare files for AppImage
|
||||
@@ -160,12 +162,13 @@ jobs:
|
||||
|
||||
- name: Package AppImage
|
||||
run: |
|
||||
./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --plugin qt
|
||||
rm -f "${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/sqldrivers/libqsqlmimer.so"
|
||||
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt
|
||||
rm qbittorrent/apprun-hooks/*
|
||||
cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh
|
||||
NO_APPSTREAM=1 \
|
||||
OUTPUT=upload/qbittorrent-CI_Ubuntu_x86_64.AppImage \
|
||||
./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --output appimage
|
||||
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --output appimage
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
5
.github/workflows/ci_webui.yaml
vendored
5
.github/workflows/ci_webui.yaml
vendored
@@ -34,7 +34,12 @@ jobs:
|
||||
run: |
|
||||
npm install
|
||||
npm ls
|
||||
echo "::group::npm ls --all"
|
||||
npm ls --all
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Lint code
|
||||
run: npm run lint
|
||||
|
||||
14
.github/workflows/ci_windows.yaml
vendored
14
.github/workflows/ci_windows.yaml
vendored
@@ -67,6 +67,7 @@ jobs:
|
||||
"set(VCPKG_BUILD_TYPE release)")
|
||||
# clear buildtrees after each package installation to reduce disk space requirements
|
||||
$packages = `
|
||||
"boost-build:x64-windows-static-md-release",
|
||||
"openssl:x64-windows-static-md-release",
|
||||
"zlib:x64-windows-static-md-release"
|
||||
${{ env.vcpkg_path }}/vcpkg.exe upgrade `
|
||||
@@ -94,11 +95,18 @@ jobs:
|
||||
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."
|
||||
}
|
||||
move "${{ github.workspace }}/../boost_*" "${{ env.boost_path }}"
|
||||
cd "${{ env.boost_path }}"
|
||||
#.\bootstrap.bat
|
||||
${{ env.vcpkg_path }}/installed/x64-windows-static-md-release/tools/boost-build/b2.exe `
|
||||
stage `
|
||||
toolset=msvc `
|
||||
--stagedir=.\ `
|
||||
--with-headers
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: "6.8.0"
|
||||
version: "6.9.0"
|
||||
arch: win64_msvc2022_64
|
||||
archives: qtbase qtsvg qttools
|
||||
cache: true
|
||||
@@ -122,7 +130,7 @@ jobs:
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON `
|
||||
-DCMAKE_INSTALL_PREFIX="${{ env.libtorrent_path }}/install" `
|
||||
-DCMAKE_TOOLCHAIN_FILE="${{ env.vcpkg_path }}/scripts/buildsystems/vcpkg.cmake" `
|
||||
-DBOOST_ROOT="${{ env.boost_path }}" `
|
||||
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" `
|
||||
-DBUILD_SHARED_LIBS=OFF `
|
||||
-Ddeprecated-functions=OFF `
|
||||
-Dstatic_runtime=OFF `
|
||||
@@ -139,7 +147,7 @@ jobs:
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo `
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON `
|
||||
-DCMAKE_TOOLCHAIN_FILE="${{ env.vcpkg_path }}/scripts/buildsystems/vcpkg.cmake" `
|
||||
-DBOOST_ROOT="${{ env.boost_path }}" `
|
||||
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" `
|
||||
-DLibtorrentRasterbar_DIR="${{ env.libtorrent_path }}/install/lib/cmake/LibtorrentRasterbar" `
|
||||
-DMSVC_RUNTIME_DYNAMIC=ON `
|
||||
-DTESTING=ON `
|
||||
|
||||
9
.github/workflows/coverity-scan.yaml
vendored
9
.github/workflows/coverity-scan.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
matrix:
|
||||
libt_version: ["2.0.11"]
|
||||
qbt_gui: ["GUI=ON"]
|
||||
qt_version: ["6.5.2"]
|
||||
qt_version: ["6.6.3"]
|
||||
|
||||
env:
|
||||
boost_path: "${{ github.workspace }}/../boost"
|
||||
@@ -52,6 +52,9 @@ jobs:
|
||||
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?"
|
||||
fi
|
||||
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
|
||||
cd "${{ env.boost_path }}"
|
||||
./bootstrap.sh
|
||||
./b2 stage --stagedir=./ --with-headers
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v4
|
||||
@@ -74,7 +77,7 @@ jobs:
|
||||
-G "Ninja" \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_CXX_STANDARD=20 \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}" \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
|
||||
-Ddeprecated-functions=OFF
|
||||
cmake --build build
|
||||
sudo cmake --install build
|
||||
@@ -98,7 +101,7 @@ jobs:
|
||||
-B build \
|
||||
-G "Ninja" \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}" \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
|
||||
-DVERBOSE_CONFIGURE=ON \
|
||||
-D${{ matrix.qbt_gui }}
|
||||
PATH="${{ env.coverity_path }}/bin:$PATH" \
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
# exception statement from your version.
|
||||
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Optional
|
||||
import argparse
|
||||
import re
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
import sys
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def traversePostOrder(root: ElementTree.Element, visitFunc: Callable[[ElementTree.Element], None]) -> None:
|
||||
|
||||
@@ -26,11 +26,11 @@
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
# exception statement from your version.
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
|
||||
@@ -69,11 +69,11 @@ repos:
|
||||
- ts
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell.git
|
||||
rev: v2.4.0
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
name: Check spelling (codespell)
|
||||
args: ["--ignore-words-list", "additionals,categor,curren,fo,ist,ket,notin,searchin,sectionin,superseeding,te,ths"]
|
||||
args: ["--ignore-words-list", "additionals,categor,curren,fo,indexIn,ist,ket,notin,searchin,sectionin,superseeding,te,ths"]
|
||||
exclude: |
|
||||
(?x)^(
|
||||
.*\.desktop |
|
||||
@@ -88,7 +88,7 @@ repos:
|
||||
- ts
|
||||
|
||||
- repo: https://github.com/crate-ci/typos.git
|
||||
rev: v1.29.4
|
||||
rev: v1.32.0
|
||||
hooks:
|
||||
- id: typos
|
||||
name: Check spelling (typos)
|
||||
|
||||
@@ -8,7 +8,7 @@ project(qBittorrent
|
||||
|
||||
# version requirements - older versions may work, but you are on your own
|
||||
set(minBoostVersion 1.76)
|
||||
set(minQt6Version 6.5.0)
|
||||
set(minQt6Version 6.6.0)
|
||||
set(minOpenSSLVersion 3.0.2)
|
||||
set(minLibtorrent1Version 1.2.19)
|
||||
set(minLibtorrentVersion 2.0.10)
|
||||
|
||||
2
INSTALL
2
INSTALL
@@ -11,7 +11,7 @@ qBittorrent - A BitTorrent client in C++ / Qt
|
||||
|
||||
- OpenSSL >= 3.0.2
|
||||
|
||||
- Qt 6.5.0 - 6.x
|
||||
- Qt 6.6.0 - 6.x
|
||||
|
||||
- zlib >= 1.2.11
|
||||
|
||||
|
||||
26
WebAPI_Changelog.md
Normal file
26
WebAPI_Changelog.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# WebAPI Changelog
|
||||
|
||||
## 2.11.8
|
||||
|
||||
* [#21349](https://github.com/qbittorrent/qBittorrent/pull/21349)
|
||||
* Handle sending `204 No Content` status code when response contains no data
|
||||
* Some endpoints still return `200 OK` to ensure smooth transition
|
||||
* [#22750](https://github.com/qbittorrent/qBittorrent/pull/22750)
|
||||
* `torrents/info` allows an optional parameter `includeFiles` that defaults to `false`
|
||||
* Each torrent will contain a new key `files` which will list all files similar to the `torrents/files` endpoint
|
||||
* [#22813](https://github.com/qbittorrent/qBittorrent/pull/22813)
|
||||
* `app/getDirectoryContent` allows an optional parameter `withMetadata` to send file metadata
|
||||
* Fields are `name`, `type`, `size`, `creation_date`, `last_access_date`, `last_modification_date`
|
||||
* See PR for TypeScript types
|
||||
|
||||
## 2.11.7
|
||||
|
||||
* [#22166](https://github.com/qbittorrent/qBittorrent/pull/22166)
|
||||
* `sync/maindata` returns 3 new torrent fields: `has_tracker_warning`, `has_tracker_error`, `has_other_announce_error`
|
||||
|
||||
## 2.11.6
|
||||
|
||||
* [#22460](https://github.com/qbittorrent/qBittorrent/pull/22460)
|
||||
* `app/setPreferences` allows only one of `max_ratio_enabled`, `max_ratio` to be present
|
||||
* `app/setPreferences` allows only one of `max_seeding_time_enabled`, `max_seeding_time` to be present
|
||||
* `app/setPreferences` allows only one of `max_inactive_seeding_time_enabled`, `max_inactive_seeding_time` to be present
|
||||
@@ -20,7 +20,7 @@ target_compile_features(qbt_common_cfg INTERFACE
|
||||
)
|
||||
|
||||
target_compile_definitions(qbt_common_cfg INTERFACE
|
||||
QT_DISABLE_DEPRECATED_UP_TO=0x060500
|
||||
QT_DISABLE_DEPRECATED_UP_TO=0x060600
|
||||
QT_NO_CAST_FROM_ASCII
|
||||
QT_NO_CAST_TO_ASCII
|
||||
QT_NO_CAST_FROM_BYTEARRAY
|
||||
|
||||
4
dist/mac/Info.plist
vendored
4
dist/mac/Info.plist
vendored
@@ -55,7 +55,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.1.0</string>
|
||||
<string>5.2.0</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -67,7 +67,7 @@
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<string>YES</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2006-2024 The qBittorrent project</string>
|
||||
<string>Copyright © 2006-2025 The qBittorrent project</string>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -105,7 +105,7 @@ GenericName[ka]=BitTorrent კლიენტი
|
||||
Comment[ka]=გადმოტვირთეთ და გააზიარეთ ფაილები BitTorrent-ის საშუალებით
|
||||
Name[ka]=qBittorrent
|
||||
GenericName[ko]=BitTorrent 클라이언트
|
||||
Comment[ko]=BitTorrent를 통한 파일 다운로드 및 공유
|
||||
Comment[ko]=BitTorrent를 통해 파일 다운로드 및 공유
|
||||
Name[ko]=qBittorrent
|
||||
GenericName[lt]=BitTorrent klientas
|
||||
Comment[lt]=Atsisiųskite bei dalinkitės failais BitTorrent tinkle
|
||||
|
||||
@@ -62,6 +62,6 @@
|
||||
<url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<releases>
|
||||
<release version="5.1.0~beta1" date="2024-12-16"/>
|
||||
<release version="5.2.0~alpha1" date="2025-02-11"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
||||
7
dist/windows/config.nsh
vendored
7
dist/windows/config.nsh
vendored
@@ -14,7 +14,7 @@
|
||||
; 4.5.1.3 -> good
|
||||
; 4.5.1.3.2 -> bad
|
||||
; 4.5.0beta -> bad
|
||||
!define /ifndef QBT_VERSION "5.1.0"
|
||||
!define /ifndef QBT_VERSION "5.2.0"
|
||||
|
||||
; Option that controls the installer's window name
|
||||
; If set, its value will be used like this:
|
||||
@@ -86,7 +86,7 @@ OutFile "qbittorrent_${QBT_INSTALLER_FILENAME}_setup.exe"
|
||||
;Installer Version Information
|
||||
VIAddVersionKey "ProductName" "qBittorrent"
|
||||
VIAddVersionKey "CompanyName" "The qBittorrent project"
|
||||
VIAddVersionKey "LegalCopyright" "Copyright ©2006-2024 The qBittorrent project"
|
||||
VIAddVersionKey "LegalCopyright" "Copyright ©2006-2025 The qBittorrent project"
|
||||
VIAddVersionKey "FileDescription" "qBittorrent - A Bittorrent Client"
|
||||
VIAddVersionKey "FileVersion" "${QBT_VERSION}"
|
||||
|
||||
@@ -111,7 +111,8 @@ RequestExecutionLevel user
|
||||
!define MUI_HEADERIMAGE
|
||||
!define MUI_COMPONENTSPAGE_NODESC
|
||||
;!define MUI_ICON "qbittorrent.ico"
|
||||
!define MUI_LICENSEPAGE_CHECKBOX
|
||||
!define MUI_LICENSEPAGE_BUTTON $(^NextBtn)
|
||||
!define MUI_LICENSEPAGE_TEXT_BOTTOM "$_CLICK"
|
||||
!define MUI_LANGDLL_ALLLANGUAGES
|
||||
|
||||
;--------------------------------
|
||||
|
||||
12
dist/windows/installer-translations/swedish.nsh
vendored
12
dist/windows/installer-translations/swedish.nsh
vendored
@@ -7,21 +7,21 @@ LangString inst_desktop ${LANG_SWEDISH} "Skapa skrivbordsgenväg"
|
||||
;LangString inst_startmenu ${LANG_ENGLISH} "Create Start Menu Shortcut"
|
||||
LangString inst_startmenu ${LANG_SWEDISH} "Skapa startmenygenväg"
|
||||
;LangString inst_startup ${LANG_ENGLISH} "Start qBittorrent on Windows start up"
|
||||
LangString inst_startup ${LANG_SWEDISH} "Starta qBittorrent vid Windows start"
|
||||
LangString inst_startup ${LANG_SWEDISH} "Starta qBittorrent vid Windows-uppstart"
|
||||
;LangString inst_torrent ${LANG_ENGLISH} "Open .torrent files with qBittorrent"
|
||||
LangString inst_torrent ${LANG_SWEDISH} "Öppna .torrent-filer med qBittorrent"
|
||||
;LangString inst_magnet ${LANG_ENGLISH} "Open magnet links with qBittorrent"
|
||||
LangString inst_magnet ${LANG_SWEDISH} "Öppna magnetlänkar med qBittorrent"
|
||||
;LangString inst_firewall ${LANG_ENGLISH} "Add Windows Firewall rule"
|
||||
LangString inst_firewall ${LANG_SWEDISH} "Lägg till Windows-brandväggregel"
|
||||
LangString inst_firewall ${LANG_SWEDISH} "Lägg till Windows-brandväggsregel"
|
||||
;LangString inst_pathlimit ${LANG_ENGLISH} "Disable Windows path length limit (260 character MAX_PATH limitation, requires Windows 10 1607 or later)"
|
||||
LangString inst_pathlimit ${LANG_SWEDISH} "Inaktivera gränsen för Windows-sökvägslängd (260 tecken MAX_PATH-begränsning, kräver Windows 10 1607 eller senare)"
|
||||
;LangString inst_firewallinfo ${LANG_ENGLISH} "Adding Windows Firewall rule"
|
||||
LangString inst_firewallinfo ${LANG_SWEDISH} "Lägger till Windows-brandväggregel"
|
||||
LangString inst_firewallinfo ${LANG_SWEDISH} "Lägger till Windows-brandväggsregel"
|
||||
;LangString inst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before installing."
|
||||
LangString inst_warning ${LANG_SWEDISH} "qBittorrent körs. Vänligen stäng programmet innan du installerar."
|
||||
LangString inst_warning ${LANG_SWEDISH} "qBittorrent körs. Stäng programmet innan du installerar."
|
||||
;LangString inst_uninstall_question ${LANG_ENGLISH} "Current version will be uninstalled. User settings and torrents will remain intact."
|
||||
LangString inst_uninstall_question ${LANG_SWEDISH} "Nuvarande version avinstalleras. Användarinställningar och torrenter kommer att förbli intakta."
|
||||
LangString inst_uninstall_question ${LANG_SWEDISH} "Aktuell version avinstalleras. Användarinställningar och torrenter kommer att förbli intakta."
|
||||
;LangString inst_unist ${LANG_ENGLISH} "Uninstalling previous version."
|
||||
LangString inst_unist ${LANG_SWEDISH} "Avinstallerar tidigare version."
|
||||
;LangString launch_qbt ${LANG_ENGLISH} "Launch qBittorrent."
|
||||
@@ -53,7 +53,7 @@ LangString remove_firewallinfo ${LANG_SWEDISH} "Tar bort Windows-brandväggsrege
|
||||
;LangString remove_cache ${LANG_ENGLISH} "Remove torrents and cached data"
|
||||
LangString remove_cache ${LANG_SWEDISH} "Ta bort torrenter och cachade data"
|
||||
;LangString uninst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before uninstalling."
|
||||
LangString uninst_warning ${LANG_SWEDISH} "qBittorrent körs. Vänligen stäng programmet innan du avinstallerar."
|
||||
LangString uninst_warning ${LANG_SWEDISH} "qBittorrent körs. Stäng programmet innan du avinstallerar."
|
||||
;LangString uninst_tor_warn ${LANG_ENGLISH} "Not removing .torrent association. It is associated with:"
|
||||
LangString uninst_tor_warn ${LANG_SWEDISH} "Tar inte bort .torrent-association. Den är associerad med:"
|
||||
;LangString uninst_mag_warn ${LANG_ENGLISH} "Not removing magnet association. It is associated with:"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -124,6 +124,28 @@ namespace
|
||||
const int PIXMAP_CACHE_SIZE = 64 * 1024 * 1024; // 64MiB
|
||||
#endif
|
||||
|
||||
const QString PARAM_ADDSTOPPED = u"@addStopped"_s;
|
||||
const QString PARAM_CATEGORY = u"@category"_s;
|
||||
const QString PARAM_FIRSTLASTPIECEPRIORITY = u"@firstLastPiecePriority"_s;
|
||||
const QString PARAM_SAVEPATH = u"@savePath"_s;
|
||||
const QString PARAM_SEQUENTIAL = u"@sequential"_s;
|
||||
const QString PARAM_SKIPCHECKING = u"@skipChecking"_s;
|
||||
const QString PARAM_SKIPDIALOG = u"@skipDialog"_s;
|
||||
|
||||
QString bindParamValue(const QStringView paramName, const QStringView paramValue)
|
||||
{
|
||||
return paramName + u'=' + paramValue;
|
||||
}
|
||||
|
||||
std::pair<QStringView, QStringView> parseParam(const QStringView param)
|
||||
{
|
||||
const qsizetype sepIndex = param.indexOf(u'=');
|
||||
if (sepIndex >= 0)
|
||||
return {param.first(sepIndex), param.sliced(sepIndex + 1)};
|
||||
|
||||
return {param, {}};
|
||||
}
|
||||
|
||||
QString serializeParams(const QBtCommandLineParameters ¶ms)
|
||||
{
|
||||
QStringList result;
|
||||
@@ -138,85 +160,86 @@ namespace
|
||||
const BitTorrent::AddTorrentParams &addTorrentParams = params.addTorrentParams;
|
||||
|
||||
if (!addTorrentParams.savePath.isEmpty())
|
||||
result.append(u"@savePath=" + addTorrentParams.savePath.data());
|
||||
result.append(bindParamValue(PARAM_SAVEPATH, addTorrentParams.savePath.data()));
|
||||
|
||||
if (addTorrentParams.addStopped.has_value())
|
||||
result.append(*addTorrentParams.addStopped ? u"@addStopped=1"_s : u"@addStopped=0"_s);
|
||||
result.append(bindParamValue(PARAM_ADDSTOPPED, (*addTorrentParams.addStopped ? u"1" : u"0")));
|
||||
|
||||
if (addTorrentParams.skipChecking)
|
||||
result.append(u"@skipChecking"_s);
|
||||
result.append(PARAM_SKIPCHECKING);
|
||||
|
||||
if (!addTorrentParams.category.isEmpty())
|
||||
result.append(u"@category=" + addTorrentParams.category);
|
||||
result.append(bindParamValue(PARAM_CATEGORY, addTorrentParams.category));
|
||||
|
||||
if (addTorrentParams.sequential)
|
||||
result.append(u"@sequential"_s);
|
||||
result.append(PARAM_SEQUENTIAL);
|
||||
|
||||
if (addTorrentParams.firstLastPiecePriority)
|
||||
result.append(u"@firstLastPiecePriority"_s);
|
||||
result.append(PARAM_FIRSTLASTPIECEPRIORITY);
|
||||
|
||||
if (params.skipDialog.has_value())
|
||||
result.append(*params.skipDialog ? u"@skipDialog=1"_s : u"@skipDialog=0"_s);
|
||||
result.append(bindParamValue(PARAM_SKIPDIALOG, (*params.skipDialog ? u"1" : u"0")));
|
||||
|
||||
result += params.torrentSources;
|
||||
|
||||
return result.join(PARAMS_SEPARATOR);
|
||||
}
|
||||
|
||||
QBtCommandLineParameters parseParams(const QString &str)
|
||||
QBtCommandLineParameters parseParams(const QStringView str)
|
||||
{
|
||||
QBtCommandLineParameters parsedParams;
|
||||
BitTorrent::AddTorrentParams &addTorrentParams = parsedParams.addTorrentParams;
|
||||
|
||||
for (QString param : asConst(str.split(PARAMS_SEPARATOR, Qt::SkipEmptyParts)))
|
||||
for (QStringView param : asConst(str.split(PARAMS_SEPARATOR, Qt::SkipEmptyParts)))
|
||||
{
|
||||
param = param.trimmed();
|
||||
const auto [paramName, paramValue] = parseParam(param);
|
||||
|
||||
// Process strings indicating options specified by the user.
|
||||
|
||||
if (param.startsWith(u"@savePath="))
|
||||
if (paramName == PARAM_SAVEPATH)
|
||||
{
|
||||
addTorrentParams.savePath = Path(param.mid(10));
|
||||
addTorrentParams.savePath = Path(paramValue.toString());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param.startsWith(u"@addStopped="))
|
||||
if (paramName == PARAM_ADDSTOPPED)
|
||||
{
|
||||
addTorrentParams.addStopped = (QStringView(param).mid(11).toInt() != 0);
|
||||
addTorrentParams.addStopped = (paramValue.toInt() != 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param == u"@skipChecking")
|
||||
if (paramName == PARAM_SKIPCHECKING)
|
||||
{
|
||||
addTorrentParams.skipChecking = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param.startsWith(u"@category="))
|
||||
if (paramName == PARAM_CATEGORY)
|
||||
{
|
||||
addTorrentParams.category = param.mid(10);
|
||||
addTorrentParams.category = paramValue.toString();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param == u"@sequential")
|
||||
if (paramName == PARAM_SEQUENTIAL)
|
||||
{
|
||||
addTorrentParams.sequential = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param == u"@firstLastPiecePriority")
|
||||
if (paramName == PARAM_FIRSTLASTPIECEPRIORITY)
|
||||
{
|
||||
addTorrentParams.firstLastPiecePriority = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param.startsWith(u"@skipDialog="))
|
||||
if (paramName == PARAM_SKIPDIALOG)
|
||||
{
|
||||
parsedParams.skipDialog = (QStringView(param).mid(12).toInt() != 0);
|
||||
parsedParams.skipDialog = (paramValue.toInt() != 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedParams.torrentSources.append(param);
|
||||
parsedParams.torrentSources.append(param.toString());
|
||||
}
|
||||
|
||||
return parsedParams;
|
||||
@@ -387,7 +410,7 @@ void Application::setMemoryWorkingSetLimit(const int size)
|
||||
return;
|
||||
|
||||
m_storeMemoryWorkingSetLimit = size;
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
|
||||
applyMemoryWorkingSetLimit();
|
||||
#endif
|
||||
}
|
||||
@@ -552,6 +575,9 @@ void Application::runExternalProgram(const QString &programTemplate, const BitTo
|
||||
case u'L':
|
||||
str.replace(i, 2, torrent->category());
|
||||
break;
|
||||
case u'M':
|
||||
str.replace(i, 2, torrent->comment());
|
||||
break;
|
||||
case u'N':
|
||||
str.replace(i, 2, torrent->name());
|
||||
break;
|
||||
@@ -636,7 +662,13 @@ void Application::runExternalProgram(const QString &programTemplate, const BitTo
|
||||
{
|
||||
// strip redundant quotes
|
||||
if (arg.startsWith(u'"') && arg.endsWith(u'"'))
|
||||
arg = arg.mid(1, (arg.size() - 2));
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
arg.slice(1, (arg.size() - 2));
|
||||
#else
|
||||
arg.removeLast().removeFirst();
|
||||
#endif
|
||||
}
|
||||
|
||||
arg = replaceVariables(arg);
|
||||
}
|
||||
@@ -645,6 +677,7 @@ void Application::runExternalProgram(const QString &programTemplate, const BitTo
|
||||
QProcess proc;
|
||||
proc.setProgram(command);
|
||||
proc.setArguments(args);
|
||||
proc.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
|
||||
|
||||
if (proc.startDetached())
|
||||
{
|
||||
@@ -814,7 +847,7 @@ int Application::exec()
|
||||
printf("%s\n", qUtf8Printable(loadingStr));
|
||||
#endif
|
||||
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
|
||||
applyMemoryWorkingSetLimit();
|
||||
#endif
|
||||
|
||||
@@ -897,10 +930,10 @@ int Application::exec()
|
||||
m_desktopIntegration->showNotification(tr("Torrent added"), tr("'%1' was added.", "e.g: xxx.avi was added.").arg(torrent->name()));
|
||||
});
|
||||
connect(m_addTorrentManager, &AddTorrentManager::addTorrentFailed, this
|
||||
, [this](const QString &source, const QString &reason)
|
||||
, [this](const QString &source, const BitTorrent::AddTorrentError &reason)
|
||||
{
|
||||
m_desktopIntegration->showNotification(tr("Add torrent failed")
|
||||
, tr("Couldn't add torrent '%1', reason: %2.").arg(source, reason));
|
||||
, tr("Couldn't add torrent '%1', reason: %2.").arg(source, reason.message));
|
||||
});
|
||||
|
||||
disconnect(m_desktopIntegration, &DesktopIntegration::activationRequested, this, &Application::createStartupProgressDialog);
|
||||
@@ -1172,7 +1205,7 @@ void Application::shutdownCleanup([[maybe_unused]] QSessionManager &manager)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
|
||||
void Application::applyMemoryWorkingSetLimit() const
|
||||
{
|
||||
const size_t MiB = 1024 * 1024;
|
||||
|
||||
@@ -465,13 +465,13 @@ QBtCommandLineParameters parseCommandLine(const QStringList &args)
|
||||
return result;
|
||||
}
|
||||
|
||||
QString wrapText(const QString &text, int initialIndentation = USAGE_TEXT_COLUMN, int wrapAtColumn = WRAP_AT_COLUMN)
|
||||
QString wrapText(const QString &text, const int initialIndentation = USAGE_TEXT_COLUMN, const int wrapAtColumn = WRAP_AT_COLUMN)
|
||||
{
|
||||
QStringList words = text.split(u' ');
|
||||
const QStringList words = text.split(u' ');
|
||||
QStringList lines = {words.first()};
|
||||
int currentLineMaxLength = wrapAtColumn - initialIndentation;
|
||||
|
||||
for (const QString &word : asConst(words.mid(1)))
|
||||
for (const QString &word : asConst(words.sliced(1)))
|
||||
{
|
||||
if (lines.last().length() + word.length() + 1 < currentLineMaxLength)
|
||||
{
|
||||
@@ -491,6 +491,12 @@ QString makeUsage(const QString &prgName)
|
||||
{
|
||||
const QString indentation {USAGE_INDENTATION, u' '};
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
const QString noSplashCommand = u"set QBT_NO_SPLASH=1 && " + prgName;
|
||||
#else
|
||||
const QString noSplashCommand = u"QBT_NO_SPLASH=1 " + prgName;
|
||||
#endif
|
||||
|
||||
const QString text = QCoreApplication::translate("CMD Options", "Usage:") + u'\n'
|
||||
+ indentation + prgName + u' ' + QCoreApplication::translate("CMD Options", "[options] [(<filename> | <url>)...]") + u'\n'
|
||||
|
||||
@@ -542,7 +548,7 @@ QString makeUsage(const QString &prgName)
|
||||
"'parameter-name', environment variable name is 'QBT_PARAMETER_NAME' (in upper "
|
||||
"case, '-' replaced with '_'). To pass flag values, set the variable to '1' or "
|
||||
"'TRUE'. For example, to disable the splash screen: "), 0) + u'\n'
|
||||
+ u"QBT_NO_SPLASH=1 " + prgName + u'\n'
|
||||
+ noSplashCommand + u'\n'
|
||||
+ wrapText(QCoreApplication::translate("CMD Options", "Command line parameters take precedence over environment variables"), 0) + u'\n';
|
||||
|
||||
return text;
|
||||
|
||||
@@ -175,12 +175,15 @@ void FileLogger::flushLog()
|
||||
|
||||
void FileLogger::openLogFile()
|
||||
{
|
||||
if (!m_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)
|
||||
|| !m_logFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner))
|
||||
if (!m_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text))
|
||||
{
|
||||
m_logFile.close();
|
||||
LogMsg(tr("An error occurred while trying to open the log file. Logging to file is disabled."), Log::CRITICAL);
|
||||
LogMsg(tr("An error occurred while trying to open the log file. Logging to file is disabled. File: \"%1\". Error: \"%2\".")
|
||||
.arg(m_logFile.fileName(), m_logFile.errorString()), Log::CRITICAL);
|
||||
return;
|
||||
}
|
||||
|
||||
// best effort, don't report error
|
||||
m_logFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
|
||||
}
|
||||
|
||||
void FileLogger::closeLogFile()
|
||||
|
||||
@@ -6,6 +6,7 @@ add_library(qbt_base STATIC
|
||||
applicationcomponent.h
|
||||
asyncfilestorage.h
|
||||
bittorrent/abstractfilestorage.h
|
||||
bittorrent/addtorrenterror.h
|
||||
bittorrent/addtorrentparams.h
|
||||
bittorrent/announcetimepoint.h
|
||||
bittorrent/bandwidthscheduler.h
|
||||
@@ -54,6 +55,7 @@ add_library(qbt_base STATIC
|
||||
concepts/stringable.h
|
||||
digest32.h
|
||||
exceptions.h
|
||||
freediskspacechecker.h
|
||||
global.h
|
||||
http/connection.h
|
||||
http/httperror.h
|
||||
@@ -159,6 +161,7 @@ add_library(qbt_base STATIC
|
||||
bittorrent/trackerentry.cpp
|
||||
bittorrent/trackerentrystatus.cpp
|
||||
exceptions.cpp
|
||||
freediskspacechecker.cpp
|
||||
http/connection.cpp
|
||||
http/httperror.cpp
|
||||
http/requestparser.cpp
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
#include "addtorrentmanager.h"
|
||||
|
||||
#include "base/bittorrent/addtorrenterror.h"
|
||||
#include "base/bittorrent/infohash.h"
|
||||
#include "base/bittorrent/session.h"
|
||||
#include "base/bittorrent/torrentdescriptor.h"
|
||||
@@ -140,7 +141,7 @@ void AddTorrentManager::onSessionTorrentAdded(BitTorrent::Torrent *torrent)
|
||||
}
|
||||
}
|
||||
|
||||
void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const QString &reason)
|
||||
void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const BitTorrent::AddTorrentError &reason)
|
||||
{
|
||||
if (const QString source = m_sourcesByInfoHash.take(infoHash); !source.isEmpty())
|
||||
{
|
||||
@@ -154,7 +155,7 @@ void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &in
|
||||
void AddTorrentManager::handleAddTorrentFailed(const QString &source, const QString &reason)
|
||||
{
|
||||
LogMsg(tr("Failed to add torrent. Source: \"%1\". Reason: \"%2\"").arg(source, reason), Log::WARNING);
|
||||
emit addTorrentFailed(source, reason);
|
||||
emit addTorrentFailed(source, {BitTorrent::AddTorrentError::Other, reason});
|
||||
}
|
||||
|
||||
void AddTorrentManager::handleDuplicateTorrent(const QString &source
|
||||
@@ -185,9 +186,9 @@ void AddTorrentManager::handleDuplicateTorrent(const QString &source
|
||||
message = tr("Trackers are merged from new source");
|
||||
}
|
||||
|
||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Source: %1. Existing torrent: %2. Result: %3")
|
||||
.arg(source, existingTorrent->name(), message));
|
||||
emit addTorrentFailed(source, message);
|
||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Source: %1. Existing torrent: \"%2\". Torrent infohash: %3. Result: %4")
|
||||
.arg(source, existingTorrent->name(), existingTorrent->infoHash().toString(), message));
|
||||
emit addTorrentFailed(source, {BitTorrent::AddTorrentError::DuplicateTorrent, message});
|
||||
}
|
||||
|
||||
void AddTorrentManager::setTorrentFileGuard(const QString &source, std::shared_ptr<TorrentFileGuard> torrentFileGuard)
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace BitTorrent
|
||||
class Session;
|
||||
class Torrent;
|
||||
class TorrentDescriptor;
|
||||
struct AddTorrentError;
|
||||
}
|
||||
|
||||
namespace Net
|
||||
@@ -66,7 +67,7 @@ public:
|
||||
|
||||
signals:
|
||||
void torrentAdded(const QString &source, BitTorrent::Torrent *torrent);
|
||||
void addTorrentFailed(const QString &source, const QString &reason);
|
||||
void addTorrentFailed(const QString &source, const BitTorrent::AddTorrentError &reason);
|
||||
|
||||
protected:
|
||||
bool addTorrentToSession(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
|
||||
@@ -79,7 +80,7 @@ protected:
|
||||
private:
|
||||
void onDownloadFinished(const Net::DownloadResult &result);
|
||||
void onSessionTorrentAdded(BitTorrent::Torrent *torrent);
|
||||
void onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const QString &reason);
|
||||
void onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const BitTorrent::AddTorrentError &reason);
|
||||
bool processTorrent(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
|
||||
, const BitTorrent::AddTorrentParams &addTorrentParams);
|
||||
|
||||
|
||||
49
src/base/bittorrent/addtorrenterror.h
Normal file
49
src/base/bittorrent/addtorrenterror.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
struct AddTorrentError
|
||||
{
|
||||
enum Kind
|
||||
{
|
||||
DuplicateTorrent,
|
||||
Other
|
||||
};
|
||||
|
||||
Kind kind = Other;
|
||||
QString message;
|
||||
};
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(BitTorrent::AddTorrentError)
|
||||
@@ -147,7 +147,7 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::load(cons
|
||||
const Path torrentFilePath = path() / Path(idString + u".torrent");
|
||||
const qint64 torrentSizeLimit = Preferences::instance()->getTorrentFileSizeLimit();
|
||||
|
||||
const auto resumeDataReadResult = Utils::IO::readFile(fastresumePath, torrentSizeLimit);
|
||||
const auto resumeDataReadResult = Utils::IO::readFile(fastresumePath, -1);
|
||||
if (!resumeDataReadResult)
|
||||
return nonstd::make_unexpected(resumeDataReadResult.error().message);
|
||||
|
||||
@@ -290,6 +290,8 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
|
||||
lt::add_torrent_params &p = torrentParams.ltAddTorrentParams;
|
||||
|
||||
p = lt::read_resume_data(resumeDataRoot, ec);
|
||||
if (ec)
|
||||
return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
|
||||
|
||||
if (!metadata.isEmpty())
|
||||
{
|
||||
@@ -320,6 +322,8 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
|
||||
|
||||
p.save_path = Profile::instance()->fromPortablePath(
|
||||
Path(fromLTString(p.save_path))).toString().toStdString();
|
||||
if (p.save_path.empty())
|
||||
return nonstd::make_unexpected(tr("Corrupted resume data: %1").arg(tr("save_path is invalid")));
|
||||
|
||||
torrentParams.stopped = (p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed);
|
||||
torrentParams.operatingMode = (p.flags & lt::torrent_flags::paused) || (p.flags & lt::torrent_flags::auto_managed)
|
||||
@@ -338,9 +342,9 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
|
||||
return torrentParams;
|
||||
}
|
||||
|
||||
void BitTorrent::BencodeResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const
|
||||
void BitTorrent::BencodeResumeDataStorage::store(const TorrentID &id, LoadTorrentParams resumeData) const
|
||||
{
|
||||
QMetaObject::invokeMethod(m_asyncWorker, [this, id, resumeData]()
|
||||
QMetaObject::invokeMethod(m_asyncWorker, [this, id, resumeData = std::move(resumeData)]
|
||||
{
|
||||
m_asyncWorker->store(id, resumeData);
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace BitTorrent
|
||||
|
||||
QList<TorrentID> registeredTorrents() const override;
|
||||
LoadResumeDataResult load(const TorrentID &id) const override;
|
||||
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override;
|
||||
void store(const TorrentID &id, LoadTorrentParams resumeData) const override;
|
||||
void remove(const TorrentID &id) const override;
|
||||
void storeQueue(const QList<TorrentID> &queue) const override;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2021-2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2021-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@@ -86,7 +86,7 @@ namespace
|
||||
class StoreJob final : public Job
|
||||
{
|
||||
public:
|
||||
StoreJob(const TorrentID &torrentID, const LoadTorrentParams &resumeData);
|
||||
StoreJob(const TorrentID &torrentID, LoadTorrentParams resumeData);
|
||||
void perform(QSqlDatabase db) override;
|
||||
|
||||
private:
|
||||
@@ -217,80 +217,6 @@ namespace
|
||||
{
|
||||
return u"%1 %2"_s.arg(quoted(column.name), definition);
|
||||
}
|
||||
|
||||
LoadTorrentParams parseQueryResultRow(const QSqlQuery &query)
|
||||
{
|
||||
LoadTorrentParams resumeData;
|
||||
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
|
||||
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
|
||||
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
|
||||
if (!tagsData.isEmpty())
|
||||
{
|
||||
const QStringList tagList = tagsData.split(u',');
|
||||
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
|
||||
}
|
||||
resumeData.hasFinishedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
|
||||
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
|
||||
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
|
||||
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
|
||||
resumeData.inactiveSeedingTimeLimit = query.value(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT.name).toInt();
|
||||
resumeData.shareLimitAction = Utils::String::toEnum<ShareLimitAction>(
|
||||
query.value(DB_COLUMN_SHARE_LIMIT_ACTION.name).toString(), ShareLimitAction::Default);
|
||||
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
|
||||
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
|
||||
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
|
||||
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
|
||||
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
|
||||
resumeData.stopCondition = Utils::String::toEnum(
|
||||
query.value(DB_COLUMN_STOP_CONDITION.name).toString(), Torrent::StopCondition::None);
|
||||
resumeData.sslParameters =
|
||||
{
|
||||
.certificate = QSslCertificate(query.value(DB_COLUMN_SSL_CERTIFICATE.name).toByteArray()),
|
||||
.privateKey = Utils::SSLKey::load(query.value(DB_COLUMN_SSL_PRIVATE_KEY.name).toByteArray()),
|
||||
.dhParams = query.value(DB_COLUMN_SSL_DH_PARAMS.name).toByteArray()
|
||||
};
|
||||
|
||||
resumeData.savePath = Profile::instance()->fromPortablePath(
|
||||
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
|
||||
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
|
||||
if (!resumeData.useAutoTMM)
|
||||
{
|
||||
resumeData.downloadPath = Profile::instance()->fromPortablePath(
|
||||
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
|
||||
}
|
||||
|
||||
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
|
||||
const auto *pref = Preferences::instance();
|
||||
const int bdecodeDepthLimit = pref->getBdecodeDepthLimit();
|
||||
const int bdecodeTokenLimit = pref->getBdecodeTokenLimit();
|
||||
|
||||
lt::error_code ec;
|
||||
const lt::bdecode_node resumeDataRoot = lt::bdecode(bencodedResumeData, ec
|
||||
, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
||||
|
||||
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
|
||||
|
||||
p = lt::read_resume_data(resumeDataRoot, ec);
|
||||
|
||||
if (const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray()
|
||||
; !bencodedMetadata.isEmpty())
|
||||
{
|
||||
const lt::bdecode_node torentInfoRoot = lt::bdecode(bencodedMetadata, ec
|
||||
, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
||||
p.ti = std::make_shared<lt::torrent_info>(torentInfoRoot, ec);
|
||||
}
|
||||
|
||||
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
|
||||
.toString().toStdString();
|
||||
|
||||
if (p.flags & lt::torrent_flags::stop_when_ready)
|
||||
{
|
||||
p.flags &= ~lt::torrent_flags::stop_when_ready;
|
||||
resumeData.stopCondition = Torrent::StopCondition::FilesChecked;
|
||||
}
|
||||
|
||||
return resumeData;
|
||||
}
|
||||
}
|
||||
|
||||
namespace BitTorrent
|
||||
@@ -305,7 +231,7 @@ namespace BitTorrent
|
||||
void run() override;
|
||||
void requestInterruption();
|
||||
|
||||
void store(const TorrentID &id, const LoadTorrentParams &resumeData);
|
||||
void store(const TorrentID &id, LoadTorrentParams resumeData);
|
||||
void remove(const TorrentID &id);
|
||||
void storeQueue(const QList<TorrentID> &queue);
|
||||
|
||||
@@ -401,9 +327,9 @@ BitTorrent::LoadResumeDataResult BitTorrent::DBResumeDataStorage::load(const Tor
|
||||
return parseQueryResultRow(query);
|
||||
}
|
||||
|
||||
void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const
|
||||
void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, LoadTorrentParams resumeData) const
|
||||
{
|
||||
m_asyncWorker->store(id, resumeData);
|
||||
m_asyncWorker->store(id, std::move(resumeData));
|
||||
}
|
||||
|
||||
void BitTorrent::DBResumeDataStorage::remove(const BitTorrent::TorrentID &id) const
|
||||
@@ -688,6 +614,90 @@ void BitTorrent::DBResumeDataStorage::enableWALMode() const
|
||||
throw RuntimeError(tr("WAL mode is probably unsupported due to filesystem limitations."));
|
||||
}
|
||||
|
||||
LoadResumeDataResult DBResumeDataStorage::parseQueryResultRow(const QSqlQuery &query) const
|
||||
{
|
||||
LoadTorrentParams resumeData;
|
||||
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
|
||||
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
|
||||
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
|
||||
if (!tagsData.isEmpty())
|
||||
{
|
||||
const QStringList tagList = tagsData.split(u',');
|
||||
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
|
||||
}
|
||||
resumeData.hasFinishedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
|
||||
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
|
||||
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
|
||||
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
|
||||
resumeData.inactiveSeedingTimeLimit = query.value(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT.name).toInt();
|
||||
resumeData.shareLimitAction = Utils::String::toEnum<ShareLimitAction>(
|
||||
query.value(DB_COLUMN_SHARE_LIMIT_ACTION.name).toString(), ShareLimitAction::Default);
|
||||
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
|
||||
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
|
||||
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
|
||||
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
|
||||
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
|
||||
resumeData.stopCondition = Utils::String::toEnum(
|
||||
query.value(DB_COLUMN_STOP_CONDITION.name).toString(), Torrent::StopCondition::None);
|
||||
resumeData.sslParameters =
|
||||
{
|
||||
.certificate = QSslCertificate(query.value(DB_COLUMN_SSL_CERTIFICATE.name).toByteArray()),
|
||||
.privateKey = Utils::SSLKey::load(query.value(DB_COLUMN_SSL_PRIVATE_KEY.name).toByteArray()),
|
||||
.dhParams = query.value(DB_COLUMN_SSL_DH_PARAMS.name).toByteArray()
|
||||
};
|
||||
|
||||
resumeData.savePath = Profile::instance()->fromPortablePath(
|
||||
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
|
||||
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
|
||||
if (!resumeData.useAutoTMM)
|
||||
{
|
||||
resumeData.downloadPath = Profile::instance()->fromPortablePath(
|
||||
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
|
||||
}
|
||||
|
||||
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
|
||||
const auto *pref = Preferences::instance();
|
||||
const int bdecodeDepthLimit = pref->getBdecodeDepthLimit();
|
||||
const int bdecodeTokenLimit = pref->getBdecodeTokenLimit();
|
||||
|
||||
lt::error_code ec;
|
||||
const lt::bdecode_node resumeDataRoot = lt::bdecode(bencodedResumeData, ec, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
||||
if (ec)
|
||||
return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
|
||||
|
||||
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
|
||||
|
||||
p = lt::read_resume_data(resumeDataRoot, ec);
|
||||
if (ec)
|
||||
return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
|
||||
|
||||
if (const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray()
|
||||
; !bencodedMetadata.isEmpty())
|
||||
{
|
||||
const lt::bdecode_node torentInfoRoot = lt::bdecode(bencodedMetadata, ec
|
||||
, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
||||
if (ec)
|
||||
return nonstd::make_unexpected(tr("Cannot parse torrent info: %1").arg(QString::fromStdString(ec.message())));
|
||||
|
||||
p.ti = std::make_shared<lt::torrent_info>(torentInfoRoot, ec);
|
||||
if (ec)
|
||||
return nonstd::make_unexpected(tr("Cannot parse torrent info: %1").arg(QString::fromStdString(ec.message())));
|
||||
}
|
||||
|
||||
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
|
||||
.toString().toStdString();
|
||||
if (p.save_path.empty())
|
||||
return nonstd::make_unexpected(tr("Corrupted resume data: %1").arg(tr("save_path is invalid")));
|
||||
|
||||
if (p.flags & lt::torrent_flags::stop_when_ready)
|
||||
{
|
||||
p.flags &= ~lt::torrent_flags::stop_when_ready;
|
||||
resumeData.stopCondition = Torrent::StopCondition::FilesChecked;
|
||||
}
|
||||
|
||||
return resumeData;
|
||||
}
|
||||
|
||||
BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, QReadWriteLock &dbLock, QObject *parent)
|
||||
: QThread(parent)
|
||||
, m_path {dbPath}
|
||||
@@ -759,9 +769,9 @@ void DBResumeDataStorage::Worker::requestInterruption()
|
||||
m_waitCondition.wakeAll();
|
||||
}
|
||||
|
||||
void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const LoadTorrentParams &resumeData)
|
||||
void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, LoadTorrentParams resumeData)
|
||||
{
|
||||
addJob(std::make_unique<StoreJob>(id, resumeData));
|
||||
addJob(std::make_unique<StoreJob>(id, std::move(resumeData)));
|
||||
}
|
||||
|
||||
void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID &id)
|
||||
@@ -787,9 +797,9 @@ namespace
|
||||
{
|
||||
using namespace BitTorrent;
|
||||
|
||||
StoreJob::StoreJob(const TorrentID &torrentID, const LoadTorrentParams &resumeData)
|
||||
StoreJob::StoreJob(const TorrentID &torrentID, LoadTorrentParams resumeData)
|
||||
: m_torrentID {torrentID}
|
||||
, m_resumeData {resumeData}
|
||||
, m_resumeData {std::move(resumeData)}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2021-2022 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2021-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@@ -31,9 +31,10 @@
|
||||
#include <QReadWriteLock>
|
||||
|
||||
#include "base/pathfwd.h"
|
||||
#include "base/utils/thread.h"
|
||||
#include "resumedatastorage.h"
|
||||
|
||||
class QSqlQuery;
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
class DBResumeDataStorage final : public ResumeDataStorage
|
||||
@@ -48,7 +49,7 @@ namespace BitTorrent
|
||||
QList<TorrentID> registeredTorrents() const override;
|
||||
LoadResumeDataResult load(const TorrentID &id) const override;
|
||||
|
||||
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override;
|
||||
void store(const TorrentID &id, LoadTorrentParams resumeData) const override;
|
||||
void remove(const TorrentID &id) const override;
|
||||
void storeQueue(const QList<TorrentID> &queue) const override;
|
||||
|
||||
@@ -58,6 +59,7 @@ namespace BitTorrent
|
||||
void createDB() const;
|
||||
void updateDB(int fromVersion) const;
|
||||
void enableWALMode() const;
|
||||
LoadResumeDataResult parseQueryResultRow(const QSqlQuery &query) const;
|
||||
|
||||
class Worker;
|
||||
Worker *m_asyncWorker = nullptr;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2020 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2020-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@@ -27,13 +27,14 @@
|
||||
*/
|
||||
|
||||
#include "filesearcher.h"
|
||||
#include "base/bittorrent/common.h"
|
||||
#include "base/bittorrent/infohash.h"
|
||||
|
||||
void FileSearcher::search(const BitTorrent::TorrentID &id, const PathList &originalFileNames
|
||||
, const Path &savePath, const Path &downloadPath, const bool forceAppendExt)
|
||||
#include <QPromise>
|
||||
|
||||
#include "base/bittorrent/common.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
const auto findInDir = [](const Path &dirPath, PathList &fileNames, const bool forceAppendExt) -> bool
|
||||
bool findInDir(const Path &dirPath, PathList &fileNames, const bool forceAppendExt)
|
||||
{
|
||||
bool found = false;
|
||||
for (Path &fileName : fileNames)
|
||||
@@ -58,8 +59,12 @@ void FileSearcher::search(const BitTorrent::TorrentID &id, const PathList &origi
|
||||
}
|
||||
|
||||
return found;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void FileSearcher::search(const PathList &originalFileNames, const Path &savePath
|
||||
, const Path &downloadPath, const bool forceAppendExt, QPromise<FileSearchResult> &promise)
|
||||
{
|
||||
Path usedPath = savePath;
|
||||
PathList adjustedFileNames = originalFileNames;
|
||||
const bool found = findInDir(usedPath, adjustedFileNames, (forceAppendExt && downloadPath.isEmpty()));
|
||||
@@ -69,5 +74,5 @@ void FileSearcher::search(const BitTorrent::TorrentID &id, const PathList &origi
|
||||
findInDir(usedPath, adjustedFileNames, forceAppendExt);
|
||||
}
|
||||
|
||||
emit searchFinished(id, usedPath, adjustedFileNames);
|
||||
promise.addResult(FileSearchResult {.savePath = usedPath, .fileNames = adjustedFileNames});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2020 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2020-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@@ -32,10 +32,13 @@
|
||||
|
||||
#include "base/path.h"
|
||||
|
||||
namespace BitTorrent
|
||||
template <typename T> class QPromise;
|
||||
|
||||
struct FileSearchResult
|
||||
{
|
||||
class TorrentID;
|
||||
}
|
||||
Path savePath;
|
||||
PathList fileNames;
|
||||
};
|
||||
|
||||
class FileSearcher final : public QObject
|
||||
{
|
||||
@@ -43,12 +46,8 @@ class FileSearcher final : public QObject
|
||||
Q_DISABLE_COPY_MOVE(FileSearcher)
|
||||
|
||||
public:
|
||||
FileSearcher() = default;
|
||||
using QObject::QObject;
|
||||
|
||||
public slots:
|
||||
void search(const BitTorrent::TorrentID &id, const PathList &originalFileNames
|
||||
, const Path &savePath, const Path &downloadPath, bool forceAppendExt);
|
||||
|
||||
signals:
|
||||
void searchFinished(const BitTorrent::TorrentID &id, const Path &savePath, const PathList &fileNames);
|
||||
void search(const PathList &originalFileNames, const Path &savePath
|
||||
, const Path &downloadPath, bool forceAppendExt, QPromise<FileSearchResult> &promise);
|
||||
};
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
#include "infohash.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
#include "base/global.h"
|
||||
|
||||
const int TorrentIDTypeId = qRegisterMetaType<BitTorrent::TorrentID>();
|
||||
|
||||
@@ -86,6 +89,28 @@ BitTorrent::TorrentID BitTorrent::InfoHash::toTorrentID() const
|
||||
#endif
|
||||
}
|
||||
|
||||
QString BitTorrent::InfoHash::toString() const
|
||||
{
|
||||
// Returns a string that is suitable for logging purpose
|
||||
|
||||
QString ret;
|
||||
ret.reserve(40 + 64 + 2); // v1 hash length + v2 hash length + comma
|
||||
|
||||
const SHA1Hash v1Hash = v1();
|
||||
const bool v1IsValid = v1Hash.isValid();
|
||||
if (v1IsValid)
|
||||
ret += v1Hash.toString();
|
||||
|
||||
if (const SHA256Hash v2Hash = v2(); v2Hash.isValid())
|
||||
{
|
||||
if (v1IsValid)
|
||||
ret += u", ";
|
||||
ret += v2Hash.toString();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
BitTorrent::InfoHash::operator WrappedType() const
|
||||
{
|
||||
return m_nativeHash;
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
|
||||
#include "base/digest32.h"
|
||||
|
||||
class QString;
|
||||
|
||||
using SHA1Hash = Digest32<160>;
|
||||
using SHA256Hash = Digest32<256>;
|
||||
|
||||
@@ -79,6 +81,8 @@ namespace BitTorrent
|
||||
SHA256Hash v2() const;
|
||||
TorrentID toTorrentID() const;
|
||||
|
||||
QString toString() const;
|
||||
|
||||
operator WrappedType() const;
|
||||
|
||||
private:
|
||||
|
||||
@@ -39,7 +39,11 @@ PeerAddress PeerAddress::parse(const QStringView address)
|
||||
if (address.startsWith(u'[') && address.contains(u"]:"))
|
||||
{ // IPv6
|
||||
ipPort = address.split(u"]:");
|
||||
ipPort[0] = ipPort[0].mid(1); // chop '['
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
ipPort[0].slice(1); // chop '['
|
||||
#else
|
||||
ipPort[0] = ipPort[0].sliced(1); // chop '['
|
||||
#endif
|
||||
}
|
||||
else if (address.contains(u':'))
|
||||
{ // IPv4
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace BitTorrent
|
||||
|
||||
virtual QList<TorrentID> registeredTorrents() const = 0;
|
||||
virtual LoadResumeDataResult load(const TorrentID &id) const = 0;
|
||||
virtual void store(const TorrentID &id, const LoadTorrentParams &resumeData) const = 0;
|
||||
virtual void store(const TorrentID &id, LoadTorrentParams resumeData) const = 0;
|
||||
virtual void remove(const TorrentID &id) const = 0;
|
||||
virtual void storeQueue(const QList<TorrentID> &queue) const = 0;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
#include "base/pathfwd.h"
|
||||
#include "base/tagset.h"
|
||||
#include "addtorrenterror.h"
|
||||
#include "addtorrentparams.h"
|
||||
#include "categoryoptions.h"
|
||||
#include "sharelimitaction.h"
|
||||
@@ -421,6 +422,8 @@ namespace BitTorrent
|
||||
virtual void setUTPRateLimited(bool limited) = 0;
|
||||
virtual MixedModeAlgorithm utpMixedMode() const = 0;
|
||||
virtual void setUtpMixedMode(MixedModeAlgorithm mode) = 0;
|
||||
virtual int hostnameCacheTTL() const = 0;
|
||||
virtual void setHostnameCacheTTL(int value) = 0;
|
||||
virtual bool isIDNSupportEnabled() const = 0;
|
||||
virtual void setIDNSupportEnabled(bool enabled) = 0;
|
||||
virtual bool multiConnectionsPerIpEnabled() const = 0;
|
||||
@@ -479,16 +482,17 @@ namespace BitTorrent
|
||||
virtual QString lastExternalIPv4Address() const = 0;
|
||||
virtual QString lastExternalIPv6Address() const = 0;
|
||||
|
||||
virtual qint64 freeDiskSpace() const = 0;
|
||||
|
||||
signals:
|
||||
void startupProgressUpdated(int progress);
|
||||
void addTorrentFailed(const InfoHash &infoHash, const QString &reason);
|
||||
void addTorrentFailed(const InfoHash &infoHash, const AddTorrentError &reason);
|
||||
void allTorrentsFinished();
|
||||
void categoryAdded(const QString &categoryName);
|
||||
void categoryRemoved(const QString &categoryName);
|
||||
void categoryOptionsChanged(const QString &categoryName);
|
||||
void fullDiskError(Torrent *torrent, const QString &msg);
|
||||
void IPFilterParsed(bool error, int ruleCount);
|
||||
void loadTorrentFailed(const QString &error);
|
||||
void metadataDownloaded(const TorrentInfo &info);
|
||||
void restored();
|
||||
void paused();
|
||||
@@ -519,5 +523,6 @@ namespace BitTorrent
|
||||
void trackerSuccess(Torrent *torrent, const QString &tracker);
|
||||
void trackerWarning(Torrent *torrent, const QString &tracker);
|
||||
void trackerEntryStatusesUpdated(Torrent *torrent, const QHash<QString, TrackerEntryStatus> &updatedTrackers);
|
||||
void freeDiskSpaceChecked(qint64 result);
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -30,6 +30,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -44,6 +45,7 @@
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QPointer>
|
||||
#include <QQueue>
|
||||
#include <QSet>
|
||||
#include <QThreadPool>
|
||||
|
||||
@@ -61,11 +63,16 @@ class QString;
|
||||
class QTimer;
|
||||
class QUrl;
|
||||
|
||||
template <typename T> class QFuture;
|
||||
|
||||
class BandwidthScheduler;
|
||||
class FileSearcher;
|
||||
class FilterParserThread;
|
||||
class FreeDiskSpaceChecker;
|
||||
class NativeSessionExtension;
|
||||
|
||||
struct FileSearchResult;
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
enum class MoveStorageMode;
|
||||
@@ -390,6 +397,8 @@ namespace BitTorrent
|
||||
void setUTPRateLimited(bool limited) override;
|
||||
MixedModeAlgorithm utpMixedMode() const override;
|
||||
void setUtpMixedMode(MixedModeAlgorithm mode) override;
|
||||
int hostnameCacheTTL() const override;
|
||||
void setHostnameCacheTTL(int value) override;
|
||||
bool isIDNSupportEnabled() const override;
|
||||
void setIDNSupportEnabled(bool enabled) override;
|
||||
bool multiConnectionsPerIpEnabled() const override;
|
||||
@@ -448,6 +457,8 @@ namespace BitTorrent
|
||||
QString lastExternalIPv4Address() const override;
|
||||
QString lastExternalIPv6Address() const override;
|
||||
|
||||
qint64 freeDiskSpace() const override;
|
||||
|
||||
// Torrent interface
|
||||
void handleTorrentResumeDataRequested(const TorrentImpl *torrent);
|
||||
void handleTorrentShareLimitChanged(TorrentImpl *torrent);
|
||||
@@ -467,14 +478,15 @@ namespace BitTorrent
|
||||
void handleTorrentTrackersChanged(TorrentImpl *torrent);
|
||||
void handleTorrentUrlSeedsAdded(TorrentImpl *torrent, const QList<QUrl> &newUrlSeeds);
|
||||
void handleTorrentUrlSeedsRemoved(TorrentImpl *torrent, const QList<QUrl> &urlSeeds);
|
||||
void handleTorrentResumeDataReady(TorrentImpl *torrent, const LoadTorrentParams &data);
|
||||
void handleTorrentResumeDataReady(TorrentImpl *torrent, LoadTorrentParams data);
|
||||
void handleTorrentInfoHashChanged(TorrentImpl *torrent, const InfoHash &prevInfoHash);
|
||||
void handleTorrentStorageMovingStateChanged(TorrentImpl *torrent);
|
||||
|
||||
bool addMoveTorrentStorageJob(TorrentImpl *torrent, const Path &newPath, MoveStorageMode mode, MoveStorageContext context);
|
||||
|
||||
void findIncompleteFiles(const TorrentInfo &torrentInfo, const Path &savePath
|
||||
, const Path &downloadPath, const PathList &filePaths = {}) const;
|
||||
lt::torrent_handle reloadTorrent(const lt::torrent_handle ¤tHandle, lt::add_torrent_params params);
|
||||
|
||||
QFuture<FileSearchResult> findIncompleteFiles(const Path &savePath, const Path &downloadPath, const PathList &filePaths = {}) const;
|
||||
|
||||
void enablePortMapping();
|
||||
void disablePortMapping();
|
||||
@@ -509,7 +521,6 @@ namespace BitTorrent
|
||||
void generateResumeData();
|
||||
void handleIPFilterParsed(int ruleCount);
|
||||
void handleIPFilterError();
|
||||
void fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames);
|
||||
void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage);
|
||||
|
||||
private:
|
||||
@@ -568,8 +579,7 @@ namespace BitTorrent
|
||||
void updateSeedingLimitTimer();
|
||||
void exportTorrentFile(const Torrent *torrent, const Path &folderPath);
|
||||
|
||||
void handleAlert(const lt::alert *alert);
|
||||
void dispatchTorrentAlert(const lt::torrent_alert *alert);
|
||||
void handleAlert(lt::alert *alert);
|
||||
void handleAddTorrentAlert(const lt::add_torrent_alert *alert);
|
||||
void handleStateUpdateAlert(const lt::state_update_alert *alert);
|
||||
void handleMetadataReceivedAlert(const lt::metadata_received_alert *alert);
|
||||
@@ -596,9 +606,20 @@ namespace BitTorrent
|
||||
void handleTrackerAlert(const lt::tracker_alert *alert);
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
void handleTorrentConflictAlert(const lt::torrent_conflict_alert *alert);
|
||||
void handleFilePrioAlert(const lt::file_prio_alert *alert);
|
||||
#endif
|
||||
void handleFastResumeRejectedAlert(const lt::fastresume_rejected_alert *alert);
|
||||
void handleFileCompletedAlert(const lt::file_completed_alert *alert);
|
||||
void handleFileRenamedAlert(const lt::file_renamed_alert *alert);
|
||||
void handleFileRenameFailedAlert(const lt::file_rename_failed_alert *alert);
|
||||
void handlePerformanceAlert(const lt::performance_alert *alert) const;
|
||||
void handleSaveResumeDataAlert(lt::save_resume_data_alert *alert);
|
||||
void handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *alert);
|
||||
void handleTorrentCheckedAlert(const lt::torrent_checked_alert *alert);
|
||||
void handleTorrentFinishedAlert(const lt::torrent_finished_alert *alert);
|
||||
|
||||
TorrentImpl *createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams ¶ms);
|
||||
TorrentImpl *getTorrent(const lt::torrent_handle &nativeHandle) const;
|
||||
|
||||
void saveResumeData();
|
||||
void saveTorrentsQueue();
|
||||
@@ -683,6 +704,7 @@ namespace BitTorrent
|
||||
CachedSettingValue<BTProtocol> m_btProtocol;
|
||||
CachedSettingValue<bool> m_isUTPRateLimited;
|
||||
CachedSettingValue<MixedModeAlgorithm> m_utpMixedMode;
|
||||
CachedSettingValue<int> m_hostnameCacheTTL;
|
||||
CachedSettingValue<bool> m_IDNSupportEnabled;
|
||||
CachedSettingValue<bool> m_multiConnectionsPerIpEnabled;
|
||||
CachedSettingValue<bool> m_validateHTTPSTrackerCertificate;
|
||||
@@ -804,11 +826,13 @@ namespace BitTorrent
|
||||
FileSearcher *m_fileSearcher = nullptr;
|
||||
TorrentContentRemover *m_torrentContentRemover = nullptr;
|
||||
|
||||
using AddTorrentAlertHandler = std::function<void (const lt::add_torrent_alert *alert)>;
|
||||
QQueue<AddTorrentAlertHandler> m_addTorrentAlertHandlers;
|
||||
|
||||
QHash<TorrentID, lt::torrent_handle> m_downloadedMetadata;
|
||||
|
||||
QHash<TorrentID, TorrentImpl *> m_torrents;
|
||||
QHash<TorrentID, TorrentImpl *> m_hybridTorrentsByAltID;
|
||||
QHash<TorrentID, LoadTorrentParams> m_loadingTorrents;
|
||||
QHash<TorrentID, RemovingTorrentData> m_removingTorrents;
|
||||
QHash<TorrentID, TorrentID> m_changedTorrentIDs;
|
||||
QMap<QString, CategoryOptions> m_categories;
|
||||
@@ -850,6 +874,10 @@ namespace BitTorrent
|
||||
|
||||
QList<TorrentImpl *> m_pendingFinishedTorrents;
|
||||
|
||||
FreeDiskSpaceChecker *m_freeDiskSpaceChecker = nullptr;
|
||||
QTimer *m_freeDiskSpaceCheckingTimer = nullptr;
|
||||
qint64 m_freeDiskSpace = -1;
|
||||
|
||||
friend void Session::initInstance();
|
||||
friend void Session::freeInstance();
|
||||
friend Session *Session::instance();
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
#include "torrent.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <QHash>
|
||||
|
||||
#include "infohash.h"
|
||||
@@ -51,9 +53,7 @@ namespace BitTorrent
|
||||
const int Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME = -2;
|
||||
const int Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT = -1;
|
||||
|
||||
const qreal Torrent::MAX_RATIO = 9999;
|
||||
const int Torrent::MAX_SEEDING_TIME = 525600;
|
||||
const int Torrent::MAX_INACTIVE_SEEDING_TIME = 525600;
|
||||
const qreal Torrent::MAX_RATIO = std::numeric_limits<qreal>::infinity();
|
||||
|
||||
TorrentID Torrent::id() const
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -45,6 +45,8 @@ class QByteArray;
|
||||
class QDateTime;
|
||||
class QUrl;
|
||||
|
||||
template <typename T> class QFuture;
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
enum class DownloadPriority;
|
||||
@@ -132,8 +134,6 @@ namespace BitTorrent
|
||||
static const int NO_INACTIVE_SEEDING_TIME_LIMIT;
|
||||
|
||||
static const qreal MAX_RATIO;
|
||||
static const int MAX_SEEDING_TIME;
|
||||
static const int MAX_INACTIVE_SEEDING_TIME;
|
||||
|
||||
using TorrentContentHandler::TorrentContentHandler;
|
||||
|
||||
@@ -275,10 +275,7 @@ namespace BitTorrent
|
||||
virtual bool isDHTDisabled() const = 0;
|
||||
virtual bool isPEXDisabled() const = 0;
|
||||
virtual bool isLSDDisabled() const = 0;
|
||||
virtual QList<PeerInfo> peers() const = 0;
|
||||
virtual QBitArray pieces() const = 0;
|
||||
virtual QBitArray downloadingPieces() const = 0;
|
||||
virtual QList<int> pieceAvailability() const = 0;
|
||||
virtual qreal distributedCopies() const = 0;
|
||||
virtual qreal maxRatio() const = 0;
|
||||
virtual int maxSeedingTime() const = 0;
|
||||
@@ -325,10 +322,10 @@ namespace BitTorrent
|
||||
virtual nonstd::expected<QByteArray, QString> exportToBuffer() const = 0;
|
||||
virtual nonstd::expected<void, QString> exportToFile(const Path &path) const = 0;
|
||||
|
||||
virtual void fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHandler) const = 0;
|
||||
virtual void fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler) const = 0;
|
||||
virtual void fetchPieceAvailability(std::function<void (QList<int>)> resultHandler) const = 0;
|
||||
virtual void fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const = 0;
|
||||
virtual QFuture<QList<PeerInfo>> fetchPeerInfo() const = 0;
|
||||
virtual QFuture<QList<QUrl>> fetchURLSeeds() const = 0;
|
||||
virtual QFuture<QList<int>> fetchPieceAvailability() const = 0;
|
||||
virtual QFuture<QBitArray> fetchDownloadingPieces() const = 0;
|
||||
|
||||
TorrentID id() const;
|
||||
bool isRunning() const;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@@ -34,6 +34,8 @@
|
||||
#include "abstractfilestorage.h"
|
||||
#include "downloadpriority.h"
|
||||
|
||||
template <typename T> class QFuture;
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
class TorrentContentHandler : public QObject, public AbstractFileStorage
|
||||
@@ -52,8 +54,7 @@ namespace BitTorrent
|
||||
* This is not the same as torrrent availability, it is just a fraction of pieces
|
||||
* that can be downloaded right now. It varies between 0 to 1.
|
||||
*/
|
||||
virtual QList<qreal> availableFileFractions() const = 0;
|
||||
virtual void fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const = 0;
|
||||
virtual QFuture<QList<qreal>> fetchAvailableFileFractions() const = 0;
|
||||
|
||||
virtual void prioritizeFiles(const QList<DownloadPriority> &priorities) = 0;
|
||||
virtual void flushCache() const = 0;
|
||||
|
||||
@@ -124,18 +124,20 @@ void TorrentCreator::run()
|
||||
// need to sort the file names by natural sort order
|
||||
QStringList dirs = {m_params.sourcePath.data()};
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
// libtorrent couldn't handle .lnk files on Windows
|
||||
// Also, Windows users do not expect torrent creator to traverse into .lnk files so skip over them
|
||||
const QDir::Filters dirFilters {QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks};
|
||||
#else
|
||||
const QDir::Filters dirFilters {QDir::AllDirs | QDir::NoDotAndDotDot};
|
||||
#endif
|
||||
QDirIterator dirIter {m_params.sourcePath.data(), dirFilters, QDirIterator::Subdirectories};
|
||||
QDirIterator dirIter {m_params.sourcePath.data(), (QDir::AllDirs | QDir::NoDotAndDotDot), QDirIterator::Subdirectories};
|
||||
while (dirIter.hasNext())
|
||||
{
|
||||
const QString filePath = dirIter.next();
|
||||
dirs.append(filePath);
|
||||
const QFileInfo dirInfo = dirIter.nextFileInfo();
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
// .lnk to directory
|
||||
// Windows users do not expect torrent creator to traverse into .lnk files so skip over them
|
||||
if (dirInfo.isShortcut())
|
||||
continue;
|
||||
#endif
|
||||
|
||||
const QString dirPath = dirInfo.filePath();
|
||||
dirs.append(dirPath);
|
||||
}
|
||||
std::sort(dirs.begin(), dirs.end(), naturalLessThan);
|
||||
|
||||
@@ -146,19 +148,29 @@ void TorrentCreator::run()
|
||||
{
|
||||
QStringList tmpNames; // natural sort files within each dir
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
const QDir::Filters fileFilters {QDir::Files | QDir::NoSymLinks};
|
||||
#else
|
||||
const QDir::Filters fileFilters {QDir::Files};
|
||||
#endif
|
||||
QDirIterator fileIter {dir, fileFilters};
|
||||
QDirIterator fileIter {dir, QDir::Files};
|
||||
while (fileIter.hasNext())
|
||||
{
|
||||
const QFileInfo fileInfo = fileIter.nextFileInfo();
|
||||
const Path filePath {fileInfo.filePath()};
|
||||
qint64 fileSize = fileInfo.size();
|
||||
|
||||
const Path relFilePath = parentPath.relativePathOf(Path(fileInfo.filePath()));
|
||||
#ifdef Q_OS_WIN
|
||||
// .lnk to file
|
||||
// libtorrent couldn't handle .lnk files on Windows
|
||||
if (fileInfo.isShortcut())
|
||||
continue;
|
||||
|
||||
// file symbolic link
|
||||
// QFileInfo::size() failed to return the target file size
|
||||
// and we need to redirect it manually
|
||||
if (fileInfo.isSymbolicLink())
|
||||
fileSize = QFileInfo(fileInfo.symLinkTarget()).size();
|
||||
#endif
|
||||
|
||||
const Path relFilePath = parentPath.relativePathOf(filePath);
|
||||
tmpNames.append(relFilePath.toString());
|
||||
fileSizeMap[tmpNames.last()] = fileInfo.size();
|
||||
fileSizeMap[tmpNames.last()] = fileSize;
|
||||
}
|
||||
|
||||
std::sort(tmpNames.begin(), tmpNames.end(), naturalLessThan);
|
||||
@@ -174,8 +186,8 @@ void TorrentCreator::run()
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
lt::create_torrent newTorrent {fs, m_params.pieceSize, toNativeTorrentFormatFlag(m_params.torrentFormat)};
|
||||
#else
|
||||
lt::create_torrent newTorrent(fs, m_params.pieceSize, m_params.paddedFileSizeLimit
|
||||
, (m_params.isAlignmentOptimized ? lt::create_torrent::optimize_alignment : lt::create_flags_t {}));
|
||||
lt::create_torrent newTorrent {fs, m_params.pieceSize, m_params.paddedFileSizeLimit
|
||||
, (m_params.isAlignmentOptimized ? lt::create_torrent::optimize_alignment : lt::create_flags_t {})};
|
||||
#endif
|
||||
|
||||
// Add url seeds
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -37,11 +37,10 @@
|
||||
#endif
|
||||
|
||||
#include <libtorrent/address.hpp>
|
||||
#include <libtorrent/alert_types.hpp>
|
||||
#include <libtorrent/create_torrent.hpp>
|
||||
#include <libtorrent/session.hpp>
|
||||
#include <libtorrent/storage_defs.hpp>
|
||||
#include <libtorrent/time.hpp>
|
||||
#include <libtorrent/write_resume_data.hpp>
|
||||
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
#include <libtorrent/info_hash.hpp>
|
||||
@@ -51,7 +50,9 @@
|
||||
#include <QByteArray>
|
||||
#include <QCache>
|
||||
#include <QDebug>
|
||||
#include <QFuture>
|
||||
#include <QPointer>
|
||||
#include <QPromise>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
@@ -67,6 +68,7 @@
|
||||
#include "common.h"
|
||||
#include "downloadpriority.h"
|
||||
#include "extensiondata.h"
|
||||
#include "filesearcher.h"
|
||||
#include "loadtorrentparams.h"
|
||||
#include "ltqbitarray.h"
|
||||
#include "lttypecast.h"
|
||||
@@ -149,35 +151,40 @@ namespace
|
||||
|
||||
if (ltAnnounceInfo.updating)
|
||||
{
|
||||
trackerEndpointStatus.state = TrackerEndpointState::Updating;
|
||||
trackerEndpointStatus.isUpdating = true;
|
||||
++numUpdating;
|
||||
}
|
||||
else if (ltAnnounceInfo.fails > 0)
|
||||
{
|
||||
if (ltAnnounceInfo.last_error == lt::errors::tracker_failure)
|
||||
{
|
||||
trackerEndpointStatus.state = TrackerEndpointState::TrackerError;
|
||||
++numTrackerError;
|
||||
}
|
||||
else if (ltAnnounceInfo.last_error == lt::errors::announce_skipped)
|
||||
{
|
||||
trackerEndpointStatus.state = TrackerEndpointState::Unreachable;
|
||||
++numUnreachable;
|
||||
}
|
||||
else
|
||||
{
|
||||
trackerEndpointStatus.state = TrackerEndpointState::NotWorking;
|
||||
++numNotWorking;
|
||||
}
|
||||
}
|
||||
else if (nativeEntry.verified)
|
||||
{
|
||||
trackerEndpointStatus.state = TrackerEndpointState::Working;
|
||||
++numWorking;
|
||||
}
|
||||
else
|
||||
{
|
||||
trackerEndpointStatus.state = TrackerEndpointState::NotContacted;
|
||||
trackerEndpointStatus.isUpdating = false;
|
||||
|
||||
if (ltAnnounceInfo.fails > 0)
|
||||
{
|
||||
if (ltAnnounceInfo.last_error == lt::errors::tracker_failure)
|
||||
{
|
||||
trackerEndpointStatus.state = TrackerEndpointState::TrackerError;
|
||||
++numTrackerError;
|
||||
}
|
||||
else if (ltAnnounceInfo.last_error == lt::errors::announce_skipped)
|
||||
{
|
||||
trackerEndpointStatus.state = TrackerEndpointState::Unreachable;
|
||||
++numUnreachable;
|
||||
}
|
||||
else
|
||||
{
|
||||
trackerEndpointStatus.state = TrackerEndpointState::NotWorking;
|
||||
++numNotWorking;
|
||||
}
|
||||
}
|
||||
else if (nativeEntry.verified)
|
||||
{
|
||||
trackerEndpointStatus.state = TrackerEndpointState::Working;
|
||||
++numWorking;
|
||||
}
|
||||
else
|
||||
{
|
||||
trackerEndpointStatus.state = TrackerEndpointState::NotContacted;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ltAnnounceInfo.message.empty())
|
||||
@@ -212,23 +219,28 @@ namespace
|
||||
{
|
||||
if (numUpdating > 0)
|
||||
{
|
||||
trackerEntryStatus.state = TrackerEndpointState::Updating;
|
||||
trackerEntryStatus.isUpdating = true;
|
||||
}
|
||||
else if (numWorking > 0)
|
||||
else
|
||||
{
|
||||
trackerEntryStatus.state = TrackerEndpointState::Working;
|
||||
}
|
||||
else if (numTrackerError > 0)
|
||||
{
|
||||
trackerEntryStatus.state = TrackerEndpointState::TrackerError;
|
||||
}
|
||||
else if (numUnreachable == numEndpoints)
|
||||
{
|
||||
trackerEntryStatus.state = TrackerEndpointState::Unreachable;
|
||||
}
|
||||
else if ((numUnreachable + numNotWorking) == numEndpoints)
|
||||
{
|
||||
trackerEntryStatus.state = TrackerEndpointState::NotWorking;
|
||||
trackerEntryStatus.isUpdating = false;
|
||||
|
||||
if (numWorking > 0)
|
||||
{
|
||||
trackerEntryStatus.state = TrackerEndpointState::Working;
|
||||
}
|
||||
else if (numTrackerError > 0)
|
||||
{
|
||||
trackerEntryStatus.state = TrackerEndpointState::TrackerError;
|
||||
}
|
||||
else if (numUnreachable == numEndpoints)
|
||||
{
|
||||
trackerEntryStatus.state = TrackerEndpointState::Unreachable;
|
||||
}
|
||||
else if ((numUnreachable + numNotWorking) == numEndpoints)
|
||||
{
|
||||
trackerEntryStatus.state = TrackerEndpointState::NotWorking;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,11 +299,9 @@ namespace
|
||||
|
||||
// TorrentImpl
|
||||
|
||||
TorrentImpl::TorrentImpl(SessionImpl *session, lt::session *nativeSession
|
||||
, const lt::torrent_handle &nativeHandle, const LoadTorrentParams ¶ms)
|
||||
TorrentImpl::TorrentImpl(SessionImpl *session, const lt::torrent_handle &nativeHandle, const LoadTorrentParams ¶ms)
|
||||
: Torrent(session)
|
||||
, m_session(session)
|
||||
, m_nativeSession(nativeSession)
|
||||
, m_nativeHandle(nativeHandle)
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
, m_infoHash(m_nativeHandle.info_hashes())
|
||||
@@ -1437,7 +1447,7 @@ int TorrentImpl::totalLeechersCount() const
|
||||
|
||||
int TorrentImpl::downloadLimit() const
|
||||
{
|
||||
return m_downloadLimit;;
|
||||
return m_downloadLimit;
|
||||
}
|
||||
|
||||
int TorrentImpl::uploadLimit() const
|
||||
@@ -1465,48 +1475,11 @@ bool TorrentImpl::isLSDDisabled() const
|
||||
return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::disable_lsd);
|
||||
}
|
||||
|
||||
QList<PeerInfo> TorrentImpl::peers() const
|
||||
{
|
||||
std::vector<lt::peer_info> nativePeers;
|
||||
m_nativeHandle.get_peer_info(nativePeers);
|
||||
|
||||
QList<PeerInfo> peers;
|
||||
peers.reserve(static_cast<decltype(peers)::size_type>(nativePeers.size()));
|
||||
|
||||
for (const lt::peer_info &peer : nativePeers)
|
||||
peers.append(PeerInfo(peer, pieces()));
|
||||
|
||||
return peers;
|
||||
}
|
||||
|
||||
QBitArray TorrentImpl::pieces() const
|
||||
{
|
||||
return m_pieces;
|
||||
}
|
||||
|
||||
QBitArray TorrentImpl::downloadingPieces() const
|
||||
{
|
||||
if (!hasMetadata())
|
||||
return {};
|
||||
|
||||
std::vector<lt::partial_piece_info> queue;
|
||||
m_nativeHandle.get_download_queue(queue);
|
||||
|
||||
QBitArray result {piecesCount()};
|
||||
for (const lt::partial_piece_info &info : queue)
|
||||
result.setBit(LT::toUnderlyingType(info.piece_index));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QList<int> TorrentImpl::pieceAvailability() const
|
||||
{
|
||||
std::vector<int> avail;
|
||||
m_nativeHandle.piece_availability(avail);
|
||||
|
||||
return {avail.cbegin(), avail.cend()};
|
||||
}
|
||||
|
||||
qreal TorrentImpl::distributedCopies() const
|
||||
{
|
||||
return m_nativeStatus.distributed_copies;
|
||||
@@ -1549,7 +1522,8 @@ qreal TorrentImpl::realRatio() const
|
||||
|
||||
const qreal ratio = upload / static_cast<qreal>(download);
|
||||
Q_ASSERT(ratio >= 0);
|
||||
return (ratio > MAX_RATIO) ? MAX_RATIO : ratio;
|
||||
|
||||
return ratio;
|
||||
}
|
||||
|
||||
int TorrentImpl::uploadPayloadRate() const
|
||||
@@ -1615,18 +1589,20 @@ bool TorrentImpl::setCategory(const QString &category)
|
||||
if (!category.isEmpty() && !m_session->categories().contains(category))
|
||||
return false;
|
||||
|
||||
if (m_session->isDisableAutoTMMWhenCategoryChanged())
|
||||
{
|
||||
// This should be done before changing the category name
|
||||
// to prevent the torrent from being moved at the path of new category.
|
||||
setAutoTMMEnabled(false);
|
||||
}
|
||||
|
||||
const QString oldCategory = m_category;
|
||||
m_category = category;
|
||||
deferredRequestResumeData();
|
||||
m_session->handleTorrentCategoryChanged(this, oldCategory);
|
||||
|
||||
if (m_useAutoTMM)
|
||||
{
|
||||
if (!m_session->isDisableAutoTMMWhenCategoryChanged())
|
||||
adjustStorageLocation();
|
||||
else
|
||||
setAutoTMMEnabled(false);
|
||||
}
|
||||
adjustStorageLocation();
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1749,12 +1725,6 @@ void TorrentImpl::applyFirstLastPiecePriority(const bool enabled)
|
||||
m_nativeHandle.prioritize_pieces(piecePriorities);
|
||||
}
|
||||
|
||||
void TorrentImpl::fileSearchFinished(const Path &savePath, const PathList &fileNames)
|
||||
{
|
||||
if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
|
||||
endReceivedMetadataHandling(savePath, fileNames);
|
||||
}
|
||||
|
||||
TrackerEntryStatus TorrentImpl::updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo)
|
||||
{
|
||||
const auto it = std::find_if(m_trackerEntryStatuses.begin(), m_trackerEntryStatuses.end()
|
||||
@@ -1865,7 +1835,7 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi
|
||||
applyFirstLastPiecePriority(true);
|
||||
|
||||
m_maintenanceJob = MaintenanceJob::None;
|
||||
prepareResumeData(p);
|
||||
prepareResumeData(std::move(p));
|
||||
|
||||
m_session->handleTorrentMetadataReceived(this);
|
||||
}
|
||||
@@ -1874,16 +1844,6 @@ void TorrentImpl::reload()
|
||||
{
|
||||
try
|
||||
{
|
||||
m_completedFiles.fill(false);
|
||||
m_filesProgress.fill(0);
|
||||
m_pieces.fill(false);
|
||||
m_nativeStatus.pieces.clear_all();
|
||||
m_nativeStatus.num_pieces = 0;
|
||||
|
||||
const auto queuePos = m_nativeHandle.queue_position();
|
||||
|
||||
m_nativeSession->remove_torrent(m_nativeHandle, lt::session::delete_partfile);
|
||||
|
||||
lt::add_torrent_params p = m_ltAddTorrentParams;
|
||||
p.flags |= lt::torrent_flags::update_subscribe
|
||||
| lt::torrent_flags::override_trackers
|
||||
@@ -1903,19 +1863,21 @@ void TorrentImpl::reload()
|
||||
p.flags &= ~(lt::torrent_flags::auto_managed | lt::torrent_flags::paused);
|
||||
}
|
||||
|
||||
auto *const extensionData = new ExtensionData;
|
||||
p.userdata = LTClientData(extensionData);
|
||||
#ifndef QBT_USES_LIBTORRENT2
|
||||
p.storage = customStorageConstructor;
|
||||
#endif
|
||||
m_nativeHandle = m_nativeSession->add_torrent(p);
|
||||
const auto queuePos = m_nativeHandle.queue_position();
|
||||
|
||||
m_nativeStatus = extensionData->status;
|
||||
m_nativeHandle = m_session->reloadTorrent(m_nativeHandle, std::move(p));
|
||||
m_nativeStatus = static_cast<ExtensionData *>(m_nativeHandle.userdata())->status;
|
||||
|
||||
if (queuePos >= lt::queue_position_t {})
|
||||
m_nativeHandle.queue_position_set(queuePos);
|
||||
m_nativeStatus.queue_position = queuePos;
|
||||
|
||||
m_completedFiles.fill(false);
|
||||
m_filesProgress.fill(0);
|
||||
m_pieces.fill(false);
|
||||
m_nativeStatus.pieces.clear_all();
|
||||
m_nativeStatus.num_pieces = 0;
|
||||
|
||||
updateState();
|
||||
}
|
||||
catch (const lt::system_error &err)
|
||||
@@ -2060,7 +2022,7 @@ void TorrentImpl::handleMoveStorageJobFinished(const Path &path, const MoveStora
|
||||
}
|
||||
}
|
||||
|
||||
void TorrentImpl::handleTorrentCheckedAlert([[maybe_unused]] const lt::torrent_checked_alert *p)
|
||||
void TorrentImpl::handleTorrentChecked()
|
||||
{
|
||||
if (!hasMetadata())
|
||||
{
|
||||
@@ -2103,7 +2065,7 @@ void TorrentImpl::handleTorrentCheckedAlert([[maybe_unused]] const lt::torrent_c
|
||||
});
|
||||
}
|
||||
|
||||
void TorrentImpl::handleTorrentFinishedAlert([[maybe_unused]] const lt::torrent_finished_alert *p)
|
||||
void TorrentImpl::handleTorrentFinished()
|
||||
{
|
||||
m_hasMissingFiles = false;
|
||||
if (m_hasFinishedStatus)
|
||||
@@ -2126,37 +2088,29 @@ void TorrentImpl::handleTorrentFinishedAlert([[maybe_unused]] const lt::torrent_
|
||||
m_hasFinishedStatus = true;
|
||||
|
||||
if (isMoveInProgress() || (m_renameCount > 0))
|
||||
m_moveFinishedTriggers.enqueue([this]() { m_session->handleTorrentFinished(this); });
|
||||
m_moveFinishedTriggers.enqueue([this] { m_session->handleTorrentFinished(this); });
|
||||
else
|
||||
m_session->handleTorrentFinished(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void TorrentImpl::handleTorrentPausedAlert([[maybe_unused]] const lt::torrent_paused_alert *p)
|
||||
void TorrentImpl::handleSaveResumeData(lt::add_torrent_params params)
|
||||
{
|
||||
}
|
||||
|
||||
void TorrentImpl::handleTorrentResumedAlert([[maybe_unused]] const lt::torrent_resumed_alert *p)
|
||||
{
|
||||
}
|
||||
|
||||
void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p)
|
||||
{
|
||||
if (m_ltAddTorrentParams.url_seeds != p->params.url_seeds)
|
||||
if (m_ltAddTorrentParams.url_seeds != params.url_seeds)
|
||||
{
|
||||
// URL seed list have been changed by libtorrent for some reason, so we need to update cached one.
|
||||
// Unfortunately, URL seed list containing in "resume data" is generated according to different rules
|
||||
// than the list we usually cache, so we have to request it from the appropriate source.
|
||||
fetchURLSeeds([this](const QList<QUrl> &urlSeeds) { m_urlSeeds = urlSeeds; });
|
||||
fetchURLSeeds().then(this, [this](const QList<QUrl> &urlSeeds) { m_urlSeeds = urlSeeds; });
|
||||
}
|
||||
|
||||
if ((m_maintenanceJob == MaintenanceJob::HandleMetadata) && p->params.ti)
|
||||
if ((m_maintenanceJob == MaintenanceJob::HandleMetadata) && params.ti)
|
||||
{
|
||||
Q_ASSERT(m_indexMap.isEmpty());
|
||||
|
||||
const auto isSeedMode = static_cast<bool>(m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode);
|
||||
m_ltAddTorrentParams = p->params;
|
||||
m_ltAddTorrentParams = std::move(params);
|
||||
if (isSeedMode)
|
||||
m_ltAddTorrentParams.flags |= lt::torrent_flags::seed_mode;
|
||||
|
||||
@@ -2195,15 +2149,20 @@ void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p)
|
||||
filePaths[i] = Path(it->second);
|
||||
}
|
||||
|
||||
m_session->findIncompleteFiles(metadata, savePath(), downloadPath(), filePaths);
|
||||
m_session->findIncompleteFiles(savePath(), downloadPath(), filePaths).then(this
|
||||
, [this](const FileSearchResult &result)
|
||||
{
|
||||
if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
|
||||
endReceivedMetadataHandling(result.savePath, result.fileNames);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
prepareResumeData(p->params);
|
||||
prepareResumeData(std::move(params));
|
||||
}
|
||||
}
|
||||
|
||||
void TorrentImpl::prepareResumeData(const lt::add_torrent_params ¶ms)
|
||||
void TorrentImpl::prepareResumeData(lt::add_torrent_params params)
|
||||
{
|
||||
{
|
||||
decltype(params.have_pieces) havePieces;
|
||||
@@ -2227,7 +2186,7 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params ¶ms)
|
||||
}
|
||||
|
||||
// Update recent resume data
|
||||
m_ltAddTorrentParams = params;
|
||||
m_ltAddTorrentParams = std::move(params);
|
||||
|
||||
if (needPreserveProgress)
|
||||
{
|
||||
@@ -2243,7 +2202,7 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params ¶ms)
|
||||
// We shouldn't save upload_mode flag to allow torrent operate normally on next run
|
||||
m_ltAddTorrentParams.flags &= ~lt::torrent_flags::upload_mode;
|
||||
|
||||
const LoadTorrentParams resumeData
|
||||
LoadTorrentParams resumeData
|
||||
{
|
||||
.ltAddTorrentParams = m_ltAddTorrentParams,
|
||||
.name = m_name,
|
||||
@@ -2266,33 +2225,20 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params ¶ms)
|
||||
.sslParameters = m_sslParams
|
||||
};
|
||||
|
||||
m_session->handleTorrentResumeDataReady(this, resumeData);
|
||||
m_session->handleTorrentResumeDataReady(this, std::move(resumeData));
|
||||
}
|
||||
|
||||
void TorrentImpl::handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *p)
|
||||
{
|
||||
if (p->error != lt::errors::resume_data_not_modified)
|
||||
{
|
||||
LogMsg(tr("Generate resume data failed. Torrent: \"%1\". Reason: \"%2\"")
|
||||
.arg(name(), Utils::String::fromLocal8Bit(p->error.message())), Log::CRITICAL);
|
||||
}
|
||||
}
|
||||
|
||||
void TorrentImpl::handleFastResumeRejectedAlert(const lt::fastresume_rejected_alert *p)
|
||||
void TorrentImpl::handleFastResumeRejected()
|
||||
{
|
||||
// Files were probably moved or storage isn't accessible
|
||||
m_hasMissingFiles = true;
|
||||
LogMsg(tr("Failed to restore torrent. Files were probably moved or storage isn't accessible. Torrent: \"%1\". Reason: \"%2\"")
|
||||
.arg(name(), QString::fromStdString(p->message())), Log::WARNING);
|
||||
}
|
||||
|
||||
void TorrentImpl::handleFileRenamedAlert(const lt::file_renamed_alert *p)
|
||||
void TorrentImpl::handleFileRenamed(const lt::file_index_t nativeFileIndex, const Path &newActualFilePath, const Path &oldActualFilePath)
|
||||
{
|
||||
const int fileIndex = m_indexMap.value(p->index, -1);
|
||||
const int fileIndex = fileIndexFromNative(nativeFileIndex);
|
||||
Q_ASSERT(fileIndex >= 0);
|
||||
|
||||
const Path newActualFilePath {QString::fromUtf8(p->new_name())};
|
||||
|
||||
const Path oldFilePath = m_filePaths.at(fileIndex);
|
||||
const Path newFilePath = makeUserPath(newActualFilePath);
|
||||
|
||||
@@ -2302,11 +2248,6 @@ void TorrentImpl::handleFileRenamedAlert(const lt::file_renamed_alert *p)
|
||||
if (oldFilePath.data() == newFilePath.data())
|
||||
{
|
||||
// Remove empty ".unwanted" folders
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
const Path oldActualFilePath {QString::fromUtf8(p->old_name())};
|
||||
#else
|
||||
const Path oldActualFilePath;
|
||||
#endif
|
||||
const Path oldActualParentPath = oldActualFilePath.parentPath();
|
||||
const Path newActualParentPath = newActualFilePath.parentPath();
|
||||
if (newActualParentPath.filename() == UNWANTED_FOLDER_NAME)
|
||||
@@ -2356,14 +2297,11 @@ void TorrentImpl::handleFileRenamedAlert(const lt::file_renamed_alert *p)
|
||||
deferredRequestResumeData();
|
||||
}
|
||||
|
||||
void TorrentImpl::handleFileRenameFailedAlert(const lt::file_rename_failed_alert *p)
|
||||
void TorrentImpl::handleFileRenameFailed(const lt::file_index_t nativeFileIndex)
|
||||
{
|
||||
const int fileIndex = m_indexMap.value(p->index, -1);
|
||||
const int fileIndex = fileIndexFromNative(nativeFileIndex);
|
||||
Q_ASSERT(fileIndex >= 0);
|
||||
|
||||
LogMsg(tr("File rename failed. Torrent: \"%1\", file: \"%2\", reason: \"%3\"")
|
||||
.arg(name(), filePath(fileIndex).toString(), Utils::String::fromLocal8Bit(p->error.message())), Log::WARNING);
|
||||
|
||||
--m_renameCount;
|
||||
while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty())
|
||||
m_moveFinishedTriggers.takeFirst()();
|
||||
@@ -2371,12 +2309,12 @@ void TorrentImpl::handleFileRenameFailedAlert(const lt::file_rename_failed_alert
|
||||
deferredRequestResumeData();
|
||||
}
|
||||
|
||||
void TorrentImpl::handleFileCompletedAlert(const lt::file_completed_alert *p)
|
||||
void TorrentImpl::handleFileCompleted(const lt::file_index_t nativeFileIndex)
|
||||
{
|
||||
if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
|
||||
return;
|
||||
|
||||
const int fileIndex = m_indexMap.value(p->index, -1);
|
||||
const int fileIndex = fileIndexFromNative(nativeFileIndex);
|
||||
Q_ASSERT(fileIndex >= 0);
|
||||
|
||||
m_completedFiles.setBit(fileIndex);
|
||||
@@ -2404,22 +2342,13 @@ void TorrentImpl::handleFileCompletedAlert(const lt::file_completed_alert *p)
|
||||
}
|
||||
}
|
||||
|
||||
void TorrentImpl::handleFileErrorAlert(const lt::file_error_alert *p)
|
||||
void TorrentImpl::handleFileError(FileErrorInfo fileError)
|
||||
{
|
||||
m_lastFileError = {p->error, p->op};
|
||||
m_lastFileError = std::move(fileError);
|
||||
}
|
||||
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
void TorrentImpl::handleFilePrioAlert(const lt::file_prio_alert *)
|
||||
void TorrentImpl::handleMetadataReceived()
|
||||
{
|
||||
deferredRequestResumeData();
|
||||
}
|
||||
#endif
|
||||
|
||||
void TorrentImpl::handleMetadataReceivedAlert([[maybe_unused]] const lt::metadata_received_alert *p)
|
||||
{
|
||||
qDebug("Metadata received for torrent %s.", qUtf8Printable(name()));
|
||||
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
const InfoHash prevInfoHash = infoHash();
|
||||
m_infoHash = InfoHash(m_nativeHandle.info_hashes());
|
||||
@@ -2431,12 +2360,6 @@ void TorrentImpl::handleMetadataReceivedAlert([[maybe_unused]] const lt::metadat
|
||||
deferredRequestResumeData();
|
||||
}
|
||||
|
||||
void TorrentImpl::handlePerformanceAlert(const lt::performance_alert *p) const
|
||||
{
|
||||
LogMsg((tr("Performance alert: %1. More info: %2").arg(QString::fromStdString(p->message()), u"https://libtorrent.org/reference-Alerts.html#enum-performance-warning-t"_s))
|
||||
, Log::INFO);
|
||||
}
|
||||
|
||||
void TorrentImpl::handleCategoryOptionsChanged()
|
||||
{
|
||||
if (m_useAutoTMM)
|
||||
@@ -2459,57 +2382,6 @@ void TorrentImpl::handleUnwantedFolderToggled()
|
||||
manageActualFilePaths();
|
||||
}
|
||||
|
||||
void TorrentImpl::handleAlert(const lt::alert *a)
|
||||
{
|
||||
switch (a->type())
|
||||
{
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
case lt::file_prio_alert::alert_type:
|
||||
handleFilePrioAlert(static_cast<const lt::file_prio_alert*>(a));
|
||||
break;
|
||||
#endif
|
||||
case lt::file_renamed_alert::alert_type:
|
||||
handleFileRenamedAlert(static_cast<const lt::file_renamed_alert*>(a));
|
||||
break;
|
||||
case lt::file_rename_failed_alert::alert_type:
|
||||
handleFileRenameFailedAlert(static_cast<const lt::file_rename_failed_alert*>(a));
|
||||
break;
|
||||
case lt::file_completed_alert::alert_type:
|
||||
handleFileCompletedAlert(static_cast<const lt::file_completed_alert*>(a));
|
||||
break;
|
||||
case lt::file_error_alert::alert_type:
|
||||
handleFileErrorAlert(static_cast<const lt::file_error_alert*>(a));
|
||||
break;
|
||||
case lt::torrent_finished_alert::alert_type:
|
||||
handleTorrentFinishedAlert(static_cast<const lt::torrent_finished_alert*>(a));
|
||||
break;
|
||||
case lt::save_resume_data_alert::alert_type:
|
||||
handleSaveResumeDataAlert(static_cast<const lt::save_resume_data_alert*>(a));
|
||||
break;
|
||||
case lt::save_resume_data_failed_alert::alert_type:
|
||||
handleSaveResumeDataFailedAlert(static_cast<const lt::save_resume_data_failed_alert*>(a));
|
||||
break;
|
||||
case lt::torrent_paused_alert::alert_type:
|
||||
handleTorrentPausedAlert(static_cast<const lt::torrent_paused_alert*>(a));
|
||||
break;
|
||||
case lt::torrent_resumed_alert::alert_type:
|
||||
handleTorrentResumedAlert(static_cast<const lt::torrent_resumed_alert*>(a));
|
||||
break;
|
||||
case lt::metadata_received_alert::alert_type:
|
||||
handleMetadataReceivedAlert(static_cast<const lt::metadata_received_alert*>(a));
|
||||
break;
|
||||
case lt::fastresume_rejected_alert::alert_type:
|
||||
handleFastResumeRejectedAlert(static_cast<const lt::fastresume_rejected_alert*>(a));
|
||||
break;
|
||||
case lt::torrent_checked_alert::alert_type:
|
||||
handleTorrentCheckedAlert(static_cast<const lt::torrent_checked_alert*>(a));
|
||||
break;
|
||||
case lt::performance_alert::alert_type:
|
||||
handlePerformanceAlert(static_cast<const lt::performance_alert*>(a));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TorrentImpl::manageActualFilePaths()
|
||||
{
|
||||
const std::shared_ptr<const lt::torrent_info> nativeInfo = nativeTorrentInfo();
|
||||
@@ -2557,6 +2429,11 @@ lt::torrent_handle TorrentImpl::nativeHandle() const
|
||||
return m_nativeHandle;
|
||||
}
|
||||
|
||||
int TorrentImpl::fileIndexFromNative(const lt::file_index_t nativeFileIndex) const
|
||||
{
|
||||
return m_indexMap.value(nativeFileIndex, -1);
|
||||
}
|
||||
|
||||
void TorrentImpl::setMetadata(const TorrentInfo &torrentInfo)
|
||||
{
|
||||
if (hasMetadata())
|
||||
@@ -2710,8 +2587,6 @@ void TorrentImpl::setRatioLimit(qreal limit)
|
||||
{
|
||||
if (limit < USE_GLOBAL_RATIO)
|
||||
limit = NO_RATIO_LIMIT;
|
||||
else if (limit > MAX_RATIO)
|
||||
limit = MAX_RATIO;
|
||||
|
||||
if (m_ratioLimit != limit)
|
||||
{
|
||||
@@ -2725,8 +2600,6 @@ void TorrentImpl::setSeedingTimeLimit(int limit)
|
||||
{
|
||||
if (limit < USE_GLOBAL_SEEDING_TIME)
|
||||
limit = NO_SEEDING_TIME_LIMIT;
|
||||
else if (limit > MAX_SEEDING_TIME)
|
||||
limit = MAX_SEEDING_TIME;
|
||||
|
||||
if (m_seedingTimeLimit != limit)
|
||||
{
|
||||
@@ -2740,8 +2613,6 @@ void TorrentImpl::setInactiveSeedingTimeLimit(int limit)
|
||||
{
|
||||
if (limit < USE_GLOBAL_INACTIVE_SEEDING_TIME)
|
||||
limit = NO_INACTIVE_SEEDING_TIME_LIMIT;
|
||||
else if (limit > MAX_INACTIVE_SEEDING_TIME)
|
||||
limit = MAX_SEEDING_TIME;
|
||||
|
||||
if (m_inactiveSeedingTimeLimit != limit)
|
||||
{
|
||||
@@ -2882,18 +2753,9 @@ nonstd::expected<lt::entry, QString> TorrentImpl::exportTorrent() const
|
||||
|
||||
try
|
||||
{
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
const std::shared_ptr<lt::torrent_info> completeTorrentInfo = m_nativeHandle.torrent_file_with_hashes();
|
||||
const std::shared_ptr<lt::torrent_info> torrentInfo = (completeTorrentInfo ? completeTorrentInfo : info().nativeInfo());
|
||||
#else
|
||||
const std::shared_ptr<lt::torrent_info> torrentInfo = info().nativeInfo();
|
||||
#endif
|
||||
lt::create_torrent creator {*torrentInfo};
|
||||
|
||||
for (const TrackerEntryStatus &status : asConst(trackers()))
|
||||
creator.add_tracker(status.url.toStdString(), status.tier);
|
||||
|
||||
return creator.generate();
|
||||
[[maybe_unused]] const auto infoGuard = qScopeGuard([this] { m_ltAddTorrentParams.ti.reset(); });
|
||||
m_ltAddTorrentParams.ti = info().nativeInfo();
|
||||
return lt::write_torrent_file(m_ltAddTorrentParams);
|
||||
}
|
||||
catch (const lt::system_error &err)
|
||||
{
|
||||
@@ -2928,9 +2790,9 @@ nonstd::expected<void, QString> TorrentImpl::exportToFile(const Path &path) cons
|
||||
return {};
|
||||
}
|
||||
|
||||
void TorrentImpl::fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHandler) const
|
||||
QFuture<QList<PeerInfo>> TorrentImpl::fetchPeerInfo() const
|
||||
{
|
||||
invokeAsync([nativeHandle = m_nativeHandle, allPieces = pieces()]() -> QList<PeerInfo>
|
||||
return invokeAsync([nativeHandle = m_nativeHandle, allPieces = pieces()]() -> QList<PeerInfo>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -2945,13 +2807,12 @@ void TorrentImpl::fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHand
|
||||
catch (const std::exception &) {}
|
||||
|
||||
return {};
|
||||
}
|
||||
, std::move(resultHandler));
|
||||
});
|
||||
}
|
||||
|
||||
void TorrentImpl::fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler) const
|
||||
QFuture<QList<QUrl>> TorrentImpl::fetchURLSeeds() const
|
||||
{
|
||||
invokeAsync([nativeHandle = m_nativeHandle]() -> QList<QUrl>
|
||||
return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<QUrl>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -2965,13 +2826,12 @@ void TorrentImpl::fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler)
|
||||
catch (const std::exception &) {}
|
||||
|
||||
return {};
|
||||
}
|
||||
, std::move(resultHandler));
|
||||
});
|
||||
}
|
||||
|
||||
void TorrentImpl::fetchPieceAvailability(std::function<void (QList<int>)> resultHandler) const
|
||||
QFuture<QList<int>> TorrentImpl::fetchPieceAvailability() const
|
||||
{
|
||||
invokeAsync([nativeHandle = m_nativeHandle]() -> QList<int>
|
||||
return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<int>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -2982,13 +2842,12 @@ void TorrentImpl::fetchPieceAvailability(std::function<void (QList<int>)> result
|
||||
catch (const std::exception &) {}
|
||||
|
||||
return {};
|
||||
}
|
||||
, std::move(resultHandler));
|
||||
});
|
||||
}
|
||||
|
||||
void TorrentImpl::fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const
|
||||
QFuture<QBitArray> TorrentImpl::fetchDownloadingPieces() const
|
||||
{
|
||||
invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QBitArray
|
||||
return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QBitArray
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -3007,13 +2866,12 @@ void TorrentImpl::fetchDownloadingPieces(std::function<void (QBitArray)> resultH
|
||||
catch (const std::exception &) {}
|
||||
|
||||
return {};
|
||||
}
|
||||
, std::move(resultHandler));
|
||||
});
|
||||
}
|
||||
|
||||
void TorrentImpl::fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const
|
||||
QFuture<QList<qreal>> TorrentImpl::fetchAvailableFileFractions() const
|
||||
{
|
||||
invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QList<qreal>
|
||||
return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QList<qreal>
|
||||
{
|
||||
if (!torrentInfo.isValid() || (torrentInfo.filesCount() <= 0))
|
||||
return {};
|
||||
@@ -3047,8 +2905,7 @@ void TorrentImpl::fetchAvailableFileFractions(std::function<void (QList<qreal>)>
|
||||
catch (const std::exception &) {}
|
||||
|
||||
return {};
|
||||
}
|
||||
, std::move(resultHandler));
|
||||
});
|
||||
}
|
||||
|
||||
void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
|
||||
@@ -3059,7 +2916,7 @@ void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
|
||||
Q_ASSERT(priorities.size() == filesCount());
|
||||
|
||||
// Reset 'm_hasSeedStatus' if needed in order to react again to
|
||||
// 'torrent_finished_alert' and eg show tray notifications
|
||||
// "torrent finished" event and e.g. show tray notifications
|
||||
const QList<DownloadPriority> oldPriorities = filePriorities();
|
||||
for (int i = 0; i < oldPriorities.size(); ++i)
|
||||
{
|
||||
@@ -3088,47 +2945,17 @@ void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
|
||||
manageActualFilePaths();
|
||||
}
|
||||
|
||||
QList<qreal> TorrentImpl::availableFileFractions() const
|
||||
template <typename Func>
|
||||
QFuture<std::invoke_result_t<Func>> TorrentImpl::invokeAsync(Func &&func) const
|
||||
{
|
||||
Q_ASSERT(hasMetadata());
|
||||
|
||||
const int filesCount = this->filesCount();
|
||||
if (filesCount <= 0) return {};
|
||||
|
||||
const QList<int> piecesAvailability = pieceAvailability();
|
||||
// libtorrent returns empty array for seeding only torrents
|
||||
if (piecesAvailability.empty()) return QList<qreal>(filesCount, -1);
|
||||
|
||||
QList<qreal> res;
|
||||
res.reserve(filesCount);
|
||||
for (int i = 0; i < filesCount; ++i)
|
||||
QPromise<std::invoke_result_t<Func>> promise;
|
||||
const auto future = promise.future();
|
||||
promise.start();
|
||||
m_session->invokeAsync([func = std::forward<Func>(func), promise = std::move(promise)]() mutable
|
||||
{
|
||||
const TorrentInfo::PieceRange filePieces = m_torrentInfo.filePieces(i);
|
||||
|
||||
int availablePieces = 0;
|
||||
for (const int piece : filePieces)
|
||||
availablePieces += (piecesAvailability[piece] > 0) ? 1 : 0;
|
||||
|
||||
const qreal availability = filePieces.isEmpty()
|
||||
? 1 // the file has no pieces, so it is available by default
|
||||
: static_cast<qreal>(availablePieces) / filePieces.size();
|
||||
res.push_back(availability);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename Func, typename Callback>
|
||||
void TorrentImpl::invokeAsync(Func func, Callback resultHandler) const
|
||||
{
|
||||
m_session->invokeAsync([session = m_session
|
||||
, func = std::move(func)
|
||||
, resultHandler = std::move(resultHandler)
|
||||
, thisTorrent = QPointer<const TorrentImpl>(this)]() mutable
|
||||
{
|
||||
session->invoke([result = func(), thisTorrent, resultHandler = std::move(resultHandler)]
|
||||
{
|
||||
if (thisTorrent)
|
||||
resultHandler(result);
|
||||
});
|
||||
promise.addResult(func());
|
||||
promise.finish();
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -94,8 +94,7 @@ namespace BitTorrent
|
||||
Q_DISABLE_COPY_MOVE(TorrentImpl)
|
||||
|
||||
public:
|
||||
TorrentImpl(SessionImpl *session, lt::session *nativeSession
|
||||
, const lt::torrent_handle &nativeHandle, const LoadTorrentParams ¶ms);
|
||||
TorrentImpl(SessionImpl *session, const lt::torrent_handle &nativeHandle, const LoadTorrentParams ¶ms);
|
||||
~TorrentImpl() override;
|
||||
|
||||
bool isValid() const;
|
||||
@@ -203,10 +202,7 @@ namespace BitTorrent
|
||||
bool isDHTDisabled() const override;
|
||||
bool isPEXDisabled() const override;
|
||||
bool isLSDDisabled() const override;
|
||||
QList<PeerInfo> peers() const override;
|
||||
QBitArray pieces() const override;
|
||||
QBitArray downloadingPieces() const override;
|
||||
QList<int> pieceAvailability() const override;
|
||||
qreal distributedCopies() const override;
|
||||
qreal maxRatio() const override;
|
||||
int maxSeedingTime() const override;
|
||||
@@ -220,7 +216,6 @@ namespace BitTorrent
|
||||
int connectionsCount() const override;
|
||||
int connectionsLimit() const override;
|
||||
qlonglong nextAnnounce() const override;
|
||||
QList<qreal> availableFileFractions() const override;
|
||||
|
||||
void setName(const QString &name) override;
|
||||
void setSequentialDownload(bool enable) override;
|
||||
@@ -258,19 +253,29 @@ namespace BitTorrent
|
||||
nonstd::expected<QByteArray, QString> exportToBuffer() const override;
|
||||
nonstd::expected<void, QString> exportToFile(const Path &path) const override;
|
||||
|
||||
void fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHandler) const override;
|
||||
void fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler) const override;
|
||||
void fetchPieceAvailability(std::function<void (QList<int>)> resultHandler) const override;
|
||||
void fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const override;
|
||||
void fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const override;
|
||||
QFuture<QList<PeerInfo>> fetchPeerInfo() const override;
|
||||
QFuture<QList<QUrl>> fetchURLSeeds() const override;
|
||||
QFuture<QList<int>> fetchPieceAvailability() const override;
|
||||
QFuture<QBitArray> fetchDownloadingPieces() const override;
|
||||
QFuture<QList<qreal>> fetchAvailableFileFractions() const override;
|
||||
|
||||
bool needSaveResumeData() const;
|
||||
|
||||
// Session interface
|
||||
lt::torrent_handle nativeHandle() const;
|
||||
|
||||
void handleAlert(const lt::alert *a);
|
||||
int fileIndexFromNative(lt::file_index_t nativeFileIndex) const;
|
||||
|
||||
void handleStateUpdate(const lt::torrent_status &nativeStatus);
|
||||
void handleFastResumeRejected();
|
||||
void handleFileCompleted(lt::file_index_t nativeFileIndex);
|
||||
void handleFileError(FileErrorInfo fileError);
|
||||
void handleFileRenamed(lt::file_index_t nativeFileIndex, const Path &newActualFilePath, const Path &oldActualFilePath);
|
||||
void handleFileRenameFailed(lt::file_index_t nativeFileIndex);
|
||||
void handleMetadataReceived();
|
||||
void handleSaveResumeData(lt::add_torrent_params params);
|
||||
void handleTorrentChecked();
|
||||
void handleTorrentFinished();
|
||||
void handleQueueingModeChanged();
|
||||
void handleCategoryOptionsChanged();
|
||||
void handleAppendExtensionToggled();
|
||||
@@ -278,7 +283,6 @@ namespace BitTorrent
|
||||
void requestResumeData(lt::resume_data_flags_t flags = {});
|
||||
void deferredRequestResumeData();
|
||||
void handleMoveStorageJobFinished(const Path &path, MoveStorageContext context, bool hasOutstandingJob);
|
||||
void fileSearchFinished(const Path &savePath, const PathList &fileNames);
|
||||
TrackerEntryStatus updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo);
|
||||
void resetTrackerEntryStatuses();
|
||||
|
||||
@@ -291,23 +295,6 @@ namespace BitTorrent
|
||||
void updateProgress();
|
||||
void updateState();
|
||||
|
||||
void handleFastResumeRejectedAlert(const lt::fastresume_rejected_alert *p);
|
||||
void handleFileCompletedAlert(const lt::file_completed_alert *p);
|
||||
void handleFileErrorAlert(const lt::file_error_alert *p);
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
void handleFilePrioAlert(const lt::file_prio_alert *p);
|
||||
#endif
|
||||
void handleFileRenamedAlert(const lt::file_renamed_alert *p);
|
||||
void handleFileRenameFailedAlert(const lt::file_rename_failed_alert *p);
|
||||
void handleMetadataReceivedAlert(const lt::metadata_received_alert *p);
|
||||
void handlePerformanceAlert(const lt::performance_alert *p) const;
|
||||
void handleSaveResumeDataAlert(const lt::save_resume_data_alert *p);
|
||||
void handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *p);
|
||||
void handleTorrentCheckedAlert(const lt::torrent_checked_alert *p);
|
||||
void handleTorrentFinishedAlert(const lt::torrent_finished_alert *p);
|
||||
void handleTorrentPausedAlert(const lt::torrent_paused_alert *p);
|
||||
void handleTorrentResumedAlert(const lt::torrent_resumed_alert *p);
|
||||
|
||||
bool isMoveInProgress() const;
|
||||
|
||||
void setAutoManaged(bool enable);
|
||||
@@ -320,17 +307,16 @@ namespace BitTorrent
|
||||
void manageActualFilePaths();
|
||||
void applyFirstLastPiecePriority(bool enabled);
|
||||
|
||||
void prepareResumeData(const lt::add_torrent_params ¶ms);
|
||||
void prepareResumeData(lt::add_torrent_params resumeData);
|
||||
void endReceivedMetadataHandling(const Path &savePath, const PathList &fileNames);
|
||||
void reload();
|
||||
|
||||
nonstd::expected<lt::entry, QString> exportTorrent() const;
|
||||
|
||||
template <typename Func, typename Callback>
|
||||
void invokeAsync(Func func, Callback resultHandler) const;
|
||||
template <typename Func>
|
||||
QFuture<std::invoke_result_t<Func>> invokeAsync(Func &&func) const;
|
||||
|
||||
SessionImpl *const m_session = nullptr;
|
||||
lt::session *m_nativeSession = nullptr;
|
||||
lt::torrent_handle m_nativeHandle;
|
||||
mutable lt::torrent_status m_nativeStatus;
|
||||
TorrentState m_state = TorrentState::Unknown;
|
||||
@@ -387,7 +373,7 @@ namespace BitTorrent
|
||||
|
||||
bool m_unchecked = false;
|
||||
|
||||
lt::add_torrent_params m_ltAddTorrentParams;
|
||||
mutable lt::add_torrent_params m_ltAddTorrentParams;
|
||||
|
||||
int m_downloadLimit = 0;
|
||||
int m_uploadLimit = 0;
|
||||
|
||||
@@ -380,7 +380,7 @@ void Tracker::registerPeer(const TrackerAnnounceRequest &announceReq)
|
||||
{
|
||||
// Reached max size, remove a random torrent
|
||||
if (m_torrents.size() >= MAX_TORRENTS)
|
||||
m_torrents.erase(m_torrents.begin());
|
||||
m_torrents.erase(m_torrents.cbegin());
|
||||
}
|
||||
|
||||
m_torrents[announceReq.torrentID].setPeer(announceReq.peer);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@@ -41,7 +41,6 @@ namespace BitTorrent
|
||||
{
|
||||
NotContacted = 1,
|
||||
Working = 2,
|
||||
Updating = 3,
|
||||
NotWorking = 4,
|
||||
TrackerError = 5,
|
||||
Unreachable = 6
|
||||
@@ -52,6 +51,7 @@ namespace BitTorrent
|
||||
QString name {};
|
||||
int btVersion = 1;
|
||||
|
||||
bool isUpdating = false;
|
||||
TrackerEndpointState state = TrackerEndpointState::NotContacted;
|
||||
QString message {};
|
||||
|
||||
@@ -69,6 +69,7 @@ namespace BitTorrent
|
||||
QString url {};
|
||||
int tier = 0;
|
||||
|
||||
bool isUpdating = false;
|
||||
TrackerEndpointState state = TrackerEndpointState::NotContacted;
|
||||
QString message {};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -29,16 +29,24 @@
|
||||
|
||||
#include "freediskspacechecker.h"
|
||||
|
||||
#include "base/bittorrent/session.h"
|
||||
#include "base/utils/fs.h"
|
||||
|
||||
qint64 FreeDiskSpaceChecker::lastResult() const
|
||||
FreeDiskSpaceChecker::FreeDiskSpaceChecker(const Path &pathToCheck)
|
||||
: m_pathToCheck {pathToCheck}
|
||||
{
|
||||
return m_lastResult;
|
||||
}
|
||||
|
||||
Path FreeDiskSpaceChecker::pathToCheck() const
|
||||
{
|
||||
return m_pathToCheck;
|
||||
}
|
||||
|
||||
void FreeDiskSpaceChecker::setPathToCheck(const Path &newPathToCheck)
|
||||
{
|
||||
m_pathToCheck = newPathToCheck;
|
||||
}
|
||||
|
||||
void FreeDiskSpaceChecker::check()
|
||||
{
|
||||
m_lastResult = Utils::Fs::freeDiskSpaceOnPath(BitTorrent::Session::instance()->savePath());
|
||||
emit checked(m_lastResult);
|
||||
emit checked(Utils::Fs::freeDiskSpaceOnPath(m_pathToCheck));
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -31,15 +31,18 @@
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "base/path.h"
|
||||
|
||||
class FreeDiskSpaceChecker final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(FreeDiskSpaceChecker)
|
||||
|
||||
public:
|
||||
using QObject::QObject;
|
||||
FreeDiskSpaceChecker(const Path &pathToCheck);
|
||||
|
||||
qint64 lastResult() const;
|
||||
Path pathToCheck() const;
|
||||
void setPathToCheck(const Path &newPathToCheck);
|
||||
|
||||
public slots:
|
||||
void check();
|
||||
@@ -48,5 +51,5 @@ signals:
|
||||
void checked(qint64 freeSpaceSize);
|
||||
|
||||
private:
|
||||
qint64 m_lastResult = 0;
|
||||
Path m_pathToCheck;
|
||||
};
|
||||
@@ -155,7 +155,11 @@ void Connection::read()
|
||||
sendResponse(resp);
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
m_receivedData.slice(result.frameSize);
|
||||
#else
|
||||
m_receivedData.remove(0, result.frameSize);
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -69,8 +69,8 @@ namespace
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString name = line.left(i).trimmed().toString().toLower();
|
||||
const QString value = line.mid(i + 1).trimmed().toString();
|
||||
const QString name = line.first(i).trimmed().toString().toLower();
|
||||
const QString value = line.sliced(i + 1).trimmed().toString();
|
||||
out[name] = value;
|
||||
|
||||
return true;
|
||||
@@ -204,15 +204,15 @@ bool RequestParser::parseRequestLine(const QString &line)
|
||||
m_request.method = match.captured(1);
|
||||
|
||||
// Request Target
|
||||
const QByteArray url {match.captured(2).toLatin1()};
|
||||
const QByteArray url {match.capturedView(2).toLatin1()};
|
||||
const int sepPos = url.indexOf('?');
|
||||
const QByteArrayView pathComponent = ((sepPos == -1) ? url : QByteArrayView(url).mid(0, sepPos));
|
||||
const QByteArrayView pathComponent = ((sepPos == -1) ? url : QByteArrayView(url).first(sepPos));
|
||||
|
||||
m_request.path = QString::fromUtf8(QByteArray::fromPercentEncoding(asQByteArray(pathComponent)));
|
||||
|
||||
if (sepPos >= 0)
|
||||
{
|
||||
const QByteArrayView query = QByteArrayView(url).mid(sepPos + 1);
|
||||
const QByteArrayView query = QByteArrayView(url).sliced(sepPos + 1);
|
||||
|
||||
// [rfc3986] 2.4 When to Encode or Decode
|
||||
// URL components should be separated before percent-decoding
|
||||
@@ -221,8 +221,8 @@ bool RequestParser::parseRequestLine(const QString &line)
|
||||
const int eqCharPos = param.indexOf('=');
|
||||
if (eqCharPos <= 0) continue; // ignores params without name
|
||||
|
||||
const QByteArrayView nameComponent = param.mid(0, eqCharPos);
|
||||
const QByteArrayView valueComponent = param.mid(eqCharPos + 1);
|
||||
const QByteArrayView nameComponent = param.first(eqCharPos);
|
||||
const QByteArrayView valueComponent = param.sliced(eqCharPos + 1);
|
||||
const QString paramName = QString::fromUtf8(
|
||||
QByteArray::fromPercentEncoding(asQByteArray(nameComponent)).replace('+', ' '));
|
||||
const QByteArray paramValue = QByteArray::fromPercentEncoding(asQByteArray(valueComponent)).replace('+', ' ');
|
||||
@@ -270,7 +270,7 @@ bool RequestParser::parsePostMessage(const QByteArrayView data)
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray delimiter = Utils::String::unquote(QStringView(contentType).mid(idx + boundaryFieldName.size())).toLatin1();
|
||||
const QByteArray delimiter = Utils::String::unquote(QStringView(contentType).sliced(idx + boundaryFieldName.size())).toLatin1();
|
||||
if (delimiter.isEmpty())
|
||||
{
|
||||
qWarning() << Q_FUNC_INFO << "boundary delimiter field empty!";
|
||||
@@ -279,7 +279,7 @@ bool RequestParser::parsePostMessage(const QByteArrayView data)
|
||||
|
||||
// split data by "dash-boundary"
|
||||
const QByteArray dashDelimiter = QByteArray("--") + delimiter + CRLF;
|
||||
QList<QByteArrayView> multipart = splitToViews(data, dashDelimiter, Qt::SkipEmptyParts);
|
||||
QList<QByteArrayView> multipart = splitToViews(data, dashDelimiter);
|
||||
if (multipart.isEmpty())
|
||||
{
|
||||
qWarning() << Q_FUNC_INFO << "multipart empty";
|
||||
@@ -310,8 +310,8 @@ bool RequestParser::parseFormData(const QByteArrayView data)
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString headers = QString::fromLatin1(data.mid(0, eohPos));
|
||||
const QByteArrayView payload = viewWithoutEndingWith(data.mid((eohPos + EOH.size()), data.size()), CRLF);
|
||||
const QString headers = QString::fromLatin1(data.first(eohPos));
|
||||
const QByteArrayView payload = viewWithoutEndingWith(data.sliced((eohPos + EOH.size())), CRLF);
|
||||
|
||||
HeaderMap headersMap;
|
||||
const QList<QStringView> headerLines = QStringView(headers).split(QString::fromLatin1(CRLF), Qt::SkipEmptyParts);
|
||||
@@ -328,8 +328,8 @@ bool RequestParser::parseFormData(const QByteArrayView data)
|
||||
if (idx < 0)
|
||||
continue;
|
||||
|
||||
const QString name = directive.left(idx).trimmed().toString().toLower();
|
||||
const QString value = Utils::String::unquote(directive.mid(idx + 1).trimmed()).toString();
|
||||
const QString name = directive.first(idx).trimmed().toString().toLower();
|
||||
const QString value = Utils::String::unquote(directive.sliced(idx + 1).trimmed()).toString();
|
||||
headersMap[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QHostAddress>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
#include "base/global.h"
|
||||
|
||||
@@ -96,9 +96,9 @@ void DNSUpdater::ipRequestFinished(const DownloadResult &result)
|
||||
const QRegularExpressionMatch ipRegexMatch = QRegularExpression(u"Current IP Address:\\s+([^<]+)</body>"_s).match(QString::fromUtf8(result.data));
|
||||
if (ipRegexMatch.hasMatch())
|
||||
{
|
||||
QString ipStr = ipRegexMatch.captured(1);
|
||||
const QString ipStr = ipRegexMatch.captured(1);
|
||||
qDebug() << Q_FUNC_INFO << "Regular expression captured the following IP:" << ipStr;
|
||||
QHostAddress newIp(ipStr);
|
||||
const QHostAddress newIp {ipStr};
|
||||
if (!newIp.isNull())
|
||||
{
|
||||
if (m_lastIP != newIp)
|
||||
|
||||
@@ -279,13 +279,13 @@ QString Net::DownloadHandlerImpl::errorCodeToString(const QNetworkReply::Network
|
||||
case QNetworkReply::ProxyAuthenticationRequiredError:
|
||||
return tr("The proxy requires authentication in order to honor the request but did not accept any credentials offered");
|
||||
case QNetworkReply::ContentAccessDenied:
|
||||
return tr("The access to the remote content was denied (401)");
|
||||
return tr("The access to the remote content was denied (403)");
|
||||
case QNetworkReply::ContentOperationNotPermittedError:
|
||||
return tr("The operation requested on the remote content is not permitted");
|
||||
case QNetworkReply::ContentNotFoundError:
|
||||
return tr("The remote content was not found at the server (404)");
|
||||
case QNetworkReply::AuthenticationRequiredError:
|
||||
return tr("The remote server requires authentication to serve the content but the credentials provided were not accepted");
|
||||
return tr("The remote server requires authentication to serve the content but the credentials provided were not accepted (401)");
|
||||
case QNetworkReply::ProtocolUnknownError:
|
||||
return tr("The Network Access API cannot honor the request because the protocol is not known");
|
||||
case QNetworkReply::ProtocolInvalidOperationError:
|
||||
|
||||
@@ -265,38 +265,37 @@ void Net::DownloadManager::applyProxySettings()
|
||||
const auto *proxyManager = ProxyConfigurationManager::instance();
|
||||
const ProxyConfiguration proxyConfig = proxyManager->proxyConfiguration();
|
||||
|
||||
m_proxy = QNetworkProxy(QNetworkProxy::NoProxy);
|
||||
|
||||
if ((proxyConfig.type == Net::ProxyType::None) || (proxyConfig.type == ProxyType::SOCKS4))
|
||||
return;
|
||||
|
||||
// Proxy enabled
|
||||
if (proxyConfig.type == ProxyType::SOCKS5)
|
||||
switch (proxyConfig.type)
|
||||
{
|
||||
qDebug() << Q_FUNC_INFO << "using SOCKS proxy";
|
||||
m_proxy.setType(QNetworkProxy::Socks5Proxy);
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << Q_FUNC_INFO << "using HTTP proxy";
|
||||
m_proxy.setType(QNetworkProxy::HttpProxy);
|
||||
}
|
||||
case Net::ProxyType::None:
|
||||
case Net::ProxyType::SOCKS4:
|
||||
m_proxy = QNetworkProxy(QNetworkProxy::NoProxy);
|
||||
break;
|
||||
|
||||
m_proxy.setHostName(proxyConfig.ip);
|
||||
m_proxy.setPort(proxyConfig.port);
|
||||
case Net::ProxyType::HTTP:
|
||||
m_proxy = QNetworkProxy(
|
||||
QNetworkProxy::HttpProxy
|
||||
, proxyConfig.ip
|
||||
, proxyConfig.port
|
||||
, (proxyConfig.authEnabled ? proxyConfig.username : QString())
|
||||
, (proxyConfig.authEnabled ? proxyConfig.password : QString()));
|
||||
m_proxy.setCapabilities(proxyConfig.hostnameLookupEnabled
|
||||
? (m_proxy.capabilities() | QNetworkProxy::HostNameLookupCapability)
|
||||
: (m_proxy.capabilities() & ~QNetworkProxy::HostNameLookupCapability));
|
||||
break;
|
||||
|
||||
// Authentication?
|
||||
if (proxyConfig.authEnabled)
|
||||
{
|
||||
qDebug("Proxy requires authentication, authenticating...");
|
||||
m_proxy.setUser(proxyConfig.username);
|
||||
m_proxy.setPassword(proxyConfig.password);
|
||||
}
|
||||
|
||||
if (proxyConfig.hostnameLookupEnabled)
|
||||
m_proxy.setCapabilities(m_proxy.capabilities() | QNetworkProxy::HostNameLookupCapability);
|
||||
else
|
||||
m_proxy.setCapabilities(m_proxy.capabilities() & ~QNetworkProxy::HostNameLookupCapability);
|
||||
case Net::ProxyType::SOCKS5:
|
||||
m_proxy = QNetworkProxy(
|
||||
QNetworkProxy::Socks5Proxy
|
||||
, proxyConfig.ip
|
||||
, proxyConfig.port
|
||||
, (proxyConfig.authEnabled ? proxyConfig.username : QString())
|
||||
, (proxyConfig.authEnabled ? proxyConfig.password : QString()));
|
||||
m_proxy.setCapabilities(proxyConfig.hostnameLookupEnabled
|
||||
? (m_proxy.capabilities() | QNetworkProxy::HostNameLookupCapability)
|
||||
: (m_proxy.capabilities() & ~QNetworkProxy::HostNameLookupCapability));
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
void Net::DownloadManager::processWaitingJobs(const ServiceID &serviceID)
|
||||
|
||||
@@ -148,8 +148,7 @@ void Smtp::sendMail(const QString &from, const QString &to, const QString &subje
|
||||
// Encode the body in base64
|
||||
QString crlfBody = body;
|
||||
const QByteArray b = crlfBody.replace(u"\n"_s, u"\r\n"_s).toUtf8().toBase64();
|
||||
const int ct = b.length();
|
||||
for (int i = 0; i < ct; i += 78)
|
||||
for (int i = 0, end = b.length(); i < end; i += 78)
|
||||
m_message += b.mid(i, 78);
|
||||
m_from = from;
|
||||
m_rcpt = to;
|
||||
@@ -190,8 +189,12 @@ void Smtp::readyRead()
|
||||
{
|
||||
const int pos = m_buffer.indexOf("\r\n");
|
||||
if (pos < 0) return; // Loop exit condition
|
||||
const QByteArray line = m_buffer.left(pos);
|
||||
const QByteArray line = m_buffer.first(pos);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
m_buffer.slice(pos + 2);
|
||||
#else
|
||||
m_buffer.remove(0, (pos + 2));
|
||||
#endif
|
||||
qDebug() << "Response line:" << line;
|
||||
// Extract response code
|
||||
const QByteArray code = line.left(3);
|
||||
|
||||
@@ -94,7 +94,13 @@ bool Path::isValid() const
|
||||
#if defined(Q_OS_WIN)
|
||||
QStringView view = m_pathStr;
|
||||
if (hasDriveLetter(view))
|
||||
view = view.mid(3);
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
view.slice(3);
|
||||
#else
|
||||
view = view.sliced(3);
|
||||
#endif
|
||||
}
|
||||
|
||||
// \\37 is using base-8 number system
|
||||
const QRegularExpression regex {u"[\\0-\\37:?\"*<>|]"_s};
|
||||
@@ -147,9 +153,9 @@ Path Path::rootItem() const
|
||||
#ifdef Q_OS_WIN
|
||||
// should be `c:/` instead of `c:`
|
||||
if ((slashIndex == 2) && hasDriveLetter(m_pathStr))
|
||||
return createUnchecked(m_pathStr.left(slashIndex + 1));
|
||||
return createUnchecked(m_pathStr.first(slashIndex + 1));
|
||||
#endif
|
||||
return createUnchecked(m_pathStr.left(slashIndex));
|
||||
return createUnchecked(m_pathStr.first(slashIndex));
|
||||
}
|
||||
|
||||
Path Path::parentPath() const
|
||||
@@ -167,9 +173,9 @@ Path Path::parentPath() const
|
||||
// should be `c:/` instead of `c:`
|
||||
// Windows "drive letter" is limited to one alphabet
|
||||
if ((slashIndex == 2) && hasDriveLetter(m_pathStr))
|
||||
return (m_pathStr.size() == 3) ? Path() : createUnchecked(m_pathStr.left(slashIndex + 1));
|
||||
return (m_pathStr.size() == 3) ? Path() : createUnchecked(m_pathStr.first(slashIndex + 1));
|
||||
#endif
|
||||
return createUnchecked(m_pathStr.left(slashIndex));
|
||||
return createUnchecked(m_pathStr.first(slashIndex));
|
||||
}
|
||||
|
||||
QString Path::filename() const
|
||||
@@ -178,7 +184,7 @@ QString Path::filename() const
|
||||
if (slashIndex == -1)
|
||||
return m_pathStr;
|
||||
|
||||
return m_pathStr.mid(slashIndex + 1);
|
||||
return m_pathStr.sliced(slashIndex + 1);
|
||||
}
|
||||
|
||||
QString Path::extension() const
|
||||
@@ -188,9 +194,9 @@ QString Path::extension() const
|
||||
return (u"." + suffix);
|
||||
|
||||
const int slashIndex = m_pathStr.lastIndexOf(u'/');
|
||||
const auto filename = QStringView(m_pathStr).mid(slashIndex + 1);
|
||||
const auto filename = QStringView(m_pathStr).sliced(slashIndex + 1);
|
||||
const int dotIndex = filename.lastIndexOf(u'.', -2);
|
||||
return ((dotIndex == -1) ? QString() : filename.mid(dotIndex).toString());
|
||||
return ((dotIndex == -1) ? QString() : filename.sliced(dotIndex).toString());
|
||||
}
|
||||
|
||||
bool Path::hasExtension(const QStringView ext) const
|
||||
@@ -293,7 +299,7 @@ Path Path::commonPath(const Path &left, const Path &right)
|
||||
if (commonItemsCount > 0)
|
||||
commonPathSize += (commonItemsCount - 1); // size of intermediate separators
|
||||
|
||||
return Path::createUnchecked(left.m_pathStr.left(commonPathSize));
|
||||
return Path::createUnchecked(left.m_pathStr.first(commonPathSize));
|
||||
}
|
||||
|
||||
Path Path::findRootFolder(const PathList &filePaths)
|
||||
@@ -322,7 +328,13 @@ void Path::stripRootFolder(PathList &filePaths)
|
||||
return;
|
||||
|
||||
for (Path &filePath : filePaths)
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
filePath.m_pathStr.slice(commonRootFolder.m_pathStr.size() + 1);
|
||||
#else
|
||||
filePath.m_pathStr.remove(0, (commonRootFolder.m_pathStr.size() + 1));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void Path::addRootFolder(PathList &filePaths, const Path &rootFolder)
|
||||
|
||||
@@ -359,6 +359,19 @@ void Preferences::setStatusbarDisplayed(const bool displayed)
|
||||
setValue(u"Preferences/General/StatusbarDisplayed"_s, displayed);
|
||||
}
|
||||
|
||||
bool Preferences::isStatusbarFreeDiskSpaceDisplayed() const
|
||||
{
|
||||
return value(u"Preferences/General/StatusbarFreeDiskSpaceDisplayed"_s, false);
|
||||
}
|
||||
|
||||
void Preferences::setStatusbarFreeDiskSpaceDisplayed(const bool displayed)
|
||||
{
|
||||
if (displayed == isStatusbarFreeDiskSpaceDisplayed())
|
||||
return;
|
||||
|
||||
setValue(u"Preferences/General/StatusbarFreeDiskSpaceDisplayed"_s, displayed);
|
||||
}
|
||||
|
||||
bool Preferences::isStatusbarExternalIPDisplayed() const
|
||||
{
|
||||
return value(u"Preferences/General/StatusbarExternalIPDisplayed"_s, false);
|
||||
@@ -2054,6 +2067,19 @@ void Preferences::setAddNewTorrentDialogSavePathHistoryLength(const int value)
|
||||
setValue(u"AddNewTorrentDialog/SavePathHistoryLength"_s, clampedValue);
|
||||
}
|
||||
|
||||
bool Preferences::isAddNewTorrentDialogAttached() const
|
||||
{
|
||||
return value(u"AddNewTorrentDialog/Attached"_s, false);
|
||||
}
|
||||
|
||||
void Preferences::setAddNewTorrentDialogAttached(const bool attached)
|
||||
{
|
||||
if (attached == isAddNewTorrentDialogAttached())
|
||||
return;
|
||||
|
||||
setValue(u"AddNewTorrentDialog/Attached"_s, attached);
|
||||
}
|
||||
|
||||
void Preferences::apply()
|
||||
{
|
||||
if (SettingsStorage::instance()->save())
|
||||
|
||||
@@ -119,6 +119,8 @@ public:
|
||||
void setHideZeroComboValues(int n);
|
||||
bool isStatusbarDisplayed() const;
|
||||
void setStatusbarDisplayed(bool displayed);
|
||||
bool isStatusbarFreeDiskSpaceDisplayed() const;
|
||||
void setStatusbarFreeDiskSpaceDisplayed(bool displayed);
|
||||
bool isStatusbarExternalIPDisplayed() const;
|
||||
void setStatusbarExternalIPDisplayed(bool displayed);
|
||||
bool isToolbarDisplayed() const;
|
||||
@@ -433,6 +435,8 @@ public:
|
||||
void setAddNewTorrentDialogTopLevel(bool value);
|
||||
int addNewTorrentDialogSavePathHistoryLength() const;
|
||||
void setAddNewTorrentDialogSavePathHistoryLength(int value);
|
||||
bool isAddNewTorrentDialogAttached() const;
|
||||
void setAddNewTorrentDialogAttached(bool attached);
|
||||
|
||||
public slots:
|
||||
void setStatusFilterState(bool checked);
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
|
||||
#include "base/addtorrentmanager.h"
|
||||
#include "base/asyncfilestorage.h"
|
||||
#include "base/bittorrent/addtorrenterror.h"
|
||||
#include "base/bittorrent/session.h"
|
||||
#include "base/bittorrent/torrentdescriptor.h"
|
||||
#include "base/global.h"
|
||||
@@ -375,10 +376,24 @@ void AutoDownloader::handleTorrentAdded(const QString &source)
|
||||
}
|
||||
}
|
||||
|
||||
void AutoDownloader::handleAddTorrentFailed(const QString &source)
|
||||
void AutoDownloader::handleAddTorrentFailed(const QString &source, const BitTorrent::AddTorrentError &error)
|
||||
{
|
||||
m_waitingJobs.remove(source);
|
||||
// TODO: Re-schedule job here.
|
||||
const auto job = m_waitingJobs.take(source);
|
||||
if (!job)
|
||||
return;
|
||||
|
||||
if (error.kind == BitTorrent::AddTorrentError::DuplicateTorrent)
|
||||
{
|
||||
if (Feed *feed = Session::instance()->feedByURL(job->feedURL))
|
||||
{
|
||||
if (Article *article = feed->articleByGUID(job->articleData.value(Article::KeyId).toString()))
|
||||
article->markAsRead();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Re-schedule job here.
|
||||
}
|
||||
}
|
||||
|
||||
void AutoDownloader::handleNewArticle(const Article *article)
|
||||
|
||||
@@ -47,6 +47,11 @@ class Application;
|
||||
class AsyncFileStorage;
|
||||
struct ProcessingJob;
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
struct AddTorrentError;
|
||||
}
|
||||
|
||||
namespace RSS
|
||||
{
|
||||
class Article;
|
||||
@@ -111,7 +116,7 @@ namespace RSS
|
||||
private slots:
|
||||
void process();
|
||||
void handleTorrentAdded(const QString &source);
|
||||
void handleAddTorrentFailed(const QString &url);
|
||||
void handleAddTorrentFailed(const QString &url, const BitTorrent::AddTorrentError &error);
|
||||
void handleNewArticle(const Article *article);
|
||||
void handleFeedURLChanged(Feed *feed, const QString &oldURL);
|
||||
|
||||
|
||||
@@ -184,14 +184,10 @@ QString computeEpisodeName(const QString &article)
|
||||
for (int i = 1; i <= match.lastCapturedIndex(); ++i)
|
||||
{
|
||||
const QString cap = match.captured(i);
|
||||
|
||||
if (cap.isEmpty())
|
||||
continue;
|
||||
|
||||
bool isInt = false;
|
||||
const int x = cap.toInt(&isInt);
|
||||
|
||||
ret.append(isInt ? QString::number(x) : cap);
|
||||
ret.append(cap);
|
||||
}
|
||||
return ret.join(u'x');
|
||||
}
|
||||
@@ -293,20 +289,26 @@ bool AutoDownloadRule::matchesEpisodeFilterExpression(const QString &articleTitl
|
||||
if (!matcher.hasMatch())
|
||||
return false;
|
||||
|
||||
const QString season {matcher.captured(1)};
|
||||
const QStringList episodes {matcher.captured(2).split(u';')};
|
||||
const QStringView season {matcher.capturedView(1)};
|
||||
const QList<QStringView> episodes {matcher.capturedView(2).split(u';')};
|
||||
const int seasonOurs {season.toInt()};
|
||||
|
||||
for (QString episode : episodes)
|
||||
for (QStringView episode : episodes)
|
||||
{
|
||||
if (episode.isEmpty())
|
||||
continue;
|
||||
|
||||
// We need to trim leading zeroes, but if it's all zeros then we want episode zero.
|
||||
while ((episode.size() > 1) && episode.startsWith(u'0'))
|
||||
episode = episode.right(episode.size() - 1);
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
episode.slice(1);
|
||||
#else
|
||||
episode = episode.sliced(1);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (episode.indexOf(u'-') != -1)
|
||||
if (episode.contains(u'-'))
|
||||
{ // Range detected
|
||||
const QString partialPattern1 {u"\\bs0?(\\d{1,4})[ -_\\.]?e(0?\\d{1,4})(?:\\D|\\b)"_s};
|
||||
const QString partialPattern2 {u"\\b(\\d{1,4})x(0?\\d{1,4})(?:\\D|\\b)"_s};
|
||||
@@ -323,24 +325,25 @@ bool AutoDownloadRule::matchesEpisodeFilterExpression(const QString &articleTitl
|
||||
|
||||
if (matched)
|
||||
{
|
||||
const int seasonTheirs {matcher.captured(1).toInt()};
|
||||
const int episodeTheirs {matcher.captured(2).toInt()};
|
||||
const int seasonTheirs {matcher.capturedView(1).toInt()};
|
||||
const int episodeTheirs {matcher.capturedView(2).toInt()};
|
||||
|
||||
if (episode.endsWith(u'-'))
|
||||
{ // Infinite range
|
||||
const int episodeOurs {QStringView(episode).left(episode.size() - 1).toInt()};
|
||||
const int episodeOurs {QStringView(episode).chopped(1).toInt()};
|
||||
if (((seasonTheirs == seasonOurs) && (episodeTheirs >= episodeOurs)) || (seasonTheirs > seasonOurs))
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{ // Normal range
|
||||
const QStringList range {episode.split(u'-')};
|
||||
const QList<QStringView> range {episode.split(u'-')};
|
||||
Q_ASSERT(range.size() == 2);
|
||||
if (range.first().toInt() > range.last().toInt())
|
||||
continue; // Ignore this subrule completely
|
||||
|
||||
const int episodeOursFirst {range.first().toInt()};
|
||||
const int episodeOursLast {range.last().toInt()};
|
||||
if (episodeOursFirst > episodeOursLast)
|
||||
continue; // Ignore this subrule completely
|
||||
|
||||
if ((seasonTheirs == seasonOurs) && ((episodeOursFirst <= episodeTheirs) && (episodeOursLast >= episodeTheirs)))
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2024 Jonathan Ketchker
|
||||
* Copyright (C) 2015-2022 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||
*
|
||||
@@ -56,19 +56,22 @@
|
||||
|
||||
const QString KEY_UID = u"uid"_s;
|
||||
const QString KEY_URL = u"url"_s;
|
||||
const QString KEY_REFRESHINTERVAL = u"refreshInterval"_s;
|
||||
const QString KEY_TITLE = u"title"_s;
|
||||
const QString KEY_LASTBUILDDATE = u"lastBuildDate"_s;
|
||||
const QString KEY_ISLOADING = u"isLoading"_s;
|
||||
const QString KEY_HASERROR = u"hasError"_s;
|
||||
const QString KEY_ARTICLES = u"articles"_s;
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace RSS;
|
||||
|
||||
Feed::Feed(const QUuid &uid, const QString &url, const QString &path, Session *session)
|
||||
Feed::Feed(Session *session, const QUuid &uid, const QString &url, const QString &path, const std::chrono::seconds refreshInterval)
|
||||
: Item(path)
|
||||
, m_session(session)
|
||||
, m_uid(uid)
|
||||
, m_url(url)
|
||||
, m_session {session}
|
||||
, m_uid {uid}
|
||||
, m_url {url}
|
||||
, m_refreshInterval {refreshInterval}
|
||||
{
|
||||
const auto uidHex = QString::fromLatin1(m_uid.toRfc4122().toHex());
|
||||
m_dataFileName = Path(uidHex + u".json");
|
||||
@@ -327,9 +330,9 @@ bool Feed::addArticle(const QVariantHash &articleData)
|
||||
|
||||
// Insertion sort
|
||||
const int maxArticles = m_session->maxArticlesPerFeed();
|
||||
const auto lowerBound = std::lower_bound(m_articlesByDate.begin(), m_articlesByDate.end()
|
||||
, articleData.value(Article::KeyDate).toDateTime(), Article::articleDateRecentThan);
|
||||
if ((lowerBound - m_articlesByDate.begin()) >= maxArticles)
|
||||
const auto lowerBound = std::lower_bound(m_articlesByDate.cbegin(), m_articlesByDate.cend()
|
||||
, articleData.value(Article::KeyDate).toDateTime(), Article::articleDateRecentThan);
|
||||
if ((lowerBound - m_articlesByDate.cbegin()) >= maxArticles)
|
||||
return false; // we reach max articles
|
||||
|
||||
auto *article = new Article(this, articleData);
|
||||
@@ -462,6 +465,20 @@ Path Feed::iconPath() const
|
||||
return m_iconPath;
|
||||
}
|
||||
|
||||
std::chrono::seconds Feed::refreshInterval() const
|
||||
{
|
||||
return m_refreshInterval;
|
||||
}
|
||||
|
||||
void Feed::setRefreshInterval(const std::chrono::seconds refreshInterval)
|
||||
{
|
||||
if (refreshInterval == m_refreshInterval)
|
||||
return;
|
||||
|
||||
const std::chrono::seconds oldRefreshInterval = std::exchange(m_refreshInterval, refreshInterval);
|
||||
emit refreshIntervalChanged(oldRefreshInterval);
|
||||
}
|
||||
|
||||
void Feed::setURL(const QString &url)
|
||||
{
|
||||
const QString oldURL = m_url;
|
||||
@@ -474,6 +491,8 @@ QJsonValue Feed::toJsonValue(const bool withData) const
|
||||
QJsonObject jsonObj;
|
||||
jsonObj.insert(KEY_UID, uid().toString());
|
||||
jsonObj.insert(KEY_URL, url());
|
||||
if (refreshInterval() > 0s)
|
||||
jsonObj.insert(KEY_REFRESHINTERVAL, static_cast<qint64>(refreshInterval().count()));
|
||||
|
||||
if (withData)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2024 Jonathan Ketchker
|
||||
* Copyright (C) 2015-2022 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||
*
|
||||
@@ -31,6 +31,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include <QtContainerFwd>
|
||||
#include <QBasicTimer>
|
||||
#include <QHash>
|
||||
@@ -68,7 +70,7 @@ namespace RSS
|
||||
|
||||
friend class Session;
|
||||
|
||||
Feed(const QUuid &uid, const QString &url, const QString &path, Session *session);
|
||||
Feed(Session *session, const QUuid &uid, const QString &url, const QString &path, std::chrono::seconds refreshInterval);
|
||||
~Feed() override;
|
||||
|
||||
public:
|
||||
@@ -87,6 +89,9 @@ namespace RSS
|
||||
Article *articleByGUID(const QString &guid) const;
|
||||
Path iconPath() const;
|
||||
|
||||
std::chrono::seconds refreshInterval() const;
|
||||
void setRefreshInterval(std::chrono::seconds refreshInterval);
|
||||
|
||||
QJsonValue toJsonValue(bool withData = false) const override;
|
||||
|
||||
signals:
|
||||
@@ -94,6 +99,7 @@ namespace RSS
|
||||
void titleChanged(Feed *feed = nullptr);
|
||||
void stateChanged(Feed *feed = nullptr);
|
||||
void urlChanged(const QString &oldURL);
|
||||
void refreshIntervalChanged(std::chrono::seconds oldRefreshInterval);
|
||||
|
||||
private slots:
|
||||
void handleSessionProcessingEnabledChanged(bool enabled);
|
||||
@@ -123,6 +129,7 @@ namespace RSS
|
||||
Private::FeedSerializer *m_serializer = nullptr;
|
||||
const QUuid m_uid;
|
||||
QString m_url;
|
||||
std::chrono::seconds m_refreshInterval;
|
||||
QString m_title;
|
||||
QString m_lastBuildDate;
|
||||
bool m_hasError = false;
|
||||
|
||||
@@ -97,7 +97,7 @@ QStringList Item::expandPath(const QString &path)
|
||||
int index = 0;
|
||||
while ((index = path.indexOf(Item::PathSeparator, index)) >= 0)
|
||||
{
|
||||
result << path.left(index);
|
||||
result << path.first(index);
|
||||
++index;
|
||||
}
|
||||
result << path;
|
||||
@@ -108,11 +108,11 @@ QStringList Item::expandPath(const QString &path)
|
||||
QString Item::parentPath(const QString &path)
|
||||
{
|
||||
const int pos = path.lastIndexOf(Item::PathSeparator);
|
||||
return (pos >= 0) ? path.left(pos) : QString();
|
||||
return (pos >= 0) ? path.first(pos) : QString();
|
||||
}
|
||||
|
||||
QString Item::relativeName(const QString &path)
|
||||
{
|
||||
int pos;
|
||||
return ((pos = path.lastIndexOf(Item::PathSeparator)) >= 0 ? path.right(path.size() - (pos + 1)) : path);
|
||||
const int pos = path.lastIndexOf(Item::PathSeparator);
|
||||
return (pos >= 0) ? path.sliced(pos + 1) : path;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2017-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2024 Jonathan Ketchker
|
||||
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||
*
|
||||
@@ -56,6 +56,7 @@ const QString CONF_FOLDER_NAME = u"rss"_s;
|
||||
const QString DATA_FOLDER_NAME = u"rss/articles"_s;
|
||||
const QString FEEDS_FILE_NAME = u"feeds.json"_s;
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace RSS;
|
||||
|
||||
QPointer<Session> Session::m_instance = nullptr;
|
||||
@@ -94,12 +95,10 @@ Session::Session()
|
||||
m_workingThread->start();
|
||||
load();
|
||||
|
||||
m_refreshTimer.setSingleShot(true);
|
||||
connect(&m_refreshTimer, &QTimer::timeout, this, &Session::refresh);
|
||||
if (isProcessingEnabled())
|
||||
{
|
||||
m_refreshTimer.start(std::chrono::minutes(refreshInterval()));
|
||||
refresh();
|
||||
}
|
||||
|
||||
// Remove legacy/corrupted settings
|
||||
// (at least on Windows, QSettings is case-insensitive and it can get
|
||||
@@ -138,19 +137,20 @@ Session *Session::instance()
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
nonstd::expected<void, QString> Session::addFolder(const QString &path)
|
||||
nonstd::expected<Folder *, QString> Session::addFolder(const QString &path)
|
||||
{
|
||||
const nonstd::expected<Folder *, QString> result = prepareItemDest(path);
|
||||
if (!result)
|
||||
return result.get_unexpected();
|
||||
|
||||
auto *destFolder = result.value();
|
||||
addItem(new Folder(path), destFolder);
|
||||
auto *folder = new Folder(path);
|
||||
addItem(folder, destFolder);
|
||||
store();
|
||||
return {};
|
||||
return folder;
|
||||
}
|
||||
|
||||
nonstd::expected<void, QString> Session::addFeed(const QString &url, const QString &path)
|
||||
nonstd::expected<Feed *, QString> Session::addFeed(const QString &url, const QString &path, const std::chrono::seconds refreshInterval)
|
||||
{
|
||||
if (m_feedsByURL.contains(url))
|
||||
return nonstd::make_unexpected(tr("RSS feed with given URL already exists: %1.").arg(url));
|
||||
@@ -160,13 +160,13 @@ nonstd::expected<void, QString> Session::addFeed(const QString &url, const QStri
|
||||
return result.get_unexpected();
|
||||
|
||||
auto *destFolder = result.value();
|
||||
auto *feed = new Feed(generateUID(), url, path, this);
|
||||
auto *feed = new Feed(this, generateUID(), url, path, refreshInterval);
|
||||
addItem(feed, destFolder);
|
||||
store();
|
||||
if (isProcessingEnabled())
|
||||
feed->refresh();
|
||||
refreshFeed(feed, std::chrono::system_clock::now());
|
||||
|
||||
return {};
|
||||
return feed;
|
||||
}
|
||||
|
||||
nonstd::expected<void, QString> Session::setFeedURL(const QString &path, const QString &url)
|
||||
@@ -192,7 +192,7 @@ nonstd::expected<void, QString> Session::setFeedURL(Feed *feed, const QString &u
|
||||
feed->setURL(url);
|
||||
store();
|
||||
if (isProcessingEnabled())
|
||||
feed->refresh();
|
||||
refreshFeed(feed, std::chrono::system_clock::now());
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -214,14 +214,20 @@ nonstd::expected<void, QString> Session::moveItem(Item *item, const QString &des
|
||||
Q_ASSERT(item);
|
||||
Q_ASSERT(item != rootFolder());
|
||||
|
||||
if (item->path() == destPath)
|
||||
return {};
|
||||
|
||||
if (auto *folder = static_cast<Folder *>(item)) // if `item` is a `Folder`
|
||||
{
|
||||
if (destPath.startsWith(folder->path() + Item::PathSeparator))
|
||||
return nonstd::make_unexpected(tr("Can't move a folder into itself or its subfolders."));
|
||||
}
|
||||
|
||||
const nonstd::expected<Folder *, QString> result = prepareItemDest(destPath);
|
||||
if (!result)
|
||||
return result.get_unexpected();
|
||||
|
||||
auto *destFolder = result.value();
|
||||
if (static_cast<Item *>(destFolder) == item)
|
||||
return nonstd::make_unexpected(tr("Couldn't move folder into itself."));
|
||||
|
||||
auto *srcFolder = static_cast<Folder *>(m_itemsByPath.value(Item::parentPath(item->path())));
|
||||
if (srcFolder != destFolder)
|
||||
{
|
||||
@@ -314,7 +320,7 @@ bool Session::loadFolder(const QJsonObject &jsonObj, Folder *folder)
|
||||
QString url = val.toString();
|
||||
if (url.isEmpty())
|
||||
url = key;
|
||||
addFeedToFolder(generateUID(), url, key, folder);
|
||||
addFeedToFolder(generateUID(), url, key, folder, 0s);
|
||||
updated = true;
|
||||
}
|
||||
else if (val.isObject())
|
||||
@@ -354,7 +360,9 @@ bool Session::loadFolder(const QJsonObject &jsonObj, Folder *folder)
|
||||
updated = true;
|
||||
}
|
||||
|
||||
addFeedToFolder(uid, valObj[u"url"].toString(), key, folder);
|
||||
const auto refreshInterval = std::chrono::seconds(valObj[u"refreshInterval"].toInteger());
|
||||
|
||||
addFeedToFolder(uid, valObj[u"url"].toString(), key, folder, refreshInterval);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -385,8 +393,14 @@ void Session::loadLegacy()
|
||||
uint i = 0;
|
||||
for (QString legacyPath : legacyFeedPaths)
|
||||
{
|
||||
if (Item::PathSeparator == legacyPath[0])
|
||||
if (legacyPath.startsWith(Item::PathSeparator))
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
legacyPath.slice(1);
|
||||
#else
|
||||
legacyPath.remove(0, 1);
|
||||
#endif
|
||||
}
|
||||
const QString parentFolderPath = Item::parentPath(legacyPath);
|
||||
const QString feedUrl = Item::relativeName(legacyPath);
|
||||
|
||||
@@ -404,7 +418,7 @@ void Session::loadLegacy()
|
||||
void Session::store()
|
||||
{
|
||||
m_confFileStorage->store(Path(FEEDS_FILE_NAME)
|
||||
, QJsonDocument(rootFolder()->toJsonValue().toObject()).toJson());
|
||||
, QJsonDocument(rootFolder()->toJsonValue().toObject()).toJson());
|
||||
}
|
||||
|
||||
nonstd::expected<Folder *, QString> Session::prepareItemDest(const QString &path)
|
||||
@@ -430,9 +444,9 @@ Folder *Session::addSubfolder(const QString &name, Folder *parentFolder)
|
||||
return folder;
|
||||
}
|
||||
|
||||
Feed *Session::addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder)
|
||||
Feed *Session::addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder, const std::chrono::seconds refreshInterval)
|
||||
{
|
||||
auto *feed = new Feed(uid, url, Item::joinPath(parentFolder->path(), name), this);
|
||||
auto *feed = new Feed(this, uid, url, Item::joinPath(parentFolder->path(), name), refreshInterval);
|
||||
addItem(feed, parentFolder);
|
||||
return feed;
|
||||
}
|
||||
@@ -454,8 +468,25 @@ void Session::addItem(Item *item, Folder *destFolder)
|
||||
|
||||
emit feedURLChanged(feed, oldURL);
|
||||
});
|
||||
connect(feed, &Feed::refreshIntervalChanged, this, [this, feed](const std::chrono::seconds oldRefreshInterval)
|
||||
{
|
||||
store();
|
||||
|
||||
std::chrono::system_clock::time_point &nextRefresh = m_refreshTimepoints[feed];
|
||||
if (nextRefresh > std::chrono::system_clock::time_point())
|
||||
nextRefresh += feed->refreshInterval() - oldRefreshInterval;
|
||||
|
||||
if (isProcessingEnabled())
|
||||
{
|
||||
const std::chrono::seconds oldEffectiveRefreshInterval = (oldRefreshInterval > 0s)
|
||||
? oldRefreshInterval : std::chrono::minutes(refreshInterval());
|
||||
if (feed->refreshInterval() < oldEffectiveRefreshInterval)
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
m_feedsByUID[feed->uid()] = feed;
|
||||
m_feedsByURL[feed->url()] = feed;
|
||||
m_refreshTimepoints.emplace(feed, std::chrono::system_clock::time_point());
|
||||
}
|
||||
|
||||
connect(item, &Item::pathChanged, this, &Session::itemPathChanged);
|
||||
@@ -476,14 +507,9 @@ void Session::setProcessingEnabled(const bool enabled)
|
||||
{
|
||||
m_storeProcessingEnabled = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
m_refreshTimer.start(std::chrono::minutes(refreshInterval()));
|
||||
refresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_refreshTimer.stop();
|
||||
}
|
||||
|
||||
emit processingStateChanged(enabled);
|
||||
}
|
||||
@@ -554,6 +580,7 @@ void Session::handleItemAboutToBeDestroyed(Item *item)
|
||||
{
|
||||
m_feedsByUID.remove(feed->uid());
|
||||
m_feedsByURL.remove(feed->url());
|
||||
m_refreshTimepoints.remove(feed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,6 +619,28 @@ void Session::setMaxArticlesPerFeed(const int n)
|
||||
|
||||
void Session::refresh()
|
||||
{
|
||||
// NOTE: Should we allow manually refreshing for disabled session?
|
||||
rootFolder()->refresh();
|
||||
const auto currentTimepoint = std::chrono::system_clock::now();
|
||||
std::chrono::seconds nextRefreshInterval = 0s;
|
||||
for (auto it = m_refreshTimepoints.begin(); it != m_refreshTimepoints.end(); ++it)
|
||||
{
|
||||
Feed *feed = it.key();
|
||||
std::chrono::system_clock::time_point &timepoint = it.value();
|
||||
|
||||
if (timepoint <= currentTimepoint)
|
||||
timepoint = refreshFeed(feed, currentTimepoint);
|
||||
|
||||
const auto interval = std::chrono::duration_cast<std::chrono::seconds>(timepoint - currentTimepoint);
|
||||
if ((interval < nextRefreshInterval) || (nextRefreshInterval == 0s))
|
||||
nextRefreshInterval = interval;
|
||||
}
|
||||
|
||||
m_refreshTimer.start(nextRefreshInterval);
|
||||
}
|
||||
|
||||
std::chrono::system_clock::time_point Session::refreshFeed(Feed *feed, const std::chrono::system_clock::time_point ¤tTimepoint)
|
||||
{
|
||||
feed->refresh();
|
||||
const std::chrono::seconds feedRefreshInterval = feed->refreshInterval();
|
||||
const std::chrono::seconds effectiveRefreshInterval = (feedRefreshInterval > 0s) ? feedRefreshInterval : std::chrono::minutes(refreshInterval());
|
||||
return currentTimepoint + effectiveRefreshInterval;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2017-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2024 Jonathan Ketchker
|
||||
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||
*
|
||||
@@ -35,26 +35,20 @@
|
||||
* RSS Session configuration file format (JSON):
|
||||
*
|
||||
* =============== BEGIN ===============
|
||||
*
|
||||
{
|
||||
* "folder1":
|
||||
{
|
||||
* "subfolder1":
|
||||
{
|
||||
* "Feed name 1 (Alias)":
|
||||
{
|
||||
* {
|
||||
* "folder1": {
|
||||
* "subfolder1": {
|
||||
* "Feed name 1 (Alias)": {
|
||||
* "uid": "feed unique identifier",
|
||||
* "url": "http://some-feed-url1"
|
||||
* }
|
||||
* "Feed name 2 (Alias)":
|
||||
{
|
||||
* "Feed name 2 (Alias)": {
|
||||
* "uid": "feed unique identifier",
|
||||
* "url": "http://some-feed-url2"
|
||||
* }
|
||||
* },
|
||||
* "subfolder2": {},
|
||||
* "Feed name 3 (Alias)":
|
||||
{
|
||||
* "Feed name 3 (Alias)": {
|
||||
* "uid": "feed unique identifier",
|
||||
* "url": "http://some-feed-url3"
|
||||
* }
|
||||
@@ -120,8 +114,8 @@ namespace RSS
|
||||
std::chrono::seconds fetchDelay() const;
|
||||
void setFetchDelay(std::chrono::seconds delay);
|
||||
|
||||
nonstd::expected<void, QString> addFolder(const QString &path);
|
||||
nonstd::expected<void, QString> addFeed(const QString &url, const QString &path);
|
||||
nonstd::expected<Folder *, QString> addFolder(const QString &path);
|
||||
nonstd::expected<Feed *, QString> addFeed(const QString &url, const QString &path, std::chrono::seconds refreshInterval = {});
|
||||
nonstd::expected<void, QString> setFeedURL(const QString &path, const QString &url);
|
||||
nonstd::expected<void, QString> setFeedURL(Feed *feed, const QString &url);
|
||||
nonstd::expected<void, QString> moveItem(const QString &itemPath, const QString &destPath);
|
||||
@@ -135,9 +129,6 @@ namespace RSS
|
||||
|
||||
Folder *rootFolder() const;
|
||||
|
||||
public slots:
|
||||
void refresh();
|
||||
|
||||
signals:
|
||||
void processingStateChanged(bool enabled);
|
||||
void maxArticlesPerFeedChanged(int n);
|
||||
@@ -160,8 +151,10 @@ namespace RSS
|
||||
void store();
|
||||
nonstd::expected<Folder *, QString> prepareItemDest(const QString &path);
|
||||
Folder *addSubfolder(const QString &name, Folder *parentFolder);
|
||||
Feed *addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder);
|
||||
Feed *addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder, std::chrono::seconds refreshInterval);
|
||||
void addItem(Item *item, Folder *destFolder);
|
||||
void refresh();
|
||||
std::chrono::system_clock::time_point refreshFeed(Feed *feed, const std::chrono::system_clock::time_point ¤tTimepoint);
|
||||
|
||||
static QPointer<Session> m_instance;
|
||||
|
||||
@@ -176,5 +169,6 @@ namespace RSS
|
||||
QHash<QString, Item *> m_itemsByPath;
|
||||
QHash<QUuid, Feed *> m_feedsByUID;
|
||||
QHash<QString, Feed *> m_feedsByURL;
|
||||
QHash<Feed *, std::chrono::system_clock::time_point> m_refreshTimepoints;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ SearchDownloadHandler::SearchDownloadHandler(const QString &pluginName, const QS
|
||||
, m_manager {manager}
|
||||
, m_downloadProcess {new QProcess(this)}
|
||||
{
|
||||
m_downloadProcess->setEnvironment(QProcess::systemEnvironment());
|
||||
m_downloadProcess->setProcessEnvironment(m_manager->proxyEnvironment());
|
||||
#ifdef Q_OS_UNIX
|
||||
m_downloadProcess->setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
|
||||
#endif
|
||||
connect(m_downloadProcess, qOverload<int, QProcess::ExitStatus>(&QProcess::finished)
|
||||
, this, &SearchDownloadHandler::downloadProcessFinished);
|
||||
const QStringList params
|
||||
@@ -52,7 +55,7 @@ SearchDownloadHandler::SearchDownloadHandler(const QString &pluginName, const QS
|
||||
url
|
||||
};
|
||||
// Launch search
|
||||
m_downloadProcess->start(Utils::ForeignApps::pythonInfo().executableName, params, QIODevice::ReadOnly);
|
||||
m_downloadProcess->start(Utils::ForeignApps::pythonInfo().executablePath.data(), params, QIODevice::ReadOnly);
|
||||
}
|
||||
|
||||
void SearchDownloadHandler::downloadProcessFinished(int exitcode)
|
||||
|
||||
@@ -70,7 +70,11 @@ SearchHandler::SearchHandler(const QString &pattern, const QString &category, co
|
||||
, m_searchTimeout {new QTimer(this)}
|
||||
{
|
||||
// Load environment variables (proxy)
|
||||
m_searchProcess->setEnvironment(QProcess::systemEnvironment());
|
||||
m_searchProcess->setProcessEnvironment(m_manager->proxyEnvironment());
|
||||
m_searchProcess->setProgram(Utils::ForeignApps::pythonInfo().executablePath.data());
|
||||
#ifdef Q_OS_UNIX
|
||||
m_searchProcess->setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
|
||||
#endif
|
||||
|
||||
const QStringList params
|
||||
{
|
||||
@@ -79,9 +83,6 @@ SearchHandler::SearchHandler(const QString &pattern, const QString &category, co
|
||||
m_usedPlugins.join(u','),
|
||||
m_category
|
||||
};
|
||||
|
||||
// Launch search
|
||||
m_searchProcess->setProgram(Utils::ForeignApps::pythonInfo().executableName);
|
||||
m_searchProcess->setArguments(params + m_pattern.split(u' '));
|
||||
|
||||
connect(m_searchProcess, &QProcess::errorOccurred, this, &SearchHandler::processFailed);
|
||||
@@ -93,6 +94,7 @@ SearchHandler::SearchHandler(const QString &pattern, const QString &category, co
|
||||
connect(m_searchTimeout, &QTimer::timeout, this, &SearchHandler::cancelSearch);
|
||||
m_searchTimeout->start(3min);
|
||||
|
||||
// Launch search
|
||||
// deferred start allows clients to handle starting-related signals
|
||||
QMetaObject::invokeMethod(this, [this]() { m_searchProcess->start(QIODevice::ReadOnly); }
|
||||
, Qt::QueuedConnection);
|
||||
|
||||
@@ -88,6 +88,7 @@ QPointer<SearchPluginManager> SearchPluginManager::m_instance = nullptr;
|
||||
|
||||
SearchPluginManager::SearchPluginManager()
|
||||
: m_updateUrl(u"https://searchplugins.qbittorrent.org/nova3/engines/"_s)
|
||||
, m_proxyEnv {QProcessEnvironment::systemEnvironment()}
|
||||
{
|
||||
Q_ASSERT(!m_instance); // only one instance is allowed
|
||||
m_instance = this;
|
||||
@@ -362,6 +363,11 @@ SearchHandler *SearchPluginManager::startSearch(const QString &pattern, const QS
|
||||
return new SearchHandler(pattern, category, usedPlugins, this);
|
||||
}
|
||||
|
||||
QProcessEnvironment SearchPluginManager::proxyEnvironment() const
|
||||
{
|
||||
return m_proxyEnv;
|
||||
}
|
||||
|
||||
QString SearchPluginManager::categoryFullName(const QString &categoryName)
|
||||
{
|
||||
const QHash<QString, QString> categoryTable
|
||||
@@ -403,50 +409,70 @@ Path SearchPluginManager::engineLocation()
|
||||
|
||||
void SearchPluginManager::applyProxySettings()
|
||||
{
|
||||
const auto *proxyManager = Net::ProxyConfigurationManager::instance();
|
||||
const Net::ProxyConfiguration proxyConfig = proxyManager->proxyConfiguration();
|
||||
// for python `urllib`: https://docs.python.org/3/library/urllib.request.html#urllib.request.ProxyHandler
|
||||
const QString HTTP_PROXY = u"http_proxy"_s;
|
||||
const QString HTTPS_PROXY = u"https_proxy"_s;
|
||||
// for `helpers.setupSOCKSProxy()`: https://everything.curl.dev/usingcurl/proxies/socks.html
|
||||
const QString SOCKS_PROXY = u"qbt_socks_proxy"_s;
|
||||
|
||||
// Define environment variables for urllib in search engine plugins
|
||||
QString proxyStrHTTP, proxyStrSOCK;
|
||||
if ((proxyConfig.type != Net::ProxyType::None) && Preferences::instance()->useProxyForGeneralPurposes())
|
||||
if (!Preferences::instance()->useProxyForGeneralPurposes())
|
||||
{
|
||||
switch (proxyConfig.type)
|
||||
{
|
||||
case Net::ProxyType::HTTP:
|
||||
if (proxyConfig.authEnabled)
|
||||
{
|
||||
proxyStrHTTP = u"http://%1:%2@%3:%4"_s.arg(proxyConfig.username
|
||||
, proxyConfig.password, proxyConfig.ip, QString::number(proxyConfig.port));
|
||||
}
|
||||
else
|
||||
{
|
||||
proxyStrHTTP = u"http://%1:%2"_s.arg(proxyConfig.ip, QString::number(proxyConfig.port));
|
||||
}
|
||||
break;
|
||||
|
||||
case Net::ProxyType::SOCKS5:
|
||||
if (proxyConfig.authEnabled)
|
||||
{
|
||||
proxyStrSOCK = u"%1:%2@%3:%4"_s.arg(proxyConfig.username
|
||||
, proxyConfig.password, proxyConfig.ip, QString::number(proxyConfig.port));
|
||||
}
|
||||
else
|
||||
{
|
||||
proxyStrSOCK = u"%1:%2"_s.arg(proxyConfig.ip, QString::number(proxyConfig.port));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
qDebug("Disabling HTTP communications proxy");
|
||||
}
|
||||
|
||||
qDebug("HTTP communications proxy string: %s"
|
||||
, qUtf8Printable((proxyConfig.type == Net::ProxyType::SOCKS5) ? proxyStrSOCK : proxyStrHTTP));
|
||||
m_proxyEnv.remove(HTTP_PROXY);
|
||||
m_proxyEnv.remove(HTTPS_PROXY);
|
||||
m_proxyEnv.remove(SOCKS_PROXY);
|
||||
return;
|
||||
}
|
||||
|
||||
qputenv("http_proxy", proxyStrHTTP.toLocal8Bit());
|
||||
qputenv("https_proxy", proxyStrHTTP.toLocal8Bit());
|
||||
qputenv("sock_proxy", proxyStrSOCK.toLocal8Bit());
|
||||
const Net::ProxyConfiguration proxyConfig = Net::ProxyConfigurationManager::instance()->proxyConfiguration();
|
||||
switch (proxyConfig.type)
|
||||
{
|
||||
case Net::ProxyType::None:
|
||||
m_proxyEnv.remove(HTTP_PROXY);
|
||||
m_proxyEnv.remove(HTTPS_PROXY);
|
||||
m_proxyEnv.remove(SOCKS_PROXY);
|
||||
break;
|
||||
|
||||
case Net::ProxyType::HTTP:
|
||||
{
|
||||
const QString credential = proxyConfig.authEnabled
|
||||
? (proxyConfig.username + u':' + proxyConfig.password + u'@')
|
||||
: QString();
|
||||
const QString proxyURL = u"http://%1%2:%3"_s
|
||||
.arg(credential, proxyConfig.ip, QString::number(proxyConfig.port));
|
||||
|
||||
m_proxyEnv.insert(HTTP_PROXY, proxyURL);
|
||||
m_proxyEnv.insert(HTTPS_PROXY, proxyURL);
|
||||
m_proxyEnv.remove(SOCKS_PROXY);
|
||||
}
|
||||
break;
|
||||
|
||||
case Net::ProxyType::SOCKS5:
|
||||
{
|
||||
const QString scheme = proxyConfig.hostnameLookupEnabled ? u"socks5h"_s : u"socks5"_s;
|
||||
const QString credential = proxyConfig.authEnabled
|
||||
? (proxyConfig.username + u':' + proxyConfig.password + u'@')
|
||||
: QString();
|
||||
const QString proxyURL = u"%1://%2%3:%4"_s
|
||||
.arg(scheme, credential, proxyConfig.ip, QString::number(proxyConfig.port));
|
||||
|
||||
m_proxyEnv.remove(HTTP_PROXY);
|
||||
m_proxyEnv.remove(HTTPS_PROXY);
|
||||
m_proxyEnv.insert(SOCKS_PROXY, proxyURL);
|
||||
}
|
||||
break;
|
||||
|
||||
case Net::ProxyType::SOCKS4:
|
||||
{
|
||||
const QString scheme = proxyConfig.hostnameLookupEnabled ? u"socks4a"_s : u"socks4"_s;
|
||||
const QString proxyURL = u"%1://%2:%3"_s
|
||||
.arg(scheme, proxyConfig.ip, QString::number(proxyConfig.port));
|
||||
|
||||
m_proxyEnv.remove(HTTP_PROXY);
|
||||
m_proxyEnv.remove(HTTPS_PROXY);
|
||||
m_proxyEnv.insert(SOCKS_PROXY, proxyURL);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SearchPluginManager::versionInfoDownloadFinished(const Net::DownloadResult &result)
|
||||
@@ -469,9 +495,9 @@ void SearchPluginManager::pluginDownloadFinished(const Net::DownloadResult &resu
|
||||
}
|
||||
else
|
||||
{
|
||||
const QString url = result.url;
|
||||
QString pluginName = url.mid(url.lastIndexOf(u'/') + 1);
|
||||
pluginName.replace(u".py"_s, u""_s, Qt::CaseInsensitive);
|
||||
const QString &url = result.url;
|
||||
const QString pluginName = url.sliced(url.lastIndexOf(u'/') + 1)
|
||||
.replace(u".py"_s, u""_s, Qt::CaseInsensitive);
|
||||
|
||||
if (pluginInfo(pluginName))
|
||||
emit pluginUpdateFailed(pluginName, tr("Failed to download the plugin file. %1").arg(result.errorString));
|
||||
@@ -497,29 +523,32 @@ void SearchPluginManager::updateNova()
|
||||
packageFile2.close();
|
||||
|
||||
// Copy search plugin files (if necessary)
|
||||
const auto updateFile = [&enginePath](const Path &filename, const bool compareVersion)
|
||||
const auto updateFile = [&enginePath](const Path &filename)
|
||||
{
|
||||
const Path filePathBundled = Path(u":/searchengine/nova3"_s) / filename;
|
||||
const Path filePathDisk = enginePath / filename;
|
||||
|
||||
if (compareVersion && (getPluginVersion(filePathBundled) <= getPluginVersion(filePathDisk)))
|
||||
if (getPluginVersion(filePathBundled) <= getPluginVersion(filePathDisk))
|
||||
return;
|
||||
|
||||
Utils::Fs::removeFile(filePathDisk);
|
||||
Utils::Fs::copyFile(filePathBundled, filePathDisk);
|
||||
};
|
||||
|
||||
updateFile(Path(u"helpers.py"_s), true);
|
||||
updateFile(Path(u"nova2.py"_s), true);
|
||||
updateFile(Path(u"nova2dl.py"_s), true);
|
||||
updateFile(Path(u"novaprinter.py"_s), true);
|
||||
updateFile(Path(u"socks.py"_s), false);
|
||||
updateFile(Path(u"helpers.py"_s));
|
||||
updateFile(Path(u"nova2.py"_s));
|
||||
updateFile(Path(u"nova2dl.py"_s));
|
||||
updateFile(Path(u"novaprinter.py"_s));
|
||||
updateFile(Path(u"socks.py"_s));
|
||||
}
|
||||
|
||||
void SearchPluginManager::update()
|
||||
{
|
||||
QProcess nova;
|
||||
nova.setProcessEnvironment(QProcessEnvironment::systemEnvironment());
|
||||
nova.setProcessEnvironment(proxyEnvironment());
|
||||
#ifdef Q_OS_UNIX
|
||||
nova.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
|
||||
#endif
|
||||
|
||||
const QStringList params
|
||||
{
|
||||
@@ -527,7 +556,7 @@ void SearchPluginManager::update()
|
||||
(engineLocation() / Path(u"/nova2.py"_s)).toString(),
|
||||
u"--capabilities"_s
|
||||
};
|
||||
nova.start(Utils::ForeignApps::pythonInfo().executableName, params, QIODevice::ReadOnly);
|
||||
nova.start(Utils::ForeignApps::pythonInfo().executablePath.data(), params, QIODevice::ReadOnly);
|
||||
nova.waitForFinished();
|
||||
|
||||
const auto capabilities = QString::fromUtf8(nova.readAllStandardOutput());
|
||||
@@ -592,14 +621,14 @@ void SearchPluginManager::parseVersionInfo(const QByteArray &info)
|
||||
QHash<QString, PluginVersion> updateInfo;
|
||||
int numCorrectData = 0;
|
||||
|
||||
const QList<QByteArrayView> lines = Utils::ByteArray::splitToViews(info, "\n", Qt::SkipEmptyParts);
|
||||
const QList<QByteArrayView> lines = Utils::ByteArray::splitToViews(info, "\n");
|
||||
for (QByteArrayView line : lines)
|
||||
{
|
||||
line = line.trimmed();
|
||||
if (line.isEmpty()) continue;
|
||||
if (line.startsWith('#')) continue;
|
||||
|
||||
const QList<QByteArrayView> list = Utils::ByteArray::splitToViews(line, ":", Qt::SkipEmptyParts);
|
||||
const QList<QByteArrayView> list = Utils::ByteArray::splitToViews(line, ":");
|
||||
if (list.size() != 2) continue;
|
||||
|
||||
const auto pluginName = QString::fromUtf8(list.first().trimmed());
|
||||
@@ -651,9 +680,10 @@ PluginVersion SearchPluginManager::getPluginVersion(const Path &filePath)
|
||||
while (!pluginFile.atEnd())
|
||||
{
|
||||
const auto line = QString::fromUtf8(pluginFile.readLine(lineMaxLength)).remove(u' ');
|
||||
if (!line.startsWith(u"#VERSION:", Qt::CaseInsensitive)) continue;
|
||||
if (!line.startsWith(u"#VERSION:", Qt::CaseInsensitive))
|
||||
continue;
|
||||
|
||||
const QString versionStr = line.mid(9);
|
||||
const QString versionStr = line.sliced(9);
|
||||
const auto version = PluginVersion::fromString(versionStr);
|
||||
if (version.isValid())
|
||||
return version;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <QHash>
|
||||
#include <QMetaType>
|
||||
#include <QObject>
|
||||
#include <QProcessEnvironment>
|
||||
|
||||
#include "base/path.h"
|
||||
#include "base/utils/version.h"
|
||||
@@ -87,6 +88,8 @@ public:
|
||||
SearchHandler *startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins);
|
||||
SearchDownloadHandler *downloadTorrent(const QString &pluginName, const QString &url);
|
||||
|
||||
QProcessEnvironment proxyEnvironment() const;
|
||||
|
||||
static PluginVersion getPluginVersion(const Path &filePath);
|
||||
static QString categoryFullName(const QString &categoryName);
|
||||
QString pluginFullName(const QString &pluginName) const;
|
||||
@@ -122,4 +125,5 @@ private:
|
||||
const QString m_updateUrl;
|
||||
|
||||
QHash<QString, PluginInfo*> m_plugins;
|
||||
QProcessEnvironment m_proxyEnv;
|
||||
};
|
||||
|
||||
@@ -170,7 +170,7 @@ bool SettingsStorage::writeNativeSettings() const
|
||||
// between deleting the file and recreating it. This is a safety measure.
|
||||
// Write everything to qBittorrent_new.ini/qBittorrent_new.conf and if it succeeds
|
||||
// replace qBittorrent.ini/qBittorrent.conf with it.
|
||||
for (auto i = m_data.begin(); i != m_data.end(); ++i)
|
||||
for (auto i = m_data.cbegin(); i != m_data.cend(); ++i)
|
||||
nativeSettings->setValue(i.key(), i.value());
|
||||
|
||||
nativeSettings->sync(); // Important to get error status
|
||||
|
||||
@@ -30,28 +30,30 @@
|
||||
#include "bytearray.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QByteArrayMatcher>
|
||||
#include <QByteArrayView>
|
||||
#include <QList>
|
||||
|
||||
QList<QByteArrayView> Utils::ByteArray::splitToViews(const QByteArrayView in, const QByteArrayView sep, const Qt::SplitBehavior behavior)
|
||||
QList<QByteArrayView> Utils::ByteArray::splitToViews(const QByteArrayView in, const QByteArrayView sep)
|
||||
{
|
||||
if (in.isEmpty())
|
||||
return {};
|
||||
if (sep.isEmpty())
|
||||
return {in};
|
||||
|
||||
const QByteArrayMatcher matcher {sep};
|
||||
QList<QByteArrayView> ret;
|
||||
ret.reserve((behavior == Qt::KeepEmptyParts)
|
||||
? (1 + (in.size() / sep.size()))
|
||||
: (1 + (in.size() / (sep.size() + 1))));
|
||||
int head = 0;
|
||||
ret.reserve(1 + (in.size() / (sep.size() + 1)));
|
||||
qsizetype head = 0;
|
||||
while (head < in.size())
|
||||
{
|
||||
int end = in.indexOf(sep, head);
|
||||
qsizetype end = matcher.indexIn(in, head);
|
||||
if (end < 0)
|
||||
end = in.size();
|
||||
|
||||
// omit empty parts
|
||||
const QByteArrayView part = in.mid(head, (end - head));
|
||||
if (!part.isEmpty() || (behavior == Qt::KeepEmptyParts))
|
||||
const QByteArrayView part = in.sliced(head, (end - head));
|
||||
if (!part.isEmpty())
|
||||
ret += part;
|
||||
|
||||
head = end + sep.size();
|
||||
|
||||
@@ -37,8 +37,8 @@ class QByteArrayView;
|
||||
|
||||
namespace Utils::ByteArray
|
||||
{
|
||||
// Mimic QStringView(in).split(sep, behavior)
|
||||
QList<QByteArrayView> splitToViews(QByteArrayView in, QByteArrayView sep, Qt::SplitBehavior behavior = Qt::KeepEmptyParts);
|
||||
// Inspired by QStringView(in).split(sep, Qt::SkipEmptyParts)
|
||||
QList<QByteArrayView> splitToViews(QByteArrayView in, QByteArrayView sep);
|
||||
QByteArray asQByteArray(QByteArrayView view);
|
||||
|
||||
QByteArray toBase32(const QByteArray &in);
|
||||
|
||||
@@ -64,7 +64,7 @@ int Utils::Compare::naturalCompare(const QString &left, const QString &right, co
|
||||
const int start = pos;
|
||||
while ((pos < str.size()) && str[pos].isDigit())
|
||||
++pos;
|
||||
return str.mid(start, (pos - start));
|
||||
return str.sliced(start, (pos - start));
|
||||
};
|
||||
|
||||
const QStringView numViewL = numberView(left, posL);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2018 Mike Tzou
|
||||
* Copyright (C) 2018-2025 Mike Tzou (Chocobo1)
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "foreignapps.h"
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include <algorithm>
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
@@ -40,24 +41,30 @@
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include <QDir>
|
||||
#include <QScopeGuard>
|
||||
#endif
|
||||
|
||||
#include "base/global.h"
|
||||
#include "base/logger.h"
|
||||
#include "base/path.h"
|
||||
#include "base/preferences.h"
|
||||
#include "base/utils/bytearray.h"
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include "base/utils/compare.h"
|
||||
#endif
|
||||
|
||||
using namespace Utils::ForeignApps;
|
||||
|
||||
namespace
|
||||
{
|
||||
bool testPythonInstallation(const QString &exeName, PythonInfo &info)
|
||||
bool testPythonInstallation(const Path &exePath, PythonInfo &info)
|
||||
{
|
||||
info = {};
|
||||
|
||||
QProcess proc;
|
||||
proc.start(exeName, {u"--version"_s}, QIODevice::ReadOnly);
|
||||
#ifdef Q_OS_UNIX
|
||||
proc.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
|
||||
#endif
|
||||
proc.start(exePath.data(), {u"--version"_s}, QIODevice::ReadOnly);
|
||||
if (proc.waitForFinished() && (proc.exitCode() == QProcess::NormalExit))
|
||||
{
|
||||
QByteArray procOutput = proc.readAllStandardOutput();
|
||||
@@ -68,7 +75,7 @@ namespace
|
||||
// Software 'Anaconda' installs its own python interpreter
|
||||
// and `python --version` returns a string like this:
|
||||
// "Python 3.4.3 :: Anaconda 2.3.0 (64-bit)"
|
||||
const QList<QByteArrayView> outputSplit = Utils::ByteArray::splitToViews(procOutput, " ", Qt::SkipEmptyParts);
|
||||
const QList<QByteArrayView> outputSplit = Utils::ByteArray::splitToViews(procOutput, " ");
|
||||
if (outputSplit.size() <= 1)
|
||||
return false;
|
||||
|
||||
@@ -80,9 +87,9 @@ namespace
|
||||
if (!version.isValid())
|
||||
return false;
|
||||
|
||||
info = {exeName, version};
|
||||
info = {.executablePath = exePath, .version = version};
|
||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Found Python executable. Name: \"%1\". Version: \"%2\"")
|
||||
.arg(info.executableName, info.version.toString()), Log::INFO);
|
||||
.arg(info.executablePath.toString(), info.version.toString()), Log::INFO);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -97,178 +104,160 @@ namespace
|
||||
SYSTEM_64BIT
|
||||
};
|
||||
|
||||
QStringList getRegSubkeys(const HKEY handle)
|
||||
PathList getRegSubkeys(const HKEY handle)
|
||||
{
|
||||
QStringList keys;
|
||||
PathList keys;
|
||||
|
||||
DWORD cSubKeys = 0;
|
||||
DWORD cMaxSubKeyLen = 0;
|
||||
LONG res = ::RegQueryInfoKeyW(handle, NULL, NULL, NULL, &cSubKeys, &cMaxSubKeyLen, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
const LSTATUS result = ::RegQueryInfoKeyW(handle, NULL, NULL, NULL, &cSubKeys, &cMaxSubKeyLen, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
|
||||
if (res == ERROR_SUCCESS)
|
||||
if (result == ERROR_SUCCESS)
|
||||
{
|
||||
++cMaxSubKeyLen; // For null character
|
||||
LPWSTR lpName = new WCHAR[cMaxSubKeyLen];
|
||||
DWORD cName;
|
||||
LPWSTR lpName = new WCHAR[cMaxSubKeyLen] {0};
|
||||
[[maybe_unused]] const auto lpNameGuard = qScopeGuard([&lpName] { delete[] lpName; });
|
||||
|
||||
keys.reserve(cSubKeys);
|
||||
|
||||
for (DWORD i = 0; i < cSubKeys; ++i)
|
||||
{
|
||||
cName = cMaxSubKeyLen;
|
||||
res = ::RegEnumKeyExW(handle, i, lpName, &cName, NULL, NULL, NULL, NULL);
|
||||
DWORD cName = cMaxSubKeyLen;
|
||||
const LSTATUS res = ::RegEnumKeyExW(handle, i, lpName, &cName, NULL, NULL, NULL, NULL);
|
||||
if (res == ERROR_SUCCESS)
|
||||
keys.push_back(QString::fromWCharArray(lpName));
|
||||
keys.append(Path(QString::fromWCharArray(lpName)));
|
||||
}
|
||||
|
||||
delete[] lpName;
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
QString getRegValue(const HKEY handle, const QString &name = {})
|
||||
Path getRegValue(const HKEY handle, const QString &name = {})
|
||||
{
|
||||
const std::wstring nameWStr = name.toStdWString();
|
||||
DWORD type = 0;
|
||||
DWORD cbData = 0;
|
||||
|
||||
// Discover the size of the value
|
||||
::RegQueryValueExW(handle, nameWStr.c_str(), NULL, &type, NULL, &cbData);
|
||||
DWORD cBuffer = (cbData / sizeof(WCHAR)) + 1;
|
||||
LPWSTR lpData = new WCHAR[cBuffer];
|
||||
LONG res = ::RegQueryValueExW(handle, nameWStr.c_str(), NULL, &type, reinterpret_cast<LPBYTE>(lpData), &cbData);
|
||||
|
||||
QString result;
|
||||
const DWORD cBuffer = (cbData / sizeof(WCHAR)) + 1;
|
||||
LPWSTR lpData = new WCHAR[cBuffer] {0};
|
||||
[[maybe_unused]] const auto lpDataGuard = qScopeGuard([&lpData] { delete[] lpData; });
|
||||
|
||||
const LSTATUS res = ::RegQueryValueExW(handle, nameWStr.c_str(), NULL, &type, reinterpret_cast<LPBYTE>(lpData), &cbData);
|
||||
if (res == ERROR_SUCCESS)
|
||||
{
|
||||
lpData[cBuffer - 1] = 0;
|
||||
result = QString::fromWCharArray(lpData);
|
||||
}
|
||||
delete[] lpData;
|
||||
return Path(QString::fromWCharArray(lpData));
|
||||
|
||||
return result;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString pythonSearchReg(const REG_SEARCH_TYPE type)
|
||||
PathList pythonSearchReg(const REG_SEARCH_TYPE type)
|
||||
{
|
||||
HKEY hkRoot;
|
||||
if (type == USER)
|
||||
hkRoot = HKEY_CURRENT_USER;
|
||||
else
|
||||
hkRoot = HKEY_LOCAL_MACHINE;
|
||||
const HKEY hkRoot = (type == USER) ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
const REGSAM samDesired = KEY_READ
|
||||
| ((type == SYSTEM_64BIT) ? KEY_WOW64_64KEY : KEY_WOW64_32KEY);
|
||||
PathList ret;
|
||||
|
||||
REGSAM samDesired = KEY_READ;
|
||||
if (type == SYSTEM_32BIT)
|
||||
samDesired |= KEY_WOW64_32KEY;
|
||||
else if (type == SYSTEM_64BIT)
|
||||
samDesired |= KEY_WOW64_64KEY;
|
||||
|
||||
QString path;
|
||||
LONG res = 0;
|
||||
HKEY hkPythonCore;
|
||||
res = ::RegOpenKeyExW(hkRoot, L"SOFTWARE\\Python\\PythonCore", 0, samDesired, &hkPythonCore);
|
||||
|
||||
if (res == ERROR_SUCCESS)
|
||||
HKEY hkPythonCore {0};
|
||||
if (::RegOpenKeyExW(hkRoot, L"SOFTWARE\\Python\\PythonCore", 0, samDesired, &hkPythonCore) == ERROR_SUCCESS)
|
||||
{
|
||||
QStringList versions = getRegSubkeys(hkPythonCore);
|
||||
versions.sort();
|
||||
[[maybe_unused]] const auto hkPythonCoreGuard = qScopeGuard([&hkPythonCore] { ::RegCloseKey(hkPythonCore); });
|
||||
|
||||
bool found = false;
|
||||
while (!found && !versions.empty())
|
||||
// start with the largest version
|
||||
PathList versions = getRegSubkeys(hkPythonCore);
|
||||
// ordinary sort won't suffice, it needs to sort ["3.9", "3.10"] correctly
|
||||
const Utils::Compare::NaturalCompare<Qt::CaseInsensitive> comparator;
|
||||
std::sort(versions.begin(), versions.end(), [&comparator](const Path &left, const Path &right)
|
||||
{
|
||||
const std::wstring version = QString(versions.takeLast() + u"\\InstallPath").toStdWString();
|
||||
return comparator(left.data(), right.data());
|
||||
});
|
||||
|
||||
HKEY hkInstallPath;
|
||||
res = ::RegOpenKeyExW(hkPythonCore, version.c_str(), 0, samDesired, &hkInstallPath);
|
||||
ret.reserve(versions.size() * 2);
|
||||
|
||||
if (res == ERROR_SUCCESS)
|
||||
while (!versions.empty())
|
||||
{
|
||||
const std::wstring version = (versions.takeLast() / Path(u"InstallPath"_s)).toString().toStdWString();
|
||||
|
||||
HKEY hkInstallPath {0};
|
||||
if (::RegOpenKeyExW(hkPythonCore, version.c_str(), 0, samDesired, &hkInstallPath) == ERROR_SUCCESS)
|
||||
{
|
||||
qDebug("Detected possible Python v%ls location", version.c_str());
|
||||
path = getRegValue(hkInstallPath);
|
||||
::RegCloseKey(hkInstallPath);
|
||||
[[maybe_unused]] const auto hkInstallPathGuard = qScopeGuard([&hkInstallPath] { ::RegCloseKey(hkInstallPath); });
|
||||
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
const QDir baseDir {path};
|
||||
const Path basePath = getRegValue(hkInstallPath);
|
||||
if (basePath.isEmpty())
|
||||
continue;
|
||||
|
||||
if (baseDir.exists(u"python3.exe"_s))
|
||||
{
|
||||
found = true;
|
||||
path = baseDir.filePath(u"python3.exe"_s);
|
||||
}
|
||||
else if (baseDir.exists(u"python.exe"_s))
|
||||
{
|
||||
found = true;
|
||||
path = baseDir.filePath(u"python.exe"_s);
|
||||
}
|
||||
}
|
||||
if (const Path path = (basePath / Path(u"python3.exe"_s)); path.exists())
|
||||
ret.append(path);
|
||||
if (const Path path = (basePath / Path(u"python.exe"_s)); path.exists())
|
||||
ret.append(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
path = QString();
|
||||
|
||||
::RegCloseKey(hkPythonCore);
|
||||
}
|
||||
|
||||
return path;
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString findPythonPath()
|
||||
PathList searchPythonPaths()
|
||||
{
|
||||
QString path = pythonSearchReg(USER);
|
||||
if (!path.isEmpty())
|
||||
return path;
|
||||
|
||||
path = pythonSearchReg(SYSTEM_32BIT);
|
||||
if (!path.isEmpty())
|
||||
return path;
|
||||
|
||||
path = pythonSearchReg(SYSTEM_64BIT);
|
||||
if (!path.isEmpty())
|
||||
return path;
|
||||
// From registry
|
||||
PathList ret = pythonSearchReg(USER)
|
||||
+ pythonSearchReg(SYSTEM_64BIT)
|
||||
+ pythonSearchReg(SYSTEM_32BIT);
|
||||
|
||||
// Fallback: Detect python from default locations
|
||||
const QFileInfoList dirs = QDir(u"C:/"_s).entryInfoList({u"Python*"_s}, QDir::Dirs, (QDir::Name | QDir::Reversed));
|
||||
for (const QFileInfo &info : dirs)
|
||||
{
|
||||
const QString py3Path {info.absolutePath() + u"/python3.exe"};
|
||||
if (QFile::exists(py3Path))
|
||||
return py3Path;
|
||||
const Path absPath {info.absolutePath()};
|
||||
|
||||
const QString pyPath {info.absolutePath() + u"/python.exe"};
|
||||
if (QFile::exists(pyPath))
|
||||
return pyPath;
|
||||
if (const Path path = (absPath / Path(u"python3.exe"_s)); path.exists())
|
||||
ret.append(path);
|
||||
if (const Path path = (absPath / Path(u"python.exe"_s)); path.exists())
|
||||
ret.append(path);
|
||||
}
|
||||
|
||||
return {};
|
||||
return ret;
|
||||
}
|
||||
#endif // Q_OS_WIN
|
||||
}
|
||||
|
||||
bool Utils::ForeignApps::PythonInfo::isValid() const
|
||||
{
|
||||
return (!executableName.isEmpty() && version.isValid());
|
||||
return (executablePath.isValid() && version.isValid());
|
||||
}
|
||||
|
||||
bool Utils::ForeignApps::PythonInfo::isSupportedVersion() const
|
||||
{
|
||||
return (version >= Version {3, 9, 0});
|
||||
return (version >= MINIMUM_SUPPORTED_VERSION);
|
||||
}
|
||||
|
||||
PythonInfo Utils::ForeignApps::pythonInfo()
|
||||
{
|
||||
static PythonInfo pyInfo;
|
||||
|
||||
const QString preferredPythonPath = Preferences::instance()->getPythonExecutablePath().toString();
|
||||
if (pyInfo.isValid() && (preferredPythonPath == pyInfo.executableName))
|
||||
const Path preferredPythonPath = Preferences::instance()->getPythonExecutablePath();
|
||||
if (pyInfo.isValid() && (preferredPythonPath == pyInfo.executablePath))
|
||||
return pyInfo;
|
||||
|
||||
const QString invalidVersionMessage = QCoreApplication::translate("Utils::ForeignApps"
|
||||
, "Python failed to meet minimum version requirement. Path: \"%1\". Found version: \"%2\". Minimum supported version: \"%3\".");
|
||||
|
||||
if (!preferredPythonPath.isEmpty())
|
||||
{
|
||||
if (testPythonInstallation(preferredPythonPath, pyInfo))
|
||||
return pyInfo;
|
||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find Python executable. Path: \"%1\".")
|
||||
.arg(preferredPythonPath), Log::WARNING);
|
||||
{
|
||||
if (pyInfo.isSupportedVersion())
|
||||
return pyInfo;
|
||||
|
||||
LogMsg(invalidVersionMessage.arg(pyInfo.executablePath.toString()
|
||||
, pyInfo.version.toString(), PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()), Log::WARNING);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find Python executable. Path: \"%1\".")
|
||||
.arg(preferredPythonPath.toString()), Log::WARNING);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -276,24 +265,40 @@ PythonInfo Utils::ForeignApps::pythonInfo()
|
||||
|
||||
if (!pyInfo.isValid())
|
||||
{
|
||||
if (testPythonInstallation(u"python3"_s, pyInfo))
|
||||
return pyInfo;
|
||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `python3` executable in PATH environment variable. PATH: \"%1\"")
|
||||
.arg(qEnvironmentVariable("PATH")), Log::INFO);
|
||||
// search in `PATH` environment variable
|
||||
const QString exeNames[] = {u"python3"_s, u"python"_s};
|
||||
for (const QString &exeName : exeNames)
|
||||
{
|
||||
if (testPythonInstallation(Path(exeName), pyInfo))
|
||||
{
|
||||
if (pyInfo.isSupportedVersion())
|
||||
return pyInfo;
|
||||
|
||||
if (testPythonInstallation(u"python"_s, pyInfo))
|
||||
return pyInfo;
|
||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `python` executable in PATH environment variable. PATH: \"%1\"")
|
||||
.arg(qEnvironmentVariable("PATH")), Log::INFO);
|
||||
LogMsg(invalidVersionMessage.arg(pyInfo.executablePath.toString()
|
||||
, pyInfo.version.toString(), PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()), Log::INFO);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `%1` executable in PATH environment variable. PATH: \"%2\"")
|
||||
.arg(exeName, qEnvironmentVariable("PATH")), Log::INFO);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
if (testPythonInstallation(findPythonPath(), pyInfo))
|
||||
return pyInfo;
|
||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `python` executable in Windows Registry."), Log::INFO);
|
||||
for (const Path &path : asConst(searchPythonPaths()))
|
||||
{
|
||||
if (testPythonInstallation(path, pyInfo))
|
||||
{
|
||||
if (pyInfo.isSupportedVersion())
|
||||
return pyInfo;
|
||||
|
||||
LogMsg(invalidVersionMessage.arg(pyInfo.executablePath.toString()
|
||||
, pyInfo.version.toString(), PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()), Log::INFO);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find Python executable"), Log::WARNING);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2018 Mike Tzou
|
||||
* Copyright (C) 2018-2025 Mike Tzou (Chocobo1)
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <QString>
|
||||
|
||||
#include "base/global.h"
|
||||
#include "base/path.h"
|
||||
#include "base/utils/version.h"
|
||||
|
||||
namespace Utils::ForeignApps
|
||||
@@ -45,8 +46,10 @@ namespace Utils::ForeignApps
|
||||
bool isValid() const;
|
||||
bool isSupportedVersion() const;
|
||||
|
||||
QString executableName;
|
||||
Path executablePath;
|
||||
Version version;
|
||||
|
||||
inline static const Version MINIMUM_SUPPORTED_VERSION {3, 9, 0};
|
||||
};
|
||||
|
||||
PythonInfo pythonInfo();
|
||||
|
||||
@@ -115,7 +115,7 @@ bool Utils::Password::PBKDF2::verify(const QByteArray &secret, const QString &pa
|
||||
|
||||
bool Utils::Password::PBKDF2::verify(const QByteArray &secret, const QByteArray &password)
|
||||
{
|
||||
const QList<QByteArrayView> list = ByteArray::splitToViews(secret, ":", Qt::SkipEmptyParts);
|
||||
const QList<QByteArrayView> list = ByteArray::splitToViews(secret, ":");
|
||||
if (list.size() != 2)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
uint32_t Utils::Random::rand(const uint32_t min, const uint32_t max)
|
||||
{
|
||||
static RandomLayer layer;
|
||||
static const RandomLayer layer;
|
||||
|
||||
// new distribution is cheap: https://stackoverflow.com/a/19036349
|
||||
std::uniform_int_distribution<uint32_t> uniform(min, max);
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
*/
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
@@ -44,6 +45,27 @@ namespace
|
||||
|
||||
RandomLayer()
|
||||
{
|
||||
if (::getrandom(nullptr, 0, 0) < 0)
|
||||
{
|
||||
if (errno == ENOSYS)
|
||||
{
|
||||
// underlying kernel does not implement this system call
|
||||
// fallback to `urandom`
|
||||
m_randDev = fopen("/dev/urandom", "rb");
|
||||
if (!m_randDev)
|
||||
qFatal("Failed to open /dev/urandom. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
else
|
||||
{
|
||||
qFatal("getrandom() error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~RandomLayer()
|
||||
{
|
||||
if (m_randDev)
|
||||
fclose(m_randDev);
|
||||
}
|
||||
|
||||
static constexpr result_type min()
|
||||
@@ -56,7 +78,15 @@ namespace
|
||||
return std::numeric_limits<result_type>::max();
|
||||
}
|
||||
|
||||
result_type operator()()
|
||||
result_type operator()() const
|
||||
{
|
||||
if (!m_randDev)
|
||||
return getRandomViaAPI();
|
||||
return getRandomViaFile();
|
||||
}
|
||||
|
||||
private:
|
||||
result_type getRandomViaAPI() const
|
||||
{
|
||||
const int RETRY_MAX = 3;
|
||||
|
||||
@@ -68,10 +98,21 @@ namespace
|
||||
return buf;
|
||||
|
||||
if (result < 0)
|
||||
qFatal("getrandom() error. Reason: %s. Error code: %d.", std::strerror(errno), errno);
|
||||
qFatal("getrandom() error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
|
||||
qFatal("getrandom() failed. Reason: too many retries.");
|
||||
}
|
||||
|
||||
result_type getRandomViaFile() const
|
||||
{
|
||||
result_type buf = 0;
|
||||
if (fread(&buf, sizeof(buf), 1, m_randDev) == 1)
|
||||
return buf;
|
||||
|
||||
qFatal("Read /dev/urandom error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
|
||||
FILE *m_randDev = nullptr;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace
|
||||
: m_randDev {fopen("/dev/urandom", "rb")}
|
||||
{
|
||||
if (!m_randDev)
|
||||
qFatal("Failed to open /dev/urandom. Reason: %s. Error code: %d.", std::strerror(errno), errno);
|
||||
qFatal("Failed to open /dev/urandom. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
|
||||
~RandomLayer()
|
||||
@@ -67,10 +67,10 @@ namespace
|
||||
result_type operator()() const
|
||||
{
|
||||
result_type buf = 0;
|
||||
if (fread(&buf, sizeof(buf), 1, m_randDev) != 1)
|
||||
qFatal("Read /dev/urandom error. Reason: %s. Error code: %d.", std::strerror(errno), errno);
|
||||
if (fread(&buf, sizeof(buf), 1, m_randDev) == 1)
|
||||
return buf;
|
||||
|
||||
return buf;
|
||||
qFatal("Read /dev/urandom error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace
|
||||
return std::numeric_limits<result_type>::max();
|
||||
}
|
||||
|
||||
result_type operator()()
|
||||
result_type operator()() const
|
||||
{
|
||||
result_type buf = 0;
|
||||
const bool result = m_processPrng(reinterpret_cast<PBYTE>(&buf), sizeof(buf));
|
||||
|
||||
@@ -61,7 +61,12 @@ QString Utils::String::fromLocal8Bit(const std::string_view string)
|
||||
|
||||
QString Utils::String::wildcardToRegexPattern(const QString &pattern)
|
||||
{
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 1))
|
||||
return QRegularExpression::wildcardToRegularExpression(pattern
|
||||
, (QRegularExpression::UnanchoredWildcardConversion | QRegularExpression::NonPathWildcardConversion));
|
||||
#else
|
||||
return QRegularExpression::wildcardToRegularExpression(pattern, QRegularExpression::UnanchoredWildcardConversion);
|
||||
#endif
|
||||
}
|
||||
|
||||
QStringList Utils::String::splitCommand(const QString &command)
|
||||
|
||||
@@ -49,12 +49,13 @@ namespace Utils::String
|
||||
template <typename T>
|
||||
T unquote(const T &str, const QString "es = u"\""_s)
|
||||
{
|
||||
if (str.length() < 2) return str;
|
||||
if (str.length() < 2)
|
||||
return str;
|
||||
|
||||
for (const QChar quote : quotes)
|
||||
{
|
||||
if (str.startsWith(quote) && str.endsWith(quote))
|
||||
return str.mid(1, (str.length() - 2));
|
||||
return str.sliced(1, (str.length() - 2));
|
||||
}
|
||||
|
||||
return str;
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
#pragma once
|
||||
|
||||
#define QBT_VERSION_MAJOR 5
|
||||
#define QBT_VERSION_MINOR 1
|
||||
#define QBT_VERSION_MINOR 2
|
||||
#define QBT_VERSION_BUGFIX 0
|
||||
#define QBT_VERSION_BUILD 0
|
||||
#define QBT_VERSION_STATUS "beta1" // Should be empty for stable releases!
|
||||
#define QBT_VERSION_STATUS "alpha1" // Should be empty for stable releases!
|
||||
|
||||
#define QBT__STRINGIFY(x) #x
|
||||
#define QBT_STRINGIFY(x) QBT__STRINGIFY(x)
|
||||
|
||||
@@ -18,6 +18,7 @@ qt_wrap_ui(UI_HEADERS
|
||||
properties/peersadditiondialog.ui
|
||||
properties/propertieswidget.ui
|
||||
rss/automatedrssdownloader.ui
|
||||
rss/rssfeeddialog.ui
|
||||
rss/rsswidget.ui
|
||||
search/pluginselectdialog.ui
|
||||
search/pluginsourcedialog.ui
|
||||
@@ -69,6 +70,7 @@ add_library(qbt_gui STATIC
|
||||
log/logmodel.h
|
||||
mainwindow.h
|
||||
optionsdialog.h
|
||||
powermanagement/inhibitor.h
|
||||
powermanagement/powermanagement.h
|
||||
previewlistdelegate.h
|
||||
previewselectdialog.h
|
||||
@@ -88,6 +90,7 @@ add_library(qbt_gui STATIC
|
||||
rss/automatedrssdownloader.h
|
||||
rss/feedlistwidget.h
|
||||
rss/htmlbrowser.h
|
||||
rss/rssfeeddialog.h
|
||||
rss/rsswidget.h
|
||||
search/pluginselectdialog.h
|
||||
search/pluginsourcedialog.h
|
||||
@@ -137,6 +140,7 @@ add_library(qbt_gui STATIC
|
||||
uithememanager.h
|
||||
uithemesource.h
|
||||
utils.h
|
||||
utils/keysequence.h
|
||||
watchedfolderoptionsdialog.h
|
||||
watchedfoldersmodel.h
|
||||
windowstate.h
|
||||
@@ -167,6 +171,7 @@ add_library(qbt_gui STATIC
|
||||
log/logmodel.cpp
|
||||
mainwindow.cpp
|
||||
optionsdialog.cpp
|
||||
powermanagement/inhibitor.cpp
|
||||
powermanagement/powermanagement.cpp
|
||||
previewlistdelegate.cpp
|
||||
previewselectdialog.cpp
|
||||
@@ -186,6 +191,7 @@ add_library(qbt_gui STATIC
|
||||
rss/automatedrssdownloader.cpp
|
||||
rss/feedlistwidget.cpp
|
||||
rss/htmlbrowser.cpp
|
||||
rss/rssfeeddialog.cpp
|
||||
rss/rsswidget.cpp
|
||||
search/pluginselectdialog.cpp
|
||||
search/pluginsourcedialog.cpp
|
||||
@@ -234,6 +240,7 @@ add_library(qbt_gui STATIC
|
||||
uithememanager.cpp
|
||||
uithemesource.cpp
|
||||
utils.cpp
|
||||
utils/keysequence.cpp
|
||||
watchedfolderoptionsdialog.cpp
|
||||
watchedfoldersmodel.cpp
|
||||
|
||||
@@ -259,8 +266,8 @@ if (DBUS)
|
||||
notifications/dbusnotifier.cpp
|
||||
notifications/dbusnotificationsinterface.h
|
||||
notifications/dbusnotificationsinterface.cpp
|
||||
powermanagement/powermanagement_x11.h
|
||||
powermanagement/powermanagement_x11.cpp
|
||||
powermanagement/inhibitordbus.h
|
||||
powermanagement/inhibitordbus.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -274,21 +281,29 @@ if (STACKTRACE)
|
||||
)
|
||||
endif()
|
||||
|
||||
if ((CMAKE_SYSTEM_NAME STREQUAL "Windows") OR (CMAKE_SYSTEM_NAME STREQUAL "Darwin"))
|
||||
target_sources(qbt_gui PRIVATE
|
||||
programupdater.h
|
||||
programupdater.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
target_sources(qbt_gui PRIVATE
|
||||
macosdockbadge/badger.h
|
||||
macosdockbadge/badger.mm
|
||||
macosdockbadge/badgeview.h
|
||||
macosdockbadge/badgeview.mm
|
||||
macosshiftclickhandler.h
|
||||
macosshiftclickhandler.cpp
|
||||
macutilities.h
|
||||
macutilities.mm
|
||||
powermanagement/inhibitormacos.h
|
||||
powermanagement/inhibitormacos.cpp
|
||||
programupdater.h
|
||||
programupdater.cpp
|
||||
)
|
||||
target_link_libraries(qbt_gui PRIVATE objc)
|
||||
endif()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
target_sources(qbt_gui PRIVATE
|
||||
powermanagement/inhibitorwindows.h
|
||||
powermanagement/inhibitorwindows.cpp
|
||||
programupdater.h
|
||||
programupdater.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -67,7 +67,7 @@ AboutDialog::AboutDialog(QWidget *parent)
|
||||
u"</p>"_s
|
||||
.arg(tr("An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.")
|
||||
.replace(u"C++"_s, u"C\u2060+\u2060+"_s) // make C++ non-breaking
|
||||
, tr("Copyright %1 2006-2024 The qBittorrent project").arg(C_COPYRIGHT)
|
||||
, tr("Copyright %1 2006-2025 The qBittorrent project").arg(C_COPYRIGHT)
|
||||
, tr("Home Page:")
|
||||
, tr("Forum:")
|
||||
, tr("Bug Tracker:"));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -32,12 +32,14 @@
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
|
||||
#include <QtVersionChecks>
|
||||
#include <QAction>
|
||||
#include <QByteArray>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QFuture>
|
||||
#include <QList>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
@@ -122,7 +124,7 @@ namespace
|
||||
fsPathEdit->setCurrentIndex(existingIndex);
|
||||
}
|
||||
|
||||
void updatePathHistory(const QString &settingsKey, const Path &path, const int maxLength)
|
||||
void updatePathHistory(const QString &settingsKey, const Path &path, const qsizetype maxLength)
|
||||
{
|
||||
// Add last used save path to the front of history
|
||||
|
||||
@@ -134,7 +136,10 @@ namespace
|
||||
else
|
||||
pathList.prepend(path.toString());
|
||||
|
||||
settings()->storeValue(settingsKey, QStringList(pathList.mid(0, maxLength)));
|
||||
if (pathList.size() > maxLength)
|
||||
pathList.resize(maxLength);
|
||||
|
||||
settings()->storeValue(settingsKey, pathList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,14 +244,13 @@ public:
|
||||
return QList<qreal>(filesCount(), 0);
|
||||
}
|
||||
|
||||
QList<qreal> availableFileFractions() const override
|
||||
QFuture<QList<qreal>> fetchAvailableFileFractions() const override
|
||||
{
|
||||
return QList<qreal>(filesCount(), 0);
|
||||
}
|
||||
|
||||
void fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const override
|
||||
{
|
||||
resultHandler(availableFileFractions());
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
|
||||
return QtFuture::makeReadyValueFuture(QList<qreal>(filesCount(), 0));
|
||||
#else
|
||||
return QtFuture::makeReadyFuture(QList<qreal>(filesCount(), 0));
|
||||
#endif
|
||||
}
|
||||
|
||||
void prioritizeFiles(const QList<BitTorrent::DownloadPriority> &priorities) override
|
||||
@@ -375,8 +379,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
|
||||
connect(Preferences::instance(), &Preferences::changed, []
|
||||
{
|
||||
const int length = Preferences::instance()->addNewTorrentDialogSavePathHistoryLength();
|
||||
settings()->storeValue(KEY_SAVEPATHHISTORY
|
||||
, QStringList(settings()->loadValue<QStringList>(KEY_SAVEPATHHISTORY).mid(0, length)));
|
||||
settings()->storeValue(KEY_SAVEPATHHISTORY, settings()->loadValue<QStringList>(KEY_SAVEPATHHISTORY).mid(0, length));
|
||||
});
|
||||
|
||||
setCurrentContext(std::make_shared<Context>(Context {torrentDescr, inParams}));
|
||||
@@ -384,7 +387,6 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
|
||||
|
||||
AddNewTorrentDialog::~AddNewTorrentDialog()
|
||||
{
|
||||
saveState();
|
||||
delete m_ui;
|
||||
}
|
||||
|
||||
@@ -398,7 +400,7 @@ void AddNewTorrentDialog::loadState()
|
||||
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
|
||||
resize(dialogSize);
|
||||
|
||||
m_ui->splitter->restoreState(m_storeSplitterState);;
|
||||
m_ui->splitter->restoreState(m_storeSplitterState);
|
||||
}
|
||||
|
||||
void AddNewTorrentDialog::saveState()
|
||||
@@ -834,6 +836,12 @@ void AddNewTorrentDialog::reject()
|
||||
QDialog::reject();
|
||||
}
|
||||
|
||||
void AddNewTorrentDialog::done(const int result)
|
||||
{
|
||||
saveState();
|
||||
QDialog::done(result);
|
||||
}
|
||||
|
||||
void AddNewTorrentDialog::updateMetadata(const BitTorrent::TorrentInfo &metadata)
|
||||
{
|
||||
Q_ASSERT(m_currentContext);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -68,6 +68,11 @@ signals:
|
||||
void torrentAccepted(const BitTorrent::TorrentDescriptor &torrentDescriptor, const BitTorrent::AddTorrentParams &addTorrentParams);
|
||||
void torrentRejected(const BitTorrent::TorrentDescriptor &torrentDescriptor);
|
||||
|
||||
public slots:
|
||||
void accept() override;
|
||||
void reject() override;
|
||||
void done(int result) override;
|
||||
|
||||
private slots:
|
||||
void updateDiskSpaceLabel();
|
||||
void onSavePathChanged(const Path &newPath);
|
||||
@@ -77,9 +82,6 @@ private slots:
|
||||
void categoryChanged(int index);
|
||||
void contentLayoutChanged();
|
||||
|
||||
void accept() override;
|
||||
void reject() override;
|
||||
|
||||
private:
|
||||
class TorrentContentAdaptor;
|
||||
struct Context;
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
#include "base/global.h"
|
||||
#include "base/preferences.h"
|
||||
#include "base/unicodestrings.h"
|
||||
#include "gui/addnewtorrentdialog.h"
|
||||
#include "gui/desktopintegration.h"
|
||||
#include "gui/mainwindow.h"
|
||||
#include "interfaces/iguiapplication.h"
|
||||
@@ -65,7 +64,7 @@ namespace
|
||||
QBITTORRENT_HEADER,
|
||||
RESUME_DATA_STORAGE,
|
||||
TORRENT_CONTENT_REMOVE_OPTION,
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
|
||||
MEMORY_WORKING_SET_LIMIT,
|
||||
#endif
|
||||
#if defined(Q_OS_WIN)
|
||||
@@ -99,6 +98,7 @@ namespace
|
||||
ENABLE_SPEED_WIDGET,
|
||||
#ifndef Q_OS_MACOS
|
||||
ENABLE_ICONS_IN_MENUS,
|
||||
USE_ATTACHED_ADD_NEW_TORRENT_DIALOG,
|
||||
#endif
|
||||
// embedded tracker
|
||||
TRACKER_STATUS,
|
||||
@@ -151,6 +151,7 @@ namespace
|
||||
UPNP_LEASE_DURATION,
|
||||
PEER_TOS,
|
||||
UTP_MIX_MODE,
|
||||
HOSTNAME_CACHE_TTL,
|
||||
IDN_SUPPORT,
|
||||
MULTI_CONNECTIONS_PER_IP,
|
||||
VALIDATE_HTTPS_TRACKER_CERTIFICATE,
|
||||
@@ -208,7 +209,7 @@ void AdvancedSettings::saveAdvancedSettings() const
|
||||
BitTorrent::Session *const session = BitTorrent::Session::instance();
|
||||
|
||||
session->setResumeDataStorageType(m_comboBoxResumeDataStorage.currentData().value<BitTorrent::ResumeDataStorageType>());
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
|
||||
// Physical memory (RAM) usage limit
|
||||
app()->setMemoryWorkingSetLimit(m_spinBoxMemoryWorkingSetLimit.value());
|
||||
#endif
|
||||
@@ -278,6 +279,8 @@ void AdvancedSettings::saveAdvancedSettings() const
|
||||
session->setPeerToS(m_spinBoxPeerToS.value());
|
||||
// uTP-TCP mixed mode
|
||||
session->setUtpMixedMode(m_comboBoxUtpMixedMode.currentData().value<BitTorrent::MixedModeAlgorithm>());
|
||||
// Hostname resolver cache TTL
|
||||
session->setHostnameCacheTTL(m_spinBoxHostnameCacheTTL.value());
|
||||
// Support internationalized domain name (IDN)
|
||||
session->setIDNSupportEnabled(m_checkBoxIDNSupport.isChecked());
|
||||
// multiple connections per IP
|
||||
@@ -330,6 +333,7 @@ void AdvancedSettings::saveAdvancedSettings() const
|
||||
pref->setSpeedWidgetEnabled(m_checkBoxSpeedWidgetEnabled.isChecked());
|
||||
#ifndef Q_OS_MACOS
|
||||
pref->setIconsInMenusEnabled(m_checkBoxIconsInMenusEnabled.isChecked());
|
||||
pref->setAddNewTorrentDialogAttached(m_checkBoxAttachedAddNewTorrentDialog.isChecked());
|
||||
#endif
|
||||
|
||||
// Tracker
|
||||
@@ -490,12 +494,11 @@ void AdvancedSettings::loadAdvancedSettings()
|
||||
m_comboBoxTorrentContentRemoveOption.setCurrentIndex(m_comboBoxTorrentContentRemoveOption.findData(QVariant::fromValue(session->torrentContentRemoveOption())));
|
||||
addRow(TORRENT_CONTENT_REMOVE_OPTION, tr("Torrent content removing mode"), &m_comboBoxTorrentContentRemoveOption);
|
||||
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
|
||||
// Physical memory (RAM) usage limit
|
||||
m_spinBoxMemoryWorkingSetLimit.setMinimum(1);
|
||||
m_spinBoxMemoryWorkingSetLimit.setMaximum(std::numeric_limits<int>::max());
|
||||
m_spinBoxMemoryWorkingSetLimit.setSuffix(tr(" MiB"));
|
||||
m_spinBoxMemoryWorkingSetLimit.setToolTip(tr("This option is less effective on Linux"));
|
||||
m_spinBoxMemoryWorkingSetLimit.setValue(app()->memoryWorkingSetLimit());
|
||||
addRow(MEMORY_WORKING_SET_LIMIT, (tr("Physical memory (RAM) usage limit") + u' ' + makeLink(u"https://wikipedia.org/wiki/Working_set", u"(?)"))
|
||||
, &m_spinBoxMemoryWorkingSetLimit);
|
||||
@@ -732,6 +735,14 @@ void AdvancedSettings::loadAdvancedSettings()
|
||||
addRow(UTP_MIX_MODE, (tr("%1-TCP mixed mode algorithm", "uTP-TCP mixed mode algorithm").arg(C_UTP)
|
||||
+ u' ' + makeLink(u"https://www.libtorrent.org/reference-Settings.html#mixed_mode_algorithm", u"(?)"))
|
||||
, &m_comboBoxUtpMixedMode);
|
||||
// Hostname resolver cache TTL
|
||||
m_spinBoxHostnameCacheTTL.setMinimum(0);
|
||||
m_spinBoxHostnameCacheTTL.setMaximum(std::numeric_limits<int>::max());
|
||||
m_spinBoxHostnameCacheTTL.setValue(session->hostnameCacheTTL());
|
||||
m_spinBoxHostnameCacheTTL.setSuffix(tr(" s", " seconds"));
|
||||
addRow(HOSTNAME_CACHE_TTL, (tr("Internal hostname resolver cache expiry interval")
|
||||
+ u' ' + makeLink(u"https://www.libtorrent.org/reference-Settings.html#resolver_cache_timeout", u"(?)"))
|
||||
, &m_spinBoxHostnameCacheTTL);
|
||||
// Support internationalized domain name (IDN)
|
||||
m_checkBoxIDNSupport.setChecked(session->isIDNSupportEnabled());
|
||||
addRow(IDN_SUPPORT, (tr("Support internationalized domain name (IDN)")
|
||||
@@ -856,6 +867,9 @@ void AdvancedSettings::loadAdvancedSettings()
|
||||
// Enable icons in menus
|
||||
m_checkBoxIconsInMenusEnabled.setChecked(pref->iconsInMenusEnabled());
|
||||
addRow(ENABLE_ICONS_IN_MENUS, tr("Enable icons in menus"), &m_checkBoxIconsInMenusEnabled);
|
||||
|
||||
m_checkBoxAttachedAddNewTorrentDialog.setChecked(pref->isAddNewTorrentDialogAttached());
|
||||
addRow(USE_ATTACHED_ADD_NEW_TORRENT_DIALOG, tr("Attach \"Add new torrent\" dialog to main window"), &m_checkBoxAttachedAddNewTorrentDialog);
|
||||
#endif
|
||||
// Tracker State
|
||||
m_checkBoxTrackerStatus.setChecked(session->isTrackerEnabled());
|
||||
|
||||
@@ -70,7 +70,7 @@ private:
|
||||
|
||||
QSpinBox m_spinBoxSaveResumeDataInterval, m_spinBoxSaveStatisticsInterval, m_spinBoxTorrentFileSizeLimit, m_spinBoxBdecodeDepthLimit, m_spinBoxBdecodeTokenLimit,
|
||||
m_spinBoxAsyncIOThreads, m_spinBoxFilePoolSize, m_spinBoxCheckingMemUsage, m_spinBoxDiskQueueSize,
|
||||
m_spinBoxOutgoingPortsMin, m_spinBoxOutgoingPortsMax, m_spinBoxUPnPLeaseDuration, m_spinBoxPeerToS,
|
||||
m_spinBoxOutgoingPortsMin, m_spinBoxOutgoingPortsMax, m_spinBoxUPnPLeaseDuration, m_spinBoxPeerToS, m_spinBoxHostnameCacheTTL,
|
||||
m_spinBoxListRefresh, m_spinBoxTrackerPort, m_spinBoxSendBufferWatermark, m_spinBoxSendBufferLowWatermark,
|
||||
m_spinBoxSendBufferWatermarkFactor, m_spinBoxConnectionSpeed, m_spinBoxSocketSendBufferSize, m_spinBoxSocketReceiveBufferSize, m_spinBoxSocketBacklogSize,
|
||||
m_spinBoxAnnouncePort, m_spinBoxMaxConcurrentHTTPAnnounces, m_spinBoxStopTrackerTimeout, m_spinBoxSessionShutdownTimeout,
|
||||
@@ -93,7 +93,7 @@ private:
|
||||
QSpinBox m_spinBoxHashingThreads;
|
||||
#endif
|
||||
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
|
||||
QSpinBox m_spinBoxMemoryWorkingSetLimit;
|
||||
#endif
|
||||
|
||||
@@ -108,6 +108,7 @@ private:
|
||||
|
||||
#ifndef Q_OS_MACOS
|
||||
QCheckBox m_checkBoxIconsInMenusEnabled;
|
||||
QCheckBox m_checkBoxAttachedAddNewTorrentDialog;
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
||||
|
||||
@@ -234,14 +234,14 @@ void FileSystemPathEdit::setFileNameFilter(const QString &val)
|
||||
const int closeBracePos = val.indexOf(u')', (openBracePos + 1));
|
||||
if ((openBracePos > 0) && (closeBracePos > 0) && (closeBracePos > openBracePos + 2))
|
||||
{
|
||||
QString filterString = val.mid(openBracePos + 1, closeBracePos - openBracePos - 1);
|
||||
const QString filterString = val.sliced((openBracePos + 1), (closeBracePos - openBracePos - 1));
|
||||
if (filterString == u"*")
|
||||
{ // no filters
|
||||
d->m_editor->setFilenameFilters({});
|
||||
}
|
||||
else
|
||||
{
|
||||
QStringList filters = filterString.split(u' ', Qt::SkipEmptyParts);
|
||||
const QStringList filters = filterString.split(u' ', Qt::SkipEmptyParts);
|
||||
d->m_editor->setFilenameFilters(filters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
|
||||
#include "base/bittorrent/session.h"
|
||||
#include "base/bittorrent/torrentdescriptor.h"
|
||||
#include "base/logger.h"
|
||||
#include "base/net/downloadmanager.h"
|
||||
#include "base/preferences.h"
|
||||
#include "base/torrentfileguard.h"
|
||||
@@ -82,6 +81,15 @@ GUIAddTorrentManager::GUIAddTorrentManager(IGUIApplication *app, BitTorrent::Ses
|
||||
connect(btSession(), &BitTorrent::Session::metadataDownloaded, this, &GUIAddTorrentManager::onMetadataDownloaded);
|
||||
}
|
||||
|
||||
GUIAddTorrentManager::~GUIAddTorrentManager()
|
||||
{
|
||||
for (AddNewTorrentDialog *dialog : asConst(m_dialogs))
|
||||
{
|
||||
dialog->disconnect(this);
|
||||
dialog->reject();
|
||||
}
|
||||
}
|
||||
|
||||
bool GUIAddTorrentManager::addTorrent(const QString &source, const BitTorrent::AddTorrentParams ¶ms, const AddTorrentOption option)
|
||||
{
|
||||
// `source`: .torrent file path, magnet URI or URL
|
||||
@@ -225,12 +233,19 @@ bool GUIAddTorrentManager::processTorrent(const QString &source
|
||||
if (!hasMetadata)
|
||||
btSession()->downloadMetadata(torrentDescr);
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
const bool attached = false;
|
||||
#else
|
||||
const bool attached = Preferences::instance()->isAddNewTorrentDialogAttached();
|
||||
#endif
|
||||
|
||||
// By not setting a parent to the "AddNewTorrentDialog", all those dialogs
|
||||
// will be displayed on top and will not overlap with the main window.
|
||||
auto *dlg = new AddNewTorrentDialog(torrentDescr, params, nullptr);
|
||||
auto *dlg = new AddNewTorrentDialog(torrentDescr, params, (attached ? app()->mainWindow() : nullptr));
|
||||
// Qt::Window is required to avoid showing only two dialog on top (see #12852).
|
||||
// Also improves the general convenience of adding multiple torrents.
|
||||
dlg->setWindowFlags(Qt::Window);
|
||||
if (!attached)
|
||||
dlg->setWindowFlags(Qt::Window);
|
||||
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
m_dialogs[infoHash] = dlg;
|
||||
|
||||
@@ -61,6 +61,7 @@ class GUIAddTorrentManager : public GUIApplicationComponent<AddTorrentManager>
|
||||
|
||||
public:
|
||||
GUIAddTorrentManager(IGUIApplication *app, BitTorrent::Session *session, QObject *parent = nullptr);
|
||||
~GUIAddTorrentManager() override;
|
||||
|
||||
bool addTorrent(const QString &source, const BitTorrent::AddTorrentParams ¶ms = {}, AddTorrentOption option = AddTorrentOption::Default);
|
||||
|
||||
|
||||
73
src/gui/macosshiftclickhandler.cpp
Normal file
73
src/gui/macosshiftclickhandler.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2025 Luke Memet (lukemmtt)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#include "macosshiftclickhandler.h"
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QTreeView>
|
||||
|
||||
MacOSShiftClickHandler::MacOSShiftClickHandler(QTreeView *treeView)
|
||||
: QObject(treeView)
|
||||
, m_treeView {treeView}
|
||||
{
|
||||
treeView->installEventFilter(this);
|
||||
}
|
||||
|
||||
bool MacOSShiftClickHandler::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if ((watched == m_treeView) && (event->type() == QEvent::MouseButtonPress))
|
||||
{
|
||||
const auto *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
if (mouseEvent->button() != Qt::LeftButton)
|
||||
return false;
|
||||
|
||||
const QModelIndex clickedIndex = m_treeView->indexAt(mouseEvent->position().toPoint());
|
||||
if (!clickedIndex.isValid())
|
||||
return false;
|
||||
|
||||
const Qt::KeyboardModifiers modifiers = mouseEvent->modifiers();
|
||||
const bool shiftPressed = modifiers.testFlag(Qt::ShiftModifier);
|
||||
|
||||
if (shiftPressed && m_lastClickedIndex.isValid())
|
||||
{
|
||||
const QItemSelection selection(m_lastClickedIndex, clickedIndex);
|
||||
const bool commandPressed = modifiers.testFlag(Qt::ControlModifier);
|
||||
if (commandPressed)
|
||||
m_treeView->selectionModel()->select(selection, (QItemSelectionModel::Select | QItemSelectionModel::Rows));
|
||||
else
|
||||
m_treeView->selectionModel()->select(selection, (QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows));
|
||||
m_treeView->selectionModel()->setCurrentIndex(clickedIndex, QItemSelectionModel::NoUpdate);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!modifiers.testFlags(Qt::AltModifier | Qt::MetaModifier))
|
||||
m_lastClickedIndex = clickedIndex;
|
||||
}
|
||||
|
||||
return QObject::eventFilter(watched, event);
|
||||
}
|
||||
50
src/gui/macosshiftclickhandler.h
Normal file
50
src/gui/macosshiftclickhandler.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2025 Luke Memet (lukemmtt)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QPersistentModelIndex>
|
||||
|
||||
class QTreeView;
|
||||
|
||||
// Workaround for QTBUG-115838: Shift-click range selection not working properly on macOS
|
||||
class MacOSShiftClickHandler final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(MacOSShiftClickHandler)
|
||||
|
||||
public:
|
||||
explicit MacOSShiftClickHandler(QTreeView *treeView);
|
||||
|
||||
private:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
QTreeView *m_treeView = nullptr;
|
||||
QPersistentModelIndex m_lastClickedIndex;
|
||||
};
|
||||
@@ -101,6 +101,7 @@
|
||||
#include "ui_mainwindow.h"
|
||||
#include "uithememanager.h"
|
||||
#include "utils.h"
|
||||
#include "utils/keysequence.h"
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
#include "macosdockbadge/badger.h"
|
||||
@@ -130,6 +131,8 @@ MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, con
|
||||
, m_ui {new Ui::MainWindow}
|
||||
, m_downloadRate {Utils::Misc::friendlyUnit(0, true)}
|
||||
, m_uploadRate {Utils::Misc::friendlyUnit(0, true)}
|
||||
, m_pwr {new PowerManagement}
|
||||
, m_preventTimer {new QTimer(this)}
|
||||
, m_storeExecutionLogEnabled {EXECUTIONLOG_SETTINGS_KEY(u"Enabled"_s)}
|
||||
, m_storeDownloadTrackerFavicon {SETTINGS_KEY(u"DownloadTrackerFavicon"_s)}
|
||||
, m_storeExecutionLogTypes {EXECUTIONLOG_SETTINGS_KEY(u"Types"_s), Log::MsgType::ALL}
|
||||
@@ -336,8 +339,6 @@ MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, con
|
||||
connect(m_ui->actionManageCookies, &QAction::triggered, this, &MainWindow::manageCookies);
|
||||
|
||||
// Initialise system sleep inhibition timer
|
||||
m_pwr = new PowerManagement(this);
|
||||
m_preventTimer = new QTimer(this);
|
||||
m_preventTimer->setSingleShot(true);
|
||||
connect(m_preventTimer, &QTimer::timeout, this, &MainWindow::updatePowerManagementState);
|
||||
connect(pref, &Preferences::changed, this, &MainWindow::updatePowerManagementState);
|
||||
@@ -837,6 +838,7 @@ void MainWindow::cleanup()
|
||||
delete m_executableWatcher;
|
||||
|
||||
m_preventTimer->stop();
|
||||
delete m_pwr;
|
||||
|
||||
#if (defined(Q_OS_WIN) || defined(Q_OS_MACOS))
|
||||
if (m_programUpdateTimer)
|
||||
@@ -882,7 +884,7 @@ void MainWindow::createKeyboardShortcuts()
|
||||
{
|
||||
m_ui->actionCreateTorrent->setShortcut(QKeySequence::New);
|
||||
m_ui->actionOpen->setShortcut(QKeySequence::Open);
|
||||
m_ui->actionDelete->setShortcut(QKeySequence::Delete);
|
||||
m_ui->actionDelete->setShortcut(Utils::KeySequence::deleteItem());
|
||||
m_ui->actionDelete->setShortcutContext(Qt::WidgetShortcut); // nullify its effect: delete key event is handled by respective widgets, not here
|
||||
m_ui->actionDownloadFromURL->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_O);
|
||||
m_ui->actionExit->setShortcut(Qt::CTRL | Qt::Key_Q);
|
||||
@@ -1614,14 +1616,14 @@ void MainWindow::on_actionSearchWidget_triggered()
|
||||
#ifdef Q_OS_WIN
|
||||
const QMessageBox::StandardButton buttonPressed = QMessageBox::question(this, tr("Old Python Runtime")
|
||||
, tr("Your Python version (%1) is outdated. Minimum requirement: %2.\nDo you want to install a newer version now?")
|
||||
.arg(pyInfo.version.toString(), u"3.9.0")
|
||||
.arg(pyInfo.version.toString(), Utils::ForeignApps::PythonInfo::MINIMUM_SUPPORTED_VERSION.toString())
|
||||
, (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
|
||||
if (buttonPressed == QMessageBox::Yes)
|
||||
installPython();
|
||||
#else
|
||||
QMessageBox::information(this, tr("Old Python Runtime")
|
||||
, tr("Your Python version (%1) is outdated. Please upgrade to latest version for search engines to work.\nMinimum requirement: %2.")
|
||||
.arg(pyInfo.version.toString(), u"3.9.0"));
|
||||
.arg(pyInfo.version.toString(), Utils::ForeignApps::PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
@@ -1826,7 +1828,7 @@ void MainWindow::updatePowerManagementState() const
|
||||
|
||||
return torrent->isMoving();
|
||||
});
|
||||
m_pwr->setActivityState(inhibitSuspend);
|
||||
m_pwr->setActivityState(inhibitSuspend ? PowerManagement::ActivityState::Busy : PowerManagement::ActivityState::Idle);
|
||||
|
||||
m_preventTimer->start(PREVENT_SUSPEND_INTERVAL);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user