Compare commits

..

209 commits

Author SHA1 Message Date
Thomas Piccirello
b7a43ea118
WebAPI: Cache metadata using TorrentID
Some checks failed
CI - File health / Check (push) Has been cancelled
CI - macOS / Build (push) Has been cancelled
CI - Python / Check (push) Has been cancelled
CI - Ubuntu / Build (push) Has been cancelled
CI - WebUI / Check (push) Has been cancelled
CI - Windows / Build (push) Has been cancelled
`m_torrentMetadataCache` previously used a torrent's InfoHash as its key. However, InfoHashes for hybrid torrents cannot be serialized and deserialized via their TorrentID (e.g. `InfoHash(TorrentID(infoHash.toTorrentID().toString())) != infoHash`). This is due to hybrid InfoHashes containing both a v1 and v2 hash, while the serialized TorrentID only contains a single truncated v2 hash. Thus we cannot expect an InfoHash serialized by its TorrentID to be able to construct an equivalent InfoHash. By switching to the TorrentID, we always have a single ID to use.

Follow up #21015.
PR #22926.
2025-07-04 17:19:14 +08:00
sledgehammer999
4f94eac235
Merge private branch for an RSS security fix
Some checks failed
CI - File health / Check (push) Has been cancelled
CI - macOS / Build (push) Has been cancelled
CI - Python / Check (push) Has been cancelled
CI - Ubuntu / Build (push) Has been cancelled
CI - WebUI / Check (push) Has been cancelled
CI - Windows / Build (push) Has been cancelled
Reported responsibly by Michael Lappas (@lappas-m)
2025-07-02 08:43:15 +03:00
sledgehammer999
a3e6d1a0ad
Merge pull request #22944 from sledgehammer999/fallback_update
Some checks are pending
CI - File health / Check (push) Waiting to run
CI - macOS / Build (push) Waiting to run
CI - Python / Check (push) Waiting to run
CI - Ubuntu / Build (push) Waiting to run
CI - WebUI / Check (push) Waiting to run
CI - Windows / Build (push) Waiting to run
Add fallback to update mechanism
2025-07-01 12:03:30 +03:00
Vladimir Golovnev
efedbcb407
Allow to customize ProgressBar color
PR #22928.
2025-07-01 10:28:26 +03:00
Chocobo1
55de9b07d2
Add AppStream metadata for qbt-nox
Also trim redundant trailing path separators.
Ref: https://www.freedesktop.org/software/appstream/docs/sect-Metadata-ConsoleApplication.html

PR #22941.
2025-07-01 14:47:14 +08:00
sledgehammer999
9ad4a94940
Store version numbers in the appropriate type 2025-06-30 15:02:58 +03:00
sledgehammer999
c47b981a56
Add fallback to update mechanism
This brings a fallback version check to the update mechanism,
which should be as stable as it can be.
It will allow migrating to another primary mechanism without
having to have updated the older primary mechanism too.
2025-06-30 15:02:57 +03:00
Ryu481
5028f68d48
Make qBittorrent quit on MacOS with main window closed
Some checks failed
CI - File health / Check (push) Has been cancelled
CI - macOS / Build (push) Has been cancelled
CI - Python / Check (push) Has been cancelled
CI - Ubuntu / Build (push) Has been cancelled
CI - WebUI / Check (push) Has been cancelled
CI - Windows / Build (push) Has been cancelled
Fixes the reported bug that you couldn't quit qBittorrent when the main window was closed on MacOS.

Closes #22849.
PR #22931.
2025-06-30 01:56:39 +08:00
Thomas Piccirello
ef4957a9f4
WebUI: Make footer scrollable on mobile
The window footer can now be scrolled.

Closes #21541.
PR #22918.
2025-06-30 01:51:06 +08:00
Chocobo1
99d25eec71
Use proper capitalization for MSVC linker flags
The linker flags are case insensitive [1] but it would be better to use the proper capitalization [2].

[1] https://learn.microsoft.com/en-us/cpp/build/reference/linking?view=msvc-170#command-line
[2] https://learn.microsoft.com/en-us/cpp/build/reference/guard-enable-guard-checks?view=msvc-170

PR #22940.
2025-06-30 01:45:10 +08:00
Chocobo1
70a6153b78
WebAPI: Trim leading whitespaces on Run External Program fields
Hacked qbt instances may contain malicious script placed in Run External Program and the script
will attempt to hide itself by adding a lot whitespaces at the start of the command string.
Users may mistake the field of being empty but is actually not.
So trim the leading whitespaces to easily expose the malicious script.

Note that GUI already trim the fields and only WebAPI doesn't trim them. This patch will unify
the behavior.
Related: https://github.com/qbittorrent/docker-qbittorrent-nox/issues/71#issuecomment-2993567440

PR #22939.
2025-06-30 01:39:03 +08:00
Bark
690a139538
WebUI: Add ability to add/remove tracker from selected torrents
Some checks failed
CI - File health / Check (push) Has been cancelled
CI - macOS / Build (push) Has been cancelled
CI - Python / Check (push) Has been cancelled
CI - Ubuntu / Build (push) Has been cancelled
CI - WebUI / Check (push) Has been cancelled
CI - Windows / Build (push) Has been cancelled
Closes #22618.
PR #22698.
2025-06-28 14:06:03 +08:00
Vladimir Golovnev
e447baa04a
Allow to customize PiecesBar colors
PR #22922.
2025-06-28 08:53:47 +03:00
Vladimir Golovnev (Glassez)
fdfdbae30c
Show warning message box on opening inappropriate URL 2025-06-27 20:59:50 +03:00
Vladimir Golovnev
dd4a2eb583
Don't expose palette colors in UI theme editor
Some checks are pending
CI - File health / Check (push) Waiting to run
CI - macOS / Build (push) Waiting to run
CI - Python / Check (push) Waiting to run
CI - Ubuntu / Build (push) Waiting to run
CI - WebUI / Check (push) Waiting to run
CI - Windows / Build (push) Waiting to run
PR #22923.
Fixes regression introduced by #22330.
2025-06-27 13:44:10 +03:00
Vladimir Golovnev
41d7d672ce
Optimize parsing of search results
PR #22906.
2025-06-26 08:49:58 +03:00
Vladimir Golovnev (Glassez)
d379fa3035
Prevent opening local files if web page is expected 2025-06-23 13:14:37 +03:00
Vladimir Golovnev
71af105a89
Avoid copying resume data when loading torrents
PR #22899.
2025-06-23 12:20:01 +03:00
Chocobo1
f6ee6b92a4
Revise label wordings
Some checks failed
CI - File health / Check (push) Has been cancelled
CI - macOS / Build (push) Has been cancelled
CI - Python / Check (push) Has been cancelled
CI - Ubuntu / Build (push) Has been cancelled
CI - WebUI / Check (push) Has been cancelled
CI - Windows / Build (push) Has been cancelled
Such that action subject is truly unambiguous to the user.

PR #22894.
2025-06-22 16:01:00 +08:00
Chocobo1
fe1679d778
Provide testing cases for path concatenation
PR #22893.
2025-06-22 15:40:14 +08:00
Thomas Piccirello
67ef356064
WebUI: Delete correct rows after re-sort
The previous logic assumed that trs was properly sorted, which is no longer the case.
Follow up to #22827.

PR #22884.
2025-06-22 15:35:07 +08:00
Thomas Piccirello
254f39f89d
WebUI: Restore node default collapse state
By default, nodes should be expanded until explicitly collapsed. This restores the default behavior which changed in b4a16f6464.

Relevant: https://github.com/qbittorrent/qBittorrent/pull/21645#discussion_r2150695297

PR #22879.
2025-06-22 14:55:37 +08:00
tehcneko
d702a02c1f
WebUI: Avoid forced reflow on virtual list rerender
Avoid forced synchronous layout caused by offsetHeight/scrollTop access.

PR #22858.
2025-06-22 14:27:16 +08:00
xavier2k6
86e11d344f
GHA CI: Bump pandoc to latest
* Bump `pandoc` to latest (3.7.0.2)
* Apply upstream suggestions

PR #22708.
2025-06-22 14:21:50 +08:00
Awqre
6972962ee0
Compress images losslessly
Some checks failed
CI - File health / Check (push) Has been cancelled
CI - macOS / Build (push) Has been cancelled
CI - Python / Check (push) Has been cancelled
CI - Ubuntu / Build (push) Has been cancelled
CI - WebUI / Check (push) Has been cancelled
CI - Windows / Build (push) Has been cancelled
Lossless reduction of file size for .icns icons for MacOS, and some small improvements for a few PNG images.

PR #22790.
2025-06-21 01:21:01 +08:00
Vladimir Golovnev
599a2d0c93
Find CorePrivate package with Qt >= 6.10
Some checks are pending
CI - File health / Check (push) Waiting to run
CI - macOS / Build (push) Waiting to run
CI - Python / Check (push) Waiting to run
CI - Ubuntu / Build (push) Waiting to run
CI - WebUI / Check (push) Waiting to run
CI - Windows / Build (push) Waiting to run
PR #22890.
Closes #22887.
2025-06-20 10:20:13 +03:00
Vladimir Golovnev
e27cbab7ee
Don't ignore QFile::open() result
PR #22889.
Closes #22888.
2025-06-20 10:19:16 +03:00
Vladimir Golovnev
794310dca9
Add WebAPI for fetching torrent metadata
Some checks failed
CI - File health / Check (push) Has been cancelled
CI - macOS / Build (push) Has been cancelled
CI - Python / Check (push) Has been cancelled
CI - Ubuntu / Build (push) Has been cancelled
CI - WebUI / Check (push) Has been cancelled
CI - Windows / Build (push) Has been cancelled
PR #21015.
2025-06-17 10:15:21 +03:00
Chocobo1
3cd40cc5a2
Merge pull request #22867 from Chocobo1/webui_defer
Some checks are pending
CI - File health / Check (push) Waiting to run
CI - macOS / Build (push) Waiting to run
CI - Python / Check (push) Waiting to run
CI - Ubuntu / Build (push) Waiting to run
CI - WebUI / Check (push) Waiting to run
CI - Windows / Build (push) Waiting to run
* WebUI: use defer when loading scripts
  So that the HTML layout can be rendered earlier.
* WebUI: move scripts into <head> section
  For consistency reasons.
2025-06-17 03:03:46 +08:00
Thomas Piccirello
380d9af34c
WebUI: Increase number of buffered virtual rows
Negligible performance hit for increased UX. Currently when scrolling I frequently see blank rows.

PR #22853.
2025-06-17 02:53:09 +08:00
Chocobo1
5605e08347
WebUI: move scripts into <head> section
For consistency reasons.
2025-06-16 03:10:37 +08:00
Chocobo1
753c6629a3
WebUI: use defer when loading scripts
So that the HTML layout can be rendered earlier.
2025-06-16 03:10:01 +08:00
xavier2k6
9b66693cb8
GHA CI: Bump dependencies
Some checks failed
CI - Ubuntu / Build (push) Has been cancelled
CI - File health / Check (push) Has been cancelled
CI - macOS / Build (push) Has been cancelled
CI - Python / Check (push) Has been cancelled
CI - WebUI / Check (push) Has been cancelled
CI - Windows / Build (push) Has been cancelled
* Bumped `Qt` to `6.9.1` on macOS/Windows
* Bumped `Qt` to `6.9.1` & `Boost` to `1.88.0` on `coverity-scan`

PR #22839.
2025-06-14 21:17:19 +08:00
Thomas Piccirello
406a389d7c
WebUI: Improve performance of re-sorting table rows
This change drastically improves the performance of changing a table's sorted column. This performance is achieved through improved data structures, namely removing operations that repeatedly spliced an array. We also no longer iterate over a potentially large array.

On a torrent with ~50,000 files, re-rendering after a sort improves from ~20 seconds to 2 seconds.

PR #22827.
2025-06-14 21:06:33 +08:00
Leon Blakey
d7a5430893
Improve resume queue load performance
PR #22831.

---------

Co-authored-by: Vladimir Golovnev <glassez@yandex.ru>
2025-06-13 09:47:23 +03:00
Thomas Piccirello
7ac160a481
Bump WebAPI version 2025-06-11 17:03:47 -07:00
Thomas Piccirello
4c190b0d4f
Move priority parsing into helper function 2025-06-11 17:03:46 -07:00
Thomas Piccirello
69bf31f4e9
Add WebAPI for downloading torrent metadata
Signed-off-by: Thomas Piccirello <thomas@piccirello.com>
2025-06-11 17:03:46 -07:00
Thomas Piccirello
e45ca3fde7
Support downloading torrent from previously fetched metadata
Signed-off-by: Thomas Piccirello <thomas@piccirello.com>
2025-06-11 17:03:46 -07:00
Thomas Piccirello
5750de6270
Add WebAPI for fetching torrent metadata
Signed-off-by: Thomas Piccirello <thomas@piccirello.com>
2025-06-11 17:03:46 -07:00
Thomas Piccirello
ff07591a87
WebUI: add missing debounce handler
PR #22830.
2025-06-09 21:46:07 +08:00
Chocobo1
c215c1e8b1
WebUI: reduce MooTools usage
WebUI is trying to be less tangled with MooTools.

PR #22822.
2025-06-09 21:39:44 +08:00
Thomas Piccirello
06756936f3
WebUI: Always show Auto Torrent Management option
This behavior is consistent with the GUI.

Closes #22702.
PR #22819.
2025-06-09 21:31:46 +08:00
Thomas Piccirello
7ed026ef78
WebUI: Reset filter selection when double clicking filter
When double clicking on a filter, all other filters will be reset. For example, double clicking on a status filter will reset the categories, tags, and trackers filters to "All". This behavior can be disabled in WebUI options.

Closes #22449.
PR #22818.
2025-06-09 21:24:58 +08:00
Thomas Piccirello
78fae0ae76
WebUI: Cache server stats for statistics window
This change ensures that the WebUI caches relevant server stats for immediate display once the statistics window is opened. Previously, all stats would remain blank until maindata was fetched. This could take a while if e.g. the user was on the search tab.

Closes #22764.
PR #22817.
2025-06-09 21:17:30 +08:00
Chocobo1
8aa1a96d71
Revise Interface section layout in Options dialog
The Language option now has its own layout since it is independent to other options (Style and Color scheme).
This avoids text in Language combobox to be left out and replaced by `...` due to Style Hint text being too long.

PR #22823.
2025-06-08 17:15:42 +08:00
Chocobo1
4132173b30
WebUI: avoid redundant operations when sorting
Avoid recomputing the same result on every sort operation.
Also clean up the caller site.

PR #22821.
2025-06-08 17:08:47 +08:00
Rémi Marseault
8e2125ee72
WebAPI: Add metadata in /app/getDirectoryContent response
## Description

Send file/folder metadata instead of just the name of a filesystem entry.
Currently the endpoint only sends a list of string, containing the path of each entry, without specifying its type (file or folder).
The optional `withMetadata` flag has been added to provide metadata and to prevent breaking changes with older versions.
If `true`, JSON response will be an array of objects instead of an array of strings.

This object contains:
- `name`: the name of the file system entry (without path)
- `type`: Whether the file system entry is a "file" or a "dir"
- `creation_date`: file system entry's creation date
- `last_access_date`: file system entry's last access date
- `last_modification_date`: file system entry's last modification date

If the entry is a file, a `size` field is present with the file size in bytes.

## Objective

Build a server file browser inside WebUIs, feature is currently being developed for VueTorrent.
It will include file metadata, filtering and sorting on the different fields.

PR #22813.
2025-06-07 23:02:48 +08:00
Vladimir Golovnev
05bcc4e175
Don't limit the size of read "resume data"
PR #22825.
2025-06-07 06:58:48 +03:00
Thomas Piccirello
5dfb51a8d2
WebUI: Add ability to refresh search
WebUI equivalent of #22122. Refreshing maintains all existing filters.

PR #22805.
2025-06-06 17:32:47 +08:00
Vladimir Golovnev
526abdf7ce
Avoid data copying when prepare resume data
PR #22812.
2025-06-06 08:59:39 +03:00
bolshoytoster
617b1da842
WebUI: Keep client session from expiring when the page is hidden
In #22567, I made it so the web UI wouldn't refresh the main data while the page is hidden. This causes the session to time out (after 1 hour by default).
This PR changes that to instead refresh every Preferences/WebUI/SessionTimeout / 2 instead of not at all, which should keep the session alive.

PR #22804.
2025-06-05 18:28:35 +08:00
Thomas Piccirello
c59ac3b970
Make modifying log file perms best effort
qBittorrent is able to write to the log file, so it's ok if the permission change fails.

PR #22800.
2025-06-05 17:32:26 +08:00
Thomas Piccirello
0c48b70e5b
Send 204 when WebAPI response contains no data
PR #21349.
2025-06-05 09:25:04 +03:00
Vladimir Golovnev
1cb6173ad1
Fix command line placeholders description
PR #22815.
2025-06-05 09:21:46 +03:00
Vladimir Golovnev
2cbfb91b88
Use modern API to export torrent
PR #22786.
2025-06-03 15:24:52 +03:00
Vladimir Golovnev
0729c9a2f8
Handle libtorrent alerts in SessionImpl only
PR #22798.
2025-06-03 08:18:12 +03:00
Shanary
7982f66fa6
WebAPI: Optionally include files info in torrent list
PR #22750.
Closes #22742.
2025-06-01 12:06:11 +03:00
Chocobo1
50d60b9589
GHA CI: enforce sorted import in Python scripts
This main point is to normalize the sorting of imports in Python scripts.
Currently the default setting from isort is used: https://pycqa.github.io/isort/docs/configuration/custom_sections_and_ordering.html

PR #22793.
2025-05-31 18:01:02 +08:00
Chocobo1
96f0eebc4e
WebUI: switch to lightweight clipboard library
The new library [1] will opt to the modern Clipboard API [2] when it is available. It will
fallback to the old method otherwise.
The new library is also smaller and without any bloat.

Note that the line `module.exports` is required to be removed/commented out. This is the only
patch required.

[1] https://github.com/feross/clipboard-copy
[2] https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API

PR #22792.
2025-05-31 17:55:10 +08:00
Chocobo1
4b07597d54
WebUI: migrate away from recursion
PR #22791.
2025-05-31 17:38:05 +08:00
Vladimir Golovnev
a9213627a9
Interpret tracker "updating" status as a separate property
PR #22787.
2025-05-30 08:34:26 +03:00
Vladimir Golovnev
28c1ba869b
WebUI: Add support for tracker status filter
PR #22166.
2025-05-30 08:32:46 +03:00
tehcneko
054003970e
WebUI: Fix path autofill in set location and new category
Attach `PathAutofill` after `DOMContentLoaded`.
Also removed pathAutofill.js in newfolder.html since it's meant for new RSS folder name.

PR #22773.
2025-05-29 21:57:43 +08:00
ivan
44bb1ac7eb
WebUI: Add support for tracker status filter 2025-05-27 17:12:43 +03:00
Vladimir Golovnev (Glassez)
e309b17732
WebAPI: Provide announce stats within "sync" data 2025-05-27 11:28:13 +03:00
Chocobo1
e10fb40a48
NSIS: revise license page
The GPL doesn't need to be agreed with and therefore remove 'I accept agreement' checkbox and
adjust related UI elements.

Closes #22660.
PR #22775.
2025-05-27 14:53:50 +08:00
Zentino
7648a2838d
Fix access denial messages
Updated error messages for access denial from (401) to (403) for remote content.
Added (401) status code to authentication error messages.

PR #22774.
2025-05-27 14:47:43 +08:00
Vladimir Golovnev
8cd1a80852
WebAPI: Add more properties to 'torrents/info' result
PR #22753.
2025-05-27 09:37:06 +03:00
tehcneko
c79a9624af
WebUI: Remove unnecessary script loading
Removed Mootools script in most iframes, there is no longer any Mootools usage in them.

PR #22772.
2025-05-27 14:33:22 +08:00
Chocobo1
4e9c514c2f
WebUI: migrate to JS native class
PR #22770.
2025-05-27 14:28:15 +08:00
tehcneko
84ed24e257
WebUI: Replace slider with native input range
Got rid of Mootools and MochaUI.

PR #22769.
2025-05-27 14:22:13 +08:00
tehcneko
48f7f6fb8c
WebUI: Convert 'Progress Bar' class to a custom element
Got rid of Mootools and manual calculation of width.

PR #22768.
2025-05-27 14:16:18 +08:00
Vladimir Golovnev
f47754838b
Allow to pass torrent comment to external program
PR #22771.
Closes #13186.
2025-05-26 15:35:51 +03:00
bolshoytoster
a3d1ff0eb2
WebUI: Fix memory leak
See #22734, there is a memory leak in the MooTools .destroy(), this replaces all uses of that with the browser native .remove().

This also overrides the MooTools Document.id function, which is used by $(id). The original function always allocates an ID to elements it selects, the override doesn't, and is also a little more efficient.

Closes #22734.
PR  #22754.

---------

Co-authored-by: Chocobo1 <Chocobo1@users.noreply.github.com>
2025-05-25 15:41:52 +08:00
Chocobo1
afcfea5b8f
Merge pull request #22761 from Chocobo1/rss_fields
Add 'Open link' to RSS article header
2025-05-25 15:12:16 +08:00
Chocobo1
84cd8e1535
Utilize Path class when finding Python executable
This is a code clean up and shouldn't affect the outcome.

PR #22760.
2025-05-25 15:00:19 +08:00
Chocobo1
eb3718fc91
Add 'Open link' to RSS article header 2025-05-23 23:32:46 +08:00
Chocobo1
7e0247fefe
Avoid redundant function calls
Fix code formatting.
2025-05-23 23:32:45 +08:00
Chocobo1
f9f031cdb4
Use HTTPS by default 2025-05-23 23:32:39 +08:00
Chocobo1
d56b353c52
Add checks for minimum supported Python version
Now it checks for all python installations and related procedures has been revised.
If the python version does not meet the minimum requirement, it will be logged.

PR #22729.
2025-05-21 14:52:11 +08:00
samet sahin
83799f4f07
WebUI: Prevent mobile keyboards from capitalizing username input
This PR improves the user experience on mobile devices by ensuring the username field in the login form does not automatically capitalize the first letter when the keyboard opens.
Mobile browsers tend to automatically capitalize the first letter of text inputs, which can lead to login failures if the username is case-sensitive. By explicitly disabling autocapitalization, the WebUI ensures a more predictable and user-friendly experience on mobile devices.

Tested on:
iOS (Safari)

Co-authored-by: Chocobo1 <Chocobo1@users.noreply.github.com>

PR #22725.
2025-05-18 15:49:40 +08:00
Chocobo1
c7caae1150
WebUI: fix wrong replacement sequence
Only IPv6 addresses may have a 'zone index' and therefore it should be replaced last for the
result to be correct.

PR #22724.
2025-05-18 15:42:36 +08:00
Chocobo1
1662a9deb2
Add fallback for random number generator
`getrandom()` is available since Linux 3.17 (2014/10/05) yet there are older devices that don't
meet this requirement.

Closes #22691.
PR #22723.
2025-05-18 15:37:17 +08:00
Atk
6c310aa311
WebUI: Make multi-rename search & replace fields use a monospace font
Before the release of 5.1.0 I believe these two fields both used monospace fonts and also had an increased font size, maybe 14px (I couldn't find in blame where/how this regressed). Ideally, I'd like to bring this back, as it makes elaborate regexes easier to grok. This PR currently just tells the browser to use its monospace font, but I could also add a similar in-line style for font size. I just don't know if this is the best way to solve this problem; if there's a better place for this sort of styling to happen let me know

PR #22719.
2025-05-18 15:32:06 +08:00
xavier2k6
368748ac52
GHA CI: Bump spellcheck related hooks revs
* Bumped `codespell` -> `2.4.1`
* Bumped `typos` -> `1.32.0`

PR #22709.
2025-05-18 15:04:49 +08:00
KanishkaHalder1771
9c81e58de6
Update help message for Windows systems
For windows environment the `--help` output will show :
```
set QBT_NO_SPLASH=1
C:\Program Files\qBittorrent\qbittorrent.exe
```
instead of
```
QBT_NO_SPLASH=1 C:\Program Files\qBittorrent\qbittorrent.exe
```

Fixes #22662.
PR #22695.
2025-05-18 14:51:01 +08:00
Chocobo1
778a158597
WebUI: add versioning to local preferences
And provide migration path for changing preferences.

Fixes #22639.
PR #22677.
2025-05-14 07:20:01 +08:00
Chocobo1
09071d2b69
Allow symbolic links in torrent creator on Windows
Note that .lnk files (shortcuts) are always ignored on Windows.

Closes #22665.
PR #22675.
2025-05-14 07:13:52 +08:00
Hanabishi
b79ac0d716
Remove "Physical memory (RAM) usage limit" option on Linux
Memory working set limit is not effective on Linux at all. See [`getrlimit(2)`](https://man7.org/linux/man-pages/man2/getrlimit.2.html) → `RLIMIT_RSS`.
Introduced in #16874, disabled for macOS in #19805. This PR hides the option for Linux too.
Worth to mention that #19805 did not deliver the change for WebUI. So there is also a small fixup, I covered both cases.
Also removed pointless "This option is less effective on Linux" remark.

PR #22680.
2025-05-13 00:49:18 +08:00
skomerko
de767871f1
WebUI: Convert 'Pieces Bar' class to a custom element
Mootools is no longer used to create PiecesBar class (+ I cleaned it up a bit and turned into custom element but everything should work as before).

PR #22670.
2025-05-13 00:43:35 +08:00
Chocobo1
2477e13b3f
GHA CI: update zizmor rules ID
zizmor 1.7.0 has changed the ID.
https://docs.zizmor.sh/release-notes/#v170

PR #22684.
2025-05-13 00:36:31 +08:00
Chocobo1
eb82c9078d
WebUI: always provide event variable
This is unifying coding style and avoid wrong usages.

PR #22676.
2025-05-13 00:11:00 +08:00
Vladimir Golovnev
663da093bd
Fix compilation with Qt 6.6.0
PR #22678.
2025-05-12 11:27:02 +03:00
dezza
13f9c20a69
WebUI: Remove unselectable from General tab
Making General-tab text `unselectable` is not an improvement.

It begs to add a new `Copy -> Save path` feature, because using `Set location` to copy save path (*which requires a request*) is not faster than simply copying it from the `General` tab by double-left clicking and pressing `CTRL+C`.

I don't see a reason why its necessary to software-restrict people from copying details from the `General`-tab - there are several reasons why you would - incl. the above mentioned usecase for quickly copying save-path, but other than that its counterproductive to limit people from copying the details displayed.

PR #22663.
2025-05-10 16:48:05 +08:00
justusaac
86e4b662ce
WebUI: Select multiple files to rename with Shift
Convenience feature in the "Rename Files" menu in the WebUI.
If you click one file's checkbox, and then click another with Shift held, all the checkboxes between those two will be selected/unselected based on the state of the first checkbox.
It's based on what the Windows file explorer does when holding Ctrl and Shift

Closes #22455.
PR #22610.
2025-05-10 16:08:19 +08:00
Vladimir Golovnev
dcaf4b6d80
Fix async result handlers
PR #22669.
Closes #22644.
2025-05-10 10:17:35 +03:00
Vladimir Golovnev
d29f47c36e
Revamp adding torrents to libtorrent session
PR #22648.
2025-05-08 16:31:02 +03:00
Vladimir Golovnev (Glassez)
ab04064adc
Store AddTorrentParams inside alert handlers 2025-05-07 06:52:33 +03:00
Vladimir Golovnev (Glassez)
9afbd47b52
Assign add_torrent_alert handler when adding torrent 2025-05-07 06:52:33 +03:00
Vladimir Golovnev (Glassez)
582dc99cae
Don't access libtorrent session directly from TorrentImpl 2025-05-07 06:52:33 +03:00
Vladimir Golovnev (Glassez)
3691eb948e
Join the similar code paths 2025-05-07 06:52:33 +03:00
Vladimir Golovnev (Glassez)
76bb4e5f98
Add helpers to get InfoHash from libtorrent classes 2025-05-07 06:52:23 +03:00
Chocobo1
0262faa915
Merge pull request #22650 from Chocobo1/webui_file_tree
* WebUI: migrate 'file tree' class to JS native class
* WebUI: avoid double lookup
2025-05-05 15:48:24 +08:00
Chocobo1
f1b7c4572b
Revise labels for 'duplicate torrent' actions
PR #22645.
2025-05-05 15:38:12 +08:00
Chocobo1
c494314a29
Use short format for displaying RSS entry date
The long format is too verbose and hard to read.

PR #22646.
2025-05-03 23:04:55 +08:00
Chocobo1
559f47ab0c
WebUI: avoid double lookup 2025-05-03 22:47:28 +08:00
Chocobo1
380b25e22f
WebUI: migrate 'file tree' class to JS native class 2025-05-03 22:41:48 +08:00
Chocobo1
7745ac19d8
Merge pull request #22636 from skomerko/webui-dynamic-classes
Dynamic table classes are now created using modern class syntax (minimal changes to remove Mootools bits).
2025-05-03 22:19:01 +08:00
Vladimir Golovnev
6cd6267c26
Fix ratio handling
PR #22638.
2025-05-01 14:18:18 +03:00
Vladimir Golovnev
e7dee969e1
Remove dubious seeding time max value
PR #22624.
2025-05-01 08:57:10 +03:00
skomerko
bb68a39b53 WebUI: Prefix private properties with # in dynamic table classes 2025-04-29 21:08:26 +02:00
skomerko
4c91cd9372 WebUI: Use modern class syntax to create dynamic table classes 2025-04-29 21:08:17 +02:00
tehcneko
0791828b84
WebUI: fix virtual list defects
Fixes https://github.com/qbittorrent/qBittorrent/pull/22502#issuecomment-2822201721 and https://github.com/qbittorrent/qBittorrent/pull/22502#issuecomment-2822253388.

PR #22597.
2025-04-29 20:16:00 +08:00
Vladimir Golovnev
c2f2a38582
Call QPromise::start() early to avoid race condition
PR #22617.
2025-04-28 18:04:35 +03:00
Chocobo1
ad4bdc0653
Merge pull request #22615 from Chocobo1/webui_eslint
* WebUI: disallow unnecessary semicolons
* WebUI: ensure consistent shorthand syntax
* WebUI: disallow async functions which have no await expression
* WebUI: remove unused variable
2025-04-28 20:20:57 +08:00
Chocobo1
f3095935ea
Merge pull request #22549 from skomerko/webui-element-by-id
Mootools `$` alias is no longer used for element lookup (excluding lib files).
Follow up PR for #22523.
2025-04-28 03:34:23 +08:00
Vladimir Golovnev
732b2bcbdb
Provide asynchronous results via QFuture
Makes asynchronous logic to look more straightforward.
Allows caller to choose blocking or non-blocking way of obtaining asynchronous results via the same interface.

PR #22598.
2025-04-27 16:24:07 +03:00
Vladimir Golovnev
33aaa867b5
Drop support of Qt 6.5
PR #22599.
2025-04-27 16:21:20 +03:00
Chocobo1
fdd17159eb
WebUI: remove unused variable 2025-04-27 16:19:51 +08:00
Chocobo1
e9fee414df
WebUI: disallow async functions which have no await expression 2025-04-27 16:19:51 +08:00
Chocobo1
1077cbba2b
WebUI: ensure consistent shorthand syntax 2025-04-27 16:19:50 +08:00
Chocobo1
70dbe9468a
WebUI: disallow unnecessary semicolons 2025-04-27 16:19:50 +08:00
Isak05
45babc336d
Fix preview not opening on Wayland
Deferring the opening of the preview slightly gives the preview select
dialog time to close and for focus to shift back to the main window.

PR #22608.
Closes #22607.

---------

Co-authored-by: Vladimir Golovnev <glassez@yandex.ru>
2025-04-27 10:02:52 +03:00
Vladimir Golovnev
70822e8942
Fix appearance of search history length spinbox
PR #22605.
2025-04-26 09:28:24 +03:00
Vladimir Golovnev
b5394e7939
Don't interpret wildcard pattern as filepath globbing
PR #22590.
Closes #22583.
2025-04-26 09:27:22 +03:00
skomerko
411ca0f668 WebUI: Use native function for selecting elements by ID 2025-04-23 19:55:04 +02:00
skomerko
7b3aa51bb1 WebUI: Eliminate redundant DOM element queries 2025-04-23 19:49:40 +02:00
sledgehammer999
0b3bce8993
Sync translations from Transifex and run lupdate 2025-04-21 12:30:57 +03:00
bolshoytoster
0160aa28b6
WebUI: Don't update UI if the page is hidden
Currently, there is unnecessary CPU/network usage by the web UI when it's running in the background, this PR prevents it from refreshing in the background.

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

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

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

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

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

Closes #21953.
PR #22558.

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

Closes #22420.
PR #22557.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

And add initial version of WebAPI changelog.

PR #22481.
2025-03-29 20:47:53 +08:00
Chocobo1
97865545c3
WebUI: fix Tag counter counting wrong
Related: https://github.com/qbittorrent/qBittorrent/pull/22103/files/73e9116d21015542caeb9a3cfd56bfb256ebed9d#r2014898781

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2
.github/FUNDING.yml vendored
View file

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

View file

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

View file

@ -20,7 +20,7 @@ jobs:
matrix: matrix:
libt_version: ["2.0.11", "1.2.20"] libt_version: ["2.0.11", "1.2.20"]
qbt_gui: ["GUI=ON", "GUI=OFF"] qbt_gui: ["GUI=ON", "GUI=OFF"]
qt_version: ["6.7.0"] qt_version: ["6.9.1"]
env: env:
boost_path: "${{ github.workspace }}/../boost" boost_path: "${{ github.workspace }}/../boost"
@ -52,7 +52,7 @@ jobs:
store_cache: ${{ github.ref == 'refs/heads/master' }} store_cache: ${{ github.ref == 'refs/heads/master' }}
update_packager_index: false update_packager_index: false
ccache_options: | ccache_options: |
max_size=2G max_size=1G
- name: Install boost - name: Install boost
env: env:
@ -70,6 +70,9 @@ jobs:
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?" tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?"
fi fi
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}" mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
cd "${{ env.boost_path }}"
./bootstrap.sh
./b2 stage --stagedir=./ --with-headers
- name: Install Qt - name: Install Qt
uses: jurplel/install-qt-action@v4 uses: jurplel/install-qt-action@v4
@ -95,7 +98,7 @@ jobs:
-DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_STANDARD=20 \ -DCMAKE_CXX_STANDARD=20 \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}" \ -DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-Ddeprecated-functions=OFF -Ddeprecated-functions=OFF
cmake --build build cmake --build build
sudo cmake --install build sudo cmake --install build
@ -109,7 +112,7 @@ jobs:
-G "Ninja" \ -G "Ninja" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}" \ -DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-DTESTING=ON \ -DTESTING=ON \
-DVERBOSE_CONFIGURE=ON \ -DVERBOSE_CONFIGURE=ON \
-D${{ matrix.qbt_gui }} -D${{ matrix.qbt_gui }}

View file

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

View file

@ -21,7 +21,7 @@ jobs:
matrix: matrix:
libt_version: ["2.0.11", "1.2.20"] libt_version: ["2.0.11", "1.2.20"]
qbt_gui: ["GUI=ON", "GUI=OFF"] qbt_gui: ["GUI=ON", "GUI=OFF"]
qt_version: ["6.5.2"] qt_version: ["6.6.3"]
env: env:
boost_path: "${{ github.workspace }}/../boost" boost_path: "${{ github.workspace }}/../boost"
@ -47,7 +47,7 @@ jobs:
store_cache: ${{ github.ref == 'refs/heads/master' }} store_cache: ${{ github.ref == 'refs/heads/master' }}
update_packager_index: false update_packager_index: false
ccache_options: | ccache_options: |
max_size=2G max_size=1G
- name: Install boost - name: Install boost
env: env:
@ -65,6 +65,9 @@ jobs:
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?" tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?"
fi fi
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}" mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
cd "${{ env.boost_path }}"
./bootstrap.sh
./b2 stage --stagedir=./ --with-headers
- name: Install Qt - name: Install Qt
uses: jurplel/install-qt-action@v4 uses: jurplel/install-qt-action@v4
@ -90,7 +93,7 @@ jobs:
-DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_STANDARD=20 \ -DCMAKE_CXX_STANDARD=20 \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}" \ -DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-Ddeprecated-functions=OFF -Ddeprecated-functions=OFF
cmake --build build cmake --build build
sudo cmake --install build sudo cmake --install build
@ -112,7 +115,7 @@ jobs:
-G "Ninja" \ -G "Ninja" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}" \ -DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-DCMAKE_INSTALL_PREFIX="/usr" \ -DCMAKE_INSTALL_PREFIX="/usr" \
-DTESTING=ON \ -DTESTING=ON \
-DVERBOSE_CONFIGURE=ON \ -DVERBOSE_CONFIGURE=ON \
@ -159,6 +162,7 @@ jobs:
- name: Package AppImage - name: Package AppImage
run: | run: |
rm -f "${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/sqldrivers/libqsqlmimer.so"
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt ./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt
rm qbittorrent/apprun-hooks/* rm qbittorrent/apprun-hooks/*
cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh

View file

@ -34,7 +34,12 @@ jobs:
run: | run: |
npm install npm install
npm ls npm ls
echo "::group::npm ls --all"
npm ls --all npm ls --all
echo "::endgroup::"
- name: Run tests
run: npm test
- name: Lint code - name: Lint code
run: npm run lint run: npm run lint

View file

@ -67,6 +67,7 @@ jobs:
"set(VCPKG_BUILD_TYPE release)") "set(VCPKG_BUILD_TYPE release)")
# clear buildtrees after each package installation to reduce disk space requirements # clear buildtrees after each package installation to reduce disk space requirements
$packages = ` $packages = `
"boost-build:x64-windows-static-md-release",
"openssl:x64-windows-static-md-release", "openssl:x64-windows-static-md-release",
"zlib:x64-windows-static-md-release" "zlib:x64-windows-static-md-release"
${{ env.vcpkg_path }}/vcpkg.exe upgrade ` ${{ env.vcpkg_path }}/vcpkg.exe upgrade `
@ -94,11 +95,18 @@ jobs:
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.." tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."
} }
move "${{ github.workspace }}/../boost_*" "${{ env.boost_path }}" 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 - name: Install Qt
uses: jurplel/install-qt-action@v4 uses: jurplel/install-qt-action@v4
with: with:
version: "6.8.0" version: "6.9.1"
arch: win64_msvc2022_64 arch: win64_msvc2022_64
archives: qtbase qtsvg qttools archives: qtbase qtsvg qttools
cache: true cache: true
@ -113,7 +121,7 @@ jobs:
${{ env.libtorrent_path }} ${{ env.libtorrent_path }}
cd ${{ env.libtorrent_path }} cd ${{ env.libtorrent_path }}
$env:CXXFLAGS+=" /guard:cf" $env:CXXFLAGS+=" /guard:cf"
$env:LDFLAGS+=" /guard:cf" $env:LDFLAGS+=" /GUARD:CF"
cmake ` cmake `
-B build ` -B build `
-G "Ninja" ` -G "Ninja" `
@ -122,7 +130,7 @@ jobs:
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON ` -DCMAKE_EXPORT_COMPILE_COMMANDS=ON `
-DCMAKE_INSTALL_PREFIX="${{ env.libtorrent_path }}/install" ` -DCMAKE_INSTALL_PREFIX="${{ env.libtorrent_path }}/install" `
-DCMAKE_TOOLCHAIN_FILE="${{ env.vcpkg_path }}/scripts/buildsystems/vcpkg.cmake" ` -DCMAKE_TOOLCHAIN_FILE="${{ env.vcpkg_path }}/scripts/buildsystems/vcpkg.cmake" `
-DBOOST_ROOT="${{ env.boost_path }}" ` -DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" `
-DBUILD_SHARED_LIBS=OFF ` -DBUILD_SHARED_LIBS=OFF `
-Ddeprecated-functions=OFF ` -Ddeprecated-functions=OFF `
-Dstatic_runtime=OFF ` -Dstatic_runtime=OFF `
@ -139,7 +147,7 @@ jobs:
-DCMAKE_BUILD_TYPE=RelWithDebInfo ` -DCMAKE_BUILD_TYPE=RelWithDebInfo `
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON ` -DCMAKE_EXPORT_COMPILE_COMMANDS=ON `
-DCMAKE_TOOLCHAIN_FILE="${{ env.vcpkg_path }}/scripts/buildsystems/vcpkg.cmake" ` -DCMAKE_TOOLCHAIN_FILE="${{ env.vcpkg_path }}/scripts/buildsystems/vcpkg.cmake" `
-DBOOST_ROOT="${{ env.boost_path }}" ` -DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" `
-DLibtorrentRasterbar_DIR="${{ env.libtorrent_path }}/install/lib/cmake/LibtorrentRasterbar" ` -DLibtorrentRasterbar_DIR="${{ env.libtorrent_path }}/install/lib/cmake/LibtorrentRasterbar" `
-DMSVC_RUNTIME_DYNAMIC=ON ` -DMSVC_RUNTIME_DYNAMIC=ON `
-DTESTING=ON ` -DTESTING=ON `

View file

@ -16,7 +16,7 @@ jobs:
matrix: matrix:
libt_version: ["2.0.11"] libt_version: ["2.0.11"]
qbt_gui: ["GUI=ON"] qbt_gui: ["GUI=ON"]
qt_version: ["6.5.2"] qt_version: ["6.9.1"]
env: env:
boost_path: "${{ github.workspace }}/../boost" boost_path: "${{ github.workspace }}/../boost"
@ -39,7 +39,7 @@ jobs:
- name: Install boost - name: Install boost
env: env:
BOOST_MAJOR_VERSION: "1" BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "86" BOOST_MINOR_VERSION: "88"
BOOST_PATCH_VERSION: "0" BOOST_PATCH_VERSION: "0"
run: | 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" 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,6 +52,9 @@ jobs:
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?" tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?"
fi fi
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}" mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
cd "${{ env.boost_path }}"
./bootstrap.sh
./b2 stage --stagedir=./ --with-headers
- name: Install Qt - name: Install Qt
uses: jurplel/install-qt-action@v4 uses: jurplel/install-qt-action@v4
@ -74,7 +77,7 @@ jobs:
-G "Ninja" \ -G "Ninja" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_STANDARD=20 \ -DCMAKE_CXX_STANDARD=20 \
-DBOOST_ROOT="${{ env.boost_path }}" \ -DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-Ddeprecated-functions=OFF -Ddeprecated-functions=OFF
cmake --build build cmake --build build
sudo cmake --install build sudo cmake --install build
@ -98,7 +101,7 @@ jobs:
-B build \ -B build \
-G "Ninja" \ -G "Ninja" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DBOOST_ROOT="${{ env.boost_path }}" \ -DBOOST_ROOT="${{ env.boost_path }}/lib/cmake" \
-DVERBOSE_CONFIGURE=ON \ -DVERBOSE_CONFIGURE=ON \
-D${{ matrix.qbt_gui }} -D${{ matrix.qbt_gui }}
PATH="${{ env.coverity_path }}/bin:$PATH" \ 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 # but you are not obligated to do so. If you do not wish to do so, delete this
# exception statement from your version. # exception statement from your version.
from collections.abc import Callable, Sequence
from typing import Optional
import argparse import argparse
import re import re
import xml.etree.ElementTree as ElementTree
import sys import sys
import xml.etree.ElementTree as ElementTree
from collections.abc import Callable, Sequence
from typing import Optional
def traversePostOrder(root: ElementTree.Element, visitFunc: Callable[[ElementTree.Element], None]) -> None: 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 # but you are not obligated to do so. If you do not wish to do so, delete this
# exception statement from your version. # exception statement from your version.
from collections.abc import Sequence
from typing import Optional
import argparse import argparse
import re import re
import sys import sys
from collections.abc import Sequence
from typing import Optional
def main(argv: Optional[Sequence[str]] = None) -> int: def main(argv: Optional[Sequence[str]] = None) -> int:

View file

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

View file

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

View file

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

111
Changelog
View file

@ -1,113 +1,4 @@
Mon Jun 23rd 2025 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.1 Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.0
- BUGFIX: Don't interpret wildcard pattern as filepath globbing (glassez)
- BUGFIX: Fix appearance of search history length spinbox (glassez)
- BUGFIX: Remove dubious seeding time max value (glassez)
- BUGFIX: Fix ratio handling (glassez)
- BUGFIX: Fix compilation with Qt 6.6.0 (glassez)
- WEBUI: Make General tab text selectable by default (dezza)
- WEBUI: Add versioning to local preferences (Chocobo1)
- WEBUI: Make multi-rename search & replace fields use a monospace font (Atk)
- WEBUI: Fix wrong replacement sequence in IPv6 string (Chocobo1)
- WEBUI: Fix memory leak (bolshoytoster)
- WEBUI: Fix path autofill in set location and new category (tehcneko)
- RSS: Mark matched article as "read" if it refers to a duplicate torrent (glassez)
- WINDOWS: Update command line help message (KanishkaHalder1771)
- WINDOWS: NSIS: Don't require agreement on the license page (Chocobo1)
- LINUX: Fix preview not opening on Wayland (Isak05)
- LINUX: Add fallback for random number generator (Chocobo1)
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 Mon Oct 28th 2024 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.1
- FEATURE: Add "Simple pread/pwrite" disk IO type (Hanabishi) - 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 - OpenSSL >= 3.0.2
- Qt 6.5.0 - 6.x - Qt 6.6.0 - 6.x
- zlib >= 1.2.11 - zlib >= 1.2.11

39
WebAPI_Changelog.md Normal file
View file

@ -0,0 +1,39 @@
# WebAPI Changelog
## 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,6 +47,9 @@ find_package(Boost ${minBoostVersion} REQUIRED)
find_package(OpenSSL ${minOpenSSLVersion} REQUIRED) find_package(OpenSSL ${minOpenSSLVersion} REQUIRED)
find_package(ZLIB ${minZlibVersion} REQUIRED) find_package(ZLIB ${minZlibVersion} REQUIRED)
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS Core Network Sql Xml LinguistTools) 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) if (DBUS)
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS DBus) find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS DBus)
set_package_properties(Qt6DBus PROPERTIES 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 target_compile_definitions(qbt_common_cfg INTERFACE
QT_DISABLE_DEPRECATED_UP_TO=0x060500 QT_DISABLE_DEPRECATED_UP_TO=0x060600
QT_NO_CAST_FROM_ASCII QT_NO_CAST_FROM_ASCII
QT_NO_CAST_TO_ASCII QT_NO_CAST_TO_ASCII
QT_NO_CAST_FROM_BYTEARRAY QT_NO_CAST_FROM_BYTEARRAY
@ -89,7 +89,7 @@ if (MSVC)
/Zc:__cplusplus /Zc:__cplusplus
) )
target_link_options(qbt_common_cfg INTERFACE target_link_options(qbt_common_cfg INTERFACE
/guard:cf /GUARD:CF
$<$<NOT:$<CONFIG:Debug>>:/OPT:REF /OPT:ICF> $<$<NOT:$<CONFIG:Debug>>:/OPT:REF /OPT:ICF>
# suppress linking warning due to /INCREMENTAL and /OPT:ICF being both ON # suppress linking warning due to /INCREMENTAL and /OPT:ICF being both ON
$<$<CONFIG:RelWithDebInfo>:/INCREMENTAL:NO> $<$<CONFIG:RelWithDebInfo>:/INCREMENTAL:NO>

2
dist/mac/Info.plist vendored
View file

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

Binary file not shown.

Binary file not shown.

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 B

After

Width:  |  Height:  |  Size: 747 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 B

After

Width:  |  Height:  |  Size: 747 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.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 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

@ -0,0 +1,57 @@
<?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> <url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url>
<content_rating type="oars-1.1"/> <content_rating type="oars-1.1"/>
<releases> <releases>
<release version="5.1.1" date="2025-06-23"/> <release version="5.2.0~alpha1" date="2025-02-11"/>
</releases> </releases>
</component> </component>

View file

@ -14,7 +14,7 @@
; 4.5.1.3 -> good ; 4.5.1.3 -> good
; 4.5.1.3.2 -> bad ; 4.5.1.3.2 -> bad
; 4.5.0beta -> bad ; 4.5.0beta -> bad
!define /ifndef QBT_VERSION "5.1.1" !define /ifndef QBT_VERSION "5.2.0"
; Option that controls the installer's window name ; Option that controls the installer's window name
; If set, its value will be used like this: ; If set, its value will be used like this:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -465,13 +465,13 @@ QBtCommandLineParameters parseCommandLine(const QStringList &args)
return result; return result;
} }
QString wrapText(const QString &text, int initialIndentation = USAGE_TEXT_COLUMN, int wrapAtColumn = WRAP_AT_COLUMN) QString wrapText(const QString &text, const int initialIndentation = USAGE_TEXT_COLUMN, const int wrapAtColumn = WRAP_AT_COLUMN)
{ {
QStringList words = text.split(u' '); const QStringList words = text.split(u' ');
QStringList lines = {words.first()}; QStringList lines = {words.first()};
int currentLineMaxLength = wrapAtColumn - initialIndentation; int currentLineMaxLength = wrapAtColumn - initialIndentation;
for (const QString &word : asConst(words.mid(1))) for (const QString &word : asConst(words.sliced(1)))
{ {
if (lines.last().length() + word.length() + 1 < currentLineMaxLength) if (lines.last().length() + word.length() + 1 < currentLineMaxLength)
{ {

View file

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

View file

@ -55,6 +55,7 @@ add_library(qbt_base STATIC
concepts/stringable.h concepts/stringable.h
digest32.h digest32.h
exceptions.h exceptions.h
freediskspacechecker.h
global.h global.h
http/connection.h http/connection.h
http/httperror.h http/httperror.h
@ -160,6 +161,7 @@ add_library(qbt_base STATIC
bittorrent/trackerentry.cpp bittorrent/trackerentry.cpp
bittorrent/trackerentrystatus.cpp bittorrent/trackerentrystatus.cpp
exceptions.cpp exceptions.cpp
freediskspacechecker.cpp
http/connection.cpp http/connection.cpp
http/httperror.cpp http/httperror.cpp
http/requestparser.cpp http/requestparser.cpp

View file

@ -29,6 +29,7 @@
#include "addtorrentmanager.h" #include "addtorrentmanager.h"
#include "base/bittorrent/addtorrenterror.h"
#include "base/bittorrent/infohash.h" #include "base/bittorrent/infohash.h"
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/bittorrent/torrentdescriptor.h" #include "base/bittorrent/torrentdescriptor.h"
@ -185,8 +186,8 @@ void AddTorrentManager::handleDuplicateTorrent(const QString &source
message = tr("Trackers are merged from new source"); message = tr("Trackers are merged from new source");
} }
LogMsg(tr("Detected an attempt to add a duplicate torrent. Source: %1. Existing torrent: %2. Result: %3") LogMsg(tr("Detected an attempt to add a duplicate torrent. Source: %1. Existing torrent: \"%2\". Torrent infohash: %3. Result: %4")
.arg(source, existingTorrent->name(), message)); .arg(source, existingTorrent->name(), existingTorrent->infoHash().toString(), message));
emit addTorrentFailed(source, {BitTorrent::AddTorrentError::DuplicateTorrent, message}); emit addTorrentFailed(source, {BitTorrent::AddTorrentError::DuplicateTorrent, message});
} }

View file

@ -35,7 +35,6 @@
#include <QObject> #include <QObject>
#include "base/applicationcomponent.h" #include "base/applicationcomponent.h"
#include "base/bittorrent/addtorrenterror.h"
#include "base/bittorrent/addtorrentparams.h" #include "base/bittorrent/addtorrentparams.h"
#include "base/torrentfileguard.h" #include "base/torrentfileguard.h"
@ -45,6 +44,7 @@ namespace BitTorrent
class Session; class Session;
class Torrent; class Torrent;
class TorrentDescriptor; class TorrentDescriptor;
struct AddTorrentError;
} }
namespace Net namespace Net

View file

@ -189,8 +189,13 @@ void BitTorrent::BencodeResumeDataStorage::loadQueue(const Path &queueFilename)
return; 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}; const QRegularExpression hashPattern {u"^([A-Fa-f0-9]{40})$"_s};
int start = 0; qsizetype queuePos = 0;
while (true) while (true)
{ {
const auto line = QString::fromLatin1(queueFile.readLine(lineMaxLength).trimmed()); const auto line = QString::fromLatin1(queueFile.readLine(lineMaxLength).trimmed());
@ -201,11 +206,15 @@ void BitTorrent::BencodeResumeDataStorage::loadQueue(const Path &queueFilename)
if (rxMatch.hasMatch()) if (rxMatch.hasMatch())
{ {
const auto torrentID = BitTorrent::TorrentID::fromString(rxMatch.captured(1)); const auto torrentID = BitTorrent::TorrentID::fromString(rxMatch.captured(1));
const int pos = m_registeredTorrents.indexOf(torrentID, start); const qsizetype pos = registeredTorrentsIndexes.value(torrentID, -1);
if (pos != -1) if (pos != -1)
{ {
std::swap(m_registeredTorrents[start], m_registeredTorrents[pos]); if (pos != queuePos)
++start; {
m_registeredTorrents.swapItemsAt(pos, queuePos);
registeredTorrentsIndexes.insert(m_registeredTorrents.at(pos), pos);
}
++queuePos;
} }
} }
} }
@ -342,9 +351,9 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
return torrentParams; return torrentParams;
} }
void BitTorrent::BencodeResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const void BitTorrent::BencodeResumeDataStorage::store(const TorrentID &id, LoadTorrentParams resumeData) const
{ {
QMetaObject::invokeMethod(m_asyncWorker, [this, id, resumeData]() QMetaObject::invokeMethod(m_asyncWorker, [this, id, resumeData = std::move(resumeData)]
{ {
m_asyncWorker->store(id, resumeData); m_asyncWorker->store(id, resumeData);
}); });

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,9 @@
#include "infohash.h" #include "infohash.h"
#include <QHash> #include <QHash>
#include <QString>
#include "base/global.h"
const int TorrentIDTypeId = qRegisterMetaType<BitTorrent::TorrentID>(); const int TorrentIDTypeId = qRegisterMetaType<BitTorrent::TorrentID>();
@ -86,6 +89,28 @@ BitTorrent::TorrentID BitTorrent::InfoHash::toTorrentID() const
#endif #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 BitTorrent::InfoHash::operator WrappedType() const
{ {
return m_nativeHash; return m_nativeHash;

View file

@ -36,6 +36,8 @@
#include "base/digest32.h" #include "base/digest32.h"
class QString;
using SHA1Hash = Digest32<160>; using SHA1Hash = Digest32<160>;
using SHA256Hash = Digest32<256>; using SHA256Hash = Digest32<256>;
@ -79,6 +81,8 @@ namespace BitTorrent
SHA256Hash v2() const; SHA256Hash v2() const;
TorrentID toTorrentID() const; TorrentID toTorrentID() const;
QString toString() const;
operator WrappedType() const; operator WrappedType() const;
private: private:

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -422,6 +422,8 @@ namespace BitTorrent
virtual void setUTPRateLimited(bool limited) = 0; virtual void setUTPRateLimited(bool limited) = 0;
virtual MixedModeAlgorithm utpMixedMode() const = 0; virtual MixedModeAlgorithm utpMixedMode() const = 0;
virtual void setUtpMixedMode(MixedModeAlgorithm mode) = 0; virtual void setUtpMixedMode(MixedModeAlgorithm mode) = 0;
virtual int hostnameCacheTTL() const = 0;
virtual void setHostnameCacheTTL(int value) = 0;
virtual bool isIDNSupportEnabled() const = 0; virtual bool isIDNSupportEnabled() const = 0;
virtual void setIDNSupportEnabled(bool enabled) = 0; virtual void setIDNSupportEnabled(bool enabled) = 0;
virtual bool multiConnectionsPerIpEnabled() const = 0; virtual bool multiConnectionsPerIpEnabled() const = 0;
@ -480,6 +482,8 @@ namespace BitTorrent
virtual QString lastExternalIPv4Address() const = 0; virtual QString lastExternalIPv4Address() const = 0;
virtual QString lastExternalIPv6Address() const = 0; virtual QString lastExternalIPv6Address() const = 0;
virtual qint64 freeDiskSpace() const = 0;
signals: signals:
void startupProgressUpdated(int progress); void startupProgressUpdated(int progress);
void addTorrentFailed(const InfoHash &infoHash, const AddTorrentError &reason); void addTorrentFailed(const InfoHash &infoHash, const AddTorrentError &reason);
@ -489,7 +493,6 @@ namespace BitTorrent
void categoryOptionsChanged(const QString &categoryName); void categoryOptionsChanged(const QString &categoryName);
void fullDiskError(Torrent *torrent, const QString &msg); void fullDiskError(Torrent *torrent, const QString &msg);
void IPFilterParsed(bool error, int ruleCount); void IPFilterParsed(bool error, int ruleCount);
void loadTorrentFailed(const QString &error);
void metadataDownloaded(const TorrentInfo &info); void metadataDownloaded(const TorrentInfo &info);
void restored(); void restored();
void paused(); void paused();
@ -520,5 +523,6 @@ namespace BitTorrent
void trackerSuccess(Torrent *torrent, const QString &tracker); void trackerSuccess(Torrent *torrent, const QString &tracker);
void trackerWarning(Torrent *torrent, const QString &tracker); void trackerWarning(Torrent *torrent, const QString &tracker);
void trackerEntryStatusesUpdated(Torrent *torrent, const QHash<QString, TrackerEntryStatus> &updatedTrackers); 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. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -30,6 +30,7 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <functional>
#include <utility> #include <utility>
#include <vector> #include <vector>
@ -61,11 +62,16 @@ class QString;
class QTimer; class QTimer;
class QUrl; class QUrl;
template <typename T> class QFuture;
class BandwidthScheduler; class BandwidthScheduler;
class FileSearcher; class FileSearcher;
class FilterParserThread; class FilterParserThread;
class FreeDiskSpaceChecker;
class NativeSessionExtension; class NativeSessionExtension;
struct FileSearchResult;
namespace BitTorrent namespace BitTorrent
{ {
enum class MoveStorageMode; enum class MoveStorageMode;
@ -390,6 +396,8 @@ namespace BitTorrent
void setUTPRateLimited(bool limited) override; void setUTPRateLimited(bool limited) override;
MixedModeAlgorithm utpMixedMode() const override; MixedModeAlgorithm utpMixedMode() const override;
void setUtpMixedMode(MixedModeAlgorithm mode) override; void setUtpMixedMode(MixedModeAlgorithm mode) override;
int hostnameCacheTTL() const override;
void setHostnameCacheTTL(int value) override;
bool isIDNSupportEnabled() const override; bool isIDNSupportEnabled() const override;
void setIDNSupportEnabled(bool enabled) override; void setIDNSupportEnabled(bool enabled) override;
bool multiConnectionsPerIpEnabled() const override; bool multiConnectionsPerIpEnabled() const override;
@ -448,6 +456,8 @@ namespace BitTorrent
QString lastExternalIPv4Address() const override; QString lastExternalIPv4Address() const override;
QString lastExternalIPv6Address() const override; QString lastExternalIPv6Address() const override;
qint64 freeDiskSpace() const override;
// Torrent interface // Torrent interface
void handleTorrentResumeDataRequested(const TorrentImpl *torrent); void handleTorrentResumeDataRequested(const TorrentImpl *torrent);
void handleTorrentShareLimitChanged(TorrentImpl *torrent); void handleTorrentShareLimitChanged(TorrentImpl *torrent);
@ -467,14 +477,15 @@ namespace BitTorrent
void handleTorrentTrackersChanged(TorrentImpl *torrent); void handleTorrentTrackersChanged(TorrentImpl *torrent);
void handleTorrentUrlSeedsAdded(TorrentImpl *torrent, const QList<QUrl> &newUrlSeeds); void handleTorrentUrlSeedsAdded(TorrentImpl *torrent, const QList<QUrl> &newUrlSeeds);
void handleTorrentUrlSeedsRemoved(TorrentImpl *torrent, const QList<QUrl> &urlSeeds); void handleTorrentUrlSeedsRemoved(TorrentImpl *torrent, const QList<QUrl> &urlSeeds);
void handleTorrentResumeDataReady(TorrentImpl *torrent, const LoadTorrentParams &data); void handleTorrentResumeDataReady(TorrentImpl *torrent, LoadTorrentParams data);
void handleTorrentInfoHashChanged(TorrentImpl *torrent, const InfoHash &prevInfoHash); void handleTorrentInfoHashChanged(TorrentImpl *torrent, const InfoHash &prevInfoHash);
void handleTorrentStorageMovingStateChanged(TorrentImpl *torrent); void handleTorrentStorageMovingStateChanged(TorrentImpl *torrent);
bool addMoveTorrentStorageJob(TorrentImpl *torrent, const Path &newPath, MoveStorageMode mode, MoveStorageContext context); bool addMoveTorrentStorageJob(TorrentImpl *torrent, const Path &newPath, MoveStorageMode mode, MoveStorageContext context);
void findIncompleteFiles(const TorrentInfo &torrentInfo, const Path &savePath lt::torrent_handle reloadTorrent(const lt::torrent_handle &currentHandle, lt::add_torrent_params params);
, const Path &downloadPath, const PathList &filePaths = {}) const;
QFuture<FileSearchResult> findIncompleteFiles(const Path &savePath, const Path &downloadPath, const PathList &filePaths = {}) const;
void enablePortMapping(); void enablePortMapping();
void disablePortMapping(); void disablePortMapping();
@ -509,7 +520,6 @@ namespace BitTorrent
void generateResumeData(); void generateResumeData();
void handleIPFilterParsed(int ruleCount); void handleIPFilterParsed(int ruleCount);
void handleIPFilterError(); void handleIPFilterError();
void fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames);
void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage); void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage);
private: private:
@ -568,8 +578,7 @@ namespace BitTorrent
void updateSeedingLimitTimer(); void updateSeedingLimitTimer();
void exportTorrentFile(const Torrent *torrent, const Path &folderPath); void exportTorrentFile(const Torrent *torrent, const Path &folderPath);
void handleAlert(const lt::alert *alert); void handleAlert(lt::alert *alert);
void dispatchTorrentAlert(const lt::torrent_alert *alert);
void handleAddTorrentAlert(const lt::add_torrent_alert *alert); void handleAddTorrentAlert(const lt::add_torrent_alert *alert);
void handleStateUpdateAlert(const lt::state_update_alert *alert); void handleStateUpdateAlert(const lt::state_update_alert *alert);
void handleMetadataReceivedAlert(const lt::metadata_received_alert *alert); void handleMetadataReceivedAlert(const lt::metadata_received_alert *alert);
@ -596,9 +605,20 @@ namespace BitTorrent
void handleTrackerAlert(const lt::tracker_alert *alert); void handleTrackerAlert(const lt::tracker_alert *alert);
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
void handleTorrentConflictAlert(const lt::torrent_conflict_alert *alert); void handleTorrentConflictAlert(const lt::torrent_conflict_alert *alert);
void handleFilePrioAlert(const lt::file_prio_alert *alert);
#endif #endif
void handleFastResumeRejectedAlert(const lt::fastresume_rejected_alert *alert);
void handleFileCompletedAlert(const lt::file_completed_alert *alert);
void handleFileRenamedAlert(const lt::file_renamed_alert *alert);
void handleFileRenameFailedAlert(const lt::file_rename_failed_alert *alert);
void handlePerformanceAlert(const lt::performance_alert *alert) const;
void handleSaveResumeDataAlert(lt::save_resume_data_alert *alert);
void handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *alert);
void handleTorrentCheckedAlert(const lt::torrent_checked_alert *alert);
void handleTorrentFinishedAlert(const lt::torrent_finished_alert *alert);
TorrentImpl *createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params); TorrentImpl *createTorrent(const lt::torrent_handle &nativeHandle, LoadTorrentParams params);
TorrentImpl *getTorrent(const lt::torrent_handle &nativeHandle) const;
void saveResumeData(); void saveResumeData();
void saveTorrentsQueue(); void saveTorrentsQueue();
@ -683,6 +703,7 @@ namespace BitTorrent
CachedSettingValue<BTProtocol> m_btProtocol; CachedSettingValue<BTProtocol> m_btProtocol;
CachedSettingValue<bool> m_isUTPRateLimited; CachedSettingValue<bool> m_isUTPRateLimited;
CachedSettingValue<MixedModeAlgorithm> m_utpMixedMode; CachedSettingValue<MixedModeAlgorithm> m_utpMixedMode;
CachedSettingValue<int> m_hostnameCacheTTL;
CachedSettingValue<bool> m_IDNSupportEnabled; CachedSettingValue<bool> m_IDNSupportEnabled;
CachedSettingValue<bool> m_multiConnectionsPerIpEnabled; CachedSettingValue<bool> m_multiConnectionsPerIpEnabled;
CachedSettingValue<bool> m_validateHTTPSTrackerCertificate; CachedSettingValue<bool> m_validateHTTPSTrackerCertificate;
@ -804,11 +825,13 @@ namespace BitTorrent
FileSearcher *m_fileSearcher = nullptr; FileSearcher *m_fileSearcher = nullptr;
TorrentContentRemover *m_torrentContentRemover = 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, lt::torrent_handle> m_downloadedMetadata;
QHash<TorrentID, TorrentImpl *> m_torrents; QHash<TorrentID, TorrentImpl *> m_torrents;
QHash<TorrentID, TorrentImpl *> m_hybridTorrentsByAltID; QHash<TorrentID, TorrentImpl *> m_hybridTorrentsByAltID;
QHash<TorrentID, LoadTorrentParams> m_loadingTorrents;
QHash<TorrentID, RemovingTorrentData> m_removingTorrents; QHash<TorrentID, RemovingTorrentData> m_removingTorrents;
QHash<TorrentID, TorrentID> m_changedTorrentIDs; QHash<TorrentID, TorrentID> m_changedTorrentIDs;
QMap<QString, CategoryOptions> m_categories; QMap<QString, CategoryOptions> m_categories;
@ -850,6 +873,10 @@ namespace BitTorrent
QList<TorrentImpl *> m_pendingFinishedTorrents; QList<TorrentImpl *> m_pendingFinishedTorrents;
FreeDiskSpaceChecker *m_freeDiskSpaceChecker = nullptr;
QTimer *m_freeDiskSpaceCheckingTimer = nullptr;
qint64 m_freeDiskSpace = -1;
friend void Session::initInstance(); friend void Session::initInstance();
friend void Session::freeInstance(); friend void Session::freeInstance();
friend Session *Session::instance(); friend Session *Session::instance();

View file

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

View file

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

View file

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

View file

@ -141,6 +141,22 @@ catch (const lt::system_error &err)
return nonstd::make_unexpected(QString::fromLocal8Bit(err.what())); 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) BitTorrent::TorrentDescriptor::TorrentDescriptor(lt::add_torrent_params ltAddTorrentParams)
: m_ltAddTorrentParams {std::move(ltAddTorrentParams)} : m_ltAddTorrentParams {std::move(ltAddTorrentParams)}
{ {

View file

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

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -37,11 +37,10 @@
#endif #endif
#include <libtorrent/address.hpp> #include <libtorrent/address.hpp>
#include <libtorrent/alert_types.hpp>
#include <libtorrent/create_torrent.hpp>
#include <libtorrent/session.hpp> #include <libtorrent/session.hpp>
#include <libtorrent/storage_defs.hpp> #include <libtorrent/storage_defs.hpp>
#include <libtorrent/time.hpp> #include <libtorrent/time.hpp>
#include <libtorrent/write_resume_data.hpp>
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
#include <libtorrent/info_hash.hpp> #include <libtorrent/info_hash.hpp>
@ -51,7 +50,9 @@
#include <QByteArray> #include <QByteArray>
#include <QCache> #include <QCache>
#include <QDebug> #include <QDebug>
#include <QFuture>
#include <QPointer> #include <QPointer>
#include <QPromise>
#include <QSet> #include <QSet>
#include <QStringList> #include <QStringList>
#include <QUrl> #include <QUrl>
@ -67,6 +68,7 @@
#include "common.h" #include "common.h"
#include "downloadpriority.h" #include "downloadpriority.h"
#include "extensiondata.h" #include "extensiondata.h"
#include "filesearcher.h"
#include "loadtorrentparams.h" #include "loadtorrentparams.h"
#include "ltqbitarray.h" #include "ltqbitarray.h"
#include "lttypecast.h" #include "lttypecast.h"
@ -149,35 +151,40 @@ namespace
if (ltAnnounceInfo.updating) if (ltAnnounceInfo.updating)
{ {
trackerEndpointStatus.state = TrackerEndpointState::Updating; trackerEndpointStatus.isUpdating = true;
++numUpdating; ++numUpdating;
} }
else if (ltAnnounceInfo.fails > 0)
{
if (ltAnnounceInfo.last_error == lt::errors::tracker_failure)
{
trackerEndpointStatus.state = TrackerEndpointState::TrackerError;
++numTrackerError;
}
else if (ltAnnounceInfo.last_error == lt::errors::announce_skipped)
{
trackerEndpointStatus.state = TrackerEndpointState::Unreachable;
++numUnreachable;
}
else
{
trackerEndpointStatus.state = TrackerEndpointState::NotWorking;
++numNotWorking;
}
}
else if (nativeEntry.verified)
{
trackerEndpointStatus.state = TrackerEndpointState::Working;
++numWorking;
}
else else
{ {
trackerEndpointStatus.state = TrackerEndpointState::NotContacted; trackerEndpointStatus.isUpdating = false;
if (ltAnnounceInfo.fails > 0)
{
if (ltAnnounceInfo.last_error == lt::errors::tracker_failure)
{
trackerEndpointStatus.state = TrackerEndpointState::TrackerError;
++numTrackerError;
}
else if (ltAnnounceInfo.last_error == lt::errors::announce_skipped)
{
trackerEndpointStatus.state = TrackerEndpointState::Unreachable;
++numUnreachable;
}
else
{
trackerEndpointStatus.state = TrackerEndpointState::NotWorking;
++numNotWorking;
}
}
else if (nativeEntry.verified)
{
trackerEndpointStatus.state = TrackerEndpointState::Working;
++numWorking;
}
else
{
trackerEndpointStatus.state = TrackerEndpointState::NotContacted;
}
} }
if (!ltAnnounceInfo.message.empty()) if (!ltAnnounceInfo.message.empty())
@ -212,23 +219,28 @@ namespace
{ {
if (numUpdating > 0) if (numUpdating > 0)
{ {
trackerEntryStatus.state = TrackerEndpointState::Updating; trackerEntryStatus.isUpdating = true;
} }
else if (numWorking > 0) else
{ {
trackerEntryStatus.state = TrackerEndpointState::Working; trackerEntryStatus.isUpdating = false;
}
else if (numTrackerError > 0) if (numWorking > 0)
{ {
trackerEntryStatus.state = TrackerEndpointState::TrackerError; trackerEntryStatus.state = TrackerEndpointState::Working;
} }
else if (numUnreachable == numEndpoints) else if (numTrackerError > 0)
{ {
trackerEntryStatus.state = TrackerEndpointState::Unreachable; trackerEntryStatus.state = TrackerEndpointState::TrackerError;
} }
else if ((numUnreachable + numNotWorking) == numEndpoints) else if (numUnreachable == numEndpoints)
{ {
trackerEntryStatus.state = TrackerEndpointState::NotWorking; trackerEntryStatus.state = TrackerEndpointState::Unreachable;
}
else if ((numUnreachable + numNotWorking) == numEndpoints)
{
trackerEntryStatus.state = TrackerEndpointState::NotWorking;
}
} }
} }
@ -287,11 +299,9 @@ namespace
// TorrentImpl // TorrentImpl
TorrentImpl::TorrentImpl(SessionImpl *session, lt::session *nativeSession TorrentImpl::TorrentImpl(SessionImpl *session, const lt::torrent_handle &nativeHandle, LoadTorrentParams params)
, const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params)
: Torrent(session) : Torrent(session)
, m_session(session) , m_session(session)
, m_nativeSession(nativeSession)
, m_nativeHandle(nativeHandle) , m_nativeHandle(nativeHandle)
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
, m_infoHash(m_nativeHandle.info_hashes()) , m_infoHash(m_nativeHandle.info_hashes())
@ -314,7 +324,7 @@ TorrentImpl::TorrentImpl(SessionImpl *session, lt::session *nativeSession
, m_useAutoTMM(params.useAutoTMM) , m_useAutoTMM(params.useAutoTMM)
, m_isStopped(params.stopped) , m_isStopped(params.stopped)
, m_sslParams(params.sslParameters) , m_sslParams(params.sslParameters)
, m_ltAddTorrentParams(params.ltAddTorrentParams) , m_ltAddTorrentParams(std::move(params.ltAddTorrentParams))
, m_downloadLimit(cleanLimitValue(m_ltAddTorrentParams.download_limit)) , m_downloadLimit(cleanLimitValue(m_ltAddTorrentParams.download_limit))
, m_uploadLimit(cleanLimitValue(m_ltAddTorrentParams.upload_limit)) , m_uploadLimit(cleanLimitValue(m_ltAddTorrentParams.upload_limit))
{ {
@ -1437,7 +1447,7 @@ int TorrentImpl::totalLeechersCount() const
int TorrentImpl::downloadLimit() const int TorrentImpl::downloadLimit() const
{ {
return m_downloadLimit;; return m_downloadLimit;
} }
int TorrentImpl::uploadLimit() const int TorrentImpl::uploadLimit() const
@ -1465,48 +1475,11 @@ bool TorrentImpl::isLSDDisabled() const
return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::disable_lsd); 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 QBitArray TorrentImpl::pieces() const
{ {
return m_pieces; 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 qreal TorrentImpl::distributedCopies() const
{ {
return m_nativeStatus.distributed_copies; return m_nativeStatus.distributed_copies;
@ -1752,12 +1725,6 @@ void TorrentImpl::applyFirstLastPiecePriority(const bool enabled)
m_nativeHandle.prioritize_pieces(piecePriorities); 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) 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() const auto it = std::find_if(m_trackerEntryStatuses.begin(), m_trackerEntryStatuses.end()
@ -1868,7 +1835,7 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi
applyFirstLastPiecePriority(true); applyFirstLastPiecePriority(true);
m_maintenanceJob = MaintenanceJob::None; m_maintenanceJob = MaintenanceJob::None;
prepareResumeData(p); prepareResumeData(std::move(p));
m_session->handleTorrentMetadataReceived(this); m_session->handleTorrentMetadataReceived(this);
} }
@ -1877,16 +1844,6 @@ void TorrentImpl::reload()
{ {
try 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; lt::add_torrent_params p = m_ltAddTorrentParams;
p.flags |= lt::torrent_flags::update_subscribe p.flags |= lt::torrent_flags::update_subscribe
| lt::torrent_flags::override_trackers | lt::torrent_flags::override_trackers
@ -1906,19 +1863,21 @@ void TorrentImpl::reload()
p.flags &= ~(lt::torrent_flags::auto_managed | lt::torrent_flags::paused); p.flags &= ~(lt::torrent_flags::auto_managed | lt::torrent_flags::paused);
} }
auto *const extensionData = new ExtensionData; const auto queuePos = m_nativeHandle.queue_position();
p.userdata = LTClientData(extensionData);
#ifndef QBT_USES_LIBTORRENT2
p.storage = customStorageConstructor;
#endif
m_nativeHandle = m_nativeSession->add_torrent(p);
m_nativeStatus = extensionData->status; m_nativeHandle = m_session->reloadTorrent(m_nativeHandle, std::move(p));
m_nativeStatus = static_cast<ExtensionData *>(m_nativeHandle.userdata())->status;
if (queuePos >= lt::queue_position_t {}) if (queuePos >= lt::queue_position_t {})
m_nativeHandle.queue_position_set(queuePos); m_nativeHandle.queue_position_set(queuePos);
m_nativeStatus.queue_position = 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(); updateState();
} }
catch (const lt::system_error &err) catch (const lt::system_error &err)
@ -2063,7 +2022,7 @@ void TorrentImpl::handleMoveStorageJobFinished(const Path &path, const MoveStora
} }
} }
void TorrentImpl::handleTorrentCheckedAlert([[maybe_unused]] const lt::torrent_checked_alert *p) void TorrentImpl::handleTorrentChecked()
{ {
if (!hasMetadata()) if (!hasMetadata())
{ {
@ -2106,7 +2065,7 @@ void TorrentImpl::handleTorrentCheckedAlert([[maybe_unused]] const lt::torrent_c
}); });
} }
void TorrentImpl::handleTorrentFinishedAlert([[maybe_unused]] const lt::torrent_finished_alert *p) void TorrentImpl::handleTorrentFinished()
{ {
m_hasMissingFiles = false; m_hasMissingFiles = false;
if (m_hasFinishedStatus) if (m_hasFinishedStatus)
@ -2129,37 +2088,29 @@ void TorrentImpl::handleTorrentFinishedAlert([[maybe_unused]] const lt::torrent_
m_hasFinishedStatus = true; m_hasFinishedStatus = true;
if (isMoveInProgress() || (m_renameCount > 0)) if (isMoveInProgress() || (m_renameCount > 0))
m_moveFinishedTriggers.enqueue([this]() { m_session->handleTorrentFinished(this); }); m_moveFinishedTriggers.enqueue([this] { m_session->handleTorrentFinished(this); });
else else
m_session->handleTorrentFinished(this); m_session->handleTorrentFinished(this);
} }
}); });
} }
void TorrentImpl::handleTorrentPausedAlert([[maybe_unused]] const lt::torrent_paused_alert *p) void TorrentImpl::handleSaveResumeData(lt::add_torrent_params params)
{ {
} 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. // 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 // 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. // than the list we usually cache, so we have to request it from the appropriate source.
fetchURLSeeds([this](const QList<QUrl> &urlSeeds) { m_urlSeeds = urlSeeds; }); fetchURLSeeds().then(this, [this](const QList<QUrl> &urlSeeds) { m_urlSeeds = urlSeeds; });
} }
if ((m_maintenanceJob == MaintenanceJob::HandleMetadata) && p->params.ti) if ((m_maintenanceJob == MaintenanceJob::HandleMetadata) && params.ti)
{ {
Q_ASSERT(m_indexMap.isEmpty()); Q_ASSERT(m_indexMap.isEmpty());
const auto isSeedMode = static_cast<bool>(m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode); const auto isSeedMode = static_cast<bool>(m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode);
m_ltAddTorrentParams = p->params; m_ltAddTorrentParams = std::move(params);
if (isSeedMode) if (isSeedMode)
m_ltAddTorrentParams.flags |= lt::torrent_flags::seed_mode; m_ltAddTorrentParams.flags |= lt::torrent_flags::seed_mode;
@ -2198,15 +2149,20 @@ void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p)
filePaths[i] = Path(it->second); filePaths[i] = Path(it->second);
} }
m_session->findIncompleteFiles(metadata, savePath(), downloadPath(), filePaths); m_session->findIncompleteFiles(savePath(), downloadPath(), filePaths).then(this
, [this](const FileSearchResult &result)
{
if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
endReceivedMetadataHandling(result.savePath, result.fileNames);
});
} }
else else
{ {
prepareResumeData(p->params); prepareResumeData(std::move(params));
} }
} }
void TorrentImpl::prepareResumeData(const lt::add_torrent_params &params) void TorrentImpl::prepareResumeData(lt::add_torrent_params params)
{ {
{ {
decltype(params.have_pieces) havePieces; decltype(params.have_pieces) havePieces;
@ -2230,7 +2186,7 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params &params)
} }
// Update recent resume data // Update recent resume data
m_ltAddTorrentParams = params; m_ltAddTorrentParams = std::move(params);
if (needPreserveProgress) if (needPreserveProgress)
{ {
@ -2246,7 +2202,7 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params &params)
// We shouldn't save upload_mode flag to allow torrent operate normally on next run // We shouldn't save upload_mode flag to allow torrent operate normally on next run
m_ltAddTorrentParams.flags &= ~lt::torrent_flags::upload_mode; m_ltAddTorrentParams.flags &= ~lt::torrent_flags::upload_mode;
const LoadTorrentParams resumeData LoadTorrentParams resumeData
{ {
.ltAddTorrentParams = m_ltAddTorrentParams, .ltAddTorrentParams = m_ltAddTorrentParams,
.name = m_name, .name = m_name,
@ -2269,33 +2225,20 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params &params)
.sslParameters = m_sslParams .sslParameters = m_sslParams
}; };
m_session->handleTorrentResumeDataReady(this, resumeData); m_session->handleTorrentResumeDataReady(this, std::move(resumeData));
} }
void TorrentImpl::handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *p) void TorrentImpl::handleFastResumeRejected()
{
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 // Files were probably moved or storage isn't accessible
m_hasMissingFiles = true; m_hasMissingFiles = true;
LogMsg(tr("Failed to restore torrent. Files were probably moved or storage isn't accessible. Torrent: \"%1\". Reason: \"%2\"")
.arg(name(), QString::fromStdString(p->message())), Log::WARNING);
} }
void TorrentImpl::handleFileRenamedAlert(const lt::file_renamed_alert *p) void TorrentImpl::handleFileRenamed(const lt::file_index_t nativeFileIndex, const Path &newActualFilePath, const Path &oldActualFilePath)
{ {
const int fileIndex = m_indexMap.value(p->index, -1); const int fileIndex = fileIndexFromNative(nativeFileIndex);
Q_ASSERT(fileIndex >= 0); Q_ASSERT(fileIndex >= 0);
const Path newActualFilePath {QString::fromUtf8(p->new_name())};
const Path oldFilePath = m_filePaths.at(fileIndex); const Path oldFilePath = m_filePaths.at(fileIndex);
const Path newFilePath = makeUserPath(newActualFilePath); const Path newFilePath = makeUserPath(newActualFilePath);
@ -2305,11 +2248,6 @@ void TorrentImpl::handleFileRenamedAlert(const lt::file_renamed_alert *p)
if (oldFilePath.data() == newFilePath.data()) if (oldFilePath.data() == newFilePath.data())
{ {
// Remove empty ".unwanted" folders // 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 oldActualParentPath = oldActualFilePath.parentPath();
const Path newActualParentPath = newActualFilePath.parentPath(); const Path newActualParentPath = newActualFilePath.parentPath();
if (newActualParentPath.filename() == UNWANTED_FOLDER_NAME) if (newActualParentPath.filename() == UNWANTED_FOLDER_NAME)
@ -2359,14 +2297,11 @@ void TorrentImpl::handleFileRenamedAlert(const lt::file_renamed_alert *p)
deferredRequestResumeData(); deferredRequestResumeData();
} }
void TorrentImpl::handleFileRenameFailedAlert(const lt::file_rename_failed_alert *p) void TorrentImpl::handleFileRenameFailed(const lt::file_index_t nativeFileIndex)
{ {
const int fileIndex = m_indexMap.value(p->index, -1); const int fileIndex = fileIndexFromNative(nativeFileIndex);
Q_ASSERT(fileIndex >= 0); 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; --m_renameCount;
while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty())
m_moveFinishedTriggers.takeFirst()(); m_moveFinishedTriggers.takeFirst()();
@ -2374,12 +2309,12 @@ void TorrentImpl::handleFileRenameFailedAlert(const lt::file_rename_failed_alert
deferredRequestResumeData(); deferredRequestResumeData();
} }
void TorrentImpl::handleFileCompletedAlert(const lt::file_completed_alert *p) void TorrentImpl::handleFileCompleted(const lt::file_index_t nativeFileIndex)
{ {
if (m_maintenanceJob == MaintenanceJob::HandleMetadata) if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
return; return;
const int fileIndex = m_indexMap.value(p->index, -1); const int fileIndex = fileIndexFromNative(nativeFileIndex);
Q_ASSERT(fileIndex >= 0); Q_ASSERT(fileIndex >= 0);
m_completedFiles.setBit(fileIndex); m_completedFiles.setBit(fileIndex);
@ -2407,22 +2342,13 @@ void TorrentImpl::handleFileCompletedAlert(const lt::file_completed_alert *p)
} }
} }
void TorrentImpl::handleFileErrorAlert(const lt::file_error_alert *p) void TorrentImpl::handleFileError(FileErrorInfo fileError)
{ {
m_lastFileError = {p->error, p->op}; m_lastFileError = std::move(fileError);
} }
#ifdef QBT_USES_LIBTORRENT2 void TorrentImpl::handleMetadataReceived()
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 #ifdef QBT_USES_LIBTORRENT2
const InfoHash prevInfoHash = infoHash(); const InfoHash prevInfoHash = infoHash();
m_infoHash = InfoHash(m_nativeHandle.info_hashes()); m_infoHash = InfoHash(m_nativeHandle.info_hashes());
@ -2434,12 +2360,6 @@ void TorrentImpl::handleMetadataReceivedAlert([[maybe_unused]] const lt::metadat
deferredRequestResumeData(); 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() void TorrentImpl::handleCategoryOptionsChanged()
{ {
if (m_useAutoTMM) if (m_useAutoTMM)
@ -2462,57 +2382,6 @@ void TorrentImpl::handleUnwantedFolderToggled()
manageActualFilePaths(); 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() void TorrentImpl::manageActualFilePaths()
{ {
const std::shared_ptr<const lt::torrent_info> nativeInfo = nativeTorrentInfo(); const std::shared_ptr<const lt::torrent_info> nativeInfo = nativeTorrentInfo();
@ -2560,6 +2429,11 @@ lt::torrent_handle TorrentImpl::nativeHandle() const
return m_nativeHandle; 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) void TorrentImpl::setMetadata(const TorrentInfo &torrentInfo)
{ {
if (hasMetadata()) if (hasMetadata())
@ -2879,18 +2753,9 @@ nonstd::expected<lt::entry, QString> TorrentImpl::exportTorrent() const
try try
{ {
#ifdef QBT_USES_LIBTORRENT2 [[maybe_unused]] const auto infoGuard = qScopeGuard([this] { m_ltAddTorrentParams.ti.reset(); });
const std::shared_ptr<lt::torrent_info> completeTorrentInfo = m_nativeHandle.torrent_file_with_hashes(); m_ltAddTorrentParams.ti = info().nativeInfo();
const std::shared_ptr<lt::torrent_info> torrentInfo = (completeTorrentInfo ? completeTorrentInfo : info().nativeInfo()); return lt::write_torrent_file(m_ltAddTorrentParams);
#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) catch (const lt::system_error &err)
{ {
@ -2925,9 +2790,9 @@ nonstd::expected<void, QString> TorrentImpl::exportToFile(const Path &path) cons
return {}; return {};
} }
void TorrentImpl::fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHandler) const QFuture<QList<PeerInfo>> TorrentImpl::fetchPeerInfo() const
{ {
invokeAsync([nativeHandle = m_nativeHandle, allPieces = pieces()]() -> QList<PeerInfo> return invokeAsync([nativeHandle = m_nativeHandle, allPieces = pieces()]() -> QList<PeerInfo>
{ {
try try
{ {
@ -2942,13 +2807,12 @@ void TorrentImpl::fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHand
catch (const std::exception &) {} catch (const std::exception &) {}
return {}; return {};
} });
, std::move(resultHandler));
} }
void TorrentImpl::fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler) const QFuture<QList<QUrl>> TorrentImpl::fetchURLSeeds() const
{ {
invokeAsync([nativeHandle = m_nativeHandle]() -> QList<QUrl> return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<QUrl>
{ {
try try
{ {
@ -2962,13 +2826,12 @@ void TorrentImpl::fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler)
catch (const std::exception &) {} catch (const std::exception &) {}
return {}; return {};
} });
, std::move(resultHandler));
} }
void TorrentImpl::fetchPieceAvailability(std::function<void (QList<int>)> resultHandler) const QFuture<QList<int>> TorrentImpl::fetchPieceAvailability() const
{ {
invokeAsync([nativeHandle = m_nativeHandle]() -> QList<int> return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<int>
{ {
try try
{ {
@ -2979,13 +2842,12 @@ void TorrentImpl::fetchPieceAvailability(std::function<void (QList<int>)> result
catch (const std::exception &) {} catch (const std::exception &) {}
return {}; return {};
} });
, std::move(resultHandler));
} }
void TorrentImpl::fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const QFuture<QBitArray> TorrentImpl::fetchDownloadingPieces() const
{ {
invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QBitArray return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QBitArray
{ {
try try
{ {
@ -3004,13 +2866,12 @@ void TorrentImpl::fetchDownloadingPieces(std::function<void (QBitArray)> resultH
catch (const std::exception &) {} catch (const std::exception &) {}
return {}; return {};
} });
, std::move(resultHandler));
} }
void TorrentImpl::fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const QFuture<QList<qreal>> TorrentImpl::fetchAvailableFileFractions() const
{ {
invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QList<qreal> return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QList<qreal>
{ {
if (!torrentInfo.isValid() || (torrentInfo.filesCount() <= 0)) if (!torrentInfo.isValid() || (torrentInfo.filesCount() <= 0))
return {}; return {};
@ -3044,8 +2905,7 @@ void TorrentImpl::fetchAvailableFileFractions(std::function<void (QList<qreal>)>
catch (const std::exception &) {} catch (const std::exception &) {}
return {}; return {};
} });
, std::move(resultHandler));
} }
void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities) void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
@ -3056,7 +2916,7 @@ void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
Q_ASSERT(priorities.size() == filesCount()); Q_ASSERT(priorities.size() == filesCount());
// Reset 'm_hasSeedStatus' if needed in order to react again to // Reset 'm_hasSeedStatus' if needed in order to react again to
// 'torrent_finished_alert' and eg show tray notifications // "torrent finished" event and e.g. show tray notifications
const QList<DownloadPriority> oldPriorities = filePriorities(); const QList<DownloadPriority> oldPriorities = filePriorities();
for (int i = 0; i < oldPriorities.size(); ++i) for (int i = 0; i < oldPriorities.size(); ++i)
{ {
@ -3085,47 +2945,17 @@ void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
manageActualFilePaths(); manageActualFilePaths();
} }
QList<qreal> TorrentImpl::availableFileFractions() const template <typename Func>
QFuture<std::invoke_result_t<Func>> TorrentImpl::invokeAsync(Func &&func) const
{ {
Q_ASSERT(hasMetadata()); QPromise<std::invoke_result_t<Func>> promise;
const auto future = promise.future();
const int filesCount = this->filesCount(); promise.start();
if (filesCount <= 0) return {}; m_session->invokeAsync([func = std::forward<Func>(func), promise = std::move(promise)]() mutable
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); promise.addResult(func());
promise.finish();
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);
});
}); });
return future;
} }

View file

@ -94,8 +94,7 @@ namespace BitTorrent
Q_DISABLE_COPY_MOVE(TorrentImpl) Q_DISABLE_COPY_MOVE(TorrentImpl)
public: public:
TorrentImpl(SessionImpl *session, lt::session *nativeSession TorrentImpl(SessionImpl *session, const lt::torrent_handle &nativeHandle, LoadTorrentParams params);
, const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params);
~TorrentImpl() override; ~TorrentImpl() override;
bool isValid() const; bool isValid() const;
@ -203,10 +202,7 @@ namespace BitTorrent
bool isDHTDisabled() const override; bool isDHTDisabled() const override;
bool isPEXDisabled() const override; bool isPEXDisabled() const override;
bool isLSDDisabled() const override; bool isLSDDisabled() const override;
QList<PeerInfo> peers() const override;
QBitArray pieces() const override; QBitArray pieces() const override;
QBitArray downloadingPieces() const override;
QList<int> pieceAvailability() const override;
qreal distributedCopies() const override; qreal distributedCopies() const override;
qreal maxRatio() const override; qreal maxRatio() const override;
int maxSeedingTime() const override; int maxSeedingTime() const override;
@ -220,7 +216,6 @@ namespace BitTorrent
int connectionsCount() const override; int connectionsCount() const override;
int connectionsLimit() const override; int connectionsLimit() const override;
qlonglong nextAnnounce() const override; qlonglong nextAnnounce() const override;
QList<qreal> availableFileFractions() const override;
void setName(const QString &name) override; void setName(const QString &name) override;
void setSequentialDownload(bool enable) override; void setSequentialDownload(bool enable) override;
@ -258,19 +253,29 @@ namespace BitTorrent
nonstd::expected<QByteArray, QString> exportToBuffer() const override; nonstd::expected<QByteArray, QString> exportToBuffer() const override;
nonstd::expected<void, QString> exportToFile(const Path &path) const override; nonstd::expected<void, QString> exportToFile(const Path &path) const override;
void fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHandler) const override; QFuture<QList<PeerInfo>> fetchPeerInfo() const override;
void fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler) const override; QFuture<QList<QUrl>> fetchURLSeeds() const override;
void fetchPieceAvailability(std::function<void (QList<int>)> resultHandler) const override; QFuture<QList<int>> fetchPieceAvailability() const override;
void fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const override; QFuture<QBitArray> fetchDownloadingPieces() const override;
void fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const override; QFuture<QList<qreal>> fetchAvailableFileFractions() const override;
bool needSaveResumeData() const; bool needSaveResumeData() const;
// Session interface // Session interface
lt::torrent_handle nativeHandle() const; lt::torrent_handle nativeHandle() const;
void handleAlert(const lt::alert *a); int fileIndexFromNative(lt::file_index_t nativeFileIndex) const;
void handleStateUpdate(const lt::torrent_status &nativeStatus); void 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 handleQueueingModeChanged();
void handleCategoryOptionsChanged(); void handleCategoryOptionsChanged();
void handleAppendExtensionToggled(); void handleAppendExtensionToggled();
@ -278,7 +283,6 @@ namespace BitTorrent
void requestResumeData(lt::resume_data_flags_t flags = {}); void requestResumeData(lt::resume_data_flags_t flags = {});
void deferredRequestResumeData(); void deferredRequestResumeData();
void handleMoveStorageJobFinished(const Path &path, MoveStorageContext context, bool hasOutstandingJob); 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); TrackerEntryStatus updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo);
void resetTrackerEntryStatuses(); void resetTrackerEntryStatuses();
@ -291,23 +295,6 @@ namespace BitTorrent
void updateProgress(); void updateProgress();
void updateState(); 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; bool isMoveInProgress() const;
void setAutoManaged(bool enable); void setAutoManaged(bool enable);
@ -320,17 +307,16 @@ namespace BitTorrent
void manageActualFilePaths(); void manageActualFilePaths();
void applyFirstLastPiecePriority(bool enabled); void applyFirstLastPiecePriority(bool enabled);
void prepareResumeData(const lt::add_torrent_params &params); void prepareResumeData(lt::add_torrent_params resumeData);
void endReceivedMetadataHandling(const Path &savePath, const PathList &fileNames); void endReceivedMetadataHandling(const Path &savePath, const PathList &fileNames);
void reload(); void reload();
nonstd::expected<lt::entry, QString> exportTorrent() const; nonstd::expected<lt::entry, QString> exportTorrent() const;
template <typename Func, typename Callback> template <typename Func>
void invokeAsync(Func func, Callback resultHandler) const; QFuture<std::invoke_result_t<Func>> invokeAsync(Func &&func) const;
SessionImpl *const m_session = nullptr; SessionImpl *const m_session = nullptr;
lt::session *m_nativeSession = nullptr;
lt::torrent_handle m_nativeHandle; lt::torrent_handle m_nativeHandle;
mutable lt::torrent_status m_nativeStatus; mutable lt::torrent_status m_nativeStatus;
TorrentState m_state = TorrentState::Unknown; TorrentState m_state = TorrentState::Unknown;
@ -387,7 +373,7 @@ namespace BitTorrent
bool m_unchecked = false; bool m_unchecked = false;
lt::add_torrent_params m_ltAddTorrentParams; mutable lt::add_torrent_params m_ltAddTorrentParams;
int m_downloadLimit = 0; int m_downloadLimit = 0;
int m_uploadLimit = 0; int m_uploadLimit = 0;

View file

@ -380,7 +380,7 @@ void Tracker::registerPeer(const TrackerAnnounceRequest &announceReq)
{ {
// Reached max size, remove a random torrent // Reached max size, remove a random torrent
if (m_torrents.size() >= MAX_TORRENTS) if (m_torrents.size() >= MAX_TORRENTS)
m_torrents.erase(m_torrents.begin()); m_torrents.erase(m_torrents.cbegin());
} }
m_torrents[announceReq.torrentID].setPeer(announceReq.peer); m_torrents[announceReq.torrentID].setPeer(announceReq.peer);

View file

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

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com> * Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -29,16 +29,24 @@
#include "freediskspacechecker.h" #include "freediskspacechecker.h"
#include "base/bittorrent/session.h"
#include "base/utils/fs.h" #include "base/utils/fs.h"
qint64 FreeDiskSpaceChecker::lastResult() const FreeDiskSpaceChecker::FreeDiskSpaceChecker(const Path &pathToCheck)
: m_pathToCheck {pathToCheck}
{ {
return m_lastResult; }
Path FreeDiskSpaceChecker::pathToCheck() const
{
return m_pathToCheck;
}
void FreeDiskSpaceChecker::setPathToCheck(const Path &newPathToCheck)
{
m_pathToCheck = newPathToCheck;
} }
void FreeDiskSpaceChecker::check() void FreeDiskSpaceChecker::check()
{ {
m_lastResult = Utils::Fs::freeDiskSpaceOnPath(BitTorrent::Session::instance()->savePath()); emit checked(Utils::Fs::freeDiskSpaceOnPath(m_pathToCheck));
emit checked(m_lastResult);
} }

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com> * Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -31,15 +31,18 @@
#include <QObject> #include <QObject>
#include "base/path.h"
class FreeDiskSpaceChecker final : public QObject class FreeDiskSpaceChecker final : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_DISABLE_COPY_MOVE(FreeDiskSpaceChecker) Q_DISABLE_COPY_MOVE(FreeDiskSpaceChecker)
public: public:
using QObject::QObject; FreeDiskSpaceChecker(const Path &pathToCheck);
qint64 lastResult() const; Path pathToCheck() const;
void setPathToCheck(const Path &newPathToCheck);
public slots: public slots:
void check(); void check();
@ -48,5 +51,5 @@ signals:
void checked(qint64 freeSpaceSize); void checked(qint64 freeSpaceSize);
private: private:
qint64 m_lastResult = 0; Path m_pathToCheck;
}; };

View file

@ -155,7 +155,11 @@ void Connection::read()
sendResponse(resp); sendResponse(resp);
} }
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
m_receivedData.slice(result.frameSize);
#else
m_receivedData.remove(0, result.frameSize); m_receivedData.remove(0, result.frameSize);
#endif
} }
break; break;

View file

@ -69,8 +69,8 @@ namespace
return false; return false;
} }
const QString name = line.left(i).trimmed().toString().toLower(); const QString name = line.first(i).trimmed().toString().toLower();
const QString value = line.mid(i + 1).trimmed().toString(); const QString value = line.sliced(i + 1).trimmed().toString();
out[name] = value; out[name] = value;
return true; return true;
@ -204,15 +204,15 @@ bool RequestParser::parseRequestLine(const QString &line)
m_request.method = match.captured(1); m_request.method = match.captured(1);
// Request Target // Request Target
const QByteArray url {match.captured(2).toLatin1()}; const QByteArray url {match.capturedView(2).toLatin1()};
const int sepPos = url.indexOf('?'); const int sepPos = url.indexOf('?');
const QByteArrayView pathComponent = ((sepPos == -1) ? url : QByteArrayView(url).mid(0, sepPos)); const QByteArrayView pathComponent = ((sepPos == -1) ? url : QByteArrayView(url).first(sepPos));
m_request.path = QString::fromUtf8(QByteArray::fromPercentEncoding(asQByteArray(pathComponent))); m_request.path = QString::fromUtf8(QByteArray::fromPercentEncoding(asQByteArray(pathComponent)));
if (sepPos >= 0) if (sepPos >= 0)
{ {
const QByteArrayView query = QByteArrayView(url).mid(sepPos + 1); const QByteArrayView query = QByteArrayView(url).sliced(sepPos + 1);
// [rfc3986] 2.4 When to Encode or Decode // [rfc3986] 2.4 When to Encode or Decode
// URL components should be separated before percent-decoding // URL components should be separated before percent-decoding
@ -221,8 +221,8 @@ bool RequestParser::parseRequestLine(const QString &line)
const int eqCharPos = param.indexOf('='); const int eqCharPos = param.indexOf('=');
if (eqCharPos <= 0) continue; // ignores params without name if (eqCharPos <= 0) continue; // ignores params without name
const QByteArrayView nameComponent = param.mid(0, eqCharPos); const QByteArrayView nameComponent = param.first(eqCharPos);
const QByteArrayView valueComponent = param.mid(eqCharPos + 1); const QByteArrayView valueComponent = param.sliced(eqCharPos + 1);
const QString paramName = QString::fromUtf8( const QString paramName = QString::fromUtf8(
QByteArray::fromPercentEncoding(asQByteArray(nameComponent)).replace('+', ' ')); QByteArray::fromPercentEncoding(asQByteArray(nameComponent)).replace('+', ' '));
const QByteArray paramValue = QByteArray::fromPercentEncoding(asQByteArray(valueComponent)).replace('+', ' '); const QByteArray paramValue = QByteArray::fromPercentEncoding(asQByteArray(valueComponent)).replace('+', ' ');
@ -270,7 +270,7 @@ bool RequestParser::parsePostMessage(const QByteArrayView data)
return false; return false;
} }
const QByteArray delimiter = Utils::String::unquote(QStringView(contentType).mid(idx + boundaryFieldName.size())).toLatin1(); const QByteArray delimiter = Utils::String::unquote(QStringView(contentType).sliced(idx + boundaryFieldName.size())).toLatin1();
if (delimiter.isEmpty()) if (delimiter.isEmpty())
{ {
qWarning() << Q_FUNC_INFO << "boundary delimiter field empty!"; qWarning() << Q_FUNC_INFO << "boundary delimiter field empty!";
@ -279,7 +279,7 @@ bool RequestParser::parsePostMessage(const QByteArrayView data)
// split data by "dash-boundary" // split data by "dash-boundary"
const QByteArray dashDelimiter = QByteArray("--") + delimiter + CRLF; const QByteArray dashDelimiter = QByteArray("--") + delimiter + CRLF;
QList<QByteArrayView> multipart = splitToViews(data, dashDelimiter, Qt::SkipEmptyParts); QList<QByteArrayView> multipart = splitToViews(data, dashDelimiter);
if (multipart.isEmpty()) if (multipart.isEmpty())
{ {
qWarning() << Q_FUNC_INFO << "multipart empty"; qWarning() << Q_FUNC_INFO << "multipart empty";
@ -310,8 +310,8 @@ bool RequestParser::parseFormData(const QByteArrayView data)
return false; return false;
} }
const QString headers = QString::fromLatin1(data.mid(0, eohPos)); const QString headers = QString::fromLatin1(data.first(eohPos));
const QByteArrayView payload = viewWithoutEndingWith(data.mid((eohPos + EOH.size()), data.size()), CRLF); const QByteArrayView payload = viewWithoutEndingWith(data.sliced((eohPos + EOH.size())), CRLF);
HeaderMap headersMap; HeaderMap headersMap;
const QList<QStringView> headerLines = QStringView(headers).split(QString::fromLatin1(CRLF), Qt::SkipEmptyParts); 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) if (idx < 0)
continue; continue;
const QString name = directive.left(idx).trimmed().toString().toLower(); const QString name = directive.first(idx).trimmed().toString().toLower();
const QString value = Utils::String::unquote(directive.mid(idx + 1).trimmed()).toString(); const QString value = Utils::String::unquote(directive.sliced(idx + 1).trimmed()).toString();
headersMap[name] = value; 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)); const QRegularExpressionMatch ipRegexMatch = QRegularExpression(u"Current IP Address:\\s+([^<]+)</body>"_s).match(QString::fromUtf8(result.data));
if (ipRegexMatch.hasMatch()) if (ipRegexMatch.hasMatch())
{ {
QString ipStr = ipRegexMatch.captured(1); const QString ipStr = ipRegexMatch.captured(1);
qDebug() << Q_FUNC_INFO << "Regular expression captured the following IP:" << ipStr; qDebug() << Q_FUNC_INFO << "Regular expression captured the following IP:" << ipStr;
QHostAddress newIp(ipStr); const QHostAddress newIp {ipStr};
if (!newIp.isNull()) if (!newIp.isNull())
{ {
if (m_lastIP != newIp) if (m_lastIP != newIp)

View file

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

View file

@ -265,38 +265,37 @@ void Net::DownloadManager::applyProxySettings()
const auto *proxyManager = ProxyConfigurationManager::instance(); const auto *proxyManager = ProxyConfigurationManager::instance();
const ProxyConfiguration proxyConfig = proxyManager->proxyConfiguration(); const ProxyConfiguration proxyConfig = proxyManager->proxyConfiguration();
m_proxy = QNetworkProxy(QNetworkProxy::NoProxy); switch (proxyConfig.type)
if ((proxyConfig.type == Net::ProxyType::None) || (proxyConfig.type == ProxyType::SOCKS4))
return;
// Proxy enabled
if (proxyConfig.type == ProxyType::SOCKS5)
{ {
qDebug() << Q_FUNC_INFO << "using SOCKS proxy"; case Net::ProxyType::None:
m_proxy.setType(QNetworkProxy::Socks5Proxy); case Net::ProxyType::SOCKS4:
} m_proxy = QNetworkProxy(QNetworkProxy::NoProxy);
else break;
{
qDebug() << Q_FUNC_INFO << "using HTTP proxy";
m_proxy.setType(QNetworkProxy::HttpProxy);
}
m_proxy.setHostName(proxyConfig.ip); case Net::ProxyType::HTTP:
m_proxy.setPort(proxyConfig.port); m_proxy = QNetworkProxy(
QNetworkProxy::HttpProxy
, proxyConfig.ip
, proxyConfig.port
, (proxyConfig.authEnabled ? proxyConfig.username : QString())
, (proxyConfig.authEnabled ? proxyConfig.password : QString()));
m_proxy.setCapabilities(proxyConfig.hostnameLookupEnabled
? (m_proxy.capabilities() | QNetworkProxy::HostNameLookupCapability)
: (m_proxy.capabilities() & ~QNetworkProxy::HostNameLookupCapability));
break;
// Authentication? case Net::ProxyType::SOCKS5:
if (proxyConfig.authEnabled) m_proxy = QNetworkProxy(
{ QNetworkProxy::Socks5Proxy
qDebug("Proxy requires authentication, authenticating..."); , proxyConfig.ip
m_proxy.setUser(proxyConfig.username); , proxyConfig.port
m_proxy.setPassword(proxyConfig.password); , (proxyConfig.authEnabled ? proxyConfig.username : QString())
} , (proxyConfig.authEnabled ? proxyConfig.password : QString()));
m_proxy.setCapabilities(proxyConfig.hostnameLookupEnabled
if (proxyConfig.hostnameLookupEnabled) ? (m_proxy.capabilities() | QNetworkProxy::HostNameLookupCapability)
m_proxy.setCapabilities(m_proxy.capabilities() | QNetworkProxy::HostNameLookupCapability); : (m_proxy.capabilities() & ~QNetworkProxy::HostNameLookupCapability));
else break;
m_proxy.setCapabilities(m_proxy.capabilities() & ~QNetworkProxy::HostNameLookupCapability); };
} }
void Net::DownloadManager::processWaitingJobs(const ServiceID &serviceID) void Net::DownloadManager::processWaitingJobs(const ServiceID &serviceID)

View file

@ -148,8 +148,7 @@ void Smtp::sendMail(const QString &from, const QString &to, const QString &subje
// Encode the body in base64 // Encode the body in base64
QString crlfBody = body; QString crlfBody = body;
const QByteArray b = crlfBody.replace(u"\n"_s, u"\r\n"_s).toUtf8().toBase64(); const QByteArray b = crlfBody.replace(u"\n"_s, u"\r\n"_s).toUtf8().toBase64();
const int ct = b.length(); for (int i = 0, end = b.length(); i < end; i += 78)
for (int i = 0; i < ct; i += 78)
m_message += b.mid(i, 78); m_message += b.mid(i, 78);
m_from = from; m_from = from;
m_rcpt = to; m_rcpt = to;
@ -190,8 +189,12 @@ void Smtp::readyRead()
{ {
const int pos = m_buffer.indexOf("\r\n"); const int pos = m_buffer.indexOf("\r\n");
if (pos < 0) return; // Loop exit condition if (pos < 0) return; // Loop exit condition
const QByteArray line = m_buffer.left(pos); const QByteArray line = m_buffer.first(pos);
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
m_buffer.slice(pos + 2);
#else
m_buffer.remove(0, (pos + 2)); m_buffer.remove(0, (pos + 2));
#endif
qDebug() << "Response line:" << line; qDebug() << "Response line:" << line;
// Extract response code // Extract response code
const QByteArray code = line.left(3); const QByteArray code = line.left(3);

View file

@ -94,7 +94,13 @@ bool Path::isValid() const
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
QStringView view = m_pathStr; QStringView view = m_pathStr;
if (hasDriveLetter(view)) if (hasDriveLetter(view))
view = view.mid(3); {
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
view.slice(3);
#else
view = view.sliced(3);
#endif
}
// \\37 is using base-8 number system // \\37 is using base-8 number system
const QRegularExpression regex {u"[\\0-\\37:?\"*<>|]"_s}; const QRegularExpression regex {u"[\\0-\\37:?\"*<>|]"_s};
@ -147,9 +153,9 @@ Path Path::rootItem() const
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
// should be `c:/` instead of `c:` // should be `c:/` instead of `c:`
if ((slashIndex == 2) && hasDriveLetter(m_pathStr)) if ((slashIndex == 2) && hasDriveLetter(m_pathStr))
return createUnchecked(m_pathStr.left(slashIndex + 1)); return createUnchecked(m_pathStr.first(slashIndex + 1));
#endif #endif
return createUnchecked(m_pathStr.left(slashIndex)); return createUnchecked(m_pathStr.first(slashIndex));
} }
Path Path::parentPath() const Path Path::parentPath() const
@ -167,9 +173,9 @@ Path Path::parentPath() const
// should be `c:/` instead of `c:` // should be `c:/` instead of `c:`
// Windows "drive letter" is limited to one alphabet // Windows "drive letter" is limited to one alphabet
if ((slashIndex == 2) && hasDriveLetter(m_pathStr)) if ((slashIndex == 2) && hasDriveLetter(m_pathStr))
return (m_pathStr.size() == 3) ? Path() : createUnchecked(m_pathStr.left(slashIndex + 1)); return (m_pathStr.size() == 3) ? Path() : createUnchecked(m_pathStr.first(slashIndex + 1));
#endif #endif
return createUnchecked(m_pathStr.left(slashIndex)); return createUnchecked(m_pathStr.first(slashIndex));
} }
QString Path::filename() const QString Path::filename() const
@ -178,7 +184,7 @@ QString Path::filename() const
if (slashIndex == -1) if (slashIndex == -1)
return m_pathStr; return m_pathStr;
return m_pathStr.mid(slashIndex + 1); return m_pathStr.sliced(slashIndex + 1);
} }
QString Path::extension() const QString Path::extension() const
@ -188,9 +194,9 @@ QString Path::extension() const
return (u"." + suffix); return (u"." + suffix);
const int slashIndex = m_pathStr.lastIndexOf(u'/'); const int slashIndex = m_pathStr.lastIndexOf(u'/');
const auto filename = QStringView(m_pathStr).mid(slashIndex + 1); const auto filename = QStringView(m_pathStr).sliced(slashIndex + 1);
const int dotIndex = filename.lastIndexOf(u'.', -2); const int dotIndex = filename.lastIndexOf(u'.', -2);
return ((dotIndex == -1) ? QString() : filename.mid(dotIndex).toString()); return ((dotIndex == -1) ? QString() : filename.sliced(dotIndex).toString());
} }
bool Path::hasExtension(const QStringView ext) const bool Path::hasExtension(const QStringView ext) const
@ -293,7 +299,7 @@ Path Path::commonPath(const Path &left, const Path &right)
if (commonItemsCount > 0) if (commonItemsCount > 0)
commonPathSize += (commonItemsCount - 1); // size of intermediate separators commonPathSize += (commonItemsCount - 1); // size of intermediate separators
return Path::createUnchecked(left.m_pathStr.left(commonPathSize)); return Path::createUnchecked(left.m_pathStr.first(commonPathSize));
} }
Path Path::findRootFolder(const PathList &filePaths) Path Path::findRootFolder(const PathList &filePaths)
@ -322,7 +328,13 @@ void Path::stripRootFolder(PathList &filePaths)
return; return;
for (Path &filePath : filePaths) 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)); filePath.m_pathStr.remove(0, (commonRootFolder.m_pathStr.size() + 1));
#endif
}
} }
void Path::addRootFolder(PathList &filePaths, const Path &rootFolder) void Path::addRootFolder(PathList &filePaths, const Path &rootFolder)

View file

@ -359,6 +359,19 @@ void Preferences::setStatusbarDisplayed(const bool displayed)
setValue(u"Preferences/General/StatusbarDisplayed"_s, displayed); 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 bool Preferences::isStatusbarExternalIPDisplayed() const
{ {
return value(u"Preferences/General/StatusbarExternalIPDisplayed"_s, false); return value(u"Preferences/General/StatusbarExternalIPDisplayed"_s, false);

View file

@ -119,6 +119,8 @@ public:
void setHideZeroComboValues(int n); void setHideZeroComboValues(int n);
bool isStatusbarDisplayed() const; bool isStatusbarDisplayed() const;
void setStatusbarDisplayed(bool displayed); void setStatusbarDisplayed(bool displayed);
bool isStatusbarFreeDiskSpaceDisplayed() const;
void setStatusbarFreeDiskSpaceDisplayed(bool displayed);
bool isStatusbarExternalIPDisplayed() const; bool isStatusbarExternalIPDisplayed() const;
void setStatusbarExternalIPDisplayed(bool displayed); void setStatusbarExternalIPDisplayed(bool displayed);
bool isToolbarDisplayed() const; bool isToolbarDisplayed() const;

View file

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

View file

@ -43,6 +43,7 @@
#include "base/addtorrentmanager.h" #include "base/addtorrentmanager.h"
#include "base/asyncfilestorage.h" #include "base/asyncfilestorage.h"
#include "base/bittorrent/addtorrenterror.h"
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/bittorrent/torrentdescriptor.h" #include "base/bittorrent/torrentdescriptor.h"
#include "base/global.h" #include "base/global.h"

View file

@ -37,7 +37,6 @@
#include <QSharedPointer> #include <QSharedPointer>
#include "base/applicationcomponent.h" #include "base/applicationcomponent.h"
#include "base/bittorrent/addtorrenterror.h"
#include "base/exceptions.h" #include "base/exceptions.h"
#include "base/settingvalue.h" #include "base/settingvalue.h"
#include "base/utils/thread.h" #include "base/utils/thread.h"
@ -48,6 +47,11 @@ class Application;
class AsyncFileStorage; class AsyncFileStorage;
struct ProcessingJob; struct ProcessingJob;
namespace BitTorrent
{
struct AddTorrentError;
}
namespace RSS namespace RSS
{ {
class Article; class Article;

View file

@ -184,14 +184,10 @@ QString computeEpisodeName(const QString &article)
for (int i = 1; i <= match.lastCapturedIndex(); ++i) for (int i = 1; i <= match.lastCapturedIndex(); ++i)
{ {
const QString cap = match.captured(i); const QString cap = match.captured(i);
if (cap.isEmpty()) if (cap.isEmpty())
continue; continue;
bool isInt = false; ret.append(cap);
const int x = cap.toInt(&isInt);
ret.append(isInt ? QString::number(x) : cap);
} }
return ret.join(u'x'); return ret.join(u'x');
} }
@ -293,20 +289,26 @@ bool AutoDownloadRule::matchesEpisodeFilterExpression(const QString &articleTitl
if (!matcher.hasMatch()) if (!matcher.hasMatch())
return false; return false;
const QString season {matcher.captured(1)}; const QStringView season {matcher.capturedView(1)};
const QStringList episodes {matcher.captured(2).split(u';')}; const QList<QStringView> episodes {matcher.capturedView(2).split(u';')};
const int seasonOurs {season.toInt()}; const int seasonOurs {season.toInt()};
for (QString episode : episodes) for (QStringView episode : episodes)
{ {
if (episode.isEmpty()) if (episode.isEmpty())
continue; continue;
// We need to trim leading zeroes, but if it's all zeros then we want episode zero. // 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')) while ((episode.size() > 1) && episode.startsWith(u'0'))
episode = episode.right(episode.size() - 1); {
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
episode.slice(1);
#else
episode = episode.sliced(1);
#endif
}
if (episode.indexOf(u'-') != -1) if (episode.contains(u'-'))
{ // Range detected { // Range detected
const QString partialPattern1 {u"\\bs0?(\\d{1,4})[ -_\\.]?e(0?\\d{1,4})(?:\\D|\\b)"_s}; 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}; const QString partialPattern2 {u"\\b(\\d{1,4})x(0?\\d{1,4})(?:\\D|\\b)"_s};
@ -323,24 +325,25 @@ bool AutoDownloadRule::matchesEpisodeFilterExpression(const QString &articleTitl
if (matched) if (matched)
{ {
const int seasonTheirs {matcher.captured(1).toInt()}; const int seasonTheirs {matcher.capturedView(1).toInt()};
const int episodeTheirs {matcher.captured(2).toInt()}; const int episodeTheirs {matcher.capturedView(2).toInt()};
if (episode.endsWith(u'-')) if (episode.endsWith(u'-'))
{ // Infinite range { // Infinite range
const int episodeOurs {QStringView(episode).left(episode.size() - 1).toInt()}; const int episodeOurs {QStringView(episode).chopped(1).toInt()};
if (((seasonTheirs == seasonOurs) && (episodeTheirs >= episodeOurs)) || (seasonTheirs > seasonOurs)) if (((seasonTheirs == seasonOurs) && (episodeTheirs >= episodeOurs)) || (seasonTheirs > seasonOurs))
return true; return true;
} }
else else
{ // Normal range { // Normal range
const QStringList range {episode.split(u'-')}; const QList<QStringView> range {episode.split(u'-')};
Q_ASSERT(range.size() == 2); 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 episodeOursFirst {range.first().toInt()};
const int episodeOursLast {range.last().toInt()}; const int episodeOursLast {range.last().toInt()};
if (episodeOursFirst > episodeOursLast)
continue; // Ignore this subrule completely
if ((seasonTheirs == seasonOurs) && ((episodeOursFirst <= episodeTheirs) && (episodeOursLast >= episodeTheirs))) if ((seasonTheirs == seasonOurs) && ((episodeOursFirst <= episodeTheirs) && (episodeOursLast >= episodeTheirs)))
return true; return true;
} }

View file

@ -1,7 +1,7 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2024 Jonathan Ketchker * 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 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org> * Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
* *
@ -56,19 +56,22 @@
const QString KEY_UID = u"uid"_s; const QString KEY_UID = u"uid"_s;
const QString KEY_URL = u"url"_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_TITLE = u"title"_s;
const QString KEY_LASTBUILDDATE = u"lastBuildDate"_s; const QString KEY_LASTBUILDDATE = u"lastBuildDate"_s;
const QString KEY_ISLOADING = u"isLoading"_s; const QString KEY_ISLOADING = u"isLoading"_s;
const QString KEY_HASERROR = u"hasError"_s; const QString KEY_HASERROR = u"hasError"_s;
const QString KEY_ARTICLES = u"articles"_s; const QString KEY_ARTICLES = u"articles"_s;
using namespace std::chrono_literals;
using namespace RSS; using namespace RSS;
Feed::Feed(const QUuid &uid, const QString &url, const QString &path, Session *session) Feed::Feed(Session *session, const QUuid &uid, const QString &url, const QString &path, const std::chrono::seconds refreshInterval)
: Item(path) : Item(path)
, m_session(session) , m_session {session}
, m_uid(uid) , m_uid {uid}
, m_url(url) , m_url {url}
, m_refreshInterval {refreshInterval}
{ {
const auto uidHex = QString::fromLatin1(m_uid.toRfc4122().toHex()); const auto uidHex = QString::fromLatin1(m_uid.toRfc4122().toHex());
m_dataFileName = Path(uidHex + u".json"); m_dataFileName = Path(uidHex + u".json");
@ -327,9 +330,9 @@ bool Feed::addArticle(const QVariantHash &articleData)
// Insertion sort // Insertion sort
const int maxArticles = m_session->maxArticlesPerFeed(); const int maxArticles = m_session->maxArticlesPerFeed();
const auto lowerBound = std::lower_bound(m_articlesByDate.begin(), m_articlesByDate.end() const auto lowerBound = std::lower_bound(m_articlesByDate.cbegin(), m_articlesByDate.cend()
, articleData.value(Article::KeyDate).toDateTime(), Article::articleDateRecentThan); , articleData.value(Article::KeyDate).toDateTime(), Article::articleDateRecentThan);
if ((lowerBound - m_articlesByDate.begin()) >= maxArticles) if ((lowerBound - m_articlesByDate.cbegin()) >= maxArticles)
return false; // we reach max articles return false; // we reach max articles
auto *article = new Article(this, articleData); auto *article = new Article(this, articleData);
@ -462,6 +465,20 @@ Path Feed::iconPath() const
return m_iconPath; 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) void Feed::setURL(const QString &url)
{ {
const QString oldURL = m_url; const QString oldURL = m_url;
@ -474,6 +491,8 @@ QJsonValue Feed::toJsonValue(const bool withData) const
QJsonObject jsonObj; QJsonObject jsonObj;
jsonObj.insert(KEY_UID, uid().toString()); jsonObj.insert(KEY_UID, uid().toString());
jsonObj.insert(KEY_URL, url()); jsonObj.insert(KEY_URL, url());
if (refreshInterval() > 0s)
jsonObj.insert(KEY_REFRESHINTERVAL, static_cast<qint64>(refreshInterval().count()));
if (withData) if (withData)
{ {

View file

@ -1,7 +1,7 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2024 Jonathan Ketchker * 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 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org> * Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
* *
@ -31,6 +31,8 @@
#pragma once #pragma once
#include <chrono>
#include <QtContainerFwd> #include <QtContainerFwd>
#include <QBasicTimer> #include <QBasicTimer>
#include <QHash> #include <QHash>
@ -68,7 +70,7 @@ namespace RSS
friend class Session; friend class Session;
Feed(const QUuid &uid, const QString &url, const QString &path, Session *session); Feed(Session *session, const QUuid &uid, const QString &url, const QString &path, std::chrono::seconds refreshInterval);
~Feed() override; ~Feed() override;
public: public:
@ -87,6 +89,9 @@ namespace RSS
Article *articleByGUID(const QString &guid) const; Article *articleByGUID(const QString &guid) const;
Path iconPath() const; Path iconPath() const;
std::chrono::seconds refreshInterval() const;
void setRefreshInterval(std::chrono::seconds refreshInterval);
QJsonValue toJsonValue(bool withData = false) const override; QJsonValue toJsonValue(bool withData = false) const override;
signals: signals:
@ -94,6 +99,7 @@ namespace RSS
void titleChanged(Feed *feed = nullptr); void titleChanged(Feed *feed = nullptr);
void stateChanged(Feed *feed = nullptr); void stateChanged(Feed *feed = nullptr);
void urlChanged(const QString &oldURL); void urlChanged(const QString &oldURL);
void refreshIntervalChanged(std::chrono::seconds oldRefreshInterval);
private slots: private slots:
void handleSessionProcessingEnabledChanged(bool enabled); void handleSessionProcessingEnabledChanged(bool enabled);
@ -123,6 +129,7 @@ namespace RSS
Private::FeedSerializer *m_serializer = nullptr; Private::FeedSerializer *m_serializer = nullptr;
const QUuid m_uid; const QUuid m_uid;
QString m_url; QString m_url;
std::chrono::seconds m_refreshInterval;
QString m_title; QString m_title;
QString m_lastBuildDate; QString m_lastBuildDate;
bool m_hasError = false; bool m_hasError = false;

View file

@ -97,7 +97,7 @@ QStringList Item::expandPath(const QString &path)
int index = 0; int index = 0;
while ((index = path.indexOf(Item::PathSeparator, index)) >= 0) while ((index = path.indexOf(Item::PathSeparator, index)) >= 0)
{ {
result << path.left(index); result << path.first(index);
++index; ++index;
} }
result << path; result << path;
@ -108,11 +108,11 @@ QStringList Item::expandPath(const QString &path)
QString Item::parentPath(const QString &path) QString Item::parentPath(const QString &path)
{ {
const int pos = path.lastIndexOf(Item::PathSeparator); const int pos = path.lastIndexOf(Item::PathSeparator);
return (pos >= 0) ? path.left(pos) : QString(); return (pos >= 0) ? path.first(pos) : QString();
} }
QString Item::relativeName(const QString &path) QString Item::relativeName(const QString &path)
{ {
int pos; const int pos = path.lastIndexOf(Item::PathSeparator);
return ((pos = path.lastIndexOf(Item::PathSeparator)) >= 0 ? path.right(path.size() - (pos + 1)) : path); return (pos >= 0) ? path.sliced(pos + 1) : path;
} }

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or

View file

@ -1,7 +1,7 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2024 Jonathan Ketchker * Copyright (C) 2024 Jonathan Ketchker
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org> * Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
* *
@ -56,6 +56,7 @@ const QString CONF_FOLDER_NAME = u"rss"_s;
const QString DATA_FOLDER_NAME = u"rss/articles"_s; const QString DATA_FOLDER_NAME = u"rss/articles"_s;
const QString FEEDS_FILE_NAME = u"feeds.json"_s; const QString FEEDS_FILE_NAME = u"feeds.json"_s;
using namespace std::chrono_literals;
using namespace RSS; using namespace RSS;
QPointer<Session> Session::m_instance = nullptr; QPointer<Session> Session::m_instance = nullptr;
@ -94,12 +95,10 @@ Session::Session()
m_workingThread->start(); m_workingThread->start();
load(); load();
m_refreshTimer.setSingleShot(true);
connect(&m_refreshTimer, &QTimer::timeout, this, &Session::refresh); connect(&m_refreshTimer, &QTimer::timeout, this, &Session::refresh);
if (isProcessingEnabled()) if (isProcessingEnabled())
{
m_refreshTimer.start(std::chrono::minutes(refreshInterval()));
refresh(); refresh();
}
// Remove legacy/corrupted settings // Remove legacy/corrupted settings
// (at least on Windows, QSettings is case-insensitive and it can get // (at least on Windows, QSettings is case-insensitive and it can get
@ -138,19 +137,20 @@ Session *Session::instance()
return m_instance; return m_instance;
} }
nonstd::expected<void, QString> Session::addFolder(const QString &path) nonstd::expected<Folder *, QString> Session::addFolder(const QString &path)
{ {
const nonstd::expected<Folder *, QString> result = prepareItemDest(path); const nonstd::expected<Folder *, QString> result = prepareItemDest(path);
if (!result) if (!result)
return result.get_unexpected(); return result.get_unexpected();
auto *destFolder = result.value(); auto *destFolder = result.value();
addItem(new Folder(path), destFolder); auto *folder = new Folder(path);
addItem(folder, destFolder);
store(); store();
return {}; return folder;
} }
nonstd::expected<void, QString> Session::addFeed(const QString &url, const QString &path) nonstd::expected<Feed *, QString> Session::addFeed(const QString &url, const QString &path, const std::chrono::seconds refreshInterval)
{ {
if (m_feedsByURL.contains(url)) if (m_feedsByURL.contains(url))
return nonstd::make_unexpected(tr("RSS feed with given URL already exists: %1.").arg(url)); return nonstd::make_unexpected(tr("RSS feed with given URL already exists: %1.").arg(url));
@ -160,13 +160,13 @@ nonstd::expected<void, QString> Session::addFeed(const QString &url, const QStri
return result.get_unexpected(); return result.get_unexpected();
auto *destFolder = result.value(); auto *destFolder = result.value();
auto *feed = new Feed(generateUID(), url, path, this); auto *feed = new Feed(this, generateUID(), url, path, refreshInterval);
addItem(feed, destFolder); addItem(feed, destFolder);
store(); store();
if (isProcessingEnabled()) if (isProcessingEnabled())
feed->refresh(); refreshFeed(feed, std::chrono::system_clock::now());
return {}; return feed;
} }
nonstd::expected<void, QString> Session::setFeedURL(const QString &path, const QString &url) 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); feed->setURL(url);
store(); store();
if (isProcessingEnabled()) if (isProcessingEnabled())
feed->refresh(); refreshFeed(feed, std::chrono::system_clock::now());
return {}; return {};
} }
@ -214,14 +214,20 @@ nonstd::expected<void, QString> Session::moveItem(Item *item, const QString &des
Q_ASSERT(item); Q_ASSERT(item);
Q_ASSERT(item != rootFolder()); 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); const nonstd::expected<Folder *, QString> result = prepareItemDest(destPath);
if (!result) if (!result)
return result.get_unexpected(); return result.get_unexpected();
auto *destFolder = result.value(); 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()))); auto *srcFolder = static_cast<Folder *>(m_itemsByPath.value(Item::parentPath(item->path())));
if (srcFolder != destFolder) if (srcFolder != destFolder)
{ {
@ -314,7 +320,7 @@ bool Session::loadFolder(const QJsonObject &jsonObj, Folder *folder)
QString url = val.toString(); QString url = val.toString();
if (url.isEmpty()) if (url.isEmpty())
url = key; url = key;
addFeedToFolder(generateUID(), url, key, folder); addFeedToFolder(generateUID(), url, key, folder, 0s);
updated = true; updated = true;
} }
else if (val.isObject()) else if (val.isObject())
@ -354,7 +360,9 @@ bool Session::loadFolder(const QJsonObject &jsonObj, Folder *folder)
updated = true; updated = true;
} }
addFeedToFolder(uid, valObj[u"url"].toString(), key, folder); const auto refreshInterval = std::chrono::seconds(valObj[u"refreshInterval"].toInteger());
addFeedToFolder(uid, valObj[u"url"].toString(), key, folder, refreshInterval);
} }
else else
{ {
@ -385,8 +393,14 @@ void Session::loadLegacy()
uint i = 0; uint i = 0;
for (QString legacyPath : legacyFeedPaths) for (QString legacyPath : legacyFeedPaths)
{ {
if (Item::PathSeparator == legacyPath[0]) if (legacyPath.startsWith(Item::PathSeparator))
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
legacyPath.slice(1);
#else
legacyPath.remove(0, 1); legacyPath.remove(0, 1);
#endif
}
const QString parentFolderPath = Item::parentPath(legacyPath); const QString parentFolderPath = Item::parentPath(legacyPath);
const QString feedUrl = Item::relativeName(legacyPath); const QString feedUrl = Item::relativeName(legacyPath);
@ -404,7 +418,7 @@ void Session::loadLegacy()
void Session::store() void Session::store()
{ {
m_confFileStorage->store(Path(FEEDS_FILE_NAME) m_confFileStorage->store(Path(FEEDS_FILE_NAME)
, QJsonDocument(rootFolder()->toJsonValue().toObject()).toJson()); , QJsonDocument(rootFolder()->toJsonValue().toObject()).toJson());
} }
nonstd::expected<Folder *, QString> Session::prepareItemDest(const QString &path) nonstd::expected<Folder *, QString> Session::prepareItemDest(const QString &path)
@ -430,9 +444,9 @@ Folder *Session::addSubfolder(const QString &name, Folder *parentFolder)
return folder; return folder;
} }
Feed *Session::addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder) Feed *Session::addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder, const std::chrono::seconds refreshInterval)
{ {
auto *feed = new Feed(uid, url, Item::joinPath(parentFolder->path(), name), this); auto *feed = new Feed(this, uid, url, Item::joinPath(parentFolder->path(), name), refreshInterval);
addItem(feed, parentFolder); addItem(feed, parentFolder);
return feed; return feed;
} }
@ -454,8 +468,25 @@ void Session::addItem(Item *item, Folder *destFolder)
emit feedURLChanged(feed, oldURL); 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_feedsByUID[feed->uid()] = feed;
m_feedsByURL[feed->url()] = feed; m_feedsByURL[feed->url()] = feed;
m_refreshTimepoints.emplace(feed, std::chrono::system_clock::time_point());
} }
connect(item, &Item::pathChanged, this, &Session::itemPathChanged); connect(item, &Item::pathChanged, this, &Session::itemPathChanged);
@ -476,14 +507,9 @@ void Session::setProcessingEnabled(const bool enabled)
{ {
m_storeProcessingEnabled = enabled; m_storeProcessingEnabled = enabled;
if (enabled) if (enabled)
{
m_refreshTimer.start(std::chrono::minutes(refreshInterval()));
refresh(); refresh();
}
else else
{
m_refreshTimer.stop(); m_refreshTimer.stop();
}
emit processingStateChanged(enabled); emit processingStateChanged(enabled);
} }
@ -554,6 +580,7 @@ void Session::handleItemAboutToBeDestroyed(Item *item)
{ {
m_feedsByUID.remove(feed->uid()); m_feedsByUID.remove(feed->uid());
m_feedsByURL.remove(feed->url()); m_feedsByURL.remove(feed->url());
m_refreshTimepoints.remove(feed);
} }
} }
@ -592,6 +619,28 @@ void Session::setMaxArticlesPerFeed(const int n)
void Session::refresh() void Session::refresh()
{ {
// NOTE: Should we allow manually refreshing for disabled session? const auto currentTimepoint = std::chrono::system_clock::now();
rootFolder()->refresh(); std::chrono::seconds nextRefreshInterval = 0s;
for (auto it = m_refreshTimepoints.begin(); it != m_refreshTimepoints.end(); ++it)
{
Feed *feed = it.key();
std::chrono::system_clock::time_point &timepoint = it.value();
if (timepoint <= currentTimepoint)
timepoint = refreshFeed(feed, currentTimepoint);
const auto interval = std::chrono::duration_cast<std::chrono::seconds>(timepoint - currentTimepoint);
if ((interval < nextRefreshInterval) || (nextRefreshInterval == 0s))
nextRefreshInterval = interval;
}
m_refreshTimer.start(nextRefreshInterval);
}
std::chrono::system_clock::time_point Session::refreshFeed(Feed *feed, const std::chrono::system_clock::time_point &currentTimepoint)
{
feed->refresh();
const std::chrono::seconds feedRefreshInterval = feed->refreshInterval();
const std::chrono::seconds effectiveRefreshInterval = (feedRefreshInterval > 0s) ? feedRefreshInterval : std::chrono::minutes(refreshInterval());
return currentTimepoint + effectiveRefreshInterval;
} }

View file

@ -1,7 +1,7 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2024 Jonathan Ketchker * Copyright (C) 2024 Jonathan Ketchker
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org> * Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
* *
@ -35,26 +35,20 @@
* RSS Session configuration file format (JSON): * RSS Session configuration file format (JSON):
* *
* =============== BEGIN =============== * =============== BEGIN ===============
* * {
{ * "folder1": {
* "folder1": * "subfolder1": {
{ * "Feed name 1 (Alias)": {
* "subfolder1":
{
* "Feed name 1 (Alias)":
{
* "uid": "feed unique identifier", * "uid": "feed unique identifier",
* "url": "http://some-feed-url1" * "url": "http://some-feed-url1"
* } * }
* "Feed name 2 (Alias)": * "Feed name 2 (Alias)": {
{
* "uid": "feed unique identifier", * "uid": "feed unique identifier",
* "url": "http://some-feed-url2" * "url": "http://some-feed-url2"
* } * }
* }, * },
* "subfolder2": {}, * "subfolder2": {},
* "Feed name 3 (Alias)": * "Feed name 3 (Alias)": {
{
* "uid": "feed unique identifier", * "uid": "feed unique identifier",
* "url": "http://some-feed-url3" * "url": "http://some-feed-url3"
* } * }
@ -120,8 +114,8 @@ namespace RSS
std::chrono::seconds fetchDelay() const; std::chrono::seconds fetchDelay() const;
void setFetchDelay(std::chrono::seconds delay); void setFetchDelay(std::chrono::seconds delay);
nonstd::expected<void, QString> addFolder(const QString &path); nonstd::expected<Folder *, QString> addFolder(const QString &path);
nonstd::expected<void, QString> addFeed(const QString &url, const QString &path); nonstd::expected<Feed *, QString> addFeed(const QString &url, const QString &path, std::chrono::seconds refreshInterval = {});
nonstd::expected<void, QString> setFeedURL(const QString &path, const QString &url); nonstd::expected<void, QString> setFeedURL(const QString &path, const QString &url);
nonstd::expected<void, QString> setFeedURL(Feed *feed, const QString &url); nonstd::expected<void, QString> setFeedURL(Feed *feed, const QString &url);
nonstd::expected<void, QString> moveItem(const QString &itemPath, const QString &destPath); nonstd::expected<void, QString> moveItem(const QString &itemPath, const QString &destPath);
@ -135,9 +129,6 @@ namespace RSS
Folder *rootFolder() const; Folder *rootFolder() const;
public slots:
void refresh();
signals: signals:
void processingStateChanged(bool enabled); void processingStateChanged(bool enabled);
void maxArticlesPerFeedChanged(int n); void maxArticlesPerFeedChanged(int n);
@ -160,8 +151,10 @@ namespace RSS
void store(); void store();
nonstd::expected<Folder *, QString> prepareItemDest(const QString &path); nonstd::expected<Folder *, QString> prepareItemDest(const QString &path);
Folder *addSubfolder(const QString &name, Folder *parentFolder); Folder *addSubfolder(const QString &name, Folder *parentFolder);
Feed *addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder); Feed *addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder, std::chrono::seconds refreshInterval);
void addItem(Item *item, Folder *destFolder); void 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; static QPointer<Session> m_instance;
@ -176,5 +169,6 @@ namespace RSS
QHash<QString, Item *> m_itemsByPath; QHash<QString, Item *> m_itemsByPath;
QHash<QUuid, Feed *> m_feedsByUID; QHash<QUuid, Feed *> m_feedsByUID;
QHash<QString, Feed *> m_feedsByURL; QHash<QString, Feed *> m_feedsByURL;
QHash<Feed *, std::chrono::system_clock::time_point> m_refreshTimepoints;
}; };
} }

View file

@ -41,7 +41,10 @@ SearchDownloadHandler::SearchDownloadHandler(const QString &pluginName, const QS
, m_manager {manager} , m_manager {manager}
, m_downloadProcess {new QProcess(this)} , m_downloadProcess {new QProcess(this)}
{ {
m_downloadProcess->setEnvironment(QProcess::systemEnvironment()); m_downloadProcess->setProcessEnvironment(m_manager->proxyEnvironment());
#ifdef Q_OS_UNIX
m_downloadProcess->setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif
connect(m_downloadProcess, qOverload<int, QProcess::ExitStatus>(&QProcess::finished) connect(m_downloadProcess, qOverload<int, QProcess::ExitStatus>(&QProcess::finished)
, this, &SearchDownloadHandler::downloadProcessFinished); , this, &SearchDownloadHandler::downloadProcessFinished);
const QStringList params const QStringList params
@ -52,7 +55,7 @@ SearchDownloadHandler::SearchDownloadHandler(const QString &pluginName, const QS
url url
}; };
// Launch search // Launch search
m_downloadProcess->start(Utils::ForeignApps::pythonInfo().executableName, params, QIODevice::ReadOnly); m_downloadProcess->start(Utils::ForeignApps::pythonInfo().executablePath.data(), params, QIODevice::ReadOnly);
} }
void SearchDownloadHandler::downloadProcessFinished(int exitcode) void SearchDownloadHandler::downloadProcessFinished(int exitcode)

View file

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

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -81,7 +81,7 @@ private:
void readSearchOutput(); void readSearchOutput();
void processFailed(); void processFailed();
void processFinished(int exitcode); void processFinished(int exitcode);
bool parseSearchResult(QStringView line, SearchResult &searchResult); bool parseSearchResult(QByteArrayView line, SearchResult &searchResult);
const QString m_pattern; const QString m_pattern;
const QString m_category; const QString m_category;

View file

@ -88,6 +88,7 @@ QPointer<SearchPluginManager> SearchPluginManager::m_instance = nullptr;
SearchPluginManager::SearchPluginManager() SearchPluginManager::SearchPluginManager()
: m_updateUrl(u"https://searchplugins.qbittorrent.org/nova3/engines/"_s) : m_updateUrl(u"https://searchplugins.qbittorrent.org/nova3/engines/"_s)
, m_proxyEnv {QProcessEnvironment::systemEnvironment()}
{ {
Q_ASSERT(!m_instance); // only one instance is allowed Q_ASSERT(!m_instance); // only one instance is allowed
m_instance = this; m_instance = this;
@ -362,6 +363,11 @@ SearchHandler *SearchPluginManager::startSearch(const QString &pattern, const QS
return new SearchHandler(pattern, category, usedPlugins, this); return new SearchHandler(pattern, category, usedPlugins, this);
} }
QProcessEnvironment SearchPluginManager::proxyEnvironment() const
{
return m_proxyEnv;
}
QString SearchPluginManager::categoryFullName(const QString &categoryName) QString SearchPluginManager::categoryFullName(const QString &categoryName)
{ {
const QHash<QString, QString> categoryTable const QHash<QString, QString> categoryTable
@ -403,50 +409,70 @@ Path SearchPluginManager::engineLocation()
void SearchPluginManager::applyProxySettings() void SearchPluginManager::applyProxySettings()
{ {
const auto *proxyManager = Net::ProxyConfigurationManager::instance(); // for python `urllib`: https://docs.python.org/3/library/urllib.request.html#urllib.request.ProxyHandler
const Net::ProxyConfiguration proxyConfig = proxyManager->proxyConfiguration(); const QString HTTP_PROXY = u"http_proxy"_s;
const QString HTTPS_PROXY = u"https_proxy"_s;
// for `helpers.setupSOCKSProxy()`: https://everything.curl.dev/usingcurl/proxies/socks.html
const QString SOCKS_PROXY = u"qbt_socks_proxy"_s;
// Define environment variables for urllib in search engine plugins if (!Preferences::instance()->useProxyForGeneralPurposes())
QString proxyStrHTTP, proxyStrSOCK;
if ((proxyConfig.type != Net::ProxyType::None) && Preferences::instance()->useProxyForGeneralPurposes())
{ {
switch (proxyConfig.type) m_proxyEnv.remove(HTTP_PROXY);
{ m_proxyEnv.remove(HTTPS_PROXY);
case Net::ProxyType::HTTP: m_proxyEnv.remove(SOCKS_PROXY);
if (proxyConfig.authEnabled) return;
{
proxyStrHTTP = u"http://%1:%2@%3:%4"_s.arg(proxyConfig.username
, proxyConfig.password, proxyConfig.ip, QString::number(proxyConfig.port));
}
else
{
proxyStrHTTP = u"http://%1:%2"_s.arg(proxyConfig.ip, QString::number(proxyConfig.port));
}
break;
case Net::ProxyType::SOCKS5:
if (proxyConfig.authEnabled)
{
proxyStrSOCK = u"%1:%2@%3:%4"_s.arg(proxyConfig.username
, proxyConfig.password, proxyConfig.ip, QString::number(proxyConfig.port));
}
else
{
proxyStrSOCK = u"%1:%2"_s.arg(proxyConfig.ip, QString::number(proxyConfig.port));
}
break;
default:
qDebug("Disabling HTTP communications proxy");
}
qDebug("HTTP communications proxy string: %s"
, qUtf8Printable((proxyConfig.type == Net::ProxyType::SOCKS5) ? proxyStrSOCK : proxyStrHTTP));
} }
qputenv("http_proxy", proxyStrHTTP.toLocal8Bit()); const Net::ProxyConfiguration proxyConfig = Net::ProxyConfigurationManager::instance()->proxyConfiguration();
qputenv("https_proxy", proxyStrHTTP.toLocal8Bit()); switch (proxyConfig.type)
qputenv("sock_proxy", proxyStrSOCK.toLocal8Bit()); {
case Net::ProxyType::None:
m_proxyEnv.remove(HTTP_PROXY);
m_proxyEnv.remove(HTTPS_PROXY);
m_proxyEnv.remove(SOCKS_PROXY);
break;
case Net::ProxyType::HTTP:
{
const QString credential = proxyConfig.authEnabled
? (proxyConfig.username + u':' + proxyConfig.password + u'@')
: QString();
const QString proxyURL = u"http://%1%2:%3"_s
.arg(credential, proxyConfig.ip, QString::number(proxyConfig.port));
m_proxyEnv.insert(HTTP_PROXY, proxyURL);
m_proxyEnv.insert(HTTPS_PROXY, proxyURL);
m_proxyEnv.remove(SOCKS_PROXY);
}
break;
case Net::ProxyType::SOCKS5:
{
const QString scheme = proxyConfig.hostnameLookupEnabled ? u"socks5h"_s : u"socks5"_s;
const QString credential = proxyConfig.authEnabled
? (proxyConfig.username + u':' + proxyConfig.password + u'@')
: QString();
const QString proxyURL = u"%1://%2%3:%4"_s
.arg(scheme, credential, proxyConfig.ip, QString::number(proxyConfig.port));
m_proxyEnv.remove(HTTP_PROXY);
m_proxyEnv.remove(HTTPS_PROXY);
m_proxyEnv.insert(SOCKS_PROXY, proxyURL);
}
break;
case Net::ProxyType::SOCKS4:
{
const QString scheme = proxyConfig.hostnameLookupEnabled ? u"socks4a"_s : u"socks4"_s;
const QString proxyURL = u"%1://%2:%3"_s
.arg(scheme, proxyConfig.ip, QString::number(proxyConfig.port));
m_proxyEnv.remove(HTTP_PROXY);
m_proxyEnv.remove(HTTPS_PROXY);
m_proxyEnv.insert(SOCKS_PROXY, proxyURL);
}
break;
}
} }
void SearchPluginManager::versionInfoDownloadFinished(const Net::DownloadResult &result) void SearchPluginManager::versionInfoDownloadFinished(const Net::DownloadResult &result)
@ -469,9 +495,9 @@ void SearchPluginManager::pluginDownloadFinished(const Net::DownloadResult &resu
} }
else else
{ {
const QString url = result.url; const QString &url = result.url;
QString pluginName = url.mid(url.lastIndexOf(u'/') + 1); const QString pluginName = url.sliced(url.lastIndexOf(u'/') + 1)
pluginName.replace(u".py"_s, u""_s, Qt::CaseInsensitive); .replace(u".py"_s, u""_s, Qt::CaseInsensitive);
if (pluginInfo(pluginName)) if (pluginInfo(pluginName))
emit pluginUpdateFailed(pluginName, tr("Failed to download the plugin file. %1").arg(result.errorString)); emit pluginUpdateFailed(pluginName, tr("Failed to download the plugin file. %1").arg(result.errorString));
@ -487,39 +513,42 @@ void SearchPluginManager::updateNova()
const Path enginePath = engineLocation(); const Path enginePath = engineLocation();
QFile packageFile {(enginePath / Path(u"__init__.py"_s)).data()}; QFile packageFile {(enginePath / Path(u"__init__.py"_s)).data()};
packageFile.open(QIODevice::WriteOnly); if (packageFile.open(QIODevice::WriteOnly))
packageFile.close(); packageFile.close();
Utils::Fs::mkdir(enginePath / Path(u"engines"_s)); Utils::Fs::mkdir(enginePath / Path(u"engines"_s));
QFile packageFile2 {(enginePath / Path(u"engines/__init__.py"_s)).data()}; QFile packageFile2 {(enginePath / Path(u"engines/__init__.py"_s)).data()};
packageFile2.open(QIODevice::WriteOnly); if (packageFile2.open(QIODevice::WriteOnly))
packageFile2.close(); packageFile2.close();
// Copy search plugin files (if necessary) // Copy search plugin files (if necessary)
const auto updateFile = [&enginePath](const Path &filename, const bool compareVersion) const auto updateFile = [&enginePath](const Path &filename)
{ {
const Path filePathBundled = Path(u":/searchengine/nova3"_s) / filename; const Path filePathBundled = Path(u":/searchengine/nova3"_s) / filename;
const Path filePathDisk = enginePath / filename; const Path filePathDisk = enginePath / filename;
if (compareVersion && (getPluginVersion(filePathBundled) <= getPluginVersion(filePathDisk))) if (getPluginVersion(filePathBundled) <= getPluginVersion(filePathDisk))
return; return;
Utils::Fs::removeFile(filePathDisk); Utils::Fs::removeFile(filePathDisk);
Utils::Fs::copyFile(filePathBundled, filePathDisk); Utils::Fs::copyFile(filePathBundled, filePathDisk);
}; };
updateFile(Path(u"helpers.py"_s), true); updateFile(Path(u"helpers.py"_s));
updateFile(Path(u"nova2.py"_s), true); updateFile(Path(u"nova2.py"_s));
updateFile(Path(u"nova2dl.py"_s), true); updateFile(Path(u"nova2dl.py"_s));
updateFile(Path(u"novaprinter.py"_s), true); updateFile(Path(u"novaprinter.py"_s));
updateFile(Path(u"socks.py"_s), false); updateFile(Path(u"socks.py"_s));
} }
void SearchPluginManager::update() void SearchPluginManager::update()
{ {
QProcess nova; QProcess nova;
nova.setProcessEnvironment(QProcessEnvironment::systemEnvironment()); nova.setProcessEnvironment(proxyEnvironment());
#ifdef Q_OS_UNIX
nova.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif
const QStringList params const QStringList params
{ {
@ -527,7 +556,7 @@ void SearchPluginManager::update()
(engineLocation() / Path(u"/nova2.py"_s)).toString(), (engineLocation() / Path(u"/nova2.py"_s)).toString(),
u"--capabilities"_s u"--capabilities"_s
}; };
nova.start(Utils::ForeignApps::pythonInfo().executableName, params, QIODevice::ReadOnly); nova.start(Utils::ForeignApps::pythonInfo().executablePath.data(), params, QIODevice::ReadOnly);
nova.waitForFinished(); nova.waitForFinished();
const auto capabilities = QString::fromUtf8(nova.readAllStandardOutput()); const auto capabilities = QString::fromUtf8(nova.readAllStandardOutput());
@ -592,14 +621,14 @@ void SearchPluginManager::parseVersionInfo(const QByteArray &info)
QHash<QString, PluginVersion> updateInfo; QHash<QString, PluginVersion> updateInfo;
int numCorrectData = 0; int numCorrectData = 0;
const QList<QByteArrayView> lines = Utils::ByteArray::splitToViews(info, "\n", Qt::SkipEmptyParts); const QList<QByteArrayView> lines = Utils::ByteArray::splitToViews(info, "\n");
for (QByteArrayView line : lines) for (QByteArrayView line : lines)
{ {
line = line.trimmed(); line = line.trimmed();
if (line.isEmpty()) continue; if (line.isEmpty()) continue;
if (line.startsWith('#')) continue; if (line.startsWith('#')) continue;
const QList<QByteArrayView> list = Utils::ByteArray::splitToViews(line, ":", Qt::SkipEmptyParts); const QList<QByteArrayView> list = Utils::ByteArray::splitToViews(line, ":");
if (list.size() != 2) continue; if (list.size() != 2) continue;
const auto pluginName = QString::fromUtf8(list.first().trimmed()); const auto pluginName = QString::fromUtf8(list.first().trimmed());
@ -651,9 +680,10 @@ PluginVersion SearchPluginManager::getPluginVersion(const Path &filePath)
while (!pluginFile.atEnd()) while (!pluginFile.atEnd())
{ {
const auto line = QString::fromUtf8(pluginFile.readLine(lineMaxLength)).remove(u' '); const auto line = QString::fromUtf8(pluginFile.readLine(lineMaxLength)).remove(u' ');
if (!line.startsWith(u"#VERSION:", Qt::CaseInsensitive)) continue; if (!line.startsWith(u"#VERSION:", Qt::CaseInsensitive))
continue;
const QString versionStr = line.mid(9); const QString versionStr = line.sliced(9);
const auto version = PluginVersion::fromString(versionStr); const auto version = PluginVersion::fromString(versionStr);
if (version.isValid()) if (version.isValid())
return version; return version;

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