Compare commits

...

81 commits

Author SHA1 Message Date
sledgehammer999
0b3bce8993
Sync translations from Transifex and run lupdate 2025-04-21 12:30:57 +03:00
bolshoytoster
0160aa28b6
WebUI: Don't update UI if the page is hidden
Currently, there is unnecessary CPU/network usage by the web UI when it's running in the background, this PR prevents it from refreshing in the background.

Closes #22565.
PR #22567.
2025-04-21 17:23:09 +08:00
Chocobo1
0187f19f60
WebUI: migrate away from recursion calls
Browsers have limited recursion depth about ~10000.

PR #22580.
2025-04-21 17:21:18 +08:00
sledgehammer999
e87dfe35f3
Bump copyright year 2025-04-20 23:38:34 +03:00
sledgehammer999
e51be45ce6
Sync translations from Transifex and run lupdate 2025-04-20 23:34:53 +03:00
tehcneko
b4a16f6464
WebUI: Optimize table performance with virtual list
Adding virtual list support to dynamic tables to improve performance on large lists, I observed a 100x performance improvement on rendering on a torrent table with 5000 torrents.
This optimization is disabled by default and can be enabled in options.

PR #22502.
2025-04-20 17:18:26 +08:00
Chocobo1
250fef4ee7
Improve error messages
Print error message to stderr instead of stdout.

PR #22581.
2025-04-20 16:54:49 +08:00
Chocobo1
8fc5d0914d
Add versioning to socks.py
Also mark variable as private in novaprinter.py.

PR #22578.
2025-04-20 16:47:45 +08:00
Kostiantyn Chernenok
fc5daf6e1d
Clamp seeding time limit in session
Add clamping for seeding and inactive seeding time limit on setting from dialog and loading from config.

Closes #21953.
PR #22558.

Signed-off-by: Kostiantyn <kos.chernenok@gmail.com>
2025-04-20 16:34:04 +08:00
Kostiantyn Chernenok
c878a09d27
Swap add file/link buttons on toolbar
Swap "Add torrent file" with "Add torrent link" button to be consistent with order in File menu.

Closes #22420.
PR #22557.

Signed-off-by: Kostiantyn <kos.chernenok@gmail.com>
2025-04-19 07:57:39 +08:00
Chocobo1
2aee875642
Enforce SOCKS proxy setting in search engine plugins
Previously it require each plugin to import helpers.py to setup SOCKS proxy.
Now it is enforced by default for all plugins.
Also added a function for plugins to ignore/restore the socket to
default state.

PR #22554.
2025-04-19 07:11:50 +08:00
Vladimir Golovnev
2785636d3f
Prevent crash due to corrupted resume data
PR #22569.
Closes #22540.
2025-04-17 11:16:17 +03:00
Vladimir Golovnev
15069b2643
Fix the torrent relocates files when switching to "manual" mode
PR #22564.
Closes #22283.
Closes #22546.
2025-04-16 10:23:34 +03:00
Chocobo1
f0361f1bed
Use the proper keyboard shortcut for deleting items on macOS
Closes #20187.
PR #22544.
2025-04-15 15:13:36 +08:00
Vladimir Golovnev
110e6d32b4
Explicitly reject opened Add torrent dialogs when exiting app
PR #22535.
Closes #19933.
Supercedes #22533.
2025-04-14 09:51:59 +03:00
Chocobo1
3d73026ff2
Add SOCKS4/SOCKS4a proxy support to search engine
Pass 'Perform hostname lookup via proxy' setting along the way.
Also add underline to variables and functions that are private to the python module.

PR #22510.
2025-04-13 16:25:38 +08:00
Chocobo1
abafbc0685
WebUI: avoid saving invalid size
Don't save the wrong size when the tab is collapsed.
Reported in: https://github.com/qbittorrent/qBittorrent/pull/21215/files#r1966052959

PR #22537.
2025-04-12 17:59:42 +08:00
Chocobo1
5465605377
WebUI: fix dark mode in RSS entry viewer
Use `allow-same-origin` sandbox directive to allow fetching the parent CSS.

PR #22536.
2025-04-12 17:54:55 +08:00
luzpaz
9331580e86
Fix grammar
ref: https://github.com/qbittorrent/qBittorrent/pull/19333#discussion_r1793252710

PR #22525.
2025-04-12 17:46:48 +08:00
FredBill1
795889c417
Migrate socks.py from SocksiPy to PySocks 1.7.1
Migrate `socks.py` from SocksiPy 1.01 to [PySocks 1.7.1](c2fa43cbe1/socks.py), allowing python 3+ compatibility, [details](https://github.com/qbittorrent/qBittorrent/issues/16447#issuecomment-2776894026).

The content of the `socks.py` is entirely copied from the [PySocks repository](c2fa43cbe1/socks.py), the only modification is the license header at the top of the file and trimming trail whitespaces.

Closes #16447.
PR #22507.
2025-04-09 17:36:50 +08:00
Chocobo1
ff03eeab5b
Show info hash in log when added a duplicate torrent
Closes #22161.
PR #22505.
2025-04-08 16:31:04 +08:00
Chocobo1
f0b9a17566
WebUI: add headers to RSS entry viewer
Introduced Author, 'Open link' headers.
Note that the Author and 'Open link' are not mandatory fields in RSS/Atom feeds. So these
headers will only be displayed when the feed includes them.

PR #22503.
2025-04-08 15:47:47 +08:00
xavier2k6
72e8b3272b
GHA CI: Use Qt 6.9.0 on Windows and macOS
PR #22509.
2025-04-08 15:35:58 +08:00
skomerko
6c36830e5e
WebUI: Set status filter to 'All' if selected filter is no longer visible
Fixup for #21145

To reproduce:
1. Select status filter with 0 torrents
2. Enable 'Auto hide zero status filters' and save settings. Hidden filter is still selected:

PR #22487.
2025-04-05 17:13:14 +08:00
Chocobo1
cdddaae939
WebUI: fix preferences not applied in magnet handler
Thanks for the diagnosis in this [post](https://github.com/qbittorrent/qBittorrent/issues/22495#issue-2958553624).

Closes #21486.
Closes #22495.
PR #22504.
2025-04-05 13:51:08 +08:00
tehcneko
f540381caf
WebUI: Support creating new torrents
Implemented the torrent creator using WebAPI from #20366 in WebUI, the interface is mostly inspired by GUI and VueTorrent.

Closes #5614.
PR #22459.
2025-04-03 17:16:12 +08:00
Vladimir Golovnev
055d82bda4
Add option to enable previous Add new torrent dialog behavior
Some people are still unhappy with "standalone window mode" of "Add new torrent dialog" so just provide them with an option to use old "modal dialog mode" in all the current qBittorrent branches.

PR #22492 (based on original PR #19874).
2025-03-31 09:18:16 +03:00
Chocobo1
0796f96ee4
Merge pull request #22482 from Chocobo1/process_env
Refine environment variable scope
2025-03-30 15:12:10 +08:00
Vladimir Golovnev
841cffafa7
Restore ability to use server-side translation by custom WebUI
PR #20968.
2025-03-30 09:47:21 +03:00
Chocobo1
ade39432be
Revise wordings related to SOCKS4 proxy
The affected options are not really incompatible with SOCKS4 but it is due to Qt missing
implementation. Therefore 'unavailable' is more suitable.

PR #22483.
2025-03-29 21:09:49 +08:00
Chocobo1
830d2c207b
WebAPI: bump version
Related: https://github.com/qbittorrent/qBittorrent/pull/22460#issuecomment-2748821812

And add initial version of WebAPI changelog.

PR #22481.
2025-03-29 20:47:53 +08:00
Chocobo1
97865545c3
WebUI: fix Tag counter counting wrong
Related: 73e9116d21 (r2014898781)

PR #22480.
2025-03-29 20:41:05 +08:00
Hanabishi
3abdc3134b
WebUI: Disable alternative UI in case of the index page being inaccessible
Initial failed access shows an error as before, but on the next reload it falls back to the default WebUI.

PR #22399.
Closes #18401.
2025-03-29 20:32:22 +08:00
Chocobo1
5a716a40fb
Simplify proxy related code 2025-03-28 18:39:25 +08:00
Chocobo1
943e403241
Refine environment variable scope
Previously the proxy environment variable will affect the qbt process globally. Now it is
limited to where it required.
2025-03-28 18:15:53 +08:00
Vladimir Golovnev
103ea813dc
RSS: Fix crash when moving a folder into its subfolder
PR #22479.
Closes #18446.
2025-03-28 09:03:59 +03:00
Vladimir Golovnev
52b1f3588a
RSS: Mark matched article as "read" if refers to duplicate torrent
PR #22477.
2025-03-28 09:01:22 +03:00
Vladimir Golovnev
4bd50672e8
Improve add torrent error handling
PR #22468.
2025-03-25 09:13:15 +03:00
Chocobo1
8c8a0ac54c
WebAPI: improve setting preferences behavior
Now the behavior is more intuitive for a few options when the client send in partial settings.
This change is backward compatible.

For example, now it is possible to have only one of `max_ratio_enabled` or `max_ratio` instead
of requiring both.

PR #22460.
2025-03-24 21:04:35 +08:00
Chocobo1
7b4a3fccc6
WebUI: replace deprecated data type
`Hash` is deprecated by mootools.
Also simplify related code.

PR #22458.
2025-03-23 15:01:39 +08:00
Chocobo1
d21653e8cf
Don't leak parent file descriptors to child processes
It is unexpected for the child process to inherit parent file descriptors.
Requires Qt >= 6.6 and only affects Linux.

Closes #10312.
PR #22457.
2025-03-23 14:48:21 +08:00
Vladimir Golovnev
627d89813c
RSS: Allow to set refresh interval per feed
PR #22448.
2025-03-22 08:43:04 +03:00
Chocobo1
b28c229f85
Add control for 'hostname resolver cache expiry interval'
Also add a few missing units in WebUI.

Closes #22267.
PR #22439.
2025-03-17 19:40:06 +08:00
Chocobo1
8d0870c953
Switch to string view where applicable
PR #22438.
2025-03-17 19:28:38 +08:00
Chocobo1
5a4b3b25d3
Use slice method where applicable
These code segments already have its boundary checked and can thus be faster.

PR #22411.
2025-03-15 14:58:59 +08:00
Vladimir Golovnev
d174bc75e4
Show free disk space in status bar
PR #22407.
Closes #19607.
2025-03-13 14:47:10 +03:00
Chocobo1
882da47609
Use Qt built-in function for comparing values
PR #22389.
2025-03-10 03:19:31 +08:00
Chocobo1
b74b334e34
Add tests for PeerAddress struct
PR #22388.
2025-03-10 03:11:08 +08:00
Vladimir Golovnev
53f919aea8
Add missing includes
PR #22362.
2025-03-05 09:03:00 +03:00
Chocobo1
62a7fd86d6
Improve "split to byte array views" function
1. Utilize string matcher
2. Remove split behavior parameter
   Previously `KeepEmptyParts` behavior doesn't match Qt's
   implementation and since our codebase doesn't really make use of it,
   we can just remove the parameter.
3. Add tests.

PR #22352.
2025-03-03 21:42:03 +08:00
Chocobo1
96295adc08
Merge pull request #22351 from Chocobo1/ci_tweak
Improve CI scripts
2025-03-03 21:28:23 +08:00
skomerko
8f53fb8178
WebUI: Maintain row highlight after rearranging table columns
This PR fixes a bug where row highlight effect would be lost after reordering columns.

PR #22339.
2025-03-02 17:15:21 +08:00
skomerko
37eb80919c
WebUI: Fix bug where the 'Tracker editing' dialog displays incorrect data
In Trackers table, moving the 'URL' column from its default (2) position caused the 'Tracker editing' dialog to display incorrect data.
Steps to reproduce:
1. Move 'URL' column in Trackers table to any position from default
2. Choose tracker URL and click 'Edit tracker URL'

PR #22338.
2025-03-02 17:08:04 +08:00
Chocobo1
1b044d9476
GHA CI: shorten Windows CI build time
Now vcpkg caches b2 tool. Boost doesn't need the exact b2 version to generate the cmake files.
2025-03-01 16:12:59 +08:00
Chocobo1
83599f1f7b
GHA CI: tweak cache size
It seems ~500MB is enough to cache all the build artifacts but we still
make it a bit larger to avoid thrashing.
2025-03-01 16:12:57 +08:00
Vladimir Golovnev
6e1b5ec18b
Don't miss to declare some of the color IDs
PR #22330.
Closes #22326.
2025-02-25 18:56:15 +03:00
Vladimir Golovnev
249c80aaaf
Improve command line parameters serialization
PR #22319.
Closes #22306.
2025-02-25 09:11:03 +03:00
Chocobo1
0ac47496d4
GHA CI: ensure compatibility with newer cmake versions
Fixes #22315.
PR #22320.
2025-02-25 14:08:09 +08:00
Chocobo1
4ec80de268
Update website URL
The website don't use php now.

PR #22321.
2025-02-25 14:03:30 +08:00
skomerko
f432c1e615
WebUI: Show 'Edit tracker URL...' only when one tracker is selected
We can only edit one URL through the dialog, so there's no point in showing this context option when more than one tracker is selected in trackers table.

PR #22311.
2025-02-25 13:55:04 +08:00
Chocobo1
41d9ee91a1
WebUI: tell web crawlers do not index the WebUI
PR #22309.
2025-02-23 15:20:22 +08:00
skomerko
ba3d89b674
WebUI: Update sort icon after changing column order
This PR fixes a bug where the sort icon did not update correctly after reordering columns.

Steps to reproduce:
1. Sort a column
2. Move it to a different position
3. The sort icon remains in its original location

PR #22299.
2025-02-23 15:13:17 +08:00
skomerko
1ca33d45ba
WebUI: Access element attribute/property natively in log tables
#21007 changed pretty much everything already but I spotted some leftovers and replaced them too.

PR #22294.
2025-02-21 20:54:26 +08:00
Chocobo1
a9b54d94a0
Merge pull request #22282 from skomerko/webui-v51-fixes
WebUI v5.1 fixes
2025-02-21 20:44:42 +08:00
Luke Memet
693390ff27
Fix shift-click selection on macOS
PR #22284.
Closes #16818.
2025-02-19 13:52:51 +03:00
Daniel Nylander
5ddc5a8b87
NSIS: Update Swedish translation
PR #22046.
2025-02-19 13:45:59 +03:00
Bark
ad9100ac07
WebAPI: Do not wrap result if offset is invalid
Closes #22158.
PR #22174.
2025-02-18 13:53:30 +08:00
Chocobo1
1043bea896
Refactor power management classes
Mainly it is about moving each platform code to its own file.

PR #22279.
2025-02-18 11:58:43 +08:00
Chocobo1
955688c125
WebUI: replace rounding function from MooTools
The `round()` returning floating point number is not a good idea. This is due to floating point
representation is imprecise and sometimes it cannot faithfully represent a number, for example
`0.09 + 0.01 !== 0.1 `. Therefore, it should be avoided and/or utilize other function
to achieve the goal.

Also, improve `window.qBittorrent.Misc.toFixedPointString()` and add test cases.

PR #22281.
2025-02-17 15:11:55 +08:00
Chocobo1
8da43a4054
Use const accessor
This avoids an unnecessary check to the container internal atomic variable and prevents
potential detachment.

PR #22280.
2025-02-16 15:51:40 +08:00
Chocobo1
ddf6dd5fa2
GHA CI: fix AppImage building
Upstream now defaults to static runtime and the previous URL is invalid now.
Upstream commits:
* c28054bab6
* ce5291e259

Also fuse2 is not needed now as stated on:
https://github.com/AppImage/type2-runtime?tab=readme-ov-file#type2-runtime-

PR #22286.
2025-02-16 05:08:39 +08:00
skomerko
8c02bbb4bc WebUI: Select next available search tab after closing last active tab with X button 2025-02-15 10:59:56 +01:00
skomerko
7e95375cec WebUI: Fix unknown country flag path 2025-02-15 10:59:56 +01:00
skomerko
29201fa016 WebUI: Apply scrollbar style to context menu elements 2025-02-15 10:59:56 +01:00
skomerko
1a3d0f6fab WebUI: Adjust context menu offsets in Search tab & Status filter list 2025-02-15 10:59:56 +01:00
skomerko
f58d6ae984 WebUI: Make context menu target selectors more precise 2025-02-15 10:59:56 +01:00
skomerko
7f0134108a WebUI: Use classlist property to set cell class in trackers table 2025-02-15 10:59:53 +01:00
Chocobo1
d79dc86d00
WebUI: require Subresource Integrity on external links
Also migrate to .mjs format.

PR #22263.
2025-02-12 15:19:07 +08:00
Chocobo1
38070c6eee
WebUI: use recommended function for checking NaN values
Also fix a few variable names along the way.

PR #22264.
2025-02-12 15:11:54 +08:00
Vladimir Golovnev
c9eb1fbac8
WebAPI: Don't trim string parameters
PR #22266.
Closes #19485.
Closes #22254.
2025-02-12 09:33:41 +03:00
sledgehammer999
7238bad5a6
Bump to v5.2.0alpha1 2025-02-11 02:04:46 +02:00
281 changed files with 106507 additions and 109814 deletions

2
.github/FUNDING.yml vendored
View file

@ -1 +1 @@
custom: "https://www.qbittorrent.org/donate.php"
custom: "https://www.qbittorrent.org/donate"

View file

@ -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 }}

View file

@ -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,12 @@ jobs:
- name: Package AppImage
run: |
./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --plugin qt
./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

View file

@ -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

View file

@ -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 `

View file

@ -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" \

View file

@ -73,7 +73,7 @@ repos:
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 |

5
WebAPI_Changelog.md Normal file
View file

@ -0,0 +1,5 @@
# 2.11.6
* 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

4
dist/mac/Info.plist vendored
View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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}"

View file

@ -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:"

View file

@ -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 &params)
{
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;
@ -636,7 +659,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 +674,9 @@ void Application::runExternalProgram(const QString &programTemplate, const BitTo
QProcess proc;
proc.setProgram(command);
proc.setArguments(args);
#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
proc.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif
if (proc.startDetached())
{
@ -897,10 +929,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);

View file

@ -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)
{

View file

@ -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

View file

@ -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)

View file

@ -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);

View 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)

View file

@ -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)

View file

@ -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
@ -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
@ -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}

View file

@ -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
@ -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;

View file

@ -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;

View file

@ -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:

View file

@ -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

View file

@ -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,9 +482,11 @@ 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);
@ -519,5 +524,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);
};
}

View file

@ -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
@ -76,6 +76,7 @@
#include <QUuid>
#include "base/algorithm.h"
#include "base/freediskspacechecker.h"
#include "base/global.h"
#include "base/logger.h"
#include "base/net/downloadmanager.h"
@ -115,6 +116,7 @@ using namespace BitTorrent;
const Path CATEGORIES_FILE_NAME {u"categories.json"_s};
const int MAX_PROCESSING_RESUMEDATA_COUNT = 50;
const std::chrono::seconds FREEDISKSPACE_CHECK_TIMEOUT = 30s;
namespace
{
@ -363,7 +365,7 @@ QString Session::subcategoryName(const QString &category)
{
const int sepIndex = category.lastIndexOf(u'/');
if (sepIndex >= 0)
return category.mid(sepIndex + 1);
return category.sliced(sepIndex + 1);
return category;
}
@ -372,7 +374,7 @@ QString Session::parentCategoryName(const QString &category)
{
const int sepIndex = category.lastIndexOf(u'/');
if (sepIndex >= 0)
return category.left(sepIndex);
return category.first(sepIndex);
return {};
}
@ -383,7 +385,7 @@ QStringList Session::expandCategory(const QString &category)
int index = 0;
while ((index = category.indexOf(u'/', index)) >= 0)
{
result << category.left(index);
result << category.first(index);
++index;
}
result << category;
@ -458,6 +460,7 @@ SessionImpl::SessionImpl(QObject *parent)
, m_isUTPRateLimited(BITTORRENT_SESSION_KEY(u"uTPRateLimited"_s), true)
, m_utpMixedMode(BITTORRENT_SESSION_KEY(u"uTPMixedMode"_s), MixedModeAlgorithm::TCP
, clampValue(MixedModeAlgorithm::TCP, MixedModeAlgorithm::Proportional))
, m_hostnameCacheTTL(BITTORRENT_SESSION_KEY(u"HostnameCacheTTL"_s), 1200)
, m_IDNSupportEnabled(BITTORRENT_SESSION_KEY(u"IDNSupportEnabled"_s), false)
, m_multiConnectionsPerIpEnabled(BITTORRENT_SESSION_KEY(u"MultiConnectionsPerIp"_s), false)
, m_validateHTTPSTrackerCertificate(BITTORRENT_SESSION_KEY(u"ValidateHTTPSTrackerCertificate"_s), true)
@ -468,8 +471,10 @@ SessionImpl::SessionImpl(QObject *parent)
, m_isAddTrackersFromURLEnabled(BITTORRENT_SESSION_KEY(u"AddTrackersFromURLEnabled"_s), false)
, m_additionalTrackersURL(BITTORRENT_SESSION_KEY(u"AdditionalTrackersURL"_s))
, m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r;})
, m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s), -1, lowerLimited(-1))
, m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s), -1, lowerLimited(-1))
, m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s), Torrent::NO_SEEDING_TIME_LIMIT
, clampValue(Torrent::NO_SEEDING_TIME_LIMIT, Torrent::MAX_SEEDING_TIME))
, m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s), Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT
, clampValue(Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT, Torrent::MAX_INACTIVE_SEEDING_TIME))
, m_isAddTorrentToQueueTop(BITTORRENT_SESSION_KEY(u"AddTorrentToTopOfQueue"_s), false)
, m_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false)
, m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_s), Torrent::StopCondition::None)
@ -540,6 +545,8 @@ SessionImpl::SessionImpl(QObject *parent)
, m_ioThread {new QThread}
, m_asyncWorker {new QThreadPool(this)}
, m_recentErroredTorrentsTimer {new QTimer(this)}
, m_freeDiskSpaceChecker {new FreeDiskSpaceChecker(savePath())}
, m_freeDiskSpaceCheckingTimer {new QTimer(this)}
{
// It is required to perform async access to libtorrent sequentially
m_asyncWorker->setMaxThreadCount(1);
@ -600,6 +607,18 @@ SessionImpl::SessionImpl(QObject *parent)
, &Net::ProxyConfigurationManager::proxyConfigurationChanged
, this, &SessionImpl::configureDeferred);
m_freeDiskSpaceChecker->moveToThread(m_ioThread.get());
connect(m_ioThread.get(), &QThread::finished, m_freeDiskSpaceChecker, &QObject::deleteLater);
m_freeDiskSpaceCheckingTimer->setInterval(FREEDISKSPACE_CHECK_TIMEOUT);
m_freeDiskSpaceCheckingTimer->setSingleShot(true);
connect(m_freeDiskSpaceCheckingTimer, &QTimer::timeout, m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::check);
connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, this, [this](const qint64 value)
{
m_freeDiskSpace = value;
m_freeDiskSpaceCheckingTimer->start();
emit freeDiskSpaceChecked(m_freeDiskSpace);
});
m_fileSearcher = new FileSearcher;
m_fileSearcher->moveToThread(m_ioThread.get());
connect(m_ioThread.get(), &QThread::finished, m_fileSearcher, &QObject::deleteLater);
@ -613,6 +632,8 @@ SessionImpl::SessionImpl(QObject *parent)
m_ioThread->setObjectName("SessionImpl m_ioThread");
m_ioThread->start();
QMetaObject::invokeMethod(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::check);
initMetrics();
loadStatistics();
@ -974,23 +995,25 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio
if (options == currentOptions)
return false;
currentOptions = options;
storeCategories();
if (isDisableAutoTMMWhenCategorySavePathChanged())
{
// This should be done before changing the category options
// to prevent the torrent from being moved at the new save path.
for (TorrentImpl *const torrent : asConst(m_torrents))
{
if (torrent->category() == name)
torrent->setAutoTMMEnabled(false);
}
}
else
currentOptions = options;
storeCategories();
for (TorrentImpl *const torrent : asConst(m_torrents))
{
for (TorrentImpl *const torrent : asConst(m_torrents))
{
if (torrent->category() == name)
torrent->handleCategoryOptionsChanged();
}
if (torrent->category() == name)
torrent->handleCategoryOptionsChanged();
}
emit categoryOptionsChanged(name);
@ -1234,8 +1257,7 @@ int SessionImpl::globalMaxSeedingMinutes() const
void SessionImpl::setGlobalMaxSeedingMinutes(int minutes)
{
if (minutes < 0)
minutes = -1;
minutes = std::clamp(minutes, Torrent::NO_SEEDING_TIME_LIMIT, Torrent::MAX_SEEDING_TIME);
if (minutes != globalMaxSeedingMinutes())
{
@ -1251,7 +1273,7 @@ int SessionImpl::globalMaxInactiveSeedingMinutes() const
void SessionImpl::setGlobalMaxInactiveSeedingMinutes(int minutes)
{
minutes = std::max(minutes, -1);
minutes = std::clamp(minutes, Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT, Torrent::MAX_INACTIVE_SEEDING_TIME);
if (minutes != globalMaxInactiveSeedingMinutes())
{
@ -2054,6 +2076,8 @@ lt::settings_pack SessionImpl::loadLTSettings() const
break;
}
settingsPack.set_int(lt::settings_pack::resolver_cache_timeout, hostnameCacheTTL());
settingsPack.set_bool(lt::settings_pack::allow_idna, isIDNSupportEnabled());
settingsPack.set_bool(lt::settings_pack::allow_multiple_connections_per_ip, multiConnectionsPerIpEnabled());
@ -2461,12 +2485,12 @@ bool SessionImpl::removeTorrent(const TorrentID &id, const TorrentRemoveOption d
m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), {}, deleteOption};
const lt::torrent_handle nativeHandle {torrent->nativeHandle()};
const auto iter = std::find_if(m_moveStorageQueue.begin(), m_moveStorageQueue.end()
const auto iter = std::find_if(m_moveStorageQueue.cbegin(), m_moveStorageQueue.cend()
, [&nativeHandle](const MoveStorageJob &job)
{
return job.torrentHandle == nativeHandle;
});
if (iter != m_moveStorageQueue.end())
if (iter != m_moveStorageQueue.cend())
{
// We shouldn't actually remove torrent until existing "move storage jobs" are done
torrentQueuePositionBottom(nativeHandle);
@ -2486,12 +2510,12 @@ bool SessionImpl::removeTorrent(const TorrentID &id, const TorrentRemoveOption d
{
// Delete "move storage job" for the deleted torrent
// (note: we shouldn't delete active job)
const auto iter = std::find_if((m_moveStorageQueue.begin() + 1), m_moveStorageQueue.end()
const auto iter = std::find_if((m_moveStorageQueue.cbegin() + 1), m_moveStorageQueue.cend()
, [torrent](const MoveStorageJob &job)
{
return job.torrentHandle == torrent->nativeHandle();
});
if (iter != m_moveStorageQueue.end())
if (iter != m_moveStorageQueue.cend())
m_moveStorageQueue.erase(iter);
}
@ -2508,8 +2532,8 @@ bool SessionImpl::removeTorrent(const TorrentID &id, const TorrentRemoveOption d
bool SessionImpl::cancelDownloadMetadata(const TorrentID &id)
{
const auto downloadedMetadataIter = m_downloadedMetadata.find(id);
if (downloadedMetadataIter == m_downloadedMetadata.end())
const auto downloadedMetadataIter = m_downloadedMetadata.constFind(id);
if (downloadedMetadataIter == m_downloadedMetadata.cend())
return false;
const lt::torrent_handle nativeHandle = downloadedMetadataIter.value();
@ -2751,7 +2775,10 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
// We should not add the torrent if it is already
// processed or is pending to add to session
if (m_loadingTorrents.contains(id) || (infoHash.isHybrid() && m_loadingTorrents.contains(altID)))
{
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, tr("Duplicate torrent")});
return false;
}
if (Torrent *torrent = findTorrent(infoHash))
{
@ -2765,16 +2792,20 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
if (!isMergeTrackersEnabled())
{
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
.arg(torrent->name(), tr("Merging of trackers is disabled")));
const QString message = tr("Merging of trackers is disabled");
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: \"%1\". Torrent infohash: %2. Result: %3")
.arg(torrent->name(), torrent->infoHash().toString(), message));
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
return false;
}
const bool isPrivate = torrent->isPrivate() || (hasMetadata && source.info()->isPrivate());
if (isPrivate)
{
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
.arg(torrent->name(), tr("Trackers cannot be merged because it is a private torrent")));
const QString message = tr("Trackers cannot be merged because it is a private torrent");
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: \"%1\". Torrent infohash: %2. Result: %3")
.arg(torrent->name(), torrent->infoHash().toString(), message));
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
return false;
}
@ -2782,8 +2813,10 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
torrent->addTrackers(source.trackers());
torrent->addUrlSeeds(source.urlSeeds());
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
.arg(torrent->name(), tr("Trackers are merged from new source")));
const QString message = tr("Trackers are merged from new source");
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: \"%1\". Torrent infohash: %2. Result: %3")
.arg(torrent->name(), torrent->infoHash().toString(), message));
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
return false;
}
@ -3247,6 +3280,9 @@ void SessionImpl::setSavePath(const Path &path)
if (isDisableAutoTMMWhenDefaultSavePathChanged())
{
// This should be done before changing the save path
// to prevent the torrent from being moved at the new save path.
QSet<QString> affectedCatogories {{}}; // includes default (unnamed) category
for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
{
@ -3266,6 +3302,14 @@ void SessionImpl::setSavePath(const Path &path)
m_savePath = newPath;
for (TorrentImpl *const torrent : asConst(m_torrents))
torrent->handleCategoryOptionsChanged();
m_freeDiskSpace = -1;
m_freeDiskSpaceCheckingTimer->stop();
QMetaObject::invokeMethod(m_freeDiskSpaceChecker, [checker = m_freeDiskSpaceChecker, pathToCheck = m_savePath]
{
checker->setPathToCheck(pathToCheck);
checker->check();
});
}
void SessionImpl::setDownloadPath(const Path &path)
@ -3276,6 +3320,9 @@ void SessionImpl::setDownloadPath(const Path &path)
if (isDisableAutoTMMWhenDefaultSavePathChanged())
{
// This should be done before changing the save path
// to prevent the torrent from being moved at the new save path.
QSet<QString> affectedCatogories {{}}; // includes default (unnamed) category
for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
{
@ -5024,6 +5071,20 @@ void SessionImpl::setUtpMixedMode(const MixedModeAlgorithm mode)
configureDeferred();
}
int SessionImpl::hostnameCacheTTL() const
{
return m_hostnameCacheTTL;
}
void SessionImpl::setHostnameCacheTTL(const int value)
{
if (value == hostnameCacheTTL())
return;
m_hostnameCacheTTL = value;
configureDeferred();
}
bool SessionImpl::isIDNSupportEnabled() const
{
return m_IDNSupportEnabled;
@ -5113,6 +5174,11 @@ QString SessionImpl::lastExternalIPv6Address() const
return m_lastExternalIPv6Address;
}
qint64 SessionImpl::freeDiskSpace() const
{
return m_freeDiskSpace;
}
bool SessionImpl::isListening() const
{
return m_nativeSessionExtension->isSessionListening();
@ -5268,8 +5334,8 @@ void SessionImpl::handleTorrentFinished(TorrentImpl *const torrent)
void SessionImpl::handleTorrentResumeDataReady(TorrentImpl *const torrent, const LoadTorrentParams &data)
{
m_resumeDataStorage->store(torrent->id(), data);
const auto iter = m_changedTorrentIDs.find(torrent->id());
if (iter != m_changedTorrentIDs.end())
const auto iter = m_changedTorrentIDs.constFind(torrent->id());
if (iter != m_changedTorrentIDs.cend())
{
m_resumeDataStorage->remove(iter.value());
m_changedTorrentIDs.erase(iter);
@ -5302,11 +5368,11 @@ bool SessionImpl::addMoveTorrentStorageJob(TorrentImpl *torrent, const Path &new
const lt::torrent_handle torrentHandle = torrent->nativeHandle();
const Path currentLocation = torrent->actualStorageLocation();
const bool torrentHasActiveJob = !m_moveStorageQueue.isEmpty() && (m_moveStorageQueue.first().torrentHandle == torrentHandle);
const bool torrentHasActiveJob = !m_moveStorageQueue.isEmpty() && (m_moveStorageQueue.constFirst().torrentHandle == torrentHandle);
if (m_moveStorageQueue.size() > 1)
{
auto iter = std::find_if((m_moveStorageQueue.begin() + 1), m_moveStorageQueue.end()
auto iter = std::find_if((m_moveStorageQueue.cbegin() + 1), m_moveStorageQueue.cend()
, [&torrentHandle](const MoveStorageJob &job)
{
return job.torrentHandle == torrentHandle;
@ -5325,7 +5391,7 @@ bool SessionImpl::addMoveTorrentStorageJob(TorrentImpl *torrent, const Path &new
{
// if there is active job for this torrent prevent creating meaningless
// job that will move torrent to the same location as current one
if (m_moveStorageQueue.first().path == newPath)
if (m_moveStorageQueue.constFirst().path == newPath)
{
LogMsg(tr("Failed to enqueue torrent move. Torrent: \"%1\". Source: \"%2\". Destination: \"%3\". Reason: torrent is currently moving to the destination")
.arg(torrent->name(), currentLocation.toString(), newPath.toString()));
@ -5370,7 +5436,7 @@ void SessionImpl::handleMoveTorrentStorageJobFinished(const Path &newPath)
{
const MoveStorageJob finishedJob = m_moveStorageQueue.takeFirst();
if (!m_moveStorageQueue.isEmpty())
moveTorrentStorage(m_moveStorageQueue.first());
moveTorrentStorage(m_moveStorageQueue.constFirst());
const auto iter = std::find_if(m_moveStorageQueue.cbegin(), m_moveStorageQueue.cend()
, [&finishedJob](const MoveStorageJob &job)
@ -5696,14 +5762,16 @@ void SessionImpl::handleAddTorrentAlert(const lt::add_torrent_alert *alert)
#else
const InfoHash infoHash {(hasMetadata ? params.ti->info_hash() : params.info_hash)};
#endif
if (const auto loadingTorrentsIter = m_loadingTorrents.find(TorrentID::fromInfoHash(infoHash))
; loadingTorrentsIter != m_loadingTorrents.end())
if (const auto loadingTorrentsIter = m_loadingTorrents.constFind(TorrentID::fromInfoHash(infoHash))
; loadingTorrentsIter != m_loadingTorrents.cend())
{
emit addTorrentFailed(infoHash, msg);
const AddTorrentError::Kind errorKind = (alert->error == lt::errors::duplicate_torrent)
? AddTorrentError::DuplicateTorrent : AddTorrentError::Other;
emit addTorrentFailed(infoHash, {errorKind, msg});
m_loadingTorrents.erase(loadingTorrentsIter);
}
else if (const auto downloadedMetadataIter = m_downloadedMetadata.find(TorrentID::fromInfoHash(infoHash))
; downloadedMetadataIter != m_downloadedMetadata.end())
else if (const auto downloadedMetadataIter = m_downloadedMetadata.constFind(TorrentID::fromInfoHash(infoHash))
; downloadedMetadataIter != m_downloadedMetadata.cend())
{
m_downloadedMetadata.erase(downloadedMetadataIter);
if (infoHash.isHybrid())
@ -5724,8 +5792,8 @@ void SessionImpl::handleAddTorrentAlert(const lt::add_torrent_alert *alert)
#endif
const auto torrentID = TorrentID::fromInfoHash(infoHash);
if (const auto loadingTorrentsIter = m_loadingTorrents.find(torrentID)
; loadingTorrentsIter != m_loadingTorrents.end())
if (const auto loadingTorrentsIter = m_loadingTorrents.constFind(torrentID)
; loadingTorrentsIter != m_loadingTorrents.cend())
{
const LoadTorrentParams params = loadingTorrentsIter.value();
m_loadingTorrents.erase(loadingTorrentsIter);
@ -5988,7 +6056,7 @@ void SessionImpl::handleMetadataReceivedAlert(const lt::metadata_received_alert
const TorrentID torrentID {alert->handle.info_hash()};
bool found = false;
if (const auto iter = m_downloadedMetadata.find(torrentID); iter != m_downloadedMetadata.end())
if (const auto iter = m_downloadedMetadata.constFind(torrentID); iter != m_downloadedMetadata.cend())
{
found = true;
m_downloadedMetadata.erase(iter);
@ -5998,7 +6066,7 @@ void SessionImpl::handleMetadataReceivedAlert(const lt::metadata_received_alert
if (infoHash.isHybrid())
{
const auto altID = TorrentID::fromSHA1Hash(infoHash.v1());
if (const auto iter = m_downloadedMetadata.find(altID); iter != m_downloadedMetadata.end())
if (const auto iter = m_downloadedMetadata.constFind(altID); iter != m_downloadedMetadata.cend())
{
found = true;
m_downloadedMetadata.erase(iter);
@ -6260,7 +6328,7 @@ void SessionImpl::handleStorageMovedAlert(const lt::storage_moved_alert *alert)
{
Q_ASSERT(!m_moveStorageQueue.isEmpty());
const MoveStorageJob &currentJob = m_moveStorageQueue.first();
const MoveStorageJob &currentJob = m_moveStorageQueue.constFirst();
Q_ASSERT(currentJob.torrentHandle == alert->handle);
const Path newPath {QString::fromUtf8(alert->storage_path())};
@ -6283,7 +6351,7 @@ void SessionImpl::handleStorageMovedFailedAlert(const lt::storage_moved_failed_a
{
Q_ASSERT(!m_moveStorageQueue.isEmpty());
const MoveStorageJob &currentJob = m_moveStorageQueue.first();
const MoveStorageJob &currentJob = m_moveStorageQueue.constFirst();
Q_ASSERT(currentJob.torrentHandle == alert->handle);
#ifdef QBT_USES_LIBTORRENT2
@ -6497,8 +6565,8 @@ void SessionImpl::updateTrackerEntryStatuses(lt::torrent_handle torrentHandle)
void SessionImpl::handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError)
{
const auto removingTorrentDataIter = m_removingTorrents.find(torrentID);
if (removingTorrentDataIter == m_removingTorrents.end())
const auto removingTorrentDataIter = m_removingTorrents.constFind(torrentID);
if (removingTorrentDataIter == m_removingTorrents.cend())
return;
if (!partfileRemoveError.isEmpty())

View file

@ -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
@ -64,6 +64,7 @@ class QUrl;
class BandwidthScheduler;
class FileSearcher;
class FilterParserThread;
class FreeDiskSpaceChecker;
class NativeSessionExtension;
namespace BitTorrent
@ -390,6 +391,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 +451,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);
@ -683,6 +688,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;
@ -850,6 +856,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();

View file

@ -1615,18 +1615,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;

View file

@ -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);

View file

@ -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));
}

View file

@ -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;
};

View file

@ -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;

View file

@ -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;
}
}

View file

@ -30,8 +30,10 @@
#pragma once
#include <QByteArray>
#include <QHash>
#include <QHostAddress>
#include <QList>
#include <QMap>
#include <QString>
#include "base/global.h"

View file

@ -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)

View file

@ -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)

View file

@ -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);

View file

@ -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)

View file

@ -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())

View file

@ -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);

View file

@ -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)

View file

@ -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);

View file

@ -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;
}

View file

@ -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)
{

View file

@ -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;

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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 &currentTimepoint)
{
feed->refresh();
const std::chrono::seconds feedRefreshInterval = feed->refreshInterval();
const std::chrono::seconds effectiveRefreshInterval = (feedRefreshInterval > 0s) ? feedRefreshInterval : std::chrono::minutes(refreshInterval());
return currentTimepoint + effectiveRefreshInterval;
}

View file

@ -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 &currentTimepoint);
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;
};
}

View file

@ -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());
#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
m_downloadProcess->setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif
connect(m_downloadProcess, qOverload<int, QProcess::ExitStatus>(&QProcess::finished)
, this, &SearchDownloadHandler::downloadProcessFinished);
const QStringList params

View file

@ -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().executableName);
#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
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);

View file

@ -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());
#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
nova.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif
const QStringList params
{
@ -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;

View file

@ -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;
};

View file

@ -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

View file

@ -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();

View file

@ -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);

View file

@ -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);

View file

@ -57,6 +57,9 @@ namespace
info = {};
QProcess proc;
#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
proc.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif
proc.start(exeName, {u"--version"_s}, QIODevice::ReadOnly);
if (proc.waitForFinished() && (proc.exitCode() == QProcess::NormalExit))
{
@ -68,7 +71,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;

View file

@ -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;

View file

@ -49,12 +49,13 @@ namespace Utils::String
template <typename T>
T unquote(const T &str, const QString &quotes = 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;

View file

@ -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)

View file

@ -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()

View file

@ -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:"));

View file

@ -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
@ -122,7 +122,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 +134,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);
}
}
@ -375,8 +378,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 +386,6 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
AddNewTorrentDialog::~AddNewTorrentDialog()
{
saveState();
delete m_ui;
}
@ -398,7 +399,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 +835,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);

View file

@ -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;

View file

@ -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"
@ -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,
@ -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
@ -732,6 +736,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 +868,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());

View file

@ -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,
@ -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)

View file

@ -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);
}
}

View file

@ -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 &params, 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;

View file

@ -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 &params = {}, AddTorrentOption option = AddTorrentOption::Default);

View 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);
}

View 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;
};

View file

@ -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);
@ -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);
}

View file

@ -149,8 +149,8 @@
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionDownloadFromURL"/>
<addaction name="actionOpen"/>
<addaction name="actionDownloadFromURL"/>
<addaction name="actionDelete"/>
<addaction name="separator"/>
<addaction name="actionStart"/>

View file

@ -355,6 +355,7 @@ void OptionsDialog::loadBehaviorTabOptions()
// Groupbox's check state must be initialized after some of its children if they are manually enabled/disabled
m_ui->checkFileLog->setChecked(app()->isFileLoggerEnabled());
m_ui->checkBoxFreeDiskSpaceStatusBar->setChecked(pref->isStatusbarFreeDiskSpaceDisplayed());
m_ui->checkBoxExternalIPStatusBar->setChecked(pref->isStatusbarExternalIPDisplayed());
m_ui->checkBoxPerformanceWarning->setChecked(session->isPerformanceWarningEnabled());
@ -443,6 +444,7 @@ void OptionsDialog::loadBehaviorTabOptions()
connect(m_ui->spinFileLogAge, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
connect(m_ui->comboFileLogAgeType, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkBoxFreeDiskSpaceStatusBar, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkBoxExternalIPStatusBar, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkBoxPerformanceWarning, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
}
@ -536,6 +538,7 @@ void OptionsDialog::saveBehaviorTabOptions() const
app()->setStartUpWindowState(m_ui->windowStateComboBox->currentData().value<WindowState>());
pref->setStatusbarFreeDiskSpaceDisplayed(m_ui->checkBoxFreeDiskSpaceStatusBar->isChecked());
pref->setStatusbarExternalIPDisplayed(m_ui->checkBoxExternalIPStatusBar->isChecked());
session->setPerformanceWarningEnabled(m_ui->checkBoxPerformanceWarning->isChecked());
}
@ -1686,7 +1689,7 @@ void OptionsDialog::adjustProxyOptions()
if (currentProxyType == Net::ProxyType::None)
{
m_ui->labelProxyTypeIncompatible->setVisible(false);
m_ui->labelProxyTypeUnavailable->setVisible(false);
m_ui->lblProxyIP->setEnabled(false);
m_ui->textProxyIP->setEnabled(false);
@ -1711,7 +1714,7 @@ void OptionsDialog::adjustProxyOptions()
if (currentProxyType == Net::ProxyType::SOCKS4)
{
m_ui->labelProxyTypeIncompatible->setVisible(true);
m_ui->labelProxyTypeUnavailable->setVisible(true);
m_ui->checkProxyHostnameLookup->setEnabled(false);
m_ui->checkProxyRSS->setEnabled(false);
@ -1720,7 +1723,7 @@ void OptionsDialog::adjustProxyOptions()
else
{
// SOCKS5 or HTTP
m_ui->labelProxyTypeIncompatible->setVisible(false);
m_ui->labelProxyTypeUnavailable->setVisible(false);
m_ui->checkProxyHostnameLookup->setEnabled(true);
m_ui->checkProxyRSS->setEnabled(true);
@ -1856,10 +1859,10 @@ void OptionsDialog::setLocale(const QString &localeStr)
if (index < 0)
{
//Attempt to find a language match without a country
int pos = name.indexOf(u'_');
const int pos = name.indexOf(u'_');
if (pos > -1)
{
QString lang = name.left(pos);
const QString lang = name.first(pos);
index = m_ui->comboLanguage->findData(lang, Qt::UserRole);
}
}

View file

@ -819,6 +819,13 @@
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBoxFreeDiskSpaceStatusBar">
<property name="text">
<string>Show free disk space in status bar</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBoxExternalIPStatusBar">
<property name="text">
@ -2070,14 +2077,14 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
</layout>
</item>
<item>
<widget class="QLabel" name="labelProxyTypeIncompatible">
<widget class="QLabel" name="labelProxyTypeUnavailable">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Some options are incompatible with the chosen proxy type!</string>
<string>Some functions are unavailable with the chosen proxy type!</string>
</property>
</widget>
</item>
@ -2137,7 +2144,7 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
<item>
<widget class="QLabel" name="label_23">
<property name="text">
<string>Info: The password is saved unencrypted</string>
<string>Note: The password is saved unencrypted</string>
</property>
</widget>
</item>

View file

@ -0,0 +1,40 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
* Copyright (C) 2011 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.
*/
#include "inhibitor.h"
bool Inhibitor::requestBusy()
{
return true;
}
bool Inhibitor::requestIdle()
{
return true;
}

View file

@ -0,0 +1,38 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
*
* 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
class Inhibitor
{
public:
virtual ~Inhibitor() = default;
virtual bool requestBusy();
virtual bool requestIdle();
};

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
* Copyright (C) 2011 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
@ -26,7 +27,7 @@
* exception statement from your version.
*/
#include "powermanagement_x11.h"
#include "inhibitordbus.h"
#include <QDBusConnection>
#include <QDBusInterface>
@ -36,7 +37,7 @@
#include "base/global.h"
#include "base/logger.h"
PowerManagementInhibitor::PowerManagementInhibitor(QObject *parent)
InhibitorDBus::InhibitorDBus(QObject *parent)
: QObject(parent)
, m_busInterface {new QDBusInterface(u"org.gnome.SessionManager"_s, u"/org/gnome/SessionManager"_s
, u"org.gnome.SessionManager"_s, QDBusConnection::sessionBus(), this)}
@ -70,38 +71,26 @@ PowerManagementInhibitor::PowerManagementInhibitor(QObject *parent)
}
else
{
LogMsg(tr("Power management error. Did not found suitable D-Bus interface."), Log::WARNING);
m_state = Error;
LogMsg(tr("Power management error. Did not find a suitable D-Bus interface."), Log::WARNING);
}
}
void PowerManagementInhibitor::requestIdle()
{
m_intendedState = Idle;
if ((m_state == Error) || (m_state == Idle) || (m_state == RequestIdle) || (m_state == RequestBusy))
return;
if (m_manager == ManagerType::Systemd)
{
m_fd = {};
m_state = Idle;
return;
}
m_state = RequestIdle;
const QString method = (m_manager == ManagerType::Gnome)
? u"Uninhibit"_s
: u"UnInhibit"_s;
const QDBusPendingCall pcall = m_busInterface->asyncCall(method, m_cookie);
const auto *watcher = new QDBusPendingCallWatcher(pcall, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, &PowerManagementInhibitor::onAsyncReply);
}
void PowerManagementInhibitor::requestBusy()
bool InhibitorDBus::requestBusy()
{
m_intendedState = Busy;
if ((m_state == Error) || (m_state == Busy) || (m_state == RequestBusy) || (m_state == RequestIdle))
return;
switch (m_state)
{
case Busy:
case RequestBusy:
return true;
case Error:
case RequestIdle:
return false;
case Idle:
break;
};
m_state = RequestBusy;
@ -123,10 +112,45 @@ void PowerManagementInhibitor::requestBusy()
const QDBusPendingCall pcall = m_busInterface->asyncCallWithArgumentList(u"Inhibit"_s, args);
const auto *watcher = new QDBusPendingCallWatcher(pcall, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, &PowerManagementInhibitor::onAsyncReply);
connect(watcher, &QDBusPendingCallWatcher::finished, this, &InhibitorDBus::onAsyncReply);
return true;
}
void PowerManagementInhibitor::onAsyncReply(QDBusPendingCallWatcher *call)
bool InhibitorDBus::requestIdle()
{
m_intendedState = Idle;
switch (m_state)
{
case Idle:
case RequestIdle:
return true;
case Error:
case RequestBusy:
return false;
case Busy:
break;
};
if (m_manager == ManagerType::Systemd)
{
m_fd = {};
m_state = Idle;
return true;
}
m_state = RequestIdle;
const QString method = (m_manager == ManagerType::Gnome)
? u"Uninhibit"_s
: u"UnInhibit"_s;
const QDBusPendingCall pcall = m_busInterface->asyncCall(method, m_cookie);
const auto *watcher = new QDBusPendingCallWatcher(pcall, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, &InhibitorDBus::onAsyncReply);
return true;
}
void InhibitorDBus::onAsyncReply(QDBusPendingCallWatcher *call)
{
call->deleteLater();

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
* Copyright (C) 2011 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
@ -31,20 +32,21 @@
#include <QDBusUnixFileDescriptor>
#include <QObject>
#include "inhibitor.h"
class QDBusInterface;
class QDBusPendingCallWatcher;
class PowerManagementInhibitor final : public QObject
class InhibitorDBus final : public QObject, public Inhibitor
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(PowerManagementInhibitor)
Q_DISABLE_COPY_MOVE(InhibitorDBus)
public:
PowerManagementInhibitor(QObject *parent = nullptr);
~PowerManagementInhibitor() override = default;
InhibitorDBus(QObject *parent = nullptr);
void requestIdle();
void requestBusy();
bool requestBusy() override;
bool requestIdle() override;
private slots:
void onAsyncReply(QDBusPendingCallWatcher *call);

View file

@ -0,0 +1,46 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
* Copyright (C) 2011 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.
*/
#include "inhibitormacos.h"
#include <QScopeGuard>
bool InhibitorMacOS::requestBusy()
{
const CFStringRef assertName = tr("PMMacOS", "qBittorrent is active").toCFString();
[[maybe_unused]] const auto assertNameGuard = qScopeGuard([&assertName] { ::CFRelease(assertName); });
const IOReturn result = ::IOPMAssertionCreateWithName(kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn
, assertName, &m_assertionID);
return result == kIOReturnSuccess;
}
bool InhibitorMacOS::requestIdle()
{
return ::IOPMAssertionRelease(m_assertionID) == kIOReturnSuccess;
}

View file

@ -0,0 +1,47 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
*
* 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 <IOKit/pwr_mgt/IOPMLib.h>
#include <QCoreApplication>
#include "inhibitor.h"
class InhibitorMacOS final : public Inhibitor
{
Q_DECLARE_TR_FUNCTIONS(InhibitorMacOS)
public:
bool requestBusy() override;
bool requestIdle() override;
private:
IOPMAssertionID m_assertionID {};
};

View file

@ -0,0 +1,42 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
* Copyright (C) 2011 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.
*/
#include "inhibitorwindows.h"
#include <windows.h>
bool InhibitorWindows::requestBusy()
{
return ::SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) != NULL;
}
bool InhibitorWindows::requestIdle()
{
return ::SetThreadExecutionState(ES_CONTINUOUS) != NULL;
}

View file

@ -0,0 +1,38 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
*
* 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 "inhibitor.h"
class InhibitorWindows final : public Inhibitor
{
public:
bool requestBusy() override;
bool requestIdle() override;
};

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
* Copyright (C) 2011 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
@ -30,71 +31,59 @@
#include <QtSystemDetection>
#ifdef Q_OS_MACOS
#include <IOKit/pwr_mgt/IOPMLib.h>
#include <QScopeGuard>
#if defined(Q_OS_MACOS)
#include "inhibitormacos.h"
using InhibitorImpl = InhibitorMacOS;
#elif defined(Q_OS_WIN)
#include "inhibitorwindows.h"
using InhibitorImpl = InhibitorWindows;
#elif defined(QBT_USES_DBUS)
#include "inhibitordbus.h"
using InhibitorImpl = InhibitorDBus;
#else
#include "inhibitor.h"
using InhibitorImpl = Inhibitor;
#endif
#ifdef Q_OS_WIN
#include <windows.h>
#endif
#ifdef QBT_USES_DBUS
#include "powermanagement_x11.h"
#endif
PowerManagement::PowerManagement(QObject *parent)
: QObject(parent)
#ifdef QBT_USES_DBUS
, m_inhibitor {new PowerManagementInhibitor(this)}
#endif
PowerManagement::PowerManagement()
: m_inhibitor {new InhibitorImpl}
{
}
PowerManagement::~PowerManagement()
{
setIdle();
delete m_inhibitor;
}
void PowerManagement::setActivityState(const bool busy)
void PowerManagement::setActivityState(const ActivityState state)
{
if (busy)
switch (state)
{
case ActivityState::Busy:
setBusy();
else
break;
case ActivityState::Idle:
setIdle();
break;
};
}
void PowerManagement::setBusy()
{
if (m_busy)
if (m_state == ActivityState::Busy)
return;
m_busy = true;
#ifdef Q_OS_WIN
::SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED);
#elif defined(QBT_USES_DBUS)
m_inhibitor->requestBusy();
#elif defined(Q_OS_MACOS)
const CFStringRef assertName = tr("qBittorrent is active").toCFString();
[[maybe_unused]] const auto assertNameGuard = qScopeGuard([&assertName] { ::CFRelease(assertName); });
const IOReturn success = ::IOPMAssertionCreateWithName(kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn
, assertName, &m_assertionID);
if (success != kIOReturnSuccess)
m_busy = false;
#endif
if (m_inhibitor->requestBusy())
m_state = ActivityState::Busy;
}
void PowerManagement::setIdle()
{
if (!m_busy)
if (m_state == ActivityState::Idle)
return;
m_busy = false;
#ifdef Q_OS_WIN
::SetThreadExecutionState(ES_CONTINUOUS);
#elif defined(QBT_USES_DBUS)
m_inhibitor->requestIdle();
#elif defined(Q_OS_MACOS)
::IOPMAssertionRelease(m_assertionID);
#endif
if (m_inhibitor->requestIdle())
m_state = ActivityState::Idle;
}

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
* Copyright (C) 2011 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
@ -28,38 +29,26 @@
#pragma once
#include <QObject>
class Inhibitor;
#ifdef Q_OS_MACOS
// Require Mac OS X >= 10.5
#include <IOKit/pwr_mgt/IOPMLib.h>
#endif
#ifdef QBT_USES_DBUS
class PowerManagementInhibitor;
#endif
class PowerManagement final : public QObject
class PowerManagement final
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(PowerManagement)
public:
PowerManagement(QObject *parent = nullptr);
~PowerManagement() override;
enum class ActivityState
{
Busy,
Idle
};
void setActivityState(bool busy);
PowerManagement();
~PowerManagement();
void setActivityState(ActivityState state);
private:
void setBusy();
void setIdle();
bool m_busy = false;
#ifdef QBT_USES_DBUS
PowerManagementInhibitor *m_inhibitor = nullptr;
#endif
#ifdef Q_OS_MACOS
IOPMAssertionID m_assertionID {};
#endif
ActivityState m_state = ActivityState::Idle;
Inhibitor *m_inhibitor = nullptr;
};

View file

@ -58,6 +58,7 @@
#include "base/utils/misc.h"
#include "base/utils/string.h"
#include "gui/uithememanager.h"
#include "gui/utils/keysequence.h"
#include "peerlistsortmodel.h"
#include "peersadditiondialog.h"
#include "propertieswidget.h"
@ -187,7 +188,7 @@ PeerListWidget::PeerListWidget(PropertiesWidget *parent)
handleSortColumnChanged(header()->sortIndicatorSection());
const auto *copyHotkey = new QShortcut(QKeySequence::Copy, this, nullptr, nullptr, Qt::WidgetShortcut);
connect(copyHotkey, &QShortcut::activated, this, &PeerListWidget::copySelectedPeers);
const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
const auto *deleteHotkey = new QShortcut(Utils::KeySequence::deleteItem(), this, nullptr, nullptr, Qt::WidgetShortcut);
connect(deleteHotkey, &QShortcut::activated, this, &PeerListWidget::banSelectedPeers);
}

View file

@ -56,6 +56,7 @@
#include "gui/trackerlist/trackerlistwidget.h"
#include "gui/uithememanager.h"
#include "gui/utils.h"
#include "gui/utils/keysequence.h"
#include "downloadedpiecesbar.h"
#include "peerlistwidget.h"
#include "pieceavailabilitybar.h"
@ -136,7 +137,7 @@ PropertiesWidget::PropertiesWidget(QWidget *parent)
const auto *editWebSeedsHotkey = new QShortcut(Qt::Key_F2, m_ui->listWebSeeds, nullptr, nullptr, Qt::WidgetShortcut);
connect(editWebSeedsHotkey, &QShortcut::activated, this, &PropertiesWidget::editWebSeed);
const auto *deleteWebSeedsHotkey = new QShortcut(QKeySequence::Delete, m_ui->listWebSeeds, nullptr, nullptr, Qt::WidgetShortcut);
const auto *deleteWebSeedsHotkey = new QShortcut(Utils::KeySequence::deleteItem(), m_ui->listWebSeeds, nullptr, nullptr, Qt::WidgetShortcut);
connect(deleteWebSeedsHotkey, &QShortcut::activated, this, &PropertiesWidget::deleteSelectedUrlSeeds);
connect(m_ui->listWebSeeds, &QListWidget::doubleClicked, this, &PropertiesWidget::editWebSeed);

View file

@ -55,6 +55,7 @@
#include "gui/torrentcategorydialog.h"
#include "gui/uithememanager.h"
#include "gui/utils.h"
#include "gui/utils/keysequence.h"
#include "ui_automatedrssdownloader.h"
const QString EXT_JSON = u".json"_s;
@ -151,7 +152,7 @@ AutomatedRssDownloader::AutomatedRssDownloader(QWidget *parent)
const auto *editHotkey = new QShortcut(Qt::Key_F2, m_ui->ruleList, nullptr, nullptr, Qt::WidgetShortcut);
connect(editHotkey, &QShortcut::activated, this, &AutomatedRssDownloader::renameSelectedRule);
const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, m_ui->ruleList, nullptr, nullptr, Qt::WidgetShortcut);
const auto *deleteHotkey = new QShortcut(Utils::KeySequence::deleteItem(), m_ui->ruleList, nullptr, nullptr, Qt::WidgetShortcut);
connect(deleteHotkey, &QShortcut::activated, this, &AutomatedRssDownloader::onRemoveRuleBtnClicked);
connect(m_ui->ruleList, &QAbstractItemView::doubleClicked, this, &AutomatedRssDownloader::renameSelectedRule);

View file

@ -0,0 +1,82 @@
/*
* 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.
*/
#include "rssfeeddialog.h"
#include <QPushButton>
#include "ui_rssfeeddialog.h"
RSSFeedDialog::RSSFeedDialog(QWidget *parent)
: QDialog(parent)
, m_ui {new Ui::RSSFeedDialog}
{
m_ui->setupUi(this);
m_ui->spinRefreshInterval->setMaximum(std::numeric_limits<int>::max());
m_ui->spinRefreshInterval->setStepType(QAbstractSpinBox::AdaptiveDecimalStepType);
m_ui->spinRefreshInterval->setSuffix(tr(" sec"));
m_ui->spinRefreshInterval->setSpecialValueText(tr("Default"));
// disable Ok button
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(m_ui->textFeedURL, &QLineEdit::textChanged, this, &RSSFeedDialog::feedURLChanged);
}
RSSFeedDialog::~RSSFeedDialog()
{
delete m_ui;
}
QString RSSFeedDialog::feedURL() const
{
return m_ui->textFeedURL->text();
}
void RSSFeedDialog::setFeedURL(const QString &feedURL)
{
m_ui->textFeedURL->setText(feedURL);
}
std::chrono::seconds RSSFeedDialog::refreshInterval() const
{
return std::chrono::seconds(m_ui->spinRefreshInterval->value());
}
void RSSFeedDialog::setRefreshInterval(const std::chrono::seconds refreshInterval)
{
m_ui->spinRefreshInterval->setValue(refreshInterval.count());
}
void RSSFeedDialog::feedURLChanged(const QString &feedURL)
{
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!feedURL.isEmpty());
}

View file

@ -0,0 +1,57 @@
/*
* 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 <chrono>
#include <QDialog>
namespace Ui
{
class RSSFeedDialog;
}
class RSSFeedDialog final : public QDialog
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(RSSFeedDialog)
public:
explicit RSSFeedDialog(QWidget *parent = nullptr);
~RSSFeedDialog() override;
QString feedURL() const;
void setFeedURL(const QString &feedURL);
std::chrono::seconds refreshInterval() const;
void setRefreshInterval(std::chrono::seconds refreshInterval);
private:
void feedURLChanged(const QString &feedURL);
Ui::RSSFeedDialog *m_ui = nullptr;
};

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RSSFeedDialog</class>
<widget class="QDialog" name="RSSFeedDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>555</width>
<height>106</height>
</rect>
</property>
<property name="windowTitle">
<string>RSS Feed Options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="labelFeedURL">
<property name="text">
<string>URL:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="textFeedURL"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelRefreshInterval">
<property name="text">
<string>Refresh interval:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinRefreshInterval">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -49,9 +49,11 @@
#include "gui/autoexpandabledialog.h"
#include "gui/interfaces/iguiapplication.h"
#include "gui/uithememanager.h"
#include "gui/utils/keysequence.h"
#include "articlelistwidget.h"
#include "automatedrssdownloader.h"
#include "feedlistwidget.h"
#include "rssfeeddialog.h"
#include "ui_rsswidget.h"
namespace
@ -69,8 +71,8 @@ namespace
while (iter.hasNext())
{
const QRegularExpressionMatch match = iter.next();
const QString scheme = match.captured(4);
const QString host = match.captured(5);
const QStringView scheme = match.capturedView(4);
const QStringView host = match.capturedView(5);
if (!scheme.isEmpty())
{
if (host.isEmpty())
@ -80,15 +82,21 @@ namespace
continue;
}
QString relativePath = match.captured(6);
QStringView relativePath = match.capturedView(6);
if (relativePath.startsWith(u'/'))
relativePath = relativePath.mid(1);
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
relativePath.slice(1);
#else
relativePath = relativePath.sliced(1);
#endif
}
const QString absoluteUrl = !host.isEmpty()
? QString(defaultScheme + u':' + host) : (normalizedBaseUrl + relativePath);
const QString fullMatch = match.captured(0);
const QString prefix = match.captured(1);
const QString suffix = match.captured(7);
const QStringView prefix = match.capturedView(1);
const QStringView suffix = match.capturedView(7);
html.replace(fullMatch, (prefix + absoluteUrl + suffix));
}
@ -106,7 +114,7 @@ RSSWidget::RSSWidget(IGUIApplication *app, QWidget *parent)
m_ui->actionCopyFeedURL->setIcon(UIThemeManager::instance()->getIcon(u"edit-copy"_s));
m_ui->actionDelete->setIcon(UIThemeManager::instance()->getIcon(u"edit-clear"_s));
m_ui->actionDownloadTorrent->setIcon(UIThemeManager::instance()->getIcon(u"downloading"_s, u"download"_s));
m_ui->actionEditFeedURL->setIcon(UIThemeManager::instance()->getIcon(u"edit-rename"_s));
m_ui->actionEditFeed->setIcon(UIThemeManager::instance()->getIcon(u"edit-rename"_s));
m_ui->actionMarkItemsRead->setIcon(UIThemeManager::instance()->getIcon(u"task-complete"_s, u"mail-mark-read"_s));
m_ui->actionNewFolder->setIcon(UIThemeManager::instance()->getIcon(u"folder-new"_s));
m_ui->actionNewSubscription->setIcon(UIThemeManager::instance()->getIcon(u"list-add"_s));
@ -133,13 +141,13 @@ RSSWidget::RSSWidget(IGUIApplication *app, QWidget *parent)
const auto *editHotkey = new QShortcut(Qt::Key_F2, m_ui->feedListWidget, nullptr, nullptr, Qt::WidgetShortcut);
connect(editHotkey, &QShortcut::activated, this, &RSSWidget::renameSelectedRSSItem);
const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, m_ui->feedListWidget, nullptr, nullptr, Qt::WidgetShortcut);
const auto *deleteHotkey = new QShortcut(Utils::KeySequence::deleteItem(), m_ui->feedListWidget, nullptr, nullptr, Qt::WidgetShortcut);
connect(deleteHotkey, &QShortcut::activated, this, &RSSWidget::deleteSelectedItems);
// Feeds list actions
connect(m_ui->actionDelete, &QAction::triggered, this, &RSSWidget::deleteSelectedItems);
connect(m_ui->actionRename, &QAction::triggered, this, &RSSWidget::renameSelectedRSSItem);
connect(m_ui->actionEditFeedURL, &QAction::triggered, this, &RSSWidget::editSelectedRSSFeedURL);
connect(m_ui->actionEditFeed, &QAction::triggered, this, &RSSWidget::editSelectedRSSFeed);
connect(m_ui->actionUpdate, &QAction::triggered, this, &RSSWidget::refreshSelectedItems);
connect(m_ui->actionNewFolder, &QAction::triggered, this, &RSSWidget::askNewFolder);
connect(m_ui->actionNewSubscription, &QAction::triggered, this, &RSSWidget::on_newFeedButton_clicked);
@ -203,7 +211,7 @@ void RSSWidget::displayRSSListMenu(const QPoint &pos)
{
menu->addAction(m_ui->actionRename);
if (m_ui->feedListWidget->isFeed(selectedItem))
menu->addAction(m_ui->actionEditFeedURL);
menu->addAction(m_ui->actionEditFeed);
menu->addAction(m_ui->actionDelete);
menu->addSeparator();
if (m_ui->feedListWidget->isFolder(selectedItem))
@ -286,36 +294,29 @@ void RSSWidget::askNewFolder()
}
// Consider the case where the user clicked on Unread item
RSS::Folder *rssDestFolder = ((!destItem || (destItem == m_ui->feedListWidget->stickyUnreadItem()))
? RSS::Session::instance()->rootFolder()
: qobject_cast<RSS::Folder *>(m_ui->feedListWidget->getRSSItem(destItem)));
? RSS::Session::instance()->rootFolder()
: qobject_cast<RSS::Folder *>(m_ui->feedListWidget->getRSSItem(destItem)));
const QString newFolderPath = RSS::Item::joinPath(rssDestFolder->path(), newName);
const nonstd::expected<void, QString> result = RSS::Session::instance()->addFolder(newFolderPath);
const nonstd::expected<RSS::Folder *, QString> result = RSS::Session::instance()->addFolder(newFolderPath);
if (!result)
{
QMessageBox::warning(this, u"qBittorrent"_s, result.error(), QMessageBox::Ok);
return;
}
RSS::Folder *newFolder = result.value();
// Expand destination folder to display new feed
if (destItem && (destItem != m_ui->feedListWidget->stickyUnreadItem()))
destItem->setExpanded(true);
// As new RSS items are added synchronously, we can do the following here.
m_ui->feedListWidget->setCurrentItem(m_ui->feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFolderPath)));
m_ui->feedListWidget->setCurrentItem(m_ui->feedListWidget->mapRSSItem(newFolder));
}
// add a stream by a button
void RSSWidget::on_newFeedButton_clicked()
{
// Ask for feed URL
const QString clipText = qApp->clipboard()->text();
const QString defaultURL = Net::DownloadManager::hasSupportedScheme(clipText) ? clipText : u"http://"_s;
bool ok = false;
QString newURL = AutoExpandableDialog::getText(
this, tr("Please type a RSS feed URL"), tr("Feed URL:"), QLineEdit::Normal, defaultURL, &ok);
if (!ok) return;
newURL = newURL.trimmed();
if (newURL.isEmpty()) return;
// Determine destination folder for new item
QTreeWidgetItem *destItem = nullptr;
QList<QTreeWidgetItem *> selectedItems = m_ui->feedListWidget->selectedItems();
@ -326,21 +327,38 @@ void RSSWidget::on_newFeedButton_clicked()
destItem = destItem->parent();
}
// Consider the case where the user clicked on Unread item
RSS::Folder *rssDestFolder = ((!destItem || (destItem == m_ui->feedListWidget->stickyUnreadItem()))
? RSS::Session::instance()->rootFolder()
: qobject_cast<RSS::Folder *>(m_ui->feedListWidget->getRSSItem(destItem)));
RSS::Folder *destFolder = ((!destItem || (destItem == m_ui->feedListWidget->stickyUnreadItem()))
? RSS::Session::instance()->rootFolder()
: qobject_cast<RSS::Folder *>(m_ui->feedListWidget->getRSSItem(destItem)));
// NOTE: We still add feed using legacy way (with URL as feed name)
const QString newFeedPath = RSS::Item::joinPath(rssDestFolder->path(), newURL);
const nonstd::expected<void, QString> result = RSS::Session::instance()->addFeed(newURL, newFeedPath);
if (!result)
QMessageBox::warning(this, u"qBittorrent"_s, result.error(), QMessageBox::Ok);
// Ask for feed URL
const QString clipText = qApp->clipboard()->text();
const QString defaultURL = Net::DownloadManager::hasSupportedScheme(clipText) ? clipText : u"http://"_s;
RSS::Feed *newFeed = nullptr;
RSSFeedDialog dialog {this};
dialog.setFeedURL(defaultURL);
while (!newFeed && (dialog.exec() == RSSFeedDialog::Accepted))
{
const QString feedURL = dialog.feedURL().trimmed();
const std::chrono::seconds refreshInterval = dialog.refreshInterval();
const QString feedPath = RSS::Item::joinPath(destFolder->path(), feedURL);
const nonstd::expected<RSS::Feed *, QString> result = RSS::Session::instance()->addFeed(feedURL, feedPath, refreshInterval);
if (result)
newFeed = result.value();
else
QMessageBox::warning(&dialog, u"qBittorrent"_s, result.error(), QMessageBox::Ok);
}
if (!newFeed)
return;
// Expand destination folder to display new feed
if (destItem && (destItem != m_ui->feedListWidget->stickyUnreadItem()))
destItem->setExpanded(true);
// As new RSS items are added synchronously, we can do the following here.
m_ui->feedListWidget->setCurrentItem(m_ui->feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFeedPath)));
m_ui->feedListWidget->setCurrentItem(m_ui->feedListWidget->mapRSSItem(newFeed));
}
void RSSWidget::deleteSelectedItems()
@ -395,7 +413,7 @@ void RSSWidget::saveFoldersOpenState()
void RSSWidget::refreshAllFeeds()
{
RSS::Session::instance()->refresh();
RSS::Session::instance()->rootFolder()->refresh();
}
void RSSWidget::downloadSelectedTorrents()
@ -457,7 +475,7 @@ void RSSWidget::renameSelectedRSSItem()
} while (!ok);
}
void RSSWidget::editSelectedRSSFeedURL()
void RSSWidget::editSelectedRSSFeed()
{
QList<QTreeWidgetItem *> selectedItems = m_ui->feedListWidget->selectedItems();
if (selectedItems.size() != 1)
@ -469,15 +487,20 @@ void RSSWidget::editSelectedRSSFeedURL()
if (!rssFeed) [[unlikely]]
return;
bool ok = false;
QString newURL = AutoExpandableDialog::getText(this, tr("Please type a RSS feed URL")
, tr("Feed URL:"), QLineEdit::Normal, rssFeed->url(), &ok).trimmed();
if (!ok || newURL.isEmpty())
return;
auto *dialog = new RSSFeedDialog(this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setFeedURL(rssFeed->url());
dialog->setRefreshInterval(rssFeed->refreshInterval());
connect(dialog, &RSSFeedDialog::accepted, this, [this, dialog, rssFeed]
{
rssFeed->setRefreshInterval(dialog->refreshInterval());
const nonstd::expected<void, QString> result = RSS::Session::instance()->setFeedURL(rssFeed, newURL);
if (!result)
QMessageBox::warning(this, u"qBittorrent"_s, result.error(), QMessageBox::Ok);
const QString newURL = dialog->feedURL();
const nonstd::expected<void, QString> result = RSS::Session::instance()->setFeedURL(rssFeed, newURL);
if (!result)
QMessageBox::warning(this, u"qBittorrent"_s, result.error(), QMessageBox::Ok);
});
dialog->open();
}
void RSSWidget::refreshSelectedItems()

View file

@ -70,7 +70,7 @@ private slots:
void displayRSSListMenu(const QPoint &pos);
void displayItemsListMenu();
void renameSelectedRSSItem();
void editSelectedRSSFeedURL();
void editSelectedRSSFeed();
void refreshSelectedItems();
void copySelectedFeedsURL();
void handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem);

View file

@ -198,12 +198,9 @@
<string>New folder...</string>
</property>
</action>
<action name="actionEditFeedURL">
<action name="actionEditFeed">
<property name="text">
<string>Edit feed URL...</string>
</property>
<property name="toolTip">
<string>Edit feed URL</string>
<string>Feed options...</string>
</property>
</action>
</widget>

View file

@ -46,7 +46,7 @@ void SearchSortModel::setNameFilter(const QString &searchTerm)
{
m_searchTerm = searchTerm;
if ((searchTerm.length() > 2) && searchTerm.startsWith(u'"') && searchTerm.endsWith(u'"'))
m_searchTermWords = QStringList(m_searchTerm.mid(1, m_searchTerm.length() - 2));
m_searchTermWords = QStringList(m_searchTerm.sliced(1, (m_searchTerm.length() - 2)));
else
m_searchTermWords = searchTerm.split(u' ', Qt::SkipEmptyParts);
}

View file

@ -172,8 +172,10 @@ namespace
return nonstd::make_unexpected(readResult.error().message);
}
const QList<QByteArrayView> lines = Utils::ByteArray::splitToViews(readResult.value(), "\n");
QStringList history;
for (const QByteArrayView line : asConst(Utils::ByteArray::splitToViews(readResult.value(), "\n")))
history.reserve(lines.size());
for (const QByteArrayView line : lines)
history.append(QString::fromUtf8(line));
return history;
@ -673,7 +675,7 @@ void SearchWidget::loadHistory()
}
if (history.size() > m_historyLength)
history = history.mid(history.size() - m_historyLength);
history.remove(0, (history.size() - m_historyLength));
m_searchPatternCompleterModel->setStringList(history);
});

View file

@ -44,6 +44,19 @@
#include "uithememanager.h"
#include "utils.h"
namespace
{
QWidget *createSeparator(QWidget *parent)
{
QFrame *separator = new QFrame(parent);
separator->setFrameStyle(QFrame::VLine);
#ifndef Q_OS_MACOS
separator->setFrameShadow(QFrame::Raised);
#endif
return separator;
}
}
StatusBar::StatusBar(QWidget *parent)
: QStatusBar(parent)
{
@ -87,11 +100,17 @@ StatusBar::StatusBar(QWidget *parent)
m_upSpeedLbl->setStyleSheet(u"text-align:left;"_s);
m_upSpeedLbl->setMinimumWidth(200);
m_freeDiskSpaceLbl = new QLabel(tr("Free space: N/A"));
m_freeDiskSpaceLbl->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
m_freeDiskSpaceSeparator = createSeparator(m_freeDiskSpaceLbl);
m_lastExternalIPsLbl = new QLabel(tr("External IP: N/A"));
m_lastExternalIPsLbl->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
m_lastExternalIPsSeparator = createSeparator(m_lastExternalIPsLbl);
m_DHTLbl = new QLabel(tr("DHT: %1 nodes").arg(0), this);
m_DHTLbl->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
m_DHTSeparator = createSeparator(m_DHTLbl);
m_altSpeedsBtn = new QPushButton(this);
m_altSpeedsBtn->setFlat(true);
@ -113,52 +132,42 @@ StatusBar::StatusBar(QWidget *parent)
m_connecStatusLblIcon->setMaximumWidth(Utils::Gui::largeIconSize().width());
m_altSpeedsBtn->setMaximumWidth(Utils::Gui::largeIconSize().width());
QFrame *statusSep1 = new QFrame(this);
statusSep1->setFrameStyle(QFrame::VLine);
#ifndef Q_OS_MACOS
statusSep1->setFrameShadow(QFrame::Raised);
#endif
QFrame *statusSep2 = new QFrame(this);
statusSep2->setFrameStyle(QFrame::VLine);
#ifndef Q_OS_MACOS
statusSep2->setFrameShadow(QFrame::Raised);
#endif
QFrame *statusSep3 = new QFrame(this);
statusSep3->setFrameStyle(QFrame::VLine);
#ifndef Q_OS_MACOS
statusSep3->setFrameShadow(QFrame::Raised);
#endif
QFrame *statusSep4 = new QFrame(this);
statusSep4->setFrameStyle(QFrame::VLine);
#ifndef Q_OS_MACOS
statusSep4->setFrameShadow(QFrame::Raised);
#endif
QFrame *statusSep5 = new QFrame(this);
statusSep5->setFrameStyle(QFrame::VLine);
#ifndef Q_OS_MACOS
statusSep5->setFrameShadow(QFrame::Raised);
#endif
layout->addWidget(m_freeDiskSpaceLbl);
layout->addWidget(m_freeDiskSpaceSeparator);
layout->addWidget(m_lastExternalIPsLbl);
layout->addWidget(statusSep1);
layout->addWidget(m_lastExternalIPsSeparator);
layout->addWidget(m_DHTLbl);
layout->addWidget(statusSep2);
layout->addWidget(m_DHTSeparator);
layout->addWidget(m_connecStatusLblIcon);
layout->addWidget(statusSep3);
layout->addWidget(createSeparator(m_connecStatusLblIcon));
layout->addWidget(m_altSpeedsBtn);
layout->addWidget(statusSep4);
layout->addWidget(createSeparator(m_altSpeedsBtn));
layout->addWidget(m_dlSpeedLbl);
layout->addWidget(statusSep5);
layout->addWidget(createSeparator(m_dlSpeedLbl));
layout->addWidget(m_upSpeedLbl);
addPermanentWidget(container);
setStyleSheet(u"QWidget {margin: 0;}"_s);
container->adjustSize();
adjustSize();
updateFreeDiskSpaceVisibility();
updateExternalAddressesVisibility();
// Is DHT enabled
m_DHTLbl->setVisible(session->isDHTEnabled());
const bool isDHTVisible = session->isDHTEnabled();
m_DHTLbl->setVisible(isDHTVisible);
m_DHTSeparator->setVisible(isDHTVisible);
refresh();
connect(session, &BitTorrent::Session::statsUpdated, this, &StatusBar::refresh);
updateFreeDiskSpaceLabel(session->freeDiskSpace());
connect(session, &BitTorrent::Session::freeDiskSpaceChecked, this, &StatusBar::updateFreeDiskSpaceLabel);
connect(Preferences::instance(), &Preferences::changed, this, &StatusBar::optionsSaved);
}
@ -216,15 +225,28 @@ void StatusBar::updateDHTNodesNumber()
if (BitTorrent::Session::instance()->isDHTEnabled())
{
m_DHTLbl->setVisible(true);
m_DHTLbl->setText(tr("DHT: %1 nodes")
.arg(BitTorrent::Session::instance()->status().dhtNodes));
m_DHTSeparator->setVisible(true);
m_DHTLbl->setText(tr("DHT: %1 nodes").arg(BitTorrent::Session::instance()->status().dhtNodes));
}
else
{
m_DHTLbl->setVisible(false);
m_DHTSeparator->setVisible(false);
}
}
void StatusBar::updateFreeDiskSpaceLabel(const qint64 value)
{
m_freeDiskSpaceLbl->setText(tr("Free space: ") + Utils::Misc::friendlyUnit(value));
}
void StatusBar::updateFreeDiskSpaceVisibility()
{
const bool isVisible = Preferences::instance()->isStatusbarFreeDiskSpaceDisplayed();
m_freeDiskSpaceLbl->setVisible(isVisible);
m_freeDiskSpaceSeparator->setVisible(isVisible);
}
void StatusBar::updateExternalAddressesLabel()
{
const QString lastExternalIPv4Address = BitTorrent::Session::instance()->lastExternalIPv4Address();
@ -244,7 +266,9 @@ void StatusBar::updateExternalAddressesLabel()
void StatusBar::updateExternalAddressesVisibility()
{
m_lastExternalIPsLbl->setVisible(Preferences::instance()->isStatusbarExternalIPDisplayed());
const bool isVisible = Preferences::instance()->isStatusbarExternalIPDisplayed();
m_lastExternalIPsLbl->setVisible(isVisible);
m_lastExternalIPsSeparator->setVisible(isVisible);
}
void StatusBar::updateSpeedLabels()
@ -300,5 +324,6 @@ void StatusBar::capSpeed()
void StatusBar::optionsSaved()
{
updateFreeDiskSpaceVisibility();
updateExternalAddressesVisibility();
}

Some files were not shown because too many files have changed in this diff Show more