Compare commits

..

31 commits

Author SHA1 Message Date
sledgehammer999
da87be2b12
Bump to 5.1.0 2025-04-27 11:53:39 +03:00
sledgehammer999
891265b390
Update Changelog 2025-04-27 11:53:25 +03:00
sledgehammer999
f46e44d3ed
Sync translations from Transifex and run lupdate 2025-04-27 11:52:43 +03:00
sledgehammer999
a4094a440d
Bump copyright year 2025-04-20 23:26:52 +03:00
sledgehammer999
46c3da21e1
Sync translations from Transifex and run lupdate 2025-04-20 23:23:23 +03:00
Vladimir Golovnev
2f06ea2587
Backport changes to v5.1.x branch
PR #22490.
2025-04-17 20:54:03 +03:00
Vladimir Golovnev
cfbf6b73ff
Prevent crash due to corrupted resume data
PR #22569.
Closes #22540.
2025-04-17 11:17:19 +03:00
Vladimir Golovnev
c687a7d0d3
Fix the torrent relocates files when switching to "manual" mode
PR #22564.
Closes #22283.
Closes #22546.
2025-04-16 10:24:34 +03:00
Vladimir Golovnev
009cc71f9b
Explicitly reject opened Add torrent dialogs when exiting app
PR #22535.
Closes #19933.
Supercedes #22533.
2025-04-14 09:53:07 +03:00
Chocobo1
de1cf208ce
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 15:13:47 +03:00
skomerko
5f49472fa4
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-12 07:12:23 +03:00
skomerko
2076302170
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-04-12 07:11:41 +03:00
skomerko
2a33e187eb
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-04-12 07:10:59 +03:00
FredBill1
00149e03c0
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 12:57:05 +03:00
Chocobo1
57d529c17a
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 08:58:38 +03:00
Vladimir Golovnev
d492fcf29a
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:19:03 +03:00
Chocobo1
d0caa35b39
WebUI: fix Tag counter counting wrong
Related: https://github.com/qbittorrent/qBittorrent/pull/22103/files/73e9116d21015542caeb9a3cfd56bfb256ebed9d#r2014898781

PR #22480.
2025-03-29 16:02:47 +03:00
Vladimir Golovnev
ec7a00af92
Restore ability to use server-side translation by custom WebUI
PR #20968.
2025-03-28 09:08:56 +03:00
Vladimir Golovnev
76a3aba7e0
Backport changes to v5.1.x branch
PR #22268.
2025-03-16 10:39:31 +03:00
Chocobo1
7003ac3f4d
WebUI v5.1 fixes
PR #22282.
2025-03-15 14:52:48 +03:00
skomerko
964be0fa1c
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-15 12:47:11 +03:00
skomerko
c1defceccf
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-15 12:46:17 +03:00
Vladimir Golovnev
260394623d
Add missing includes
PR #22362.
2025-03-05 09:07:47 +03:00
Vladimir Golovnev
478c2d5b12
Don't miss to declare some of the color IDs
PR #22330.
Closes #22326.
2025-02-25 18:57:22 +03:00
Vladimir Golovnev
49cfbd9a49
Improve command line parameters serialization
PR #22319.
Closes #22306.
2025-02-25 09:12:26 +03:00
Luke Memet
d028f46fab
Fix shift-click selection on macOS
PR #22284.
Closes #16818.
2025-02-19 13:53:49 +03:00
Daniel Nylander
57b24a200e
NSIS: Update Swedish translation
PR #22046.
2025-02-19 13:49:25 +03:00
Chocobo1
269dfe87e0
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 08:22:26 +03:00
Vladimir Golovnev
6a1c465d85
WebAPI: Don't trim string parameters
PR #22266.
Closes #19485.
Closes #22254.
2025-02-12 09:35:29 +03:00
sledgehammer999
bc7d5c1f8f
Bump to 5.1.0rc1 2025-02-11 02:01:34 +02:00
sledgehammer999
8aabef423c
Create new resources for this branch for Transifex 2025-02-11 01:59:07 +02:00
410 changed files with 72649 additions and 90710 deletions

2
.github/FUNDING.yml vendored
View file

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

View file

@ -36,7 +36,7 @@ jobs:
curl \
-L \
-o "${{ runner.temp }}/pandoc.tar.gz" \
"https://github.com/jgm/pandoc/releases/download/3.7.0.2/pandoc-3.7.0.2-linux-amd64.tar.gz"
"https://github.com/jgm/pandoc/releases/download/3.6/pandoc-3.6-linux-amd64.tar.gz"
tar -xf "${{ runner.temp }}/pandoc.tar.gz" -C "${{ github.workspace }}/.."
mv "${{ github.workspace }}/.."/pandoc-* "${{ env.pandoc_path }}"
# run pandoc
@ -52,13 +52,13 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pip install zizmor
IGNORE_RULEID='(.ruleId != "zizmor/template-injection")
and (.ruleId != "zizmor/unpinned-uses")'
IGNORE_ID='(.id != "zizmor/template-injection")
and (.id != "zizmor/unpinned-uses")'
IGNORE_RULEID='(.ruleId != "template-injection")
and (.ruleId != "unpinned-uses")'
IGNORE_ID='(.id != "template-injection")
and (.id != "unpinned-uses")'
zizmor \
--format sarif \
--persona auditor \
--pedantic \
./ \
| jq "(.runs[].results |= map(select($IGNORE_RULEID)))
| (.runs[].tool.driver.rules |= map(select($IGNORE_ID)))" \

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.9.1"]
qt_version: ["6.7.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=1G
max_size=2G
- name: Install boost
env:
@ -70,9 +70,6 @@ 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
@ -98,7 +95,7 @@ jobs:
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_STANDARD=20 \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-DBOOST_ROOT="${{ env.boost_path }}" \
-Ddeprecated-functions=OFF
cmake --build build
sudo cmake --install build
@ -112,7 +109,7 @@ jobs:
-G "Ninja" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-DBOOST_ROOT="${{ env.boost_path }}" \
-DTESTING=ON \
-DVERBOSE_CONFIGURE=ON \
-D${{ matrix.qbt_gui }}

View file

@ -25,7 +25,7 @@ jobs:
python-version: '3' # use default version
- name: Install tools (auxiliary scripts)
run: pip install bandit isort pycodestyle pyflakes
run: pip install bandit pycodestyle pyflakes
- name: Gather files (auxiliary scripts)
run: |
@ -44,10 +44,6 @@ jobs:
--max-line-length=1000 \
--statistics \
$PY_FILES
isort \
--check \
--diff \
$PY_FILES
- name: Build code (auxiliary scripts)
run: |
@ -59,7 +55,7 @@ jobs:
python-version: '3.9'
- name: Install tools (search engine)
run: pip install bandit isort mypy pycodestyle pyflakes pyright
run: pip install bandit mypy pycodestyle pyflakes pyright
- name: Gather files (search engine)
run: |
@ -89,10 +85,6 @@ jobs:
--max-line-length=1000 \
--statistics \
$PY_FILES
isort \
--check \
--diff \
$PY_FILES
- name: Build code (search engine)
run: |

View file

@ -21,7 +21,7 @@ jobs:
matrix:
libt_version: ["2.0.11", "1.2.20"]
qbt_gui: ["GUI=ON", "GUI=OFF"]
qt_version: ["6.6.3"]
qt_version: ["6.5.2"]
env:
boost_path: "${{ github.workspace }}/../boost"
@ -47,7 +47,7 @@ jobs:
store_cache: ${{ github.ref == 'refs/heads/master' }}
update_packager_index: false
ccache_options: |
max_size=1G
max_size=2G
- name: Install boost
env:
@ -65,9 +65,6 @@ 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
@ -93,7 +90,7 @@ jobs:
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_STANDARD=20 \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-DBOOST_ROOT="${{ env.boost_path }}" \
-Ddeprecated-functions=OFF
cmake --build build
sudo cmake --install build
@ -115,7 +112,7 @@ jobs:
-G "Ninja" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-DBOOST_ROOT="${{ env.boost_path }}" \
-DCMAKE_INSTALL_PREFIX="/usr" \
-DTESTING=ON \
-DVERBOSE_CONFIGURE=ON \
@ -162,7 +159,6 @@ jobs:
- name: Package AppImage
run: |
rm -f "${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/sqldrivers/libqsqlmimer.so"
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt
rm qbittorrent/apprun-hooks/*
cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh

View file

@ -34,12 +34,7 @@ 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,7 +67,6 @@ 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 `
@ -95,18 +94,11 @@ 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.9.1"
version: "6.8.0"
arch: win64_msvc2022_64
archives: qtbase qtsvg qttools
cache: true
@ -121,7 +113,7 @@ jobs:
${{ env.libtorrent_path }}
cd ${{ env.libtorrent_path }}
$env:CXXFLAGS+=" /guard:cf"
$env:LDFLAGS+=" /GUARD:CF"
$env:LDFLAGS+=" /guard:cf"
cmake `
-B build `
-G "Ninja" `
@ -130,7 +122,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 }}/lib/cmake" `
-DBOOST_ROOT="${{ env.boost_path }}" `
-DBUILD_SHARED_LIBS=OFF `
-Ddeprecated-functions=OFF `
-Dstatic_runtime=OFF `
@ -147,7 +139,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 }}/lib/cmake" `
-DBOOST_ROOT="${{ env.boost_path }}" `
-DLibtorrentRasterbar_DIR="${{ env.libtorrent_path }}/install/lib/cmake/LibtorrentRasterbar" `
-DMSVC_RUNTIME_DYNAMIC=ON `
-DTESTING=ON `

View file

@ -16,7 +16,7 @@ jobs:
matrix:
libt_version: ["2.0.11"]
qbt_gui: ["GUI=ON"]
qt_version: ["6.9.1"]
qt_version: ["6.5.2"]
env:
boost_path: "${{ github.workspace }}/../boost"
@ -39,7 +39,7 @@ jobs:
- name: Install boost
env:
BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "88"
BOOST_MINOR_VERSION: "86"
BOOST_PATCH_VERSION: "0"
run: |
boost_url="https://archives.boost.io/release/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/source/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz"
@ -52,9 +52,6 @@ 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
@ -77,7 +74,7 @@ jobs:
-G "Ninja" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_STANDARD=20 \
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-DBOOST_ROOT="${{ env.boost_path }}" \
-Ddeprecated-functions=OFF
cmake --build build
sudo cmake --install build
@ -101,7 +98,7 @@ jobs:
-B build \
-G "Ninja" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-DBOOST_ROOT="${{ env.boost_path }}" \
-DVERBOSE_CONFIGURE=ON \
-D${{ matrix.qbt_gui }}
PATH="${{ env.coverity_path }}/bin:$PATH" \

View file

@ -26,12 +26,12 @@
# but you are not obligated to do so. If you do not wish to do so, delete this
# exception statement from your version.
import argparse
import re
import sys
import xml.etree.ElementTree as ElementTree
from collections.abc import Callable, Sequence
from typing import Optional
import argparse
import re
import xml.etree.ElementTree as ElementTree
import sys
def traversePostOrder(root: ElementTree.Element, visitFunc: Callable[[ElementTree.Element], None]) -> None:

View file

@ -26,11 +26,11 @@
# but you are not obligated to do so. If you do not wish to do so, delete this
# exception statement from your version.
from collections.abc import Sequence
from typing import Optional
import argparse
import re
import sys
from collections.abc import Sequence
from typing import Optional
def main(argv: Optional[Sequence[str]] = None) -> int:

View file

@ -69,11 +69,11 @@ repos:
- ts
- repo: https://github.com/codespell-project/codespell.git
rev: v2.4.1
rev: v2.4.0
hooks:
- id: codespell
name: Check spelling (codespell)
args: ["--ignore-words-list", "additionals,categor,curren,fo,indexIn,ist,ket,notin,searchin,sectionin,superseeding,te,ths"]
args: ["--ignore-words-list", "additionals,categor,curren,fo,ist,ket,notin,searchin,sectionin,superseeding,te,ths"]
exclude: |
(?x)^(
.*\.desktop |
@ -88,7 +88,7 @@ repos:
- ts
- repo: https://github.com/crate-ci/typos.git
rev: v1.32.0
rev: v1.29.4
hooks:
- id: typos
name: Check spelling (typos)

View file

@ -1,7 +1,7 @@
[main]
host = https://www.transifex.com
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_master]
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_v51x]
file_filter = src/lang/qbittorrent_<lang>.ts
source_file = src/lang/qbittorrent_en.ts
source_lang = en
@ -9,7 +9,7 @@ type = QT
minimum_perc = 23
lang_map = pt: pt_PT, zh: zh_CN
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_webui]
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_webui_v51x]
file_filter = src/webui/www/translations/webui_<lang>.ts
source_file = src/webui/www/translations/webui_en.ts
source_lang = en

View file

@ -8,7 +8,7 @@ project(qBittorrent
# version requirements - older versions may work, but you are on your own
set(minBoostVersion 1.76)
set(minQt6Version 6.6.0)
set(minQt6Version 6.5.0)
set(minOpenSSLVersion 3.0.2)
set(minLibtorrent1Version 1.2.19)
set(minLibtorrentVersion 2.0.10)

View file

@ -1,4 +1,95 @@
Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.0
Sun Apr 27th 2025 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.0
- FEATURE: Enable customizing the save statistics time interval (Burnerelu)
- FEATURE: Add drag support to torrent content widget (Chocobo1)
- FEATURE: Display External IP Address in status bar (Thomas Piccirello)
- FEATURE: Use modern functions to get random numbers under Linux/Windows (security related) (Chocobo1)
- FEATURE: Add eXact Length parameter when creating magnet URI (antanilol)
- FEATURE: Support fetching tracker list from URL (Thomas Piccirello)
- FEATURE: Add `announce_port` support (Maxime Thiebaut)
- BUGFIX: Enable adaptive step size for upload and download limits (Harald Nordgren)
- BUGFIX: Add URL link for reverse proxy setup examples (Chocobo1)
- BUGFIX: Allow drop action only on transfer list (Chocobo1)
- BUGFIX: Fix the tab order in dialogs (thalieht)
- BUGFIX: Fix filesize sorting in preview dialog (DoubleSpicy)
- BUGFIX: Improve the speed icons in the status bar (Mahdi Hosseinzadeh)
- BUGFIX: Update link to news (tinyboxvk)
- BUGFIX: Fix tab stop order in various dialogs and UI elements (Chocobo1)
- BUGFIX: Make links accessible by keyboard (Chocobo1)
- BUGFIX: Make tab key switch focus (Chocobo1)
- BUGFIX: Revise DHT bootstrap node list (stalkerok, Chocobo1)
- BUGFIX: Return first tracker as fallback for "current tracker" (glassez)
- BUGFIX: Prevent crash when exiting app with `Add torrent` dialogs opened (glassez)
- BUGFIX: Fix torrent relocating files when switching to "manual" mode (glassez)
- BUGFIX: Prevent crash due to corrupted resume data (glassez)
- WEBUI: Improvements that should help with assistive technologies (Chocobo1)
- WEBUI: Internal refactoring to migrate away from MooTools and towards native browser APIs (Chocobo1, skomerko)
- WEBUI: Implement path autocompletion (Paweł Kotiuk)
- WEBUI: Implement double-click behavior controls (Hanabishi)
- WEBUI: Add ability to toggle alternating row colors in tables (skomerko)
- WEBUI: Improve visibility of unread RSS articles (skomerko)
- WEBUI: Remove deleted torrents even if they are currently filtered out (Carmelo Scandaliato)
- WEBUI: Highlight torrent category in context menu (skomerko)
- WEBUI: Implement 'Auto hide zero status filters' (skomerko)
- WEBUI: Allow to filter torrent list by save path (skomerko)
- WEBUI: Handle regex syntax error for torrent filtering (HamletDuFromage)
- WEBUI: Add missing icons (skomerko)
- WEBUI: Add link to 'List of alternative WebUI' wiki page in Options (Chocobo1)
- WEBUI: Improve properties panel, torrent deletion dialog, filter list, subcategories, torrent deletion, statistics window (skomerko)
- WEBUI: Allow to display only hostname in the Tracker column (skomerko)
- WEBUI: Show country/region name next to its flag (skomerko)
- WEBUI: Improve hash copy actions in context menu (skomerko)
- WEBUI: Support removing tracker from all torrents in WebUI/WebAPI (Thomas Piccirello)
- WEBUI: Display DHT information in the Status bar only when DHT is enabled (skomerko)
- WEBUI: Add 'Confirm torrent recheck' option (skomerko)
- WEBUI: Support managing web seeds (Thomas Piccirello)
- WEBUI: Add colors to log table rows (skomerko)
- WEBUI: Prevent text selection within tabs, menu items (skomerko)
- WEBUI: Use correct text and background colors in RSS details view (skomerko)
- WEBUI: Reduce padding in torrents table (skomerko)
- WEBUI: Add WebAPI/WebUI for managing cookies (Thomas Piccirello)
- WEBUI: Support updating RSS feed URL (Thomas Piccirello)
- WEBUI: Add 'Engine' column to Search table (skomerko)
- WEBUI: Add confirm dialog for Auto TMM (skomerko)
- WEBUI: Add context menu to search tabs (skomerko)
- WEBUI: Show file filter when Content tab selected on load (Thomas Piccirello)
- WEBUI: DHT, PeX and LSD rows are now always on top in Trackers table (skomerko)
- WEBUI: Clear properties panel when torrent no longer selected (skomerko)
- WEBUI: Support auto resizing table columns (Thomas Piccirello)
- WEBUI: Fix displaying RSS panel on load (Thomas Piccirello)
- WEBUI: Add tooltip to regex filter button (Patrik Elfström)
- WEBUI: Hide context menu when clicking on a table row (Patrik Elfström)
- WEBUI: Display torrent progress percentage in General tab (skomerko)
- WEBUI: Use thin scrollbars (skomerko)
- WEBUI: Show 'Rename...' context menu item only when one torrent is selected (skomerko)
- WEBUI: Display error when download fails (Thomas Piccirello)
- WEBUI: Add colors to 'Status' column in Trackers table (skomerko)
- WEBUI: Add missing icon to 'Queue' context menu item (skomerko)
- WEBUI: Change filter inputs to type search (Patrik Elfström)
- WEBUI: Allow to move state icon to name column in torrents table (skomerko)
- WEBUI: Fix bug where the 'Tracker editing' dialog displays incorrect data (skomerko)
- WEBUI: Maintain row highlight after rearranging table columns (skomerko)
- WEBUI: Fix preferences not applied in magnet handler (Chocobo1)
- WEBUI: Update sort icon after changing column order (skomerko)
- WEBUI: Show 'Edit tracker URL...' only when one tracker is selected (skomerko)
- WEBUI: Set status filter to 'All' if selected filter is no longer visible (skomerko)
- WEBAPI: Don't reannounce when removing tracker via WebAPI (Thomas Piccirello)
- WEBAPI: Add WebAPI for managing torrent webseeds (Thomas Piccirello)
- WEBAPI: Add `forced` parameter to `torrents/add` (Chris B)
- WEBAPI: Optionally include trackers list in torrent info response (ze0s)
- WEBAPI: Add new method `setTags` to upsert tags on torrents (ze0s)
- RSS: Resolve relative URLs within RSS article description (Zentino)
- SEARCH: Provide SSL context field (Chocobo1)
- SEARCH: Allow to refresh existing search (glassez)
- SEARCH: Allow multiple simultaneous searches (glassez)
- SEARCH: Store opened search tabs (glassez)
- SEARCH: Store search history (glassez)
- SEARCH: Migrate socks.py from SocksiPy to PySocks 1.7.1 (FredBill1)
- SEARCH: Bump Python version minimum requirement (Chocobo1)
- WINDOWS: Opt into Windows SegmentHeap (Andarwinux)
- WINDOWS: Allow to choose color scheme on Windows (glassez)
- WINDOWS: Verify hash of Python installer (Chocobo1)
- LINUX: Add support for Thunar file manager (algebnaly)
- MACOS: Fix shift-click selection on macOS (Luke Memet)
Mon Oct 28th 2024 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.1
- FEATURE: Add "Simple pread/pwrite" disk IO type (Hanabishi)

View file

@ -11,7 +11,7 @@ qBittorrent - A BitTorrent client in C++ / Qt
- OpenSSL >= 3.0.2
- Qt 6.6.0 - 6.x
- Qt 6.5.0 - 6.x
- zlib >= 1.2.11

View file

@ -1,44 +0,0 @@
# WebAPI Changelog
## 2.11.10
* [#22932](https://github.com/qbittorrent/qBittorrent/pull/22932)
* `torrents/categories` and `sync/maindata` now serialize categories' `downloadPath` to `null`, rather than `undefined`
## 2.11.9
* [#21015](https://github.com/qbittorrent/qBittorrent/pull/21015)
* Add `torrents/fetchMetadata` endpoint for retrieving torrent metadata associated with a URL
* Add `torrents/parseMetadata` endpoint for retrieving torrent metadata associated with a .torrent file
* Add `torrents/saveMetadata` endpoint for saving retrieved torrent metadata to a .torrent file
* `torrents/add` allows adding a torrent with metadata previously retrieved via `torrents/fetchMetadata` or `torrents/parseMetadata`
* `torrents/add` allows specifying a torrent's file priorities
* [#22698](https://github.com/qbittorrent/qBittorrent/pull/22698)
* `torrents/addTrackers` and `torrents/removeTrackers` now accept `hash=all` and adds/removes the tracker to/from *all* torrents
* For compatibility, `torrents/removeTrackers` still accepts `hash=*` internally we transform it into `all`
* Allow passing a pipe (`|`) separated list of hashes in `hash` for `torrents/addTrackers` and `torrents/removeTrackers`
## 2.11.8
* [#21349](https://github.com/qbittorrent/qBittorrent/pull/21349)
* Handle sending `204 No Content` status code when response contains no data
* Some endpoints still return `200 OK` to ensure smooth transition
* [#22750](https://github.com/qbittorrent/qBittorrent/pull/22750)
* `torrents/info` allows an optional parameter `includeFiles` that defaults to `false`
* Each torrent will contain a new key `files` which will list all files similar to the `torrents/files` endpoint
* [#22813](https://github.com/qbittorrent/qBittorrent/pull/22813)
* `app/getDirectoryContent` allows an optional parameter `withMetadata` to send file metadata
* Fields are `name`, `type`, `size`, `creation_date`, `last_access_date`, `last_modification_date`
* See PR for TypeScript types
## 2.11.7
* [#22166](https://github.com/qbittorrent/qBittorrent/pull/22166)
* `sync/maindata` returns 3 new torrent fields: `has_tracker_warning`, `has_tracker_error`, `has_other_announce_error`
## 2.11.6
* [#22460](https://github.com/qbittorrent/qBittorrent/pull/22460)
* `app/setPreferences` allows only one of `max_ratio_enabled`, `max_ratio` to be present
* `app/setPreferences` allows only one of `max_seeding_time_enabled`, `max_seeding_time` to be present
* `app/setPreferences` allows only one of `max_inactive_seeding_time_enabled`, `max_inactive_seeding_time` to be present

View file

@ -47,9 +47,6 @@ find_package(Boost ${minBoostVersion} REQUIRED)
find_package(OpenSSL ${minOpenSSLVersion} REQUIRED)
find_package(ZLIB ${minZlibVersion} REQUIRED)
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS Core Network Sql Xml LinguistTools)
if (Qt6_FOUND AND (Qt6_VERSION VERSION_GREATER_EQUAL 6.10))
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS CorePrivate)
endif()
if (DBUS)
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS DBus)
set_package_properties(Qt6DBus PROPERTIES

View file

@ -20,7 +20,7 @@ target_compile_features(qbt_common_cfg INTERFACE
)
target_compile_definitions(qbt_common_cfg INTERFACE
QT_DISABLE_DEPRECATED_UP_TO=0x060600
QT_DISABLE_DEPRECATED_UP_TO=0x060500
QT_NO_CAST_FROM_ASCII
QT_NO_CAST_TO_ASCII
QT_NO_CAST_FROM_BYTEARRAY
@ -89,7 +89,7 @@ if (MSVC)
/Zc:__cplusplus
)
target_link_options(qbt_common_cfg INTERFACE
/GUARD:CF
/guard:cf
$<$<NOT:$<CONFIG:Debug>>:/OPT:REF /OPT:ICF>
# suppress linking warning due to /INCREMENTAL and /OPT:ICF being both ON
$<$<CONFIG:RelWithDebInfo>:/INCREMENTAL:NO>

2
dist/mac/Info.plist vendored
View file

@ -55,7 +55,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>5.2.0</string>
<string>5.1.0</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>

Binary file not shown.

Binary file not shown.

View file

@ -34,12 +34,12 @@ endforeach()
if (GUI)
install(FILES org.qbittorrent.qBittorrent.desktop
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications/
COMPONENT data
)
install(FILES org.qbittorrent.qBittorrent.metainfo.xml
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo/
COMPONENT data
)
@ -55,9 +55,4 @@ if (GUI)
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status
COMPONENT data
)
else()
install(FILES org.qbittorrent.qBittorrent-nox.metainfo.xml
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo
COMPONENT data
)
endif()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 747 B

After

Width:  |  Height:  |  Size: 750 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 747 B

After

Width:  |  Height:  |  Size: 750 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before After
Before After

View file

@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2014 sledgehammer999 <sledgehammer999@qbittorrent.org> -->
<component type="console-application">
<id>org.qbittorrent.qBittorrent-nox</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later and OpenSSL</project_license>
<name>qBittorrent-nox</name>
<summary>An open-source Bittorrent client (nox version)</summary>
<description>
<p>
The qBittorrent project aims to provide an open-source software alternative to µTorrent.
Additionally, qBittorrent runs and provides the same features on all major platforms (FreeBSD, Linux, macOS, OS/2, Windows).
qBittorrent is based on the Qt toolkit and libtorrent-rasterbar library.
</p>
<ul>
<li>Polished µTorrent-like User Interface</li>
<li>Well-integrated and extensible Search Engine</li>
<li>RSS feed support with advanced download filters (incl. regex)</li>
<li>Many Bittorrent extensions supported</li>
<li>Remote control through Web user interface, written with AJAX</li>
<li>Sequential downloading (Download in order)</li>
<li>Advanced control over torrents, trackers and peers</li>
<li>Bandwidth scheduler</li>
<li>Torrent creation tool</li>
<li>IP Filtering (eMule &amp; PeerGuardian format compatible)</li>
<li>IPv6 compliant</li>
<li>UPnP / NAT-PMP port forwarding support</li>
<li>Available on all platforms: Windows, Linux, macOS, FreeBSD, OS/2</li>
<li>Available in ~70 languages</li>
</ul>
</description>
<provides>
<binary>qbittorrent-nox</binary>
</provides>
<screenshots>
<screenshot type="default">
<caption>Running headless (nox) version</caption>
<image>https://raw.githubusercontent.com/qbittorrent/qBittorrent-website/43fcf4550f567c38fb879b984922b659e90982cc/src/img/screenshots/linux/5.webp</image>
</screenshot>
</screenshots>
<update_contact>sledgehammer999@qbittorrent.org</update_contact>
<developer id="org.qbittorrent">
<name>The qBittorrent Project</name>
</developer>
<url type="homepage">https://www.qbittorrent.org/</url>
<url type="bugtracker">https://bugs.qbittorrent.org/</url>
<url type="faq">https://wiki.qbittorrent.org/Frequently-Asked-Questions</url>
<url type="help">https://forum.qbittorrent.org/</url>
<url type="donation">https://www.qbittorrent.org/donate</url>
<url type="translate">https://wiki.qbittorrent.org/How-to-translate-qBittorrent</url>
<url type="vcs-browser">https://github.com/qbittorrent/qBittorrent</url>
<url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url>
<content_rating type="oars-1.1"/>
<releases>
<release version="5.2.0~alpha1" date="2025-02-11"/>
</releases>
</component>

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.2.0~alpha1" date="2025-02-11"/>
<release version="5.1.0" date="2025-04-27"/>
</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.2.0"
!define /ifndef QBT_VERSION "5.1.0"
; Option that controls the installer's window name
; If set, its value will be used like this:
@ -111,8 +111,7 @@ RequestExecutionLevel user
!define MUI_HEADERIMAGE
!define MUI_COMPONENTSPAGE_NODESC
;!define MUI_ICON "qbittorrent.ico"
!define MUI_LICENSEPAGE_BUTTON $(^NextBtn)
!define MUI_LICENSEPAGE_TEXT_BOTTOM "$_CLICK"
!define MUI_LICENSEPAGE_CHECKBOX
!define MUI_LANGDLL_ALLLANGUAGES
;--------------------------------

View file

@ -1,4 +1,4 @@
.\" Automatically generated by Pandoc 3.7.0.2
.\" Automatically generated by Pandoc 3.4
.\"
.TH "QBITTORRENT\-NOX" "1" "January 16th 2010" "Command line Bittorrent client written in C++ / Qt"
.SH NAME
@ -26,7 +26,7 @@ compatible).
qBittorrent\-nox is meant to be controlled via its feature\-rich Web UI
which is accessible as a default on http://localhost:8080.
The Web UI access is secured and the default account user name is
\(lqadmin\(rq with \(lqadminadmin\(rq as a password.
\[lq]admin\[rq] with \[lq]adminadmin\[rq] as a password.
.SH OPTIONS
\f[B]\f[CB]\-\-help\f[B]\f[R] Prints the command line options.
.PP

View file

@ -1,4 +1,4 @@
.\" Automatically generated by Pandoc 3.7.0.2
.\" Automatically generated by Pandoc 3.4
.\"
.TH "QBITTORRENT" "1" "January 16th 2010" "Bittorrent client written in C++ / Qt"
.SH NAME

View file

@ -1,8 +1,8 @@
.\" Automatically generated by Pandoc 3.7.0.2
.\" Automatically generated by Pandoc 3.4
.\"
.TH "QBITTORRENT\-NOX" "1" "16 января 2010" "Клиент сети БитТоррент для командной строки"
.SH НАЗВАНИЕ
qBittorrent\-nox \(em клиент сети БитТоррент для командной строки.
qBittorrent\-nox \[em] клиент сети БитТоррент для командной строки.
.SH АВТОРЫ
Christophe Dumez \c
.MT chris@qbittorrent.org

View file

@ -1,8 +1,8 @@
.\" Automatically generated by Pandoc 3.7.0.2
.\" Automatically generated by Pandoc 3.4
.\"
.TH "QBITTORRENT" "1" "16 января 2010" "Клиент сети БитТоррент"
.SH НАЗВАНИЕ
qBittorrent \(em клиент сети БитТоррент.
qBittorrent \[em] клиент сети БитТоррент.
.SH АВТОРЫ
Christophe Dumez \c
.MT chris@qbittorrent.org

View file

@ -86,19 +86,6 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
PROPERTIES
MACOSX_PACKAGE_LOCATION Resources
)
# Generate lproj folders for the translations
foreach(TS_FILE IN LISTS QBT_TS_FILES)
string(FIND "${TS_FILE}" "_" POS)
math(EXPR START "${POS} + 1")
string(SUBSTRING "${TS_FILE}" ${START} -1 LPROJ_FOLDER)
string(REPLACE ".ts" ".lproj" LPROJ_FOLDER "${LPROJ_FOLDER}")
# @ is not valid as a language code for a lproj folder on MacOS
string(REPLACE "@" "-" LPROJ_FOLDER "${LPROJ_FOLDER}")
add_custom_command(TARGET qbt_app POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"$<TARGET_FILE_DIR:qbt_app>/../Resources/${LPROJ_FOLDER}"
)
endforeach()
# provide variables for substitution in dist/mac/Info.plist
get_target_property(EXECUTABLE_NAME qbt_app OUTPUT_NAME)
# This variable name should be changed once qmake is no longer used. Refer to the discussion in PR #14813

View file

@ -410,7 +410,7 @@ void Application::setMemoryWorkingSetLimit(const int size)
return;
m_storeMemoryWorkingSetLimit = size;
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
applyMemoryWorkingSetLimit();
#endif
}
@ -575,9 +575,6 @@ void Application::runExternalProgram(const QString &programTemplate, const BitTo
case u'L':
str.replace(i, 2, torrent->category());
break;
case u'M':
str.replace(i, 2, torrent->comment());
break;
case u'N':
str.replace(i, 2, torrent->name());
break;
@ -662,13 +659,7 @@ void Application::runExternalProgram(const QString &programTemplate, const BitTo
{
// strip redundant quotes
if (arg.startsWith(u'"') && arg.endsWith(u'"'))
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
arg.slice(1, (arg.size() - 2));
#else
arg.removeLast().removeFirst();
#endif
}
arg = arg.mid(1, (arg.size() - 2));
arg = replaceVariables(arg);
}
@ -677,7 +668,6 @@ void Application::runExternalProgram(const QString &programTemplate, const BitTo
QProcess proc;
proc.setProgram(command);
proc.setArguments(args);
proc.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
if (proc.startDetached())
{
@ -847,7 +837,7 @@ int Application::exec()
printf("%s\n", qUtf8Printable(loadingStr));
#endif
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
applyMemoryWorkingSetLimit();
#endif
@ -930,10 +920,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 BitTorrent::AddTorrentError &reason)
, [this](const QString &source, const QString &reason)
{
m_desktopIntegration->showNotification(tr("Add torrent failed")
, tr("Couldn't add torrent '%1', reason: %2.").arg(source, reason.message));
, tr("Couldn't add torrent '%1', reason: %2.").arg(source, reason));
});
disconnect(m_desktopIntegration, &DesktopIntegration::activationRequested, this, &Application::createStartupProgressDialog);
@ -1205,7 +1195,7 @@ void Application::shutdownCleanup([[maybe_unused]] QSessionManager &manager)
}
#endif
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
void Application::applyMemoryWorkingSetLimit() const
{
const size_t MiB = 1024 * 1024;

View file

@ -465,13 +465,13 @@ QBtCommandLineParameters parseCommandLine(const QStringList &args)
return result;
}
QString wrapText(const QString &text, const int initialIndentation = USAGE_TEXT_COLUMN, const int wrapAtColumn = WRAP_AT_COLUMN)
QString wrapText(const QString &text, int initialIndentation = USAGE_TEXT_COLUMN, int wrapAtColumn = WRAP_AT_COLUMN)
{
const QStringList words = text.split(u' ');
QStringList words = text.split(u' ');
QStringList lines = {words.first()};
int currentLineMaxLength = wrapAtColumn - initialIndentation;
for (const QString &word : asConst(words.sliced(1)))
for (const QString &word : asConst(words.mid(1)))
{
if (lines.last().length() + word.length() + 1 < currentLineMaxLength)
{
@ -491,12 +491,6 @@ QString makeUsage(const QString &prgName)
{
const QString indentation {USAGE_INDENTATION, u' '};
#if defined(Q_OS_WIN)
const QString noSplashCommand = u"set QBT_NO_SPLASH=1 && " + prgName;
#else
const QString noSplashCommand = u"QBT_NO_SPLASH=1 " + prgName;
#endif
const QString text = QCoreApplication::translate("CMD Options", "Usage:") + u'\n'
+ indentation + prgName + u' ' + QCoreApplication::translate("CMD Options", "[options] [(<filename> | <url>)...]") + u'\n'
@ -548,7 +542,7 @@ QString makeUsage(const QString &prgName)
"'parameter-name', environment variable name is 'QBT_PARAMETER_NAME' (in upper "
"case, '-' replaced with '_'). To pass flag values, set the variable to '1' or "
"'TRUE'. For example, to disable the splash screen: "), 0) + u'\n'
+ noSplashCommand + u'\n'
+ u"QBT_NO_SPLASH=1 " + prgName + u'\n'
+ wrapText(QCoreApplication::translate("CMD Options", "Command line parameters take precedence over environment variables"), 0) + u'\n';
return text;

View file

@ -175,15 +175,12 @@ void FileLogger::flushLog()
void FileLogger::openLogFile()
{
if (!m_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text))
if (!m_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)
|| !m_logFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner))
{
LogMsg(tr("An error occurred while trying to open the log file. Logging to file is disabled. File: \"%1\". Error: \"%2\".")
.arg(m_logFile.fileName(), m_logFile.errorString()), Log::CRITICAL);
return;
m_logFile.close();
LogMsg(tr("An error occurred while trying to open the log file. Logging to file is disabled."), Log::CRITICAL);
}
// best effort, don't report error
m_logFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
}
void FileLogger::closeLogFile()

View file

@ -6,7 +6,6 @@ add_library(qbt_base STATIC
applicationcomponent.h
asyncfilestorage.h
bittorrent/abstractfilestorage.h
bittorrent/addtorrenterror.h
bittorrent/addtorrentparams.h
bittorrent/announcetimepoint.h
bittorrent/bandwidthscheduler.h
@ -55,7 +54,6 @@ add_library(qbt_base STATIC
concepts/stringable.h
digest32.h
exceptions.h
freediskspacechecker.h
global.h
http/connection.h
http/httperror.h
@ -161,7 +159,6 @@ 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,7 +29,6 @@
#include "addtorrentmanager.h"
#include "base/bittorrent/addtorrenterror.h"
#include "base/bittorrent/infohash.h"
#include "base/bittorrent/session.h"
#include "base/bittorrent/torrentdescriptor.h"
@ -141,7 +140,7 @@ void AddTorrentManager::onSessionTorrentAdded(BitTorrent::Torrent *torrent)
}
}
void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const BitTorrent::AddTorrentError &reason)
void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const QString &reason)
{
if (const QString source = m_sourcesByInfoHash.take(infoHash); !source.isEmpty())
{
@ -155,7 +154,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, {BitTorrent::AddTorrentError::Other, reason});
emit addTorrentFailed(source, reason);
}
void AddTorrentManager::handleDuplicateTorrent(const QString &source
@ -186,9 +185,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\". Torrent infohash: %3. Result: %4")
.arg(source, existingTorrent->name(), existingTorrent->infoHash().toString(), message));
emit addTorrentFailed(source, {BitTorrent::AddTorrentError::DuplicateTorrent, message});
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);
}
void AddTorrentManager::setTorrentFileGuard(const QString &source, std::shared_ptr<TorrentFileGuard> torrentFileGuard)

View file

@ -44,7 +44,6 @@ namespace BitTorrent
class Session;
class Torrent;
class TorrentDescriptor;
struct AddTorrentError;
}
namespace Net
@ -67,7 +66,7 @@ public:
signals:
void torrentAdded(const QString &source, BitTorrent::Torrent *torrent);
void addTorrentFailed(const QString &source, const BitTorrent::AddTorrentError &reason);
void addTorrentFailed(const QString &source, const QString &reason);
protected:
bool addTorrentToSession(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
@ -80,7 +79,7 @@ protected:
private:
void onDownloadFinished(const Net::DownloadResult &result);
void onSessionTorrentAdded(BitTorrent::Torrent *torrent);
void onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const BitTorrent::AddTorrentError &reason);
void onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const QString &reason);
bool processTorrent(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
, const BitTorrent::AddTorrentParams &addTorrentParams);

View file

@ -1,49 +0,0 @@
/*
* 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

@ -147,7 +147,7 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::load(cons
const Path torrentFilePath = path() / Path(idString + u".torrent");
const qint64 torrentSizeLimit = Preferences::instance()->getTorrentFileSizeLimit();
const auto resumeDataReadResult = Utils::IO::readFile(fastresumePath, -1);
const auto resumeDataReadResult = Utils::IO::readFile(fastresumePath, torrentSizeLimit);
if (!resumeDataReadResult)
return nonstd::make_unexpected(resumeDataReadResult.error().message);
@ -189,13 +189,8 @@ void BitTorrent::BencodeResumeDataStorage::loadQueue(const Path &queueFilename)
return;
}
QHash<TorrentID, qsizetype> registeredTorrentsIndexes;
registeredTorrentsIndexes.reserve(m_registeredTorrents.length());
for (qsizetype i = 0; i < m_registeredTorrents.length(); ++i)
registeredTorrentsIndexes.insert(m_registeredTorrents.at(i), i);
const QRegularExpression hashPattern {u"^([A-Fa-f0-9]{40})$"_s};
qsizetype queuePos = 0;
int start = 0;
while (true)
{
const auto line = QString::fromLatin1(queueFile.readLine(lineMaxLength).trimmed());
@ -206,15 +201,11 @@ void BitTorrent::BencodeResumeDataStorage::loadQueue(const Path &queueFilename)
if (rxMatch.hasMatch())
{
const auto torrentID = BitTorrent::TorrentID::fromString(rxMatch.captured(1));
const qsizetype pos = registeredTorrentsIndexes.value(torrentID, -1);
const int pos = m_registeredTorrents.indexOf(torrentID, start);
if (pos != -1)
{
if (pos != queuePos)
{
m_registeredTorrents.swapItemsAt(pos, queuePos);
registeredTorrentsIndexes.insert(m_registeredTorrents.at(pos), pos);
}
++queuePos;
std::swap(m_registeredTorrents[start], m_registeredTorrents[pos]);
++start;
}
}
}
@ -351,9 +342,9 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
return torrentParams;
}
void BitTorrent::BencodeResumeDataStorage::store(const TorrentID &id, LoadTorrentParams resumeData) const
void BitTorrent::BencodeResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const
{
QMetaObject::invokeMethod(m_asyncWorker, [this, id, resumeData = std::move(resumeData)]
QMetaObject::invokeMethod(m_asyncWorker, [this, id, resumeData]()
{
m_asyncWorker->store(id, resumeData);
});

View file

@ -50,7 +50,7 @@ namespace BitTorrent
QList<TorrentID> registeredTorrents() const override;
LoadResumeDataResult load(const TorrentID &id) const override;
void store(const TorrentID &id, LoadTorrentParams resumeData) const override;
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override;
void remove(const TorrentID &id) const override;
void storeQueue(const QList<TorrentID> &queue) const override;

View file

@ -52,7 +52,7 @@ BitTorrent::CategoryOptions BitTorrent::CategoryOptions::fromJSON(const QJsonObj
QJsonObject BitTorrent::CategoryOptions::toJSON() const
{
QJsonValue downloadPathValue = QJsonValue::Null;
QJsonValue downloadPathValue = QJsonValue::Undefined;
if (downloadPath)
{
if (downloadPath->enabled)

View file

@ -86,7 +86,7 @@ namespace
class StoreJob final : public Job
{
public:
StoreJob(const TorrentID &torrentID, LoadTorrentParams resumeData);
StoreJob(const TorrentID &torrentID, const LoadTorrentParams &resumeData);
void perform(QSqlDatabase db) override;
private:
@ -231,7 +231,7 @@ namespace BitTorrent
void run() override;
void requestInterruption();
void store(const TorrentID &id, LoadTorrentParams resumeData);
void store(const TorrentID &id, const LoadTorrentParams &resumeData);
void remove(const TorrentID &id);
void storeQueue(const QList<TorrentID> &queue);
@ -327,9 +327,9 @@ BitTorrent::LoadResumeDataResult BitTorrent::DBResumeDataStorage::load(const Tor
return parseQueryResultRow(query);
}
void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, LoadTorrentParams resumeData) const
void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const
{
m_asyncWorker->store(id, std::move(resumeData));
m_asyncWorker->store(id, resumeData);
}
void BitTorrent::DBResumeDataStorage::remove(const BitTorrent::TorrentID &id) const
@ -769,9 +769,9 @@ void DBResumeDataStorage::Worker::requestInterruption()
m_waitCondition.wakeAll();
}
void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, LoadTorrentParams resumeData)
void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const LoadTorrentParams &resumeData)
{
addJob(std::make_unique<StoreJob>(id, std::move(resumeData)));
addJob(std::make_unique<StoreJob>(id, resumeData));
}
void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID &id)
@ -797,9 +797,9 @@ namespace
{
using namespace BitTorrent;
StoreJob::StoreJob(const TorrentID &torrentID, LoadTorrentParams resumeData)
StoreJob::StoreJob(const TorrentID &torrentID, const LoadTorrentParams &resumeData)
: m_torrentID {torrentID}
, m_resumeData {std::move(resumeData)}
, m_resumeData {resumeData}
{
}

View file

@ -49,7 +49,7 @@ namespace BitTorrent
QList<TorrentID> registeredTorrents() const override;
LoadResumeDataResult load(const TorrentID &id) const override;
void store(const TorrentID &id, LoadTorrentParams resumeData) const override;
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override;
void remove(const TorrentID &id) const override;
void storeQueue(const QList<TorrentID> &queue) const override;

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2020-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2020 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -27,14 +27,13 @@
*/
#include "filesearcher.h"
#include <QPromise>
#include "base/bittorrent/common.h"
#include "base/bittorrent/infohash.h"
namespace
void FileSearcher::search(const BitTorrent::TorrentID &id, const PathList &originalFileNames
, const Path &savePath, const Path &downloadPath, const bool forceAppendExt)
{
bool findInDir(const Path &dirPath, PathList &fileNames, const bool forceAppendExt)
const auto findInDir = [](const Path &dirPath, PathList &fileNames, const bool forceAppendExt) -> bool
{
bool found = false;
for (Path &fileName : fileNames)
@ -59,12 +58,8 @@ namespace
}
return found;
}
}
};
void FileSearcher::search(const PathList &originalFileNames, const Path &savePath
, const Path &downloadPath, const bool forceAppendExt, QPromise<FileSearchResult> &promise)
{
Path usedPath = savePath;
PathList adjustedFileNames = originalFileNames;
const bool found = findInDir(usedPath, adjustedFileNames, (forceAppendExt && downloadPath.isEmpty()));
@ -74,5 +69,5 @@ void FileSearcher::search(const PathList &originalFileNames, const Path &savePat
findInDir(usedPath, adjustedFileNames, forceAppendExt);
}
promise.addResult(FileSearchResult {.savePath = usedPath, .fileNames = adjustedFileNames});
emit searchFinished(id, usedPath, adjustedFileNames);
}

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2020-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2020 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -32,13 +32,10 @@
#include "base/path.h"
template <typename T> class QPromise;
struct FileSearchResult
namespace BitTorrent
{
Path savePath;
PathList fileNames;
};
class TorrentID;
}
class FileSearcher final : public QObject
{
@ -46,8 +43,12 @@ class FileSearcher final : public QObject
Q_DISABLE_COPY_MOVE(FileSearcher)
public:
using QObject::QObject;
FileSearcher() = default;
void search(const PathList &originalFileNames, const Path &savePath
, const Path &downloadPath, bool forceAppendExt, QPromise<FileSearchResult> &promise);
public slots:
void search(const BitTorrent::TorrentID &id, const PathList &originalFileNames
, const Path &savePath, const Path &downloadPath, bool forceAppendExt);
signals:
void searchFinished(const BitTorrent::TorrentID &id, const Path &savePath, const PathList &fileNames);
};

View file

@ -29,9 +29,6 @@
#include "infohash.h"
#include <QHash>
#include <QString>
#include "base/global.h"
const int TorrentIDTypeId = qRegisterMetaType<BitTorrent::TorrentID>();
@ -89,28 +86,6 @@ 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,8 +36,6 @@
#include "base/digest32.h"
class QString;
using SHA1Hash = Digest32<160>;
using SHA256Hash = Digest32<256>;
@ -81,8 +79,6 @@ namespace BitTorrent
SHA256Hash v2() const;
TorrentID toTorrentID() const;
QString toString() const;
operator WrappedType() const;
private:

View file

@ -39,11 +39,7 @@ PeerAddress PeerAddress::parse(const QStringView address)
if (address.startsWith(u'[') && address.contains(u"]:"))
{ // IPv6
ipPort = address.split(u"]:");
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
ipPort[0].slice(1); // chop '['
#else
ipPort[0] = ipPort[0].sliced(1); // chop '['
#endif
ipPort[0] = ipPort[0].mid(1); // chop '['
}
else if (address.contains(u':'))
{ // IPv4

View file

@ -71,8 +71,8 @@ QList<BitTorrent::LoadedResumeData> BitTorrent::ResumeDataStorage::fetchLoadedRe
return loadedResumeData;
}
void BitTorrent::ResumeDataStorage::onResumeDataLoaded(const TorrentID &torrentID, LoadResumeDataResult loadResumeDataResult) const
void BitTorrent::ResumeDataStorage::onResumeDataLoaded(const TorrentID &torrentID, const LoadResumeDataResult &loadResumeDataResult) const
{
const QMutexLocker locker {&m_loadedResumeDataMutex};
m_loadedResumeData.append({.torrentID = torrentID, .result = std::move(loadResumeDataResult)});
m_loadedResumeData.append({torrentID, loadResumeDataResult});
}

View file

@ -60,7 +60,7 @@ namespace BitTorrent
virtual QList<TorrentID> registeredTorrents() const = 0;
virtual LoadResumeDataResult load(const TorrentID &id) const = 0;
virtual void store(const TorrentID &id, LoadTorrentParams resumeData) const = 0;
virtual void store(const TorrentID &id, const LoadTorrentParams &resumeData) const = 0;
virtual void remove(const TorrentID &id) const = 0;
virtual void storeQueue(const QList<TorrentID> &queue) const = 0;
@ -72,7 +72,7 @@ namespace BitTorrent
void loadFinished();
protected:
void onResumeDataLoaded(const TorrentID &torrentID, LoadResumeDataResult loadResumeDataResult) const;
void onResumeDataLoaded(const TorrentID &torrentID, const LoadResumeDataResult &loadResumeDataResult) const;
private:
virtual void doLoadAll() const = 0;

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 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,7 +34,6 @@
#include "base/pathfwd.h"
#include "base/tagset.h"
#include "addtorrenterror.h"
#include "addtorrentparams.h"
#include "categoryoptions.h"
#include "sharelimitaction.h"
@ -422,8 +421,6 @@ 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;
@ -482,17 +479,16 @@ 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 AddTorrentError &reason);
void addTorrentFailed(const InfoHash &infoHash, const QString &reason);
void allTorrentsFinished();
void categoryAdded(const QString &categoryName);
void categoryRemoved(const QString &categoryName);
void categoryOptionsChanged(const QString &categoryName);
void fullDiskError(Torrent *torrent, const QString &msg);
void IPFilterParsed(bool error, int ruleCount);
void loadTorrentFailed(const QString &error);
void metadataDownloaded(const TorrentInfo &info);
void restored();
void paused();
@ -523,6 +519,5 @@ namespace BitTorrent
void trackerSuccess(Torrent *torrent, const QString &tracker);
void trackerWarning(Torrent *torrent, const QString &tracker);
void trackerEntryStatusesUpdated(Torrent *torrent, const QHash<QString, TrackerEntryStatus> &updatedTrackers);
void freeDiskSpaceChecked(qint64 result);
};
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -30,7 +30,6 @@
#pragma once
#include <chrono>
#include <functional>
#include <utility>
#include <vector>
@ -62,16 +61,11 @@ class QString;
class QTimer;
class QUrl;
template <typename T> class QFuture;
class BandwidthScheduler;
class FileSearcher;
class FilterParserThread;
class FreeDiskSpaceChecker;
class NativeSessionExtension;
struct FileSearchResult;
namespace BitTorrent
{
enum class MoveStorageMode;
@ -396,8 +390,6 @@ 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;
@ -456,8 +448,6 @@ 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);
@ -477,15 +467,14 @@ namespace BitTorrent
void handleTorrentTrackersChanged(TorrentImpl *torrent);
void handleTorrentUrlSeedsAdded(TorrentImpl *torrent, const QList<QUrl> &newUrlSeeds);
void handleTorrentUrlSeedsRemoved(TorrentImpl *torrent, const QList<QUrl> &urlSeeds);
void handleTorrentResumeDataReady(TorrentImpl *torrent, LoadTorrentParams data);
void handleTorrentResumeDataReady(TorrentImpl *torrent, const LoadTorrentParams &data);
void handleTorrentInfoHashChanged(TorrentImpl *torrent, const InfoHash &prevInfoHash);
void handleTorrentStorageMovingStateChanged(TorrentImpl *torrent);
bool addMoveTorrentStorageJob(TorrentImpl *torrent, const Path &newPath, MoveStorageMode mode, MoveStorageContext context);
lt::torrent_handle reloadTorrent(const lt::torrent_handle &currentHandle, lt::add_torrent_params params);
QFuture<FileSearchResult> findIncompleteFiles(const Path &savePath, const Path &downloadPath, const PathList &filePaths = {}) const;
void findIncompleteFiles(const TorrentInfo &torrentInfo, const Path &savePath
, const Path &downloadPath, const PathList &filePaths = {}) const;
void enablePortMapping();
void disablePortMapping();
@ -520,6 +509,7 @@ namespace BitTorrent
void generateResumeData();
void handleIPFilterParsed(int ruleCount);
void handleIPFilterError();
void fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames);
void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage);
private:
@ -578,7 +568,8 @@ namespace BitTorrent
void updateSeedingLimitTimer();
void exportTorrentFile(const Torrent *torrent, const Path &folderPath);
void handleAlert(lt::alert *alert);
void handleAlert(const lt::alert *alert);
void dispatchTorrentAlert(const lt::torrent_alert *alert);
void handleAddTorrentAlert(const lt::add_torrent_alert *alert);
void handleStateUpdateAlert(const lt::state_update_alert *alert);
void handleMetadataReceivedAlert(const lt::metadata_received_alert *alert);
@ -605,20 +596,9 @@ namespace BitTorrent
void handleTrackerAlert(const lt::tracker_alert *alert);
#ifdef QBT_USES_LIBTORRENT2
void handleTorrentConflictAlert(const lt::torrent_conflict_alert *alert);
void handleFilePrioAlert(const lt::file_prio_alert *alert);
#endif
void handleFastResumeRejectedAlert(const lt::fastresume_rejected_alert *alert);
void handleFileCompletedAlert(const lt::file_completed_alert *alert);
void handleFileRenamedAlert(const lt::file_renamed_alert *alert);
void handleFileRenameFailedAlert(const lt::file_rename_failed_alert *alert);
void handlePerformanceAlert(const lt::performance_alert *alert) const;
void handleSaveResumeDataAlert(lt::save_resume_data_alert *alert);
void handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *alert);
void handleTorrentCheckedAlert(const lt::torrent_checked_alert *alert);
void handleTorrentFinishedAlert(const lt::torrent_finished_alert *alert);
TorrentImpl *createTorrent(const lt::torrent_handle &nativeHandle, LoadTorrentParams params);
TorrentImpl *getTorrent(const lt::torrent_handle &nativeHandle) const;
TorrentImpl *createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params);
void saveResumeData();
void saveTorrentsQueue();
@ -703,7 +683,6 @@ 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;
@ -825,13 +804,11 @@ namespace BitTorrent
FileSearcher *m_fileSearcher = nullptr;
TorrentContentRemover *m_torrentContentRemover = nullptr;
using AddTorrentAlertHandler = std::function<void (const lt::add_torrent_alert *alert)>;
QList<AddTorrentAlertHandler> m_addTorrentAlertHandlers;
QHash<TorrentID, lt::torrent_handle> m_downloadedMetadata;
QHash<TorrentID, TorrentImpl *> m_torrents;
QHash<TorrentID, TorrentImpl *> m_hybridTorrentsByAltID;
QHash<TorrentID, LoadTorrentParams> m_loadingTorrents;
QHash<TorrentID, RemovingTorrentData> m_removingTorrents;
QHash<TorrentID, TorrentID> m_changedTorrentIDs;
QMap<QString, CategoryOptions> m_categories;
@ -873,10 +850,6 @@ 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

@ -29,8 +29,6 @@
#include "torrent.h"
#include <limits>
#include <QHash>
#include "infohash.h"
@ -53,7 +51,9 @@ namespace BitTorrent
const int Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME = -2;
const int Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT = -1;
const qreal Torrent::MAX_RATIO = std::numeric_limits<qreal>::infinity();
const qreal Torrent::MAX_RATIO = 9999;
const int Torrent::MAX_SEEDING_TIME = 525600;
const int Torrent::MAX_INACTIVE_SEEDING_TIME = 525600;
TorrentID Torrent::id() const
{

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -45,8 +45,6 @@ class QByteArray;
class QDateTime;
class QUrl;
template <typename T> class QFuture;
namespace BitTorrent
{
enum class DownloadPriority;
@ -134,6 +132,8 @@ namespace BitTorrent
static const int NO_INACTIVE_SEEDING_TIME_LIMIT;
static const qreal MAX_RATIO;
static const int MAX_SEEDING_TIME;
static const int MAX_INACTIVE_SEEDING_TIME;
using TorrentContentHandler::TorrentContentHandler;
@ -275,7 +275,10 @@ namespace BitTorrent
virtual bool isDHTDisabled() const = 0;
virtual bool isPEXDisabled() const = 0;
virtual bool isLSDDisabled() const = 0;
virtual QList<PeerInfo> peers() const = 0;
virtual QBitArray pieces() const = 0;
virtual QBitArray downloadingPieces() const = 0;
virtual QList<int> pieceAvailability() const = 0;
virtual qreal distributedCopies() const = 0;
virtual qreal maxRatio() const = 0;
virtual int maxSeedingTime() const = 0;
@ -322,10 +325,10 @@ namespace BitTorrent
virtual nonstd::expected<QByteArray, QString> exportToBuffer() const = 0;
virtual nonstd::expected<void, QString> exportToFile(const Path &path) const = 0;
virtual QFuture<QList<PeerInfo>> fetchPeerInfo() const = 0;
virtual QFuture<QList<QUrl>> fetchURLSeeds() const = 0;
virtual QFuture<QList<int>> fetchPieceAvailability() const = 0;
virtual QFuture<QBitArray> fetchDownloadingPieces() const = 0;
virtual void fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHandler) const = 0;
virtual void fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler) const = 0;
virtual void fetchPieceAvailability(std::function<void (QList<int>)> resultHandler) const = 0;
virtual void fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const = 0;
TorrentID id() const;
bool isRunning() const;

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -34,8 +34,6 @@
#include "abstractfilestorage.h"
#include "downloadpriority.h"
template <typename T> class QFuture;
namespace BitTorrent
{
class TorrentContentHandler : public QObject, public AbstractFileStorage
@ -54,7 +52,8 @@ namespace BitTorrent
* This is not the same as torrrent availability, it is just a fraction of pieces
* that can be downloaded right now. It varies between 0 to 1.
*/
virtual QFuture<QList<qreal>> fetchAvailableFileFractions() const = 0;
virtual QList<qreal> availableFileFractions() const = 0;
virtual void fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const = 0;
virtual void prioritizeFiles(const QList<DownloadPriority> &priorities) = 0;
virtual void flushCache() const = 0;

View file

@ -124,20 +124,18 @@ void TorrentCreator::run()
// need to sort the file names by natural sort order
QStringList dirs = {m_params.sourcePath.data()};
QDirIterator dirIter {m_params.sourcePath.data(), (QDir::AllDirs | QDir::NoDotAndDotDot), QDirIterator::Subdirectories};
#ifdef Q_OS_WIN
// libtorrent couldn't handle .lnk files on Windows
// Also, Windows users do not expect torrent creator to traverse into .lnk files so skip over them
const QDir::Filters dirFilters {QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks};
#else
const QDir::Filters dirFilters {QDir::AllDirs | QDir::NoDotAndDotDot};
#endif
QDirIterator dirIter {m_params.sourcePath.data(), dirFilters, QDirIterator::Subdirectories};
while (dirIter.hasNext())
{
const QFileInfo dirInfo = dirIter.nextFileInfo();
#ifdef Q_OS_WIN
// .lnk to directory
// Windows users do not expect torrent creator to traverse into .lnk files so skip over them
if (dirInfo.isShortcut())
continue;
#endif
const QString dirPath = dirInfo.filePath();
dirs.append(dirPath);
const QString filePath = dirIter.next();
dirs.append(filePath);
}
std::sort(dirs.begin(), dirs.end(), naturalLessThan);
@ -148,29 +146,19 @@ void TorrentCreator::run()
{
QStringList tmpNames; // natural sort files within each dir
QDirIterator fileIter {dir, QDir::Files};
#ifdef Q_OS_WIN
const QDir::Filters fileFilters {QDir::Files | QDir::NoSymLinks};
#else
const QDir::Filters fileFilters {QDir::Files};
#endif
QDirIterator fileIter {dir, fileFilters};
while (fileIter.hasNext())
{
const QFileInfo fileInfo = fileIter.nextFileInfo();
const Path filePath {fileInfo.filePath()};
qint64 fileSize = fileInfo.size();
#ifdef Q_OS_WIN
// .lnk to file
// libtorrent couldn't handle .lnk files on Windows
if (fileInfo.isShortcut())
continue;
// file symbolic link
// QFileInfo::size() failed to return the target file size
// and we need to redirect it manually
if (fileInfo.isSymbolicLink())
fileSize = QFileInfo(fileInfo.symLinkTarget()).size();
#endif
const Path relFilePath = parentPath.relativePathOf(filePath);
const Path relFilePath = parentPath.relativePathOf(Path(fileInfo.filePath()));
tmpNames.append(relFilePath.toString());
fileSizeMap[tmpNames.last()] = fileSize;
fileSizeMap[tmpNames.last()] = fileInfo.size();
}
std::sort(tmpNames.begin(), tmpNames.end(), naturalLessThan);
@ -186,8 +174,8 @@ void TorrentCreator::run()
#ifdef QBT_USES_LIBTORRENT2
lt::create_torrent newTorrent {fs, m_params.pieceSize, toNativeTorrentFormatFlag(m_params.torrentFormat)};
#else
lt::create_torrent newTorrent {fs, m_params.pieceSize, m_params.paddedFileSizeLimit
, (m_params.isAlignmentOptimized ? lt::create_torrent::optimize_alignment : lt::create_flags_t {})};
lt::create_torrent newTorrent(fs, m_params.pieceSize, m_params.paddedFileSizeLimit
, (m_params.isAlignmentOptimized ? lt::create_torrent::optimize_alignment : lt::create_flags_t {}));
#endif
// Add url seeds

View file

@ -141,22 +141,6 @@ catch (const lt::system_error &err)
return nonstd::make_unexpected(QString::fromLocal8Bit(err.what()));
}
nonstd::expected<QByteArray, QString> BitTorrent::TorrentDescriptor::saveToBuffer() const
try
{
const lt::entry torrentEntry = lt::write_torrent_file(m_ltAddTorrentParams);
// usually torrent size should be smaller than 1 MB,
// however there are >100 MB v2/hybrid torrent files out in the wild
QByteArray buffer;
buffer.reserve(1024 * 1024);
lt::bencode(std::back_inserter(buffer), torrentEntry);
return buffer;
}
catch (const lt::system_error &err)
{
return nonstd::make_unexpected(QString::fromLocal8Bit(err.what()));
}
BitTorrent::TorrentDescriptor::TorrentDescriptor(lt::add_torrent_params ltAddTorrentParams)
: m_ltAddTorrentParams {std::move(ltAddTorrentParams)}
{

View file

@ -69,7 +69,6 @@ namespace BitTorrent
static nonstd::expected<TorrentDescriptor, QString> loadFromFile(const Path &path) noexcept;
static nonstd::expected<TorrentDescriptor, QString> parse(const QString &str) noexcept;
nonstd::expected<void, QString> saveToFile(const Path &path) const;
nonstd::expected<QByteArray, QString> saveToBuffer() const;
const lt::add_torrent_params &ltAddTorrentParams() const;

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -37,10 +37,11 @@
#endif
#include <libtorrent/address.hpp>
#include <libtorrent/alert_types.hpp>
#include <libtorrent/create_torrent.hpp>
#include <libtorrent/session.hpp>
#include <libtorrent/storage_defs.hpp>
#include <libtorrent/time.hpp>
#include <libtorrent/write_resume_data.hpp>
#ifdef QBT_USES_LIBTORRENT2
#include <libtorrent/info_hash.hpp>
@ -50,9 +51,7 @@
#include <QByteArray>
#include <QCache>
#include <QDebug>
#include <QFuture>
#include <QPointer>
#include <QPromise>
#include <QSet>
#include <QStringList>
#include <QUrl>
@ -68,7 +67,6 @@
#include "common.h"
#include "downloadpriority.h"
#include "extensiondata.h"
#include "filesearcher.h"
#include "loadtorrentparams.h"
#include "ltqbitarray.h"
#include "lttypecast.h"
@ -151,14 +149,10 @@ namespace
if (ltAnnounceInfo.updating)
{
trackerEndpointStatus.isUpdating = true;
trackerEndpointStatus.state = TrackerEndpointState::Updating;
++numUpdating;
}
else
{
trackerEndpointStatus.isUpdating = false;
if (ltAnnounceInfo.fails > 0)
else if (ltAnnounceInfo.fails > 0)
{
if (ltAnnounceInfo.last_error == lt::errors::tracker_failure)
{
@ -185,7 +179,6 @@ namespace
{
trackerEndpointStatus.state = TrackerEndpointState::NotContacted;
}
}
if (!ltAnnounceInfo.message.empty())
{
@ -219,13 +212,9 @@ namespace
{
if (numUpdating > 0)
{
trackerEntryStatus.isUpdating = true;
trackerEntryStatus.state = TrackerEndpointState::Updating;
}
else
{
trackerEntryStatus.isUpdating = false;
if (numWorking > 0)
else if (numWorking > 0)
{
trackerEntryStatus.state = TrackerEndpointState::Working;
}
@ -242,7 +231,6 @@ namespace
trackerEntryStatus.state = TrackerEndpointState::NotWorking;
}
}
}
trackerEntryStatus.numPeers = -1;
trackerEntryStatus.numSeeds = -1;
@ -299,9 +287,11 @@ namespace
// TorrentImpl
TorrentImpl::TorrentImpl(SessionImpl *session, const lt::torrent_handle &nativeHandle, LoadTorrentParams params)
TorrentImpl::TorrentImpl(SessionImpl *session, lt::session *nativeSession
, const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params)
: Torrent(session)
, m_session(session)
, m_nativeSession(nativeSession)
, m_nativeHandle(nativeHandle)
#ifdef QBT_USES_LIBTORRENT2
, m_infoHash(m_nativeHandle.info_hashes())
@ -324,7 +314,7 @@ TorrentImpl::TorrentImpl(SessionImpl *session, const lt::torrent_handle &nativeH
, m_useAutoTMM(params.useAutoTMM)
, m_isStopped(params.stopped)
, m_sslParams(params.sslParameters)
, m_ltAddTorrentParams(std::move(params.ltAddTorrentParams))
, m_ltAddTorrentParams(params.ltAddTorrentParams)
, m_downloadLimit(cleanLimitValue(m_ltAddTorrentParams.download_limit))
, m_uploadLimit(cleanLimitValue(m_ltAddTorrentParams.upload_limit))
{
@ -1447,7 +1437,7 @@ int TorrentImpl::totalLeechersCount() const
int TorrentImpl::downloadLimit() const
{
return m_downloadLimit;
return m_downloadLimit;;
}
int TorrentImpl::uploadLimit() const
@ -1475,11 +1465,48 @@ bool TorrentImpl::isLSDDisabled() const
return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::disable_lsd);
}
QList<PeerInfo> TorrentImpl::peers() const
{
std::vector<lt::peer_info> nativePeers;
m_nativeHandle.get_peer_info(nativePeers);
QList<PeerInfo> peers;
peers.reserve(static_cast<decltype(peers)::size_type>(nativePeers.size()));
for (const lt::peer_info &peer : nativePeers)
peers.append(PeerInfo(peer, pieces()));
return peers;
}
QBitArray TorrentImpl::pieces() const
{
return m_pieces;
}
QBitArray TorrentImpl::downloadingPieces() const
{
if (!hasMetadata())
return {};
std::vector<lt::partial_piece_info> queue;
m_nativeHandle.get_download_queue(queue);
QBitArray result {piecesCount()};
for (const lt::partial_piece_info &info : queue)
result.setBit(LT::toUnderlyingType(info.piece_index));
return result;
}
QList<int> TorrentImpl::pieceAvailability() const
{
std::vector<int> avail;
m_nativeHandle.piece_availability(avail);
return {avail.cbegin(), avail.cend()};
}
qreal TorrentImpl::distributedCopies() const
{
return m_nativeStatus.distributed_copies;
@ -1522,8 +1549,7 @@ qreal TorrentImpl::realRatio() const
const qreal ratio = upload / static_cast<qreal>(download);
Q_ASSERT(ratio >= 0);
return ratio;
return (ratio > MAX_RATIO) ? MAX_RATIO : ratio;
}
int TorrentImpl::uploadPayloadRate() const
@ -1725,6 +1751,12 @@ void TorrentImpl::applyFirstLastPiecePriority(const bool enabled)
m_nativeHandle.prioritize_pieces(piecePriorities);
}
void TorrentImpl::fileSearchFinished(const Path &savePath, const PathList &fileNames)
{
if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
endReceivedMetadataHandling(savePath, fileNames);
}
TrackerEntryStatus TorrentImpl::updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo)
{
const auto it = std::find_if(m_trackerEntryStatuses.begin(), m_trackerEntryStatuses.end()
@ -1835,7 +1867,7 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi
applyFirstLastPiecePriority(true);
m_maintenanceJob = MaintenanceJob::None;
prepareResumeData(std::move(p));
prepareResumeData(p);
m_session->handleTorrentMetadataReceived(this);
}
@ -1844,6 +1876,16 @@ void TorrentImpl::reload()
{
try
{
m_completedFiles.fill(false);
m_filesProgress.fill(0);
m_pieces.fill(false);
m_nativeStatus.pieces.clear_all();
m_nativeStatus.num_pieces = 0;
const auto queuePos = m_nativeHandle.queue_position();
m_nativeSession->remove_torrent(m_nativeHandle, lt::session::delete_partfile);
lt::add_torrent_params p = m_ltAddTorrentParams;
p.flags |= lt::torrent_flags::update_subscribe
| lt::torrent_flags::override_trackers
@ -1863,21 +1905,19 @@ void TorrentImpl::reload()
p.flags &= ~(lt::torrent_flags::auto_managed | lt::torrent_flags::paused);
}
const auto queuePos = m_nativeHandle.queue_position();
auto *const extensionData = new ExtensionData;
p.userdata = LTClientData(extensionData);
#ifndef QBT_USES_LIBTORRENT2
p.storage = customStorageConstructor;
#endif
m_nativeHandle = m_nativeSession->add_torrent(p);
m_nativeHandle = m_session->reloadTorrent(m_nativeHandle, std::move(p));
m_nativeStatus = static_cast<ExtensionData *>(m_nativeHandle.userdata())->status;
m_nativeStatus = extensionData->status;
if (queuePos >= lt::queue_position_t {})
m_nativeHandle.queue_position_set(queuePos);
m_nativeStatus.queue_position = queuePos;
m_completedFiles.fill(false);
m_filesProgress.fill(0);
m_pieces.fill(false);
m_nativeStatus.pieces.clear_all();
m_nativeStatus.num_pieces = 0;
updateState();
}
catch (const lt::system_error &err)
@ -2022,7 +2062,7 @@ void TorrentImpl::handleMoveStorageJobFinished(const Path &path, const MoveStora
}
}
void TorrentImpl::handleTorrentChecked()
void TorrentImpl::handleTorrentCheckedAlert([[maybe_unused]] const lt::torrent_checked_alert *p)
{
if (!hasMetadata())
{
@ -2065,7 +2105,7 @@ void TorrentImpl::handleTorrentChecked()
});
}
void TorrentImpl::handleTorrentFinished()
void TorrentImpl::handleTorrentFinishedAlert([[maybe_unused]] const lt::torrent_finished_alert *p)
{
m_hasMissingFiles = false;
if (m_hasFinishedStatus)
@ -2088,29 +2128,37 @@ void TorrentImpl::handleTorrentFinished()
m_hasFinishedStatus = true;
if (isMoveInProgress() || (m_renameCount > 0))
m_moveFinishedTriggers.enqueue([this] { m_session->handleTorrentFinished(this); });
m_moveFinishedTriggers.enqueue([this]() { m_session->handleTorrentFinished(this); });
else
m_session->handleTorrentFinished(this);
}
});
}
void TorrentImpl::handleSaveResumeData(lt::add_torrent_params params)
void TorrentImpl::handleTorrentPausedAlert([[maybe_unused]] const lt::torrent_paused_alert *p)
{
if (m_ltAddTorrentParams.url_seeds != params.url_seeds)
}
void TorrentImpl::handleTorrentResumedAlert([[maybe_unused]] const lt::torrent_resumed_alert *p)
{
}
void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p)
{
if (m_ltAddTorrentParams.url_seeds != p->params.url_seeds)
{
// URL seed list have been changed by libtorrent for some reason, so we need to update cached one.
// Unfortunately, URL seed list containing in "resume data" is generated according to different rules
// than the list we usually cache, so we have to request it from the appropriate source.
fetchURLSeeds().then(this, [this](const QList<QUrl> &urlSeeds) { m_urlSeeds = urlSeeds; });
fetchURLSeeds([this](const QList<QUrl> &urlSeeds) { m_urlSeeds = urlSeeds; });
}
if ((m_maintenanceJob == MaintenanceJob::HandleMetadata) && params.ti)
if ((m_maintenanceJob == MaintenanceJob::HandleMetadata) && p->params.ti)
{
Q_ASSERT(m_indexMap.isEmpty());
const auto isSeedMode = static_cast<bool>(m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode);
m_ltAddTorrentParams = std::move(params);
m_ltAddTorrentParams = p->params;
if (isSeedMode)
m_ltAddTorrentParams.flags |= lt::torrent_flags::seed_mode;
@ -2149,20 +2197,15 @@ void TorrentImpl::handleSaveResumeData(lt::add_torrent_params params)
filePaths[i] = Path(it->second);
}
m_session->findIncompleteFiles(savePath(), downloadPath(), filePaths).then(this
, [this](const FileSearchResult &result)
{
if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
endReceivedMetadataHandling(result.savePath, result.fileNames);
});
m_session->findIncompleteFiles(metadata, savePath(), downloadPath(), filePaths);
}
else
{
prepareResumeData(std::move(params));
prepareResumeData(p->params);
}
}
void TorrentImpl::prepareResumeData(lt::add_torrent_params params)
void TorrentImpl::prepareResumeData(const lt::add_torrent_params &params)
{
{
decltype(params.have_pieces) havePieces;
@ -2186,7 +2229,7 @@ void TorrentImpl::prepareResumeData(lt::add_torrent_params params)
}
// Update recent resume data
m_ltAddTorrentParams = std::move(params);
m_ltAddTorrentParams = params;
if (needPreserveProgress)
{
@ -2202,7 +2245,7 @@ void TorrentImpl::prepareResumeData(lt::add_torrent_params params)
// We shouldn't save upload_mode flag to allow torrent operate normally on next run
m_ltAddTorrentParams.flags &= ~lt::torrent_flags::upload_mode;
LoadTorrentParams resumeData
const LoadTorrentParams resumeData
{
.ltAddTorrentParams = m_ltAddTorrentParams,
.name = m_name,
@ -2225,20 +2268,33 @@ void TorrentImpl::prepareResumeData(lt::add_torrent_params params)
.sslParameters = m_sslParams
};
m_session->handleTorrentResumeDataReady(this, std::move(resumeData));
m_session->handleTorrentResumeDataReady(this, resumeData);
}
void TorrentImpl::handleFastResumeRejected()
void TorrentImpl::handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *p)
{
if (p->error != lt::errors::resume_data_not_modified)
{
LogMsg(tr("Generate resume data failed. Torrent: \"%1\". Reason: \"%2\"")
.arg(name(), Utils::String::fromLocal8Bit(p->error.message())), Log::CRITICAL);
}
}
void TorrentImpl::handleFastResumeRejectedAlert(const lt::fastresume_rejected_alert *p)
{
// Files were probably moved or storage isn't accessible
m_hasMissingFiles = true;
LogMsg(tr("Failed to restore torrent. Files were probably moved or storage isn't accessible. Torrent: \"%1\". Reason: \"%2\"")
.arg(name(), QString::fromStdString(p->message())), Log::WARNING);
}
void TorrentImpl::handleFileRenamed(const lt::file_index_t nativeFileIndex, const Path &newActualFilePath, const Path &oldActualFilePath)
void TorrentImpl::handleFileRenamedAlert(const lt::file_renamed_alert *p)
{
const int fileIndex = fileIndexFromNative(nativeFileIndex);
const int fileIndex = m_indexMap.value(p->index, -1);
Q_ASSERT(fileIndex >= 0);
const Path newActualFilePath {QString::fromUtf8(p->new_name())};
const Path oldFilePath = m_filePaths.at(fileIndex);
const Path newFilePath = makeUserPath(newActualFilePath);
@ -2248,6 +2304,11 @@ void TorrentImpl::handleFileRenamed(const lt::file_index_t nativeFileIndex, cons
if (oldFilePath.data() == newFilePath.data())
{
// Remove empty ".unwanted" folders
#ifdef QBT_USES_LIBTORRENT2
const Path oldActualFilePath {QString::fromUtf8(p->old_name())};
#else
const Path oldActualFilePath;
#endif
const Path oldActualParentPath = oldActualFilePath.parentPath();
const Path newActualParentPath = newActualFilePath.parentPath();
if (newActualParentPath.filename() == UNWANTED_FOLDER_NAME)
@ -2297,11 +2358,14 @@ void TorrentImpl::handleFileRenamed(const lt::file_index_t nativeFileIndex, cons
deferredRequestResumeData();
}
void TorrentImpl::handleFileRenameFailed(const lt::file_index_t nativeFileIndex)
void TorrentImpl::handleFileRenameFailedAlert(const lt::file_rename_failed_alert *p)
{
const int fileIndex = fileIndexFromNative(nativeFileIndex);
const int fileIndex = m_indexMap.value(p->index, -1);
Q_ASSERT(fileIndex >= 0);
LogMsg(tr("File rename failed. Torrent: \"%1\", file: \"%2\", reason: \"%3\"")
.arg(name(), filePath(fileIndex).toString(), Utils::String::fromLocal8Bit(p->error.message())), Log::WARNING);
--m_renameCount;
while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty())
m_moveFinishedTriggers.takeFirst()();
@ -2309,12 +2373,12 @@ void TorrentImpl::handleFileRenameFailed(const lt::file_index_t nativeFileIndex)
deferredRequestResumeData();
}
void TorrentImpl::handleFileCompleted(const lt::file_index_t nativeFileIndex)
void TorrentImpl::handleFileCompletedAlert(const lt::file_completed_alert *p)
{
if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
return;
const int fileIndex = fileIndexFromNative(nativeFileIndex);
const int fileIndex = m_indexMap.value(p->index, -1);
Q_ASSERT(fileIndex >= 0);
m_completedFiles.setBit(fileIndex);
@ -2342,13 +2406,22 @@ void TorrentImpl::handleFileCompleted(const lt::file_index_t nativeFileIndex)
}
}
void TorrentImpl::handleFileError(FileErrorInfo fileError)
void TorrentImpl::handleFileErrorAlert(const lt::file_error_alert *p)
{
m_lastFileError = std::move(fileError);
m_lastFileError = {p->error, p->op};
}
void TorrentImpl::handleMetadataReceived()
#ifdef QBT_USES_LIBTORRENT2
void TorrentImpl::handleFilePrioAlert(const lt::file_prio_alert *)
{
deferredRequestResumeData();
}
#endif
void TorrentImpl::handleMetadataReceivedAlert([[maybe_unused]] const lt::metadata_received_alert *p)
{
qDebug("Metadata received for torrent %s.", qUtf8Printable(name()));
#ifdef QBT_USES_LIBTORRENT2
const InfoHash prevInfoHash = infoHash();
m_infoHash = InfoHash(m_nativeHandle.info_hashes());
@ -2360,6 +2433,12 @@ void TorrentImpl::handleMetadataReceived()
deferredRequestResumeData();
}
void TorrentImpl::handlePerformanceAlert(const lt::performance_alert *p) const
{
LogMsg((tr("Performance alert: %1. More info: %2").arg(QString::fromStdString(p->message()), u"https://libtorrent.org/reference-Alerts.html#enum-performance-warning-t"_s))
, Log::INFO);
}
void TorrentImpl::handleCategoryOptionsChanged()
{
if (m_useAutoTMM)
@ -2382,6 +2461,57 @@ void TorrentImpl::handleUnwantedFolderToggled()
manageActualFilePaths();
}
void TorrentImpl::handleAlert(const lt::alert *a)
{
switch (a->type())
{
#ifdef QBT_USES_LIBTORRENT2
case lt::file_prio_alert::alert_type:
handleFilePrioAlert(static_cast<const lt::file_prio_alert*>(a));
break;
#endif
case lt::file_renamed_alert::alert_type:
handleFileRenamedAlert(static_cast<const lt::file_renamed_alert*>(a));
break;
case lt::file_rename_failed_alert::alert_type:
handleFileRenameFailedAlert(static_cast<const lt::file_rename_failed_alert*>(a));
break;
case lt::file_completed_alert::alert_type:
handleFileCompletedAlert(static_cast<const lt::file_completed_alert*>(a));
break;
case lt::file_error_alert::alert_type:
handleFileErrorAlert(static_cast<const lt::file_error_alert*>(a));
break;
case lt::torrent_finished_alert::alert_type:
handleTorrentFinishedAlert(static_cast<const lt::torrent_finished_alert*>(a));
break;
case lt::save_resume_data_alert::alert_type:
handleSaveResumeDataAlert(static_cast<const lt::save_resume_data_alert*>(a));
break;
case lt::save_resume_data_failed_alert::alert_type:
handleSaveResumeDataFailedAlert(static_cast<const lt::save_resume_data_failed_alert*>(a));
break;
case lt::torrent_paused_alert::alert_type:
handleTorrentPausedAlert(static_cast<const lt::torrent_paused_alert*>(a));
break;
case lt::torrent_resumed_alert::alert_type:
handleTorrentResumedAlert(static_cast<const lt::torrent_resumed_alert*>(a));
break;
case lt::metadata_received_alert::alert_type:
handleMetadataReceivedAlert(static_cast<const lt::metadata_received_alert*>(a));
break;
case lt::fastresume_rejected_alert::alert_type:
handleFastResumeRejectedAlert(static_cast<const lt::fastresume_rejected_alert*>(a));
break;
case lt::torrent_checked_alert::alert_type:
handleTorrentCheckedAlert(static_cast<const lt::torrent_checked_alert*>(a));
break;
case lt::performance_alert::alert_type:
handlePerformanceAlert(static_cast<const lt::performance_alert*>(a));
break;
}
}
void TorrentImpl::manageActualFilePaths()
{
const std::shared_ptr<const lt::torrent_info> nativeInfo = nativeTorrentInfo();
@ -2429,11 +2559,6 @@ lt::torrent_handle TorrentImpl::nativeHandle() const
return m_nativeHandle;
}
int TorrentImpl::fileIndexFromNative(const lt::file_index_t nativeFileIndex) const
{
return m_indexMap.value(nativeFileIndex, -1);
}
void TorrentImpl::setMetadata(const TorrentInfo &torrentInfo)
{
if (hasMetadata())
@ -2587,6 +2712,8 @@ void TorrentImpl::setRatioLimit(qreal limit)
{
if (limit < USE_GLOBAL_RATIO)
limit = NO_RATIO_LIMIT;
else if (limit > MAX_RATIO)
limit = MAX_RATIO;
if (m_ratioLimit != limit)
{
@ -2600,6 +2727,8 @@ void TorrentImpl::setSeedingTimeLimit(int limit)
{
if (limit < USE_GLOBAL_SEEDING_TIME)
limit = NO_SEEDING_TIME_LIMIT;
else if (limit > MAX_SEEDING_TIME)
limit = MAX_SEEDING_TIME;
if (m_seedingTimeLimit != limit)
{
@ -2613,6 +2742,8 @@ void TorrentImpl::setInactiveSeedingTimeLimit(int limit)
{
if (limit < USE_GLOBAL_INACTIVE_SEEDING_TIME)
limit = NO_INACTIVE_SEEDING_TIME_LIMIT;
else if (limit > MAX_INACTIVE_SEEDING_TIME)
limit = MAX_SEEDING_TIME;
if (m_inactiveSeedingTimeLimit != limit)
{
@ -2753,9 +2884,18 @@ nonstd::expected<lt::entry, QString> TorrentImpl::exportTorrent() const
try
{
[[maybe_unused]] const auto infoGuard = qScopeGuard([this] { m_ltAddTorrentParams.ti.reset(); });
m_ltAddTorrentParams.ti = info().nativeInfo();
return lt::write_torrent_file(m_ltAddTorrentParams);
#ifdef QBT_USES_LIBTORRENT2
const std::shared_ptr<lt::torrent_info> completeTorrentInfo = m_nativeHandle.torrent_file_with_hashes();
const std::shared_ptr<lt::torrent_info> torrentInfo = (completeTorrentInfo ? completeTorrentInfo : info().nativeInfo());
#else
const std::shared_ptr<lt::torrent_info> torrentInfo = info().nativeInfo();
#endif
lt::create_torrent creator {*torrentInfo};
for (const TrackerEntryStatus &status : asConst(trackers()))
creator.add_tracker(status.url.toStdString(), status.tier);
return creator.generate();
}
catch (const lt::system_error &err)
{
@ -2790,9 +2930,9 @@ nonstd::expected<void, QString> TorrentImpl::exportToFile(const Path &path) cons
return {};
}
QFuture<QList<PeerInfo>> TorrentImpl::fetchPeerInfo() const
void TorrentImpl::fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHandler) const
{
return invokeAsync([nativeHandle = m_nativeHandle, allPieces = pieces()]() -> QList<PeerInfo>
invokeAsync([nativeHandle = m_nativeHandle, allPieces = pieces()]() -> QList<PeerInfo>
{
try
{
@ -2807,12 +2947,13 @@ QFuture<QList<PeerInfo>> TorrentImpl::fetchPeerInfo() const
catch (const std::exception &) {}
return {};
});
}
, std::move(resultHandler));
}
QFuture<QList<QUrl>> TorrentImpl::fetchURLSeeds() const
void TorrentImpl::fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler) const
{
return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<QUrl>
invokeAsync([nativeHandle = m_nativeHandle]() -> QList<QUrl>
{
try
{
@ -2826,12 +2967,13 @@ QFuture<QList<QUrl>> TorrentImpl::fetchURLSeeds() const
catch (const std::exception &) {}
return {};
});
}
, std::move(resultHandler));
}
QFuture<QList<int>> TorrentImpl::fetchPieceAvailability() const
void TorrentImpl::fetchPieceAvailability(std::function<void (QList<int>)> resultHandler) const
{
return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<int>
invokeAsync([nativeHandle = m_nativeHandle]() -> QList<int>
{
try
{
@ -2842,12 +2984,13 @@ QFuture<QList<int>> TorrentImpl::fetchPieceAvailability() const
catch (const std::exception &) {}
return {};
});
}
, std::move(resultHandler));
}
QFuture<QBitArray> TorrentImpl::fetchDownloadingPieces() const
void TorrentImpl::fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const
{
return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QBitArray
invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QBitArray
{
try
{
@ -2866,12 +3009,13 @@ QFuture<QBitArray> TorrentImpl::fetchDownloadingPieces() const
catch (const std::exception &) {}
return {};
});
}
, std::move(resultHandler));
}
QFuture<QList<qreal>> TorrentImpl::fetchAvailableFileFractions() const
void TorrentImpl::fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const
{
return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QList<qreal>
invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QList<qreal>
{
if (!torrentInfo.isValid() || (torrentInfo.filesCount() <= 0))
return {};
@ -2905,7 +3049,8 @@ QFuture<QList<qreal>> TorrentImpl::fetchAvailableFileFractions() const
catch (const std::exception &) {}
return {};
});
}
, std::move(resultHandler));
}
void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
@ -2916,7 +3061,7 @@ void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
Q_ASSERT(priorities.size() == filesCount());
// Reset 'm_hasSeedStatus' if needed in order to react again to
// "torrent finished" event and e.g. show tray notifications
// 'torrent_finished_alert' and eg show tray notifications
const QList<DownloadPriority> oldPriorities = filePriorities();
for (int i = 0; i < oldPriorities.size(); ++i)
{
@ -2945,17 +3090,47 @@ void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
manageActualFilePaths();
}
template <typename Func>
QFuture<std::invoke_result_t<Func>> TorrentImpl::invokeAsync(Func &&func) const
QList<qreal> TorrentImpl::availableFileFractions() const
{
QPromise<std::invoke_result_t<Func>> promise;
const auto future = promise.future();
promise.start();
m_session->invokeAsync([func = std::forward<Func>(func), promise = std::move(promise)]() mutable
{
promise.addResult(func());
promise.finish();
});
Q_ASSERT(hasMetadata());
return future;
const int filesCount = this->filesCount();
if (filesCount <= 0) return {};
const QList<int> piecesAvailability = pieceAvailability();
// libtorrent returns empty array for seeding only torrents
if (piecesAvailability.empty()) return QList<qreal>(filesCount, -1);
QList<qreal> res;
res.reserve(filesCount);
for (int i = 0; i < filesCount; ++i)
{
const TorrentInfo::PieceRange filePieces = m_torrentInfo.filePieces(i);
int availablePieces = 0;
for (const int piece : filePieces)
availablePieces += (piecesAvailability[piece] > 0) ? 1 : 0;
const qreal availability = filePieces.isEmpty()
? 1 // the file has no pieces, so it is available by default
: static_cast<qreal>(availablePieces) / filePieces.size();
res.push_back(availability);
}
return res;
}
template <typename Func, typename Callback>
void TorrentImpl::invokeAsync(Func func, Callback resultHandler) const
{
m_session->invokeAsync([session = m_session
, func = std::move(func)
, resultHandler = std::move(resultHandler)
, thisTorrent = QPointer<const TorrentImpl>(this)]() mutable
{
session->invoke([result = func(), thisTorrent, resultHandler = std::move(resultHandler)]
{
if (thisTorrent)
resultHandler(result);
});
});
}

View file

@ -94,7 +94,8 @@ namespace BitTorrent
Q_DISABLE_COPY_MOVE(TorrentImpl)
public:
TorrentImpl(SessionImpl *session, const lt::torrent_handle &nativeHandle, LoadTorrentParams params);
TorrentImpl(SessionImpl *session, lt::session *nativeSession
, const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params);
~TorrentImpl() override;
bool isValid() const;
@ -202,7 +203,10 @@ namespace BitTorrent
bool isDHTDisabled() const override;
bool isPEXDisabled() const override;
bool isLSDDisabled() const override;
QList<PeerInfo> peers() const override;
QBitArray pieces() const override;
QBitArray downloadingPieces() const override;
QList<int> pieceAvailability() const override;
qreal distributedCopies() const override;
qreal maxRatio() const override;
int maxSeedingTime() const override;
@ -216,6 +220,7 @@ namespace BitTorrent
int connectionsCount() const override;
int connectionsLimit() const override;
qlonglong nextAnnounce() const override;
QList<qreal> availableFileFractions() const override;
void setName(const QString &name) override;
void setSequentialDownload(bool enable) override;
@ -253,29 +258,19 @@ namespace BitTorrent
nonstd::expected<QByteArray, QString> exportToBuffer() const override;
nonstd::expected<void, QString> exportToFile(const Path &path) const override;
QFuture<QList<PeerInfo>> fetchPeerInfo() const override;
QFuture<QList<QUrl>> fetchURLSeeds() const override;
QFuture<QList<int>> fetchPieceAvailability() const override;
QFuture<QBitArray> fetchDownloadingPieces() const override;
QFuture<QList<qreal>> fetchAvailableFileFractions() const override;
void fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHandler) const override;
void fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler) const override;
void fetchPieceAvailability(std::function<void (QList<int>)> resultHandler) const override;
void fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const override;
void fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const override;
bool needSaveResumeData() const;
// Session interface
lt::torrent_handle nativeHandle() const;
int fileIndexFromNative(lt::file_index_t nativeFileIndex) const;
void handleAlert(const lt::alert *a);
void handleStateUpdate(const lt::torrent_status &nativeStatus);
void handleFastResumeRejected();
void handleFileCompleted(lt::file_index_t nativeFileIndex);
void handleFileError(FileErrorInfo fileError);
void handleFileRenamed(lt::file_index_t nativeFileIndex, const Path &newActualFilePath, const Path &oldActualFilePath);
void handleFileRenameFailed(lt::file_index_t nativeFileIndex);
void handleMetadataReceived();
void handleSaveResumeData(lt::add_torrent_params params);
void handleTorrentChecked();
void handleTorrentFinished();
void handleQueueingModeChanged();
void handleCategoryOptionsChanged();
void handleAppendExtensionToggled();
@ -283,6 +278,7 @@ namespace BitTorrent
void requestResumeData(lt::resume_data_flags_t flags = {});
void deferredRequestResumeData();
void handleMoveStorageJobFinished(const Path &path, MoveStorageContext context, bool hasOutstandingJob);
void fileSearchFinished(const Path &savePath, const PathList &fileNames);
TrackerEntryStatus updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo);
void resetTrackerEntryStatuses();
@ -295,6 +291,23 @@ namespace BitTorrent
void updateProgress();
void updateState();
void handleFastResumeRejectedAlert(const lt::fastresume_rejected_alert *p);
void handleFileCompletedAlert(const lt::file_completed_alert *p);
void handleFileErrorAlert(const lt::file_error_alert *p);
#ifdef QBT_USES_LIBTORRENT2
void handleFilePrioAlert(const lt::file_prio_alert *p);
#endif
void handleFileRenamedAlert(const lt::file_renamed_alert *p);
void handleFileRenameFailedAlert(const lt::file_rename_failed_alert *p);
void handleMetadataReceivedAlert(const lt::metadata_received_alert *p);
void handlePerformanceAlert(const lt::performance_alert *p) const;
void handleSaveResumeDataAlert(const lt::save_resume_data_alert *p);
void handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *p);
void handleTorrentCheckedAlert(const lt::torrent_checked_alert *p);
void handleTorrentFinishedAlert(const lt::torrent_finished_alert *p);
void handleTorrentPausedAlert(const lt::torrent_paused_alert *p);
void handleTorrentResumedAlert(const lt::torrent_resumed_alert *p);
bool isMoveInProgress() const;
void setAutoManaged(bool enable);
@ -307,16 +320,17 @@ namespace BitTorrent
void manageActualFilePaths();
void applyFirstLastPiecePriority(bool enabled);
void prepareResumeData(lt::add_torrent_params resumeData);
void prepareResumeData(const lt::add_torrent_params &params);
void endReceivedMetadataHandling(const Path &savePath, const PathList &fileNames);
void reload();
nonstd::expected<lt::entry, QString> exportTorrent() const;
template <typename Func>
QFuture<std::invoke_result_t<Func>> invokeAsync(Func &&func) const;
template <typename Func, typename Callback>
void invokeAsync(Func func, Callback resultHandler) const;
SessionImpl *const m_session = nullptr;
lt::session *m_nativeSession = nullptr;
lt::torrent_handle m_nativeHandle;
mutable lt::torrent_status m_nativeStatus;
TorrentState m_state = TorrentState::Unknown;
@ -373,7 +387,7 @@ namespace BitTorrent
bool m_unchecked = false;
mutable lt::add_torrent_params m_ltAddTorrentParams;
lt::add_torrent_params m_ltAddTorrentParams;
int m_downloadLimit = 0;
int m_uploadLimit = 0;

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.cbegin());
m_torrents.erase(m_torrents.begin());
}
m_torrents[announceReq.torrentID].setPeer(announceReq.peer);

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -41,6 +41,7 @@ namespace BitTorrent
{
NotContacted = 1,
Working = 2,
Updating = 3,
NotWorking = 4,
TrackerError = 5,
Unreachable = 6
@ -51,7 +52,6 @@ namespace BitTorrent
QString name {};
int btVersion = 1;
bool isUpdating = false;
TrackerEndpointState state = TrackerEndpointState::NotContacted;
QString message {};
@ -69,7 +69,6 @@ namespace BitTorrent
QString url {};
int tier = 0;
bool isUpdating = false;
TrackerEndpointState state = TrackerEndpointState::NotContacted;
QString message {};

View file

@ -155,11 +155,7 @@ 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.first(i).trimmed().toString().toLower();
const QString value = line.sliced(i + 1).trimmed().toString();
const QString name = line.left(i).trimmed().toString().toLower();
const QString value = line.mid(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.capturedView(2).toLatin1()};
const QByteArray url {match.captured(2).toLatin1()};
const int sepPos = url.indexOf('?');
const QByteArrayView pathComponent = ((sepPos == -1) ? url : QByteArrayView(url).first(sepPos));
const QByteArrayView pathComponent = ((sepPos == -1) ? url : QByteArrayView(url).mid(0, sepPos));
m_request.path = QString::fromUtf8(QByteArray::fromPercentEncoding(asQByteArray(pathComponent)));
if (sepPos >= 0)
{
const QByteArrayView query = QByteArrayView(url).sliced(sepPos + 1);
const QByteArrayView query = QByteArrayView(url).mid(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.first(eqCharPos);
const QByteArrayView valueComponent = param.sliced(eqCharPos + 1);
const QByteArrayView nameComponent = param.mid(0, eqCharPos);
const QByteArrayView valueComponent = param.mid(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).sliced(idx + boundaryFieldName.size())).toLatin1();
const QByteArray delimiter = Utils::String::unquote(QStringView(contentType).mid(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);
QList<QByteArrayView> multipart = splitToViews(data, dashDelimiter, Qt::SkipEmptyParts);
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.first(eohPos));
const QByteArrayView payload = viewWithoutEndingWith(data.sliced((eohPos + EOH.size())), CRLF);
const QString headers = QString::fromLatin1(data.mid(0, eohPos));
const QByteArrayView payload = viewWithoutEndingWith(data.mid((eohPos + EOH.size()), data.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.first(idx).trimmed().toString().toLower();
const QString value = Utils::String::unquote(directive.sliced(idx + 1).trimmed()).toString();
const QString name = directive.left(idx).trimmed().toString().toLower();
const QString value = Utils::String::unquote(directive.mid(idx + 1).trimmed()).toString();
headersMap[name] = value;
}
}

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())
{
const QString ipStr = ipRegexMatch.captured(1);
QString ipStr = ipRegexMatch.captured(1);
qDebug() << Q_FUNC_INFO << "Regular expression captured the following IP:" << ipStr;
const QHostAddress newIp {ipStr};
QHostAddress newIp(ipStr);
if (!newIp.isNull())
{
if (m_lastIP != newIp)

View file

@ -279,13 +279,13 @@ QString Net::DownloadHandlerImpl::errorCodeToString(const QNetworkReply::Network
case QNetworkReply::ProxyAuthenticationRequiredError:
return tr("The proxy requires authentication in order to honor the request but did not accept any credentials offered");
case QNetworkReply::ContentAccessDenied:
return tr("The access to the remote content was denied (403)");
return tr("The access to the remote content was denied (401)");
case QNetworkReply::ContentOperationNotPermittedError:
return tr("The operation requested on the remote content is not permitted");
case QNetworkReply::ContentNotFoundError:
return tr("The remote content was not found at the server (404)");
case QNetworkReply::AuthenticationRequiredError:
return tr("The remote server requires authentication to serve the content but the credentials provided were not accepted (401)");
return tr("The remote server requires authentication to serve the content but the credentials provided were not accepted");
case QNetworkReply::ProtocolUnknownError:
return tr("The Network Access API cannot honor the request because the protocol is not known");
case QNetworkReply::ProtocolInvalidOperationError:

View file

@ -265,37 +265,38 @@ void Net::DownloadManager::applyProxySettings()
const auto *proxyManager = ProxyConfigurationManager::instance();
const ProxyConfiguration proxyConfig = proxyManager->proxyConfiguration();
switch (proxyConfig.type)
{
case Net::ProxyType::None:
case Net::ProxyType::SOCKS4:
m_proxy = QNetworkProxy(QNetworkProxy::NoProxy);
break;
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;
if ((proxyConfig.type == Net::ProxyType::None) || (proxyConfig.type == ProxyType::SOCKS4))
return;
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;
};
// Proxy enabled
if (proxyConfig.type == ProxyType::SOCKS5)
{
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);
}
m_proxy.setHostName(proxyConfig.ip);
m_proxy.setPort(proxyConfig.port);
// 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);
}
void Net::DownloadManager::processWaitingJobs(const ServiceID &serviceID)

View file

@ -148,7 +148,8 @@ 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();
for (int i = 0, end = b.length(); i < end; i += 78)
const int ct = b.length();
for (int i = 0; i < ct; i += 78)
m_message += b.mid(i, 78);
m_from = from;
m_rcpt = to;
@ -189,12 +190,8 @@ void Smtp::readyRead()
{
const int pos = m_buffer.indexOf("\r\n");
if (pos < 0) return; // Loop exit condition
const QByteArray line = m_buffer.first(pos);
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
m_buffer.slice(pos + 2);
#else
const QByteArray line = m_buffer.left(pos);
m_buffer.remove(0, (pos + 2));
#endif
qDebug() << "Response line:" << line;
// Extract response code
const QByteArray code = line.left(3);

View file

@ -94,13 +94,7 @@ bool Path::isValid() const
#if defined(Q_OS_WIN)
QStringView view = m_pathStr;
if (hasDriveLetter(view))
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
view.slice(3);
#else
view = view.sliced(3);
#endif
}
view = view.mid(3);
// \\37 is using base-8 number system
const QRegularExpression regex {u"[\\0-\\37:?\"*<>|]"_s};
@ -153,9 +147,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.first(slashIndex + 1));
return createUnchecked(m_pathStr.left(slashIndex + 1));
#endif
return createUnchecked(m_pathStr.first(slashIndex));
return createUnchecked(m_pathStr.left(slashIndex));
}
Path Path::parentPath() const
@ -173,9 +167,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.first(slashIndex + 1));
return (m_pathStr.size() == 3) ? Path() : createUnchecked(m_pathStr.left(slashIndex + 1));
#endif
return createUnchecked(m_pathStr.first(slashIndex));
return createUnchecked(m_pathStr.left(slashIndex));
}
QString Path::filename() const
@ -184,7 +178,7 @@ QString Path::filename() const
if (slashIndex == -1)
return m_pathStr;
return m_pathStr.sliced(slashIndex + 1);
return m_pathStr.mid(slashIndex + 1);
}
QString Path::extension() const
@ -194,9 +188,9 @@ QString Path::extension() const
return (u"." + suffix);
const int slashIndex = m_pathStr.lastIndexOf(u'/');
const auto filename = QStringView(m_pathStr).sliced(slashIndex + 1);
const auto filename = QStringView(m_pathStr).mid(slashIndex + 1);
const int dotIndex = filename.lastIndexOf(u'.', -2);
return ((dotIndex == -1) ? QString() : filename.sliced(dotIndex).toString());
return ((dotIndex == -1) ? QString() : filename.mid(dotIndex).toString());
}
bool Path::hasExtension(const QStringView ext) const
@ -299,7 +293,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.first(commonPathSize));
return Path::createUnchecked(left.m_pathStr.left(commonPathSize));
}
Path Path::findRootFolder(const PathList &filePaths)
@ -328,13 +322,7 @@ 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,19 +359,6 @@ 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);

View file

@ -119,8 +119,6 @@ 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;

View file

@ -48,16 +48,16 @@ const QString Article::KeyIsRead = u"isRead"_s;
Article::Article(Feed *feed, const QVariantHash &varHash)
: QObject(feed)
, m_feed {feed}
, m_guid {varHash.value(KeyId).toString()}
, m_date {varHash.value(KeyDate).toDateTime()}
, m_title {varHash.value(KeyTitle).toString()}
, m_author {varHash.value(KeyAuthor).toString()}
, m_description {varHash.value(KeyDescription).toString()}
, m_torrentURL {varHash.value(KeyTorrentURL).toString()}
, m_link {varHash.value(KeyLink).toString()}
, m_isRead {varHash.value(KeyIsRead, false).toBool()}
, m_data {varHash}
, m_feed(feed)
, m_guid(varHash.value(KeyId).toString())
, m_date(varHash.value(KeyDate).toDateTime())
, m_title(varHash.value(KeyTitle).toString())
, m_author(varHash.value(KeyAuthor).toString())
, m_description(varHash.value(KeyDescription).toString())
, m_torrentURL(varHash.value(KeyTorrentURL).toString())
, m_link(varHash.value(KeyLink).toString())
, m_isRead(varHash.value(KeyIsRead, false).toBool())
, m_data(varHash)
{
}

View file

@ -43,7 +43,6 @@
#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"
@ -376,25 +375,11 @@ void AutoDownloader::handleTorrentAdded(const QString &source)
}
}
void AutoDownloader::handleAddTorrentFailed(const QString &source, const BitTorrent::AddTorrentError &error)
{
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
void AutoDownloader::handleAddTorrentFailed(const QString &source)
{
m_waitingJobs.remove(source);
// TODO: Re-schedule job here.
}
}
void AutoDownloader::handleNewArticle(const Article *article)
{

View file

@ -47,11 +47,6 @@ class Application;
class AsyncFileStorage;
struct ProcessingJob;
namespace BitTorrent
{
struct AddTorrentError;
}
namespace RSS
{
class Article;
@ -116,7 +111,7 @@ namespace RSS
private slots:
void process();
void handleTorrentAdded(const QString &source);
void handleAddTorrentFailed(const QString &url, const BitTorrent::AddTorrentError &error);
void handleAddTorrentFailed(const QString &url);
void handleNewArticle(const Article *article);
void handleFeedURLChanged(Feed *feed, const QString &oldURL);

View file

@ -184,10 +184,14 @@ QString computeEpisodeName(const QString &article)
for (int i = 1; i <= match.lastCapturedIndex(); ++i)
{
const QString cap = match.captured(i);
if (cap.isEmpty())
continue;
ret.append(cap);
bool isInt = false;
const int x = cap.toInt(&isInt);
ret.append(isInt ? QString::number(x) : cap);
}
return ret.join(u'x');
}
@ -289,26 +293,20 @@ bool AutoDownloadRule::matchesEpisodeFilterExpression(const QString &articleTitl
if (!matcher.hasMatch())
return false;
const QStringView season {matcher.capturedView(1)};
const QList<QStringView> episodes {matcher.capturedView(2).split(u';')};
const QString season {matcher.captured(1)};
const QStringList episodes {matcher.captured(2).split(u';')};
const int seasonOurs {season.toInt()};
for (QStringView episode : episodes)
for (QString 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'))
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
episode.slice(1);
#else
episode = episode.sliced(1);
#endif
}
episode = episode.right(episode.size() - 1);
if (episode.contains(u'-'))
if (episode.indexOf(u'-') != -1)
{ // 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};
@ -325,25 +323,24 @@ bool AutoDownloadRule::matchesEpisodeFilterExpression(const QString &articleTitl
if (matched)
{
const int seasonTheirs {matcher.capturedView(1).toInt()};
const int episodeTheirs {matcher.capturedView(2).toInt()};
const int seasonTheirs {matcher.captured(1).toInt()};
const int episodeTheirs {matcher.captured(2).toInt()};
if (episode.endsWith(u'-'))
{ // Infinite range
const int episodeOurs {QStringView(episode).chopped(1).toInt()};
const int episodeOurs {QStringView(episode).left(episode.size() - 1).toInt()};
if (((seasonTheirs == seasonOurs) && (episodeTheirs >= episodeOurs)) || (seasonTheirs > seasonOurs))
return true;
}
else
{ // Normal range
const QList<QStringView> range {episode.split(u'-')};
const QStringList 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,22 +56,19 @@
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(Session *session, const QUuid &uid, const QString &url, const QString &path, const std::chrono::seconds refreshInterval)
Feed::Feed(const QUuid &uid, const QString &url, const QString &path, Session *session)
: Item(path)
, m_session {session}
, m_uid {uid}
, m_url {url}
, m_refreshInterval {refreshInterval}
, m_session(session)
, m_uid(uid)
, m_url(url)
{
const auto uidHex = QString::fromLatin1(m_uid.toRfc4122().toHex());
m_dataFileName = Path(uidHex + u".json");
@ -330,9 +327,9 @@ bool Feed::addArticle(const QVariantHash &articleData)
// Insertion sort
const int maxArticles = m_session->maxArticlesPerFeed();
const auto lowerBound = std::lower_bound(m_articlesByDate.cbegin(), m_articlesByDate.cend()
const auto lowerBound = std::lower_bound(m_articlesByDate.begin(), m_articlesByDate.end()
, articleData.value(Article::KeyDate).toDateTime(), Article::articleDateRecentThan);
if ((lowerBound - m_articlesByDate.cbegin()) >= maxArticles)
if ((lowerBound - m_articlesByDate.begin()) >= maxArticles)
return false; // we reach max articles
auto *article = new Article(this, articleData);
@ -465,20 +462,6 @@ 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;
@ -491,8 +474,6 @@ 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,8 +31,6 @@
#pragma once
#include <chrono>
#include <QtContainerFwd>
#include <QBasicTimer>
#include <QHash>
@ -70,7 +68,7 @@ namespace RSS
friend class Session;
Feed(Session *session, const QUuid &uid, const QString &url, const QString &path, std::chrono::seconds refreshInterval);
Feed(const QUuid &uid, const QString &url, const QString &path, Session *session);
~Feed() override;
public:
@ -89,9 +87,6 @@ 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:
@ -99,7 +94,6 @@ 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);
@ -129,7 +123,6 @@ 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.first(index);
result << path.left(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.first(pos) : QString();
return (pos >= 0) ? path.left(pos) : QString();
}
QString Item::relativeName(const QString &path)
{
const int pos = path.lastIndexOf(Item::PathSeparator);
return (pos >= 0) ? path.sliced(pos + 1) : path;
int pos;
return ((pos = path.lastIndexOf(Item::PathSeparator)) >= 0 ? path.right(path.size() - (pos + 1)) : path);
}

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 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-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 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,7 +56,6 @@ 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;
@ -95,10 +94,12 @@ 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
@ -137,20 +138,19 @@ Session *Session::instance()
return m_instance;
}
nonstd::expected<Folder *, QString> Session::addFolder(const QString &path)
nonstd::expected<void, QString> Session::addFolder(const QString &path)
{
const nonstd::expected<Folder *, QString> result = prepareItemDest(path);
if (!result)
return result.get_unexpected();
auto *destFolder = result.value();
auto *folder = new Folder(path);
addItem(folder, destFolder);
addItem(new Folder(path), destFolder);
store();
return folder;
return {};
}
nonstd::expected<Feed *, QString> Session::addFeed(const QString &url, const QString &path, const std::chrono::seconds refreshInterval)
nonstd::expected<void, QString> Session::addFeed(const QString &url, const QString &path)
{
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<Feed *, QString> Session::addFeed(const QString &url, const QSt
return result.get_unexpected();
auto *destFolder = result.value();
auto *feed = new Feed(this, generateUID(), url, path, refreshInterval);
auto *feed = new Feed(generateUID(), url, path, this);
addItem(feed, destFolder);
store();
if (isProcessingEnabled())
refreshFeed(feed, std::chrono::system_clock::now());
feed->refresh();
return feed;
return {};
}
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())
refreshFeed(feed, std::chrono::system_clock::now());
feed->refresh();
return {};
}
@ -214,20 +214,14 @@ 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)
{
@ -320,7 +314,7 @@ bool Session::loadFolder(const QJsonObject &jsonObj, Folder *folder)
QString url = val.toString();
if (url.isEmpty())
url = key;
addFeedToFolder(generateUID(), url, key, folder, 0s);
addFeedToFolder(generateUID(), url, key, folder);
updated = true;
}
else if (val.isObject())
@ -360,9 +354,7 @@ bool Session::loadFolder(const QJsonObject &jsonObj, Folder *folder)
updated = true;
}
const auto refreshInterval = std::chrono::seconds(valObj[u"refreshInterval"].toInteger());
addFeedToFolder(uid, valObj[u"url"].toString(), key, folder, refreshInterval);
addFeedToFolder(uid, valObj[u"url"].toString(), key, folder);
}
else
{
@ -393,14 +385,8 @@ void Session::loadLegacy()
uint i = 0;
for (QString legacyPath : legacyFeedPaths)
{
if (legacyPath.startsWith(Item::PathSeparator))
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
legacyPath.slice(1);
#else
if (Item::PathSeparator == legacyPath[0])
legacyPath.remove(0, 1);
#endif
}
const QString parentFolderPath = Item::parentPath(legacyPath);
const QString feedUrl = Item::relativeName(legacyPath);
@ -444,9 +430,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, const std::chrono::seconds refreshInterval)
Feed *Session::addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder)
{
auto *feed = new Feed(this, uid, url, Item::joinPath(parentFolder->path(), name), refreshInterval);
auto *feed = new Feed(uid, url, Item::joinPath(parentFolder->path(), name), this);
addItem(feed, parentFolder);
return feed;
}
@ -468,25 +454,8 @@ 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);
@ -507,9 +476,14 @@ 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);
}
@ -580,7 +554,6 @@ void Session::handleItemAboutToBeDestroyed(Item *item)
{
m_feedsByUID.remove(feed->uid());
m_feedsByURL.remove(feed->url());
m_refreshTimepoints.remove(feed);
}
}
@ -619,28 +592,6 @@ void Session::setMaxArticlesPerFeed(const int n)
void Session::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;
// NOTE: Should we allow manually refreshing for disabled session?
rootFolder()->refresh();
}

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,20 +35,26 @@
* 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"
* }
@ -114,8 +120,8 @@ namespace RSS
std::chrono::seconds fetchDelay() const;
void setFetchDelay(std::chrono::seconds delay);
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> addFolder(const QString &path);
nonstd::expected<void, QString> addFeed(const QString &url, const QString &path);
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);
@ -129,6 +135,9 @@ namespace RSS
Folder *rootFolder() const;
public slots:
void refresh();
signals:
void processingStateChanged(bool enabled);
void maxArticlesPerFeedChanged(int n);
@ -151,10 +160,8 @@ 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, std::chrono::seconds refreshInterval);
Feed *addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder);
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;
@ -169,6 +176,5 @@ 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,10 +41,7 @@ SearchDownloadHandler::SearchDownloadHandler(const QString &pluginName, const QS
, m_manager {manager}
, m_downloadProcess {new QProcess(this)}
{
m_downloadProcess->setProcessEnvironment(m_manager->proxyEnvironment());
#ifdef Q_OS_UNIX
m_downloadProcess->setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif
m_downloadProcess->setEnvironment(QProcess::systemEnvironment());
connect(m_downloadProcess, qOverload<int, QProcess::ExitStatus>(&QProcess::finished)
, this, &SearchDownloadHandler::downloadProcessFinished);
const QStringList params
@ -55,7 +52,7 @@ SearchDownloadHandler::SearchDownloadHandler(const QString &pluginName, const QS
url
};
// Launch search
m_downloadProcess->start(Utils::ForeignApps::pythonInfo().executablePath.data(), params, QIODevice::ReadOnly);
m_downloadProcess->start(Utils::ForeignApps::pythonInfo().executableName, params, QIODevice::ReadOnly);
}
void SearchDownloadHandler::downloadProcessFinished(int exitcode)

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -38,7 +38,6 @@
#include "base/global.h"
#include "base/path.h"
#include "base/utils/bytearray.h"
#include "base/utils/foreignapps.h"
#include "base/utils/fs.h"
#include "searchpluginmanager.h"
@ -71,11 +70,7 @@ SearchHandler::SearchHandler(const QString &pattern, const QString &category, co
, m_searchTimeout {new QTimer(this)}
{
// Load environment variables (proxy)
m_searchProcess->setProcessEnvironment(m_manager->proxyEnvironment());
m_searchProcess->setProgram(Utils::ForeignApps::pythonInfo().executablePath.data());
#ifdef Q_OS_UNIX
m_searchProcess->setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif
m_searchProcess->setEnvironment(QProcess::systemEnvironment());
const QStringList params
{
@ -84,6 +79,9 @@ 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);
@ -95,7 +93,6 @@ 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);
@ -140,23 +137,28 @@ void SearchHandler::processFinished(const int exitcode)
// line to SearchResult calling parseSearchResult().
void SearchHandler::readSearchOutput()
{
const QByteArray output = m_searchResultLineTruncated + m_searchProcess->readAllStandardOutput();
QList<QByteArrayView> lines = Utils::ByteArray::splitToViews(output, "\n", Qt::KeepEmptyParts);
QByteArray output = m_searchProcess->readAllStandardOutput();
output.replace('\r', "");
m_searchResultLineTruncated = lines.takeLast().trimmed().toByteArray();
QList<QByteArray> lines = output.split('\n');
if (!m_searchResultLineTruncated.isEmpty())
lines.prepend(m_searchResultLineTruncated + lines.takeFirst());
m_searchResultLineTruncated = lines.takeLast().trimmed();
QList<SearchResult> searchResultList;
searchResultList.reserve(lines.size());
for (const QByteArrayView &line : asConst(lines))
for (const QByteArray &line : asConst(lines))
{
if (SearchResult searchResult; parseSearchResult(line, searchResult))
searchResultList.append(std::move(searchResult));
SearchResult searchResult;
if (parseSearchResult(QString::fromUtf8(line), searchResult))
searchResultList << searchResult;
}
if (!searchResultList.isEmpty())
{
m_results.append(searchResultList);
for (const SearchResult &result : searchResultList)
m_results.append(result);
emit newSearchResults(searchResultList);
}
}
@ -170,17 +172,17 @@ void SearchHandler::processFailed()
// Parse one line of search results list
// Line is in the following form:
// file url | file name | file size | nb seeds | nb leechers | Search engine url
bool SearchHandler::parseSearchResult(const QByteArrayView line, SearchResult &searchResult)
bool SearchHandler::parseSearchResult(const QStringView line, SearchResult &searchResult)
{
const QList<QByteArrayView> parts = Utils::ByteArray::splitToViews(line, "|");
const QList<QStringView> parts = line.split(u'|');
const int nbFields = parts.size();
if (nbFields <= PL_ENGINE_URL)
return false; // Anything after ENGINE_URL is optional
searchResult = SearchResult();
searchResult.fileUrl = QString::fromUtf8(parts.at(PL_DL_LINK).trimmed()); // download URL
searchResult.fileName = QString::fromUtf8(parts.at(PL_NAME).trimmed()); // Name
searchResult.fileUrl = parts.at(PL_DL_LINK).trimmed().toString(); // download URL
searchResult.fileName = parts.at(PL_NAME).trimmed().toString(); // Name
searchResult.fileSize = parts.at(PL_SIZE).trimmed().toLongLong(); // Size
bool ok = false;
@ -193,11 +195,11 @@ bool SearchHandler::parseSearchResult(const QByteArrayView line, SearchResult &s
if (!ok || (searchResult.nbLeechers < 0))
searchResult.nbLeechers = -1;
searchResult.siteUrl = QString::fromUtf8(parts.at(PL_ENGINE_URL).trimmed()); // Search engine site URL
searchResult.siteUrl = parts.at(PL_ENGINE_URL).trimmed().toString(); // Search engine site URL
searchResult.engineName = m_manager->pluginNameBySiteURL(searchResult.siteUrl); // Search engine name
if (nbFields > PL_DESC_LINK)
searchResult.descrLink = QString::fromUtf8(parts.at(PL_DESC_LINK).trimmed()); // Description Link
searchResult.descrLink = parts.at(PL_DESC_LINK).trimmed().toString(); // Description Link
if (nbFields > PL_PUB_DATE)
{

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