You've already forked qBittorrent
mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-10-16 20:32:23 +02:00
Compare commits
65 Commits
release-5.
...
v5_1_x
Author | SHA1 | Date | |
---|---|---|---|
![]() |
202ff8a099 | ||
![]() |
c0585441fb | ||
![]() |
a8b6cbceb0 | ||
![]() |
6ad073e0bc | ||
![]() |
ad68813fe8 | ||
![]() |
22df0b45c5 | ||
![]() |
bb34444ddc | ||
![]() |
dd5c934103 | ||
![]() |
3fca180e98 | ||
![]() |
9b29d37d21 | ||
![]() |
206d5abf84 | ||
![]() |
101f35dcf2 | ||
![]() |
13282d94ef | ||
![]() |
1daa42e4fe | ||
![]() |
ea9f3800ce | ||
![]() |
af14584772 | ||
![]() |
7d51524251 | ||
![]() |
7a9aac79f9 | ||
![]() |
085ae0d1c4 | ||
![]() |
f748a682ca | ||
![]() |
df987cc954 | ||
![]() |
535fc42747 | ||
![]() |
1da31bc2e1 | ||
![]() |
9515ca59f2 | ||
![]() |
eaf9017aa4 | ||
![]() |
f51ad39ad9 | ||
![]() |
9133b16431 | ||
![]() |
909a3eb44e | ||
![]() |
778aa64c54 | ||
![]() |
7049f80a01 | ||
![]() |
87b90b7fd7 | ||
![]() |
b3690494ab | ||
![]() |
f4e6b515c2 | ||
![]() |
a721540e6c | ||
![]() |
3fd05d001f | ||
![]() |
f04b114b64 | ||
![]() |
da87be2b12 | ||
![]() |
891265b390 | ||
![]() |
f46e44d3ed | ||
![]() |
a4094a440d | ||
![]() |
46c3da21e1 | ||
![]() |
2f06ea2587 | ||
![]() |
cfbf6b73ff | ||
![]() |
c687a7d0d3 | ||
![]() |
009cc71f9b | ||
![]() |
de1cf208ce | ||
![]() |
5f49472fa4 | ||
![]() |
2076302170 | ||
![]() |
2a33e187eb | ||
![]() |
00149e03c0 | ||
![]() |
57d529c17a | ||
![]() |
d492fcf29a | ||
![]() |
d0caa35b39 | ||
![]() |
ec7a00af92 | ||
![]() |
76a3aba7e0 | ||
![]() |
7003ac3f4d | ||
![]() |
964be0fa1c | ||
![]() |
c1defceccf | ||
![]() |
260394623d | ||
![]() |
478c2d5b12 | ||
![]() |
49cfbd9a49 | ||
![]() |
d028f46fab | ||
![]() |
57b24a200e | ||
![]() |
269dfe87e0 | ||
![]() |
6a1c465d85 |
13
.github/workflows/ci_ubuntu.yaml
vendored
13
.github/workflows/ci_ubuntu.yaml
vendored
@@ -138,16 +138,15 @@ jobs:
|
||||
|
||||
- name: Install AppImage
|
||||
run: |
|
||||
sudo apt install libfuse2
|
||||
curl \
|
||||
-L \
|
||||
-Z \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-static-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-static-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage
|
||||
chmod +x \
|
||||
linuxdeploy-static-x86_64.AppImage \
|
||||
linuxdeploy-plugin-qt-static-x86_64.AppImage \
|
||||
linuxdeploy-x86_64.AppImage \
|
||||
linuxdeploy-plugin-qt-x86_64.AppImage \
|
||||
linuxdeploy-plugin-appimage-x86_64.AppImage
|
||||
|
||||
- name: Prepare files for AppImage
|
||||
@@ -160,12 +159,12 @@ jobs:
|
||||
|
||||
- name: Package AppImage
|
||||
run: |
|
||||
./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --plugin qt
|
||||
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt
|
||||
rm qbittorrent/apprun-hooks/*
|
||||
cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh
|
||||
NO_APPSTREAM=1 \
|
||||
OUTPUT=upload/qbittorrent-CI_Ubuntu_x86_64.AppImage \
|
||||
./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --output appimage
|
||||
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --output appimage
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
119
Changelog
119
Changelog
@@ -1,4 +1,121 @@
|
||||
Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.0
|
||||
Wed Jul 02nd 2025 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.2
|
||||
- BUGFIX: Don't expose palette colors in UI theme editor since they are not customizable (glassez)
|
||||
- BUGFIX: Add fallback to update mechanism (sledgehammer999)
|
||||
- WEBUI: Fix incorrectly backported changes (glassez)
|
||||
- WEBAPI: Trim leading whitespaces on Run External Program fields (Chocobo1)
|
||||
- RSS/SEARCH: Prevent opening local files if web page is expected (glassez)
|
||||
- MACOS: Make qBittorrent quit on MacOS with main window closed (Ryu481)
|
||||
|
||||
Mon Jun 23rd 2025 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.1
|
||||
- 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
|
||||
- FEATURE: Add "Simple pread/pwrite" disk IO type (Hanabishi)
|
||||
|
@@ -47,6 +47,9 @@ find_package(Boost ${minBoostVersion} REQUIRED)
|
||||
find_package(OpenSSL ${minOpenSSLVersion} REQUIRED)
|
||||
find_package(ZLIB ${minZlibVersion} REQUIRED)
|
||||
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS Core Network Sql Xml LinguistTools)
|
||||
if (Qt6_FOUND AND (Qt6_VERSION VERSION_GREATER_EQUAL 6.10))
|
||||
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS CorePrivate)
|
||||
endif()
|
||||
if (DBUS)
|
||||
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS DBus)
|
||||
set_package_properties(Qt6DBus PROPERTIES
|
||||
|
4
dist/mac/Info.plist
vendored
4
dist/mac/Info.plist
vendored
@@ -55,7 +55,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.1.0</string>
|
||||
<string>5.1.2</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -67,7 +67,7 @@
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<string>YES</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2006-2024 The qBittorrent project</string>
|
||||
<string>Copyright © 2006-2025 The qBittorrent project</string>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
@@ -105,7 +105,7 @@ GenericName[ka]=BitTorrent კლიენტი
|
||||
Comment[ka]=გადმოტვირთეთ და გააზიარეთ ფაილები BitTorrent-ის საშუალებით
|
||||
Name[ka]=qBittorrent
|
||||
GenericName[ko]=BitTorrent 클라이언트
|
||||
Comment[ko]=BitTorrent를 통한 파일 다운로드 및 공유
|
||||
Comment[ko]=BitTorrent를 통해 파일 다운로드 및 공유
|
||||
Name[ko]=qBittorrent
|
||||
GenericName[lt]=BitTorrent klientas
|
||||
Comment[lt]=Atsisiųskite bei dalinkitės failais BitTorrent tinkle
|
||||
|
@@ -62,6 +62,6 @@
|
||||
<url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<releases>
|
||||
<release version="5.1.0~rc1" date="2025-02-11"/>
|
||||
<release version="5.1.2" date="2025-07-02"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
7
dist/windows/config.nsh
vendored
7
dist/windows/config.nsh
vendored
@@ -14,7 +14,7 @@
|
||||
; 4.5.1.3 -> good
|
||||
; 4.5.1.3.2 -> bad
|
||||
; 4.5.0beta -> bad
|
||||
!define /ifndef QBT_VERSION "5.1.0"
|
||||
!define /ifndef QBT_VERSION "5.1.2"
|
||||
|
||||
; Option that controls the installer's window name
|
||||
; If set, its value will be used like this:
|
||||
@@ -86,7 +86,7 @@ OutFile "qbittorrent_${QBT_INSTALLER_FILENAME}_setup.exe"
|
||||
;Installer Version Information
|
||||
VIAddVersionKey "ProductName" "qBittorrent"
|
||||
VIAddVersionKey "CompanyName" "The qBittorrent project"
|
||||
VIAddVersionKey "LegalCopyright" "Copyright ©2006-2024 The qBittorrent project"
|
||||
VIAddVersionKey "LegalCopyright" "Copyright ©2006-2025 The qBittorrent project"
|
||||
VIAddVersionKey "FileDescription" "qBittorrent - A Bittorrent Client"
|
||||
VIAddVersionKey "FileVersion" "${QBT_VERSION}"
|
||||
|
||||
@@ -111,7 +111,8 @@ RequestExecutionLevel user
|
||||
!define MUI_HEADERIMAGE
|
||||
!define MUI_COMPONENTSPAGE_NODESC
|
||||
;!define MUI_ICON "qbittorrent.ico"
|
||||
!define MUI_LICENSEPAGE_CHECKBOX
|
||||
!define MUI_LICENSEPAGE_BUTTON $(^NextBtn)
|
||||
!define MUI_LICENSEPAGE_TEXT_BOTTOM "$_CLICK"
|
||||
!define MUI_LANGDLL_ALLLANGUAGES
|
||||
|
||||
;--------------------------------
|
||||
|
12
dist/windows/installer-translations/swedish.nsh
vendored
12
dist/windows/installer-translations/swedish.nsh
vendored
@@ -7,21 +7,21 @@ LangString inst_desktop ${LANG_SWEDISH} "Skapa skrivbordsgenväg"
|
||||
;LangString inst_startmenu ${LANG_ENGLISH} "Create Start Menu Shortcut"
|
||||
LangString inst_startmenu ${LANG_SWEDISH} "Skapa startmenygenväg"
|
||||
;LangString inst_startup ${LANG_ENGLISH} "Start qBittorrent on Windows start up"
|
||||
LangString inst_startup ${LANG_SWEDISH} "Starta qBittorrent vid Windows start"
|
||||
LangString inst_startup ${LANG_SWEDISH} "Starta qBittorrent vid Windows-uppstart"
|
||||
;LangString inst_torrent ${LANG_ENGLISH} "Open .torrent files with qBittorrent"
|
||||
LangString inst_torrent ${LANG_SWEDISH} "Öppna .torrent-filer med qBittorrent"
|
||||
;LangString inst_magnet ${LANG_ENGLISH} "Open magnet links with qBittorrent"
|
||||
LangString inst_magnet ${LANG_SWEDISH} "Öppna magnetlänkar med qBittorrent"
|
||||
;LangString inst_firewall ${LANG_ENGLISH} "Add Windows Firewall rule"
|
||||
LangString inst_firewall ${LANG_SWEDISH} "Lägg till Windows-brandväggregel"
|
||||
LangString inst_firewall ${LANG_SWEDISH} "Lägg till Windows-brandväggsregel"
|
||||
;LangString inst_pathlimit ${LANG_ENGLISH} "Disable Windows path length limit (260 character MAX_PATH limitation, requires Windows 10 1607 or later)"
|
||||
LangString inst_pathlimit ${LANG_SWEDISH} "Inaktivera gränsen för Windows-sökvägslängd (260 tecken MAX_PATH-begränsning, kräver Windows 10 1607 eller senare)"
|
||||
;LangString inst_firewallinfo ${LANG_ENGLISH} "Adding Windows Firewall rule"
|
||||
LangString inst_firewallinfo ${LANG_SWEDISH} "Lägger till Windows-brandväggregel"
|
||||
LangString inst_firewallinfo ${LANG_SWEDISH} "Lägger till Windows-brandväggsregel"
|
||||
;LangString inst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before installing."
|
||||
LangString inst_warning ${LANG_SWEDISH} "qBittorrent körs. Vänligen stäng programmet innan du installerar."
|
||||
LangString inst_warning ${LANG_SWEDISH} "qBittorrent körs. Stäng programmet innan du installerar."
|
||||
;LangString inst_uninstall_question ${LANG_ENGLISH} "Current version will be uninstalled. User settings and torrents will remain intact."
|
||||
LangString inst_uninstall_question ${LANG_SWEDISH} "Nuvarande version avinstalleras. Användarinställningar och torrenter kommer att förbli intakta."
|
||||
LangString inst_uninstall_question ${LANG_SWEDISH} "Aktuell version avinstalleras. Användarinställningar och torrenter kommer att förbli intakta."
|
||||
;LangString inst_unist ${LANG_ENGLISH} "Uninstalling previous version."
|
||||
LangString inst_unist ${LANG_SWEDISH} "Avinstallerar tidigare version."
|
||||
;LangString launch_qbt ${LANG_ENGLISH} "Launch qBittorrent."
|
||||
@@ -53,7 +53,7 @@ LangString remove_firewallinfo ${LANG_SWEDISH} "Tar bort Windows-brandväggsrege
|
||||
;LangString remove_cache ${LANG_ENGLISH} "Remove torrents and cached data"
|
||||
LangString remove_cache ${LANG_SWEDISH} "Ta bort torrenter och cachade data"
|
||||
;LangString uninst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before uninstalling."
|
||||
LangString uninst_warning ${LANG_SWEDISH} "qBittorrent körs. Vänligen stäng programmet innan du avinstallerar."
|
||||
LangString uninst_warning ${LANG_SWEDISH} "qBittorrent körs. Stäng programmet innan du avinstallerar."
|
||||
;LangString uninst_tor_warn ${LANG_ENGLISH} "Not removing .torrent association. It is associated with:"
|
||||
LangString uninst_tor_warn ${LANG_SWEDISH} "Tar inte bort .torrent-association. Den är associerad med:"
|
||||
;LangString uninst_mag_warn ${LANG_ENGLISH} "Not removing magnet association. It is associated with:"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
@@ -124,6 +124,28 @@ namespace
|
||||
const int PIXMAP_CACHE_SIZE = 64 * 1024 * 1024; // 64MiB
|
||||
#endif
|
||||
|
||||
const QString PARAM_ADDSTOPPED = u"@addStopped"_s;
|
||||
const QString PARAM_CATEGORY = u"@category"_s;
|
||||
const QString PARAM_FIRSTLASTPIECEPRIORITY = u"@firstLastPiecePriority"_s;
|
||||
const QString PARAM_SAVEPATH = u"@savePath"_s;
|
||||
const QString PARAM_SEQUENTIAL = u"@sequential"_s;
|
||||
const QString PARAM_SKIPCHECKING = u"@skipChecking"_s;
|
||||
const QString PARAM_SKIPDIALOG = u"@skipDialog"_s;
|
||||
|
||||
QString bindParamValue(const QStringView paramName, const QStringView paramValue)
|
||||
{
|
||||
return paramName + u'=' + paramValue;
|
||||
}
|
||||
|
||||
std::pair<QStringView, QStringView> parseParam(const QStringView param)
|
||||
{
|
||||
const qsizetype sepIndex = param.indexOf(u'=');
|
||||
if (sepIndex >= 0)
|
||||
return {param.first(sepIndex), param.sliced(sepIndex + 1)};
|
||||
|
||||
return {param, {}};
|
||||
}
|
||||
|
||||
QString serializeParams(const QBtCommandLineParameters ¶ms)
|
||||
{
|
||||
QStringList result;
|
||||
@@ -138,85 +160,86 @@ namespace
|
||||
const BitTorrent::AddTorrentParams &addTorrentParams = params.addTorrentParams;
|
||||
|
||||
if (!addTorrentParams.savePath.isEmpty())
|
||||
result.append(u"@savePath=" + addTorrentParams.savePath.data());
|
||||
result.append(bindParamValue(PARAM_SAVEPATH, addTorrentParams.savePath.data()));
|
||||
|
||||
if (addTorrentParams.addStopped.has_value())
|
||||
result.append(*addTorrentParams.addStopped ? u"@addStopped=1"_s : u"@addStopped=0"_s);
|
||||
result.append(bindParamValue(PARAM_ADDSTOPPED, (*addTorrentParams.addStopped ? u"1" : u"0")));
|
||||
|
||||
if (addTorrentParams.skipChecking)
|
||||
result.append(u"@skipChecking"_s);
|
||||
result.append(PARAM_SKIPCHECKING);
|
||||
|
||||
if (!addTorrentParams.category.isEmpty())
|
||||
result.append(u"@category=" + addTorrentParams.category);
|
||||
result.append(bindParamValue(PARAM_CATEGORY, addTorrentParams.category));
|
||||
|
||||
if (addTorrentParams.sequential)
|
||||
result.append(u"@sequential"_s);
|
||||
result.append(PARAM_SEQUENTIAL);
|
||||
|
||||
if (addTorrentParams.firstLastPiecePriority)
|
||||
result.append(u"@firstLastPiecePriority"_s);
|
||||
result.append(PARAM_FIRSTLASTPIECEPRIORITY);
|
||||
|
||||
if (params.skipDialog.has_value())
|
||||
result.append(*params.skipDialog ? u"@skipDialog=1"_s : u"@skipDialog=0"_s);
|
||||
result.append(bindParamValue(PARAM_SKIPDIALOG, (*params.skipDialog ? u"1" : u"0")));
|
||||
|
||||
result += params.torrentSources;
|
||||
|
||||
return result.join(PARAMS_SEPARATOR);
|
||||
}
|
||||
|
||||
QBtCommandLineParameters parseParams(const QString &str)
|
||||
QBtCommandLineParameters parseParams(const QStringView str)
|
||||
{
|
||||
QBtCommandLineParameters parsedParams;
|
||||
BitTorrent::AddTorrentParams &addTorrentParams = parsedParams.addTorrentParams;
|
||||
|
||||
for (QString param : asConst(str.split(PARAMS_SEPARATOR, Qt::SkipEmptyParts)))
|
||||
for (QStringView param : asConst(str.split(PARAMS_SEPARATOR, Qt::SkipEmptyParts)))
|
||||
{
|
||||
param = param.trimmed();
|
||||
const auto [paramName, paramValue] = parseParam(param);
|
||||
|
||||
// Process strings indicating options specified by the user.
|
||||
|
||||
if (param.startsWith(u"@savePath="))
|
||||
if (paramName == PARAM_SAVEPATH)
|
||||
{
|
||||
addTorrentParams.savePath = Path(param.mid(10));
|
||||
addTorrentParams.savePath = Path(paramValue.toString());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param.startsWith(u"@addStopped="))
|
||||
if (paramName == PARAM_ADDSTOPPED)
|
||||
{
|
||||
addTorrentParams.addStopped = (QStringView(param).mid(11).toInt() != 0);
|
||||
addTorrentParams.addStopped = (paramValue.toInt() != 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param == u"@skipChecking")
|
||||
if (paramName == PARAM_SKIPCHECKING)
|
||||
{
|
||||
addTorrentParams.skipChecking = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param.startsWith(u"@category="))
|
||||
if (paramName == PARAM_CATEGORY)
|
||||
{
|
||||
addTorrentParams.category = param.mid(10);
|
||||
addTorrentParams.category = paramValue.toString();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param == u"@sequential")
|
||||
if (paramName == PARAM_SEQUENTIAL)
|
||||
{
|
||||
addTorrentParams.sequential = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param == u"@firstLastPiecePriority")
|
||||
if (paramName == PARAM_FIRSTLASTPIECEPRIORITY)
|
||||
{
|
||||
addTorrentParams.firstLastPiecePriority = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param.startsWith(u"@skipDialog="))
|
||||
if (paramName == PARAM_SKIPDIALOG)
|
||||
{
|
||||
parsedParams.skipDialog = (QStringView(param).mid(12).toInt() != 0);
|
||||
parsedParams.skipDialog = (paramValue.toInt() != 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedParams.torrentSources.append(param);
|
||||
parsedParams.torrentSources.append(param.toString());
|
||||
}
|
||||
|
||||
return parsedParams;
|
||||
@@ -897,10 +920,10 @@ int Application::exec()
|
||||
m_desktopIntegration->showNotification(tr("Torrent added"), tr("'%1' was added.", "e.g: xxx.avi was added.").arg(torrent->name()));
|
||||
});
|
||||
connect(m_addTorrentManager, &AddTorrentManager::addTorrentFailed, this
|
||||
, [this](const QString &source, const QString &reason)
|
||||
, [this](const QString &source, const BitTorrent::AddTorrentError &reason)
|
||||
{
|
||||
m_desktopIntegration->showNotification(tr("Add torrent failed")
|
||||
, tr("Couldn't add torrent '%1', reason: %2.").arg(source, reason));
|
||||
, tr("Couldn't add torrent '%1', reason: %2.").arg(source, reason.message));
|
||||
});
|
||||
|
||||
disconnect(m_desktopIntegration, &DesktopIntegration::activationRequested, this, &Application::createStartupProgressDialog);
|
||||
|
@@ -491,6 +491,12 @@ QString makeUsage(const QString &prgName)
|
||||
{
|
||||
const QString indentation {USAGE_INDENTATION, u' '};
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
const QString noSplashCommand = u"set QBT_NO_SPLASH=1 && " + prgName;
|
||||
#else
|
||||
const QString noSplashCommand = u"QBT_NO_SPLASH=1 " + prgName;
|
||||
#endif
|
||||
|
||||
const QString text = QCoreApplication::translate("CMD Options", "Usage:") + u'\n'
|
||||
+ indentation + prgName + u' ' + QCoreApplication::translate("CMD Options", "[options] [(<filename> | <url>)...]") + u'\n'
|
||||
|
||||
@@ -542,7 +548,7 @@ QString makeUsage(const QString &prgName)
|
||||
"'parameter-name', environment variable name is 'QBT_PARAMETER_NAME' (in upper "
|
||||
"case, '-' replaced with '_'). To pass flag values, set the variable to '1' or "
|
||||
"'TRUE'. For example, to disable the splash screen: "), 0) + u'\n'
|
||||
+ u"QBT_NO_SPLASH=1 " + prgName + u'\n'
|
||||
+ noSplashCommand + u'\n'
|
||||
+ wrapText(QCoreApplication::translate("CMD Options", "Command line parameters take precedence over environment variables"), 0) + u'\n';
|
||||
|
||||
return text;
|
||||
|
@@ -6,6 +6,7 @@ add_library(qbt_base STATIC
|
||||
applicationcomponent.h
|
||||
asyncfilestorage.h
|
||||
bittorrent/abstractfilestorage.h
|
||||
bittorrent/addtorrenterror.h
|
||||
bittorrent/addtorrentparams.h
|
||||
bittorrent/announcetimepoint.h
|
||||
bittorrent/bandwidthscheduler.h
|
||||
|
@@ -140,7 +140,7 @@ void AddTorrentManager::onSessionTorrentAdded(BitTorrent::Torrent *torrent)
|
||||
}
|
||||
}
|
||||
|
||||
void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const QString &reason)
|
||||
void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const BitTorrent::AddTorrentError &reason)
|
||||
{
|
||||
if (const QString source = m_sourcesByInfoHash.take(infoHash); !source.isEmpty())
|
||||
{
|
||||
@@ -154,7 +154,7 @@ void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &in
|
||||
void AddTorrentManager::handleAddTorrentFailed(const QString &source, const QString &reason)
|
||||
{
|
||||
LogMsg(tr("Failed to add torrent. Source: \"%1\". Reason: \"%2\"").arg(source, reason), Log::WARNING);
|
||||
emit addTorrentFailed(source, reason);
|
||||
emit addTorrentFailed(source, {BitTorrent::AddTorrentError::Other, reason});
|
||||
}
|
||||
|
||||
void AddTorrentManager::handleDuplicateTorrent(const QString &source
|
||||
@@ -187,7 +187,7 @@ void AddTorrentManager::handleDuplicateTorrent(const QString &source
|
||||
|
||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Source: %1. Existing torrent: %2. Result: %3")
|
||||
.arg(source, existingTorrent->name(), message));
|
||||
emit addTorrentFailed(source, message);
|
||||
emit addTorrentFailed(source, {BitTorrent::AddTorrentError::DuplicateTorrent, message});
|
||||
}
|
||||
|
||||
void AddTorrentManager::setTorrentFileGuard(const QString &source, std::shared_ptr<TorrentFileGuard> torrentFileGuard)
|
||||
|
@@ -35,6 +35,7 @@
|
||||
#include <QObject>
|
||||
|
||||
#include "base/applicationcomponent.h"
|
||||
#include "base/bittorrent/addtorrenterror.h"
|
||||
#include "base/bittorrent/addtorrentparams.h"
|
||||
#include "base/torrentfileguard.h"
|
||||
|
||||
@@ -66,7 +67,7 @@ public:
|
||||
|
||||
signals:
|
||||
void torrentAdded(const QString &source, BitTorrent::Torrent *torrent);
|
||||
void addTorrentFailed(const QString &source, const QString &reason);
|
||||
void addTorrentFailed(const QString &source, const BitTorrent::AddTorrentError &reason);
|
||||
|
||||
protected:
|
||||
bool addTorrentToSession(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
|
||||
@@ -79,7 +80,7 @@ protected:
|
||||
private:
|
||||
void onDownloadFinished(const Net::DownloadResult &result);
|
||||
void onSessionTorrentAdded(BitTorrent::Torrent *torrent);
|
||||
void onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const QString &reason);
|
||||
void onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const BitTorrent::AddTorrentError &reason);
|
||||
bool processTorrent(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
|
||||
, const BitTorrent::AddTorrentParams &addTorrentParams);
|
||||
|
||||
|
49
src/base/bittorrent/addtorrenterror.h
Normal file
49
src/base/bittorrent/addtorrenterror.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
struct AddTorrentError
|
||||
{
|
||||
enum Kind
|
||||
{
|
||||
DuplicateTorrent,
|
||||
Other
|
||||
};
|
||||
|
||||
Kind kind = Other;
|
||||
QString message;
|
||||
};
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(BitTorrent::AddTorrentError)
|
@@ -147,7 +147,7 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::load(cons
|
||||
const Path torrentFilePath = path() / Path(idString + u".torrent");
|
||||
const qint64 torrentSizeLimit = Preferences::instance()->getTorrentFileSizeLimit();
|
||||
|
||||
const auto resumeDataReadResult = Utils::IO::readFile(fastresumePath, torrentSizeLimit);
|
||||
const auto resumeDataReadResult = Utils::IO::readFile(fastresumePath, -1);
|
||||
if (!resumeDataReadResult)
|
||||
return nonstd::make_unexpected(resumeDataReadResult.error().message);
|
||||
|
||||
@@ -290,6 +290,8 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
|
||||
lt::add_torrent_params &p = torrentParams.ltAddTorrentParams;
|
||||
|
||||
p = lt::read_resume_data(resumeDataRoot, ec);
|
||||
if (ec)
|
||||
return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
|
||||
|
||||
if (!metadata.isEmpty())
|
||||
{
|
||||
@@ -320,6 +322,8 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
|
||||
|
||||
p.save_path = Profile::instance()->fromPortablePath(
|
||||
Path(fromLTString(p.save_path))).toString().toStdString();
|
||||
if (p.save_path.empty())
|
||||
return nonstd::make_unexpected(tr("Corrupted resume data: %1").arg(tr("save_path is invalid")));
|
||||
|
||||
torrentParams.stopped = (p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed);
|
||||
torrentParams.operatingMode = (p.flags & lt::torrent_flags::paused) || (p.flags & lt::torrent_flags::auto_managed)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2021-2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2021-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@@ -217,80 +217,6 @@ namespace
|
||||
{
|
||||
return u"%1 %2"_s.arg(quoted(column.name), definition);
|
||||
}
|
||||
|
||||
LoadTorrentParams parseQueryResultRow(const QSqlQuery &query)
|
||||
{
|
||||
LoadTorrentParams resumeData;
|
||||
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
|
||||
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
|
||||
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
|
||||
if (!tagsData.isEmpty())
|
||||
{
|
||||
const QStringList tagList = tagsData.split(u',');
|
||||
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
|
||||
}
|
||||
resumeData.hasFinishedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
|
||||
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
|
||||
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
|
||||
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
|
||||
resumeData.inactiveSeedingTimeLimit = query.value(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT.name).toInt();
|
||||
resumeData.shareLimitAction = Utils::String::toEnum<ShareLimitAction>(
|
||||
query.value(DB_COLUMN_SHARE_LIMIT_ACTION.name).toString(), ShareLimitAction::Default);
|
||||
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
|
||||
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
|
||||
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
|
||||
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
|
||||
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
|
||||
resumeData.stopCondition = Utils::String::toEnum(
|
||||
query.value(DB_COLUMN_STOP_CONDITION.name).toString(), Torrent::StopCondition::None);
|
||||
resumeData.sslParameters =
|
||||
{
|
||||
.certificate = QSslCertificate(query.value(DB_COLUMN_SSL_CERTIFICATE.name).toByteArray()),
|
||||
.privateKey = Utils::SSLKey::load(query.value(DB_COLUMN_SSL_PRIVATE_KEY.name).toByteArray()),
|
||||
.dhParams = query.value(DB_COLUMN_SSL_DH_PARAMS.name).toByteArray()
|
||||
};
|
||||
|
||||
resumeData.savePath = Profile::instance()->fromPortablePath(
|
||||
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
|
||||
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
|
||||
if (!resumeData.useAutoTMM)
|
||||
{
|
||||
resumeData.downloadPath = Profile::instance()->fromPortablePath(
|
||||
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
|
||||
}
|
||||
|
||||
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
|
||||
const auto *pref = Preferences::instance();
|
||||
const int bdecodeDepthLimit = pref->getBdecodeDepthLimit();
|
||||
const int bdecodeTokenLimit = pref->getBdecodeTokenLimit();
|
||||
|
||||
lt::error_code ec;
|
||||
const lt::bdecode_node resumeDataRoot = lt::bdecode(bencodedResumeData, ec
|
||||
, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
||||
|
||||
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
|
||||
|
||||
p = lt::read_resume_data(resumeDataRoot, ec);
|
||||
|
||||
if (const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray()
|
||||
; !bencodedMetadata.isEmpty())
|
||||
{
|
||||
const lt::bdecode_node torentInfoRoot = lt::bdecode(bencodedMetadata, ec
|
||||
, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
||||
p.ti = std::make_shared<lt::torrent_info>(torentInfoRoot, ec);
|
||||
}
|
||||
|
||||
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
|
||||
.toString().toStdString();
|
||||
|
||||
if (p.flags & lt::torrent_flags::stop_when_ready)
|
||||
{
|
||||
p.flags &= ~lt::torrent_flags::stop_when_ready;
|
||||
resumeData.stopCondition = Torrent::StopCondition::FilesChecked;
|
||||
}
|
||||
|
||||
return resumeData;
|
||||
}
|
||||
}
|
||||
|
||||
namespace BitTorrent
|
||||
@@ -688,6 +614,90 @@ void BitTorrent::DBResumeDataStorage::enableWALMode() const
|
||||
throw RuntimeError(tr("WAL mode is probably unsupported due to filesystem limitations."));
|
||||
}
|
||||
|
||||
LoadResumeDataResult DBResumeDataStorage::parseQueryResultRow(const QSqlQuery &query) const
|
||||
{
|
||||
LoadTorrentParams resumeData;
|
||||
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
|
||||
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
|
||||
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
|
||||
if (!tagsData.isEmpty())
|
||||
{
|
||||
const QStringList tagList = tagsData.split(u',');
|
||||
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
|
||||
}
|
||||
resumeData.hasFinishedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
|
||||
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
|
||||
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
|
||||
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
|
||||
resumeData.inactiveSeedingTimeLimit = query.value(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT.name).toInt();
|
||||
resumeData.shareLimitAction = Utils::String::toEnum<ShareLimitAction>(
|
||||
query.value(DB_COLUMN_SHARE_LIMIT_ACTION.name).toString(), ShareLimitAction::Default);
|
||||
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
|
||||
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
|
||||
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
|
||||
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
|
||||
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
|
||||
resumeData.stopCondition = Utils::String::toEnum(
|
||||
query.value(DB_COLUMN_STOP_CONDITION.name).toString(), Torrent::StopCondition::None);
|
||||
resumeData.sslParameters =
|
||||
{
|
||||
.certificate = QSslCertificate(query.value(DB_COLUMN_SSL_CERTIFICATE.name).toByteArray()),
|
||||
.privateKey = Utils::SSLKey::load(query.value(DB_COLUMN_SSL_PRIVATE_KEY.name).toByteArray()),
|
||||
.dhParams = query.value(DB_COLUMN_SSL_DH_PARAMS.name).toByteArray()
|
||||
};
|
||||
|
||||
resumeData.savePath = Profile::instance()->fromPortablePath(
|
||||
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
|
||||
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
|
||||
if (!resumeData.useAutoTMM)
|
||||
{
|
||||
resumeData.downloadPath = Profile::instance()->fromPortablePath(
|
||||
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
|
||||
}
|
||||
|
||||
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
|
||||
const auto *pref = Preferences::instance();
|
||||
const int bdecodeDepthLimit = pref->getBdecodeDepthLimit();
|
||||
const int bdecodeTokenLimit = pref->getBdecodeTokenLimit();
|
||||
|
||||
lt::error_code ec;
|
||||
const lt::bdecode_node resumeDataRoot = lt::bdecode(bencodedResumeData, ec, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
||||
if (ec)
|
||||
return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
|
||||
|
||||
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
|
||||
|
||||
p = lt::read_resume_data(resumeDataRoot, ec);
|
||||
if (ec)
|
||||
return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
|
||||
|
||||
if (const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray()
|
||||
; !bencodedMetadata.isEmpty())
|
||||
{
|
||||
const lt::bdecode_node torentInfoRoot = lt::bdecode(bencodedMetadata, ec
|
||||
, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
||||
if (ec)
|
||||
return nonstd::make_unexpected(tr("Cannot parse torrent info: %1").arg(QString::fromStdString(ec.message())));
|
||||
|
||||
p.ti = std::make_shared<lt::torrent_info>(torentInfoRoot, ec);
|
||||
if (ec)
|
||||
return nonstd::make_unexpected(tr("Cannot parse torrent info: %1").arg(QString::fromStdString(ec.message())));
|
||||
}
|
||||
|
||||
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
|
||||
.toString().toStdString();
|
||||
if (p.save_path.empty())
|
||||
return nonstd::make_unexpected(tr("Corrupted resume data: %1").arg(tr("save_path is invalid")));
|
||||
|
||||
if (p.flags & lt::torrent_flags::stop_when_ready)
|
||||
{
|
||||
p.flags &= ~lt::torrent_flags::stop_when_ready;
|
||||
resumeData.stopCondition = Torrent::StopCondition::FilesChecked;
|
||||
}
|
||||
|
||||
return resumeData;
|
||||
}
|
||||
|
||||
BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, QReadWriteLock &dbLock, QObject *parent)
|
||||
: QThread(parent)
|
||||
, m_path {dbPath}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2021-2022 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2021-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@@ -31,9 +31,10 @@
|
||||
#include <QReadWriteLock>
|
||||
|
||||
#include "base/pathfwd.h"
|
||||
#include "base/utils/thread.h"
|
||||
#include "resumedatastorage.h"
|
||||
|
||||
class QSqlQuery;
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
class DBResumeDataStorage final : public ResumeDataStorage
|
||||
@@ -58,6 +59,7 @@ namespace BitTorrent
|
||||
void createDB() const;
|
||||
void updateDB(int fromVersion) const;
|
||||
void enableWALMode() const;
|
||||
LoadResumeDataResult parseQueryResultRow(const QSqlQuery &query) const;
|
||||
|
||||
class Worker;
|
||||
Worker *m_asyncWorker = nullptr;
|
||||
|
@@ -34,6 +34,7 @@
|
||||
|
||||
#include "base/pathfwd.h"
|
||||
#include "base/tagset.h"
|
||||
#include "addtorrenterror.h"
|
||||
#include "addtorrentparams.h"
|
||||
#include "categoryoptions.h"
|
||||
#include "sharelimitaction.h"
|
||||
@@ -481,7 +482,7 @@ namespace BitTorrent
|
||||
|
||||
signals:
|
||||
void startupProgressUpdated(int progress);
|
||||
void addTorrentFailed(const InfoHash &infoHash, const QString &reason);
|
||||
void addTorrentFailed(const InfoHash &infoHash, const AddTorrentError &reason);
|
||||
void allTorrentsFinished();
|
||||
void categoryAdded(const QString &categoryName);
|
||||
void categoryRemoved(const QString &categoryName);
|
||||
|
@@ -467,9 +467,11 @@ SessionImpl::SessionImpl(QObject *parent)
|
||||
, m_additionalTrackers(BITTORRENT_SESSION_KEY(u"AdditionalTrackers"_s))
|
||||
, m_isAddTrackersFromURLEnabled(BITTORRENT_SESSION_KEY(u"AddTrackersFromURLEnabled"_s), false)
|
||||
, m_additionalTrackersURL(BITTORRENT_SESSION_KEY(u"AdditionalTrackersURL"_s))
|
||||
, m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r;})
|
||||
, m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s), -1, lowerLimited(-1))
|
||||
, m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s), -1, lowerLimited(-1))
|
||||
, m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r; })
|
||||
, m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s)
|
||||
, Torrent::NO_SEEDING_TIME_LIMIT, lowerLimited(Torrent::NO_SEEDING_TIME_LIMIT))
|
||||
, m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s)
|
||||
, Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT, lowerLimited(Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT))
|
||||
, m_isAddTorrentToQueueTop(BITTORRENT_SESSION_KEY(u"AddTorrentToTopOfQueue"_s), false)
|
||||
, m_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false)
|
||||
, m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_s), Torrent::StopCondition::None)
|
||||
@@ -974,23 +976,25 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio
|
||||
if (options == currentOptions)
|
||||
return false;
|
||||
|
||||
currentOptions = options;
|
||||
storeCategories();
|
||||
if (isDisableAutoTMMWhenCategorySavePathChanged())
|
||||
{
|
||||
// This should be done before changing the category options
|
||||
// to prevent the torrent from being moved at the new save path.
|
||||
|
||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||
{
|
||||
if (torrent->category() == name)
|
||||
torrent->setAutoTMMEnabled(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
currentOptions = options;
|
||||
storeCategories();
|
||||
|
||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||
{
|
||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||
{
|
||||
if (torrent->category() == name)
|
||||
torrent->handleCategoryOptionsChanged();
|
||||
}
|
||||
if (torrent->category() == name)
|
||||
torrent->handleCategoryOptionsChanged();
|
||||
}
|
||||
|
||||
emit categoryOptionsChanged(name);
|
||||
@@ -1218,7 +1222,7 @@ qreal SessionImpl::globalMaxRatio() const
|
||||
void SessionImpl::setGlobalMaxRatio(qreal ratio)
|
||||
{
|
||||
if (ratio < 0)
|
||||
ratio = -1.;
|
||||
ratio = Torrent::NO_RATIO_LIMIT;
|
||||
|
||||
if (ratio != globalMaxRatio())
|
||||
{
|
||||
@@ -1234,8 +1238,7 @@ int SessionImpl::globalMaxSeedingMinutes() const
|
||||
|
||||
void SessionImpl::setGlobalMaxSeedingMinutes(int minutes)
|
||||
{
|
||||
if (minutes < 0)
|
||||
minutes = -1;
|
||||
minutes = std::max(minutes, Torrent::NO_SEEDING_TIME_LIMIT);
|
||||
|
||||
if (minutes != globalMaxSeedingMinutes())
|
||||
{
|
||||
@@ -1251,7 +1254,7 @@ int SessionImpl::globalMaxInactiveSeedingMinutes() const
|
||||
|
||||
void SessionImpl::setGlobalMaxInactiveSeedingMinutes(int minutes)
|
||||
{
|
||||
minutes = std::max(minutes, -1);
|
||||
minutes = std::max(minutes, Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT);
|
||||
|
||||
if (minutes != globalMaxInactiveSeedingMinutes())
|
||||
{
|
||||
@@ -2310,19 +2313,19 @@ void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
|
||||
QString description;
|
||||
|
||||
if (const qreal ratio = torrent->realRatio();
|
||||
(ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit))
|
||||
(ratioLimit >= 0) && (ratio >= ratioLimit))
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the share ratio limit.");
|
||||
}
|
||||
else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60;
|
||||
(seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit))
|
||||
(seedingTimeLimit >= 0) && (seedingTimeInMinutes >= seedingTimeLimit))
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the seeding time limit.");
|
||||
}
|
||||
else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60;
|
||||
(inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
|
||||
(inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the inactive seeding time limit.");
|
||||
@@ -2751,7 +2754,10 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
|
||||
// We should not add the torrent if it is already
|
||||
// processed or is pending to add to session
|
||||
if (m_loadingTorrents.contains(id) || (infoHash.isHybrid() && m_loadingTorrents.contains(altID)))
|
||||
{
|
||||
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, tr("Duplicate torrent")});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Torrent *torrent = findTorrent(infoHash))
|
||||
{
|
||||
@@ -2765,16 +2771,20 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
|
||||
|
||||
if (!isMergeTrackersEnabled())
|
||||
{
|
||||
const QString message = tr("Merging of trackers is disabled");
|
||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
|
||||
.arg(torrent->name(), tr("Merging of trackers is disabled")));
|
||||
.arg(torrent->name(), message));
|
||||
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool isPrivate = torrent->isPrivate() || (hasMetadata && source.info()->isPrivate());
|
||||
if (isPrivate)
|
||||
{
|
||||
const QString message = tr("Trackers cannot be merged because it is a private torrent");
|
||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
|
||||
.arg(torrent->name(), tr("Trackers cannot be merged because it is a private torrent")));
|
||||
.arg(torrent->name(), message));
|
||||
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2782,8 +2792,10 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
|
||||
torrent->addTrackers(source.trackers());
|
||||
torrent->addUrlSeeds(source.urlSeeds());
|
||||
|
||||
const QString message = tr("Trackers are merged from new source");
|
||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
|
||||
.arg(torrent->name(), tr("Trackers are merged from new source")));
|
||||
.arg(torrent->name(), message));
|
||||
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3247,6 +3259,9 @@ void SessionImpl::setSavePath(const Path &path)
|
||||
|
||||
if (isDisableAutoTMMWhenDefaultSavePathChanged())
|
||||
{
|
||||
// This should be done before changing the save path
|
||||
// to prevent the torrent from being moved at the new save path.
|
||||
|
||||
QSet<QString> affectedCatogories {{}}; // includes default (unnamed) category
|
||||
for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
|
||||
{
|
||||
@@ -3276,6 +3291,9 @@ void SessionImpl::setDownloadPath(const Path &path)
|
||||
|
||||
if (isDisableAutoTMMWhenDefaultSavePathChanged())
|
||||
{
|
||||
// This should be done before changing the save path
|
||||
// to prevent the torrent from being moved at the new save path.
|
||||
|
||||
QSet<QString> affectedCatogories {{}}; // includes default (unnamed) category
|
||||
for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
|
||||
{
|
||||
@@ -5699,7 +5717,9 @@ void SessionImpl::handleAddTorrentAlert(const lt::add_torrent_alert *alert)
|
||||
if (const auto loadingTorrentsIter = m_loadingTorrents.find(TorrentID::fromInfoHash(infoHash))
|
||||
; loadingTorrentsIter != m_loadingTorrents.end())
|
||||
{
|
||||
emit addTorrentFailed(infoHash, msg);
|
||||
const AddTorrentError::Kind errorKind = (alert->error == lt::errors::duplicate_torrent)
|
||||
? AddTorrentError::DuplicateTorrent : AddTorrentError::Other;
|
||||
emit addTorrentFailed(infoHash, {errorKind, msg});
|
||||
m_loadingTorrents.erase(loadingTorrentsIter);
|
||||
}
|
||||
else if (const auto downloadedMetadataIter = m_downloadedMetadata.find(TorrentID::fromInfoHash(infoHash))
|
||||
|
@@ -29,6 +29,8 @@
|
||||
|
||||
#include "torrent.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <QHash>
|
||||
|
||||
#include "infohash.h"
|
||||
@@ -51,9 +53,7 @@ namespace BitTorrent
|
||||
const int Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME = -2;
|
||||
const int Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT = -1;
|
||||
|
||||
const qreal Torrent::MAX_RATIO = 9999;
|
||||
const int Torrent::MAX_SEEDING_TIME = 525600;
|
||||
const int Torrent::MAX_INACTIVE_SEEDING_TIME = 525600;
|
||||
const qreal Torrent::MAX_RATIO = std::numeric_limits<qreal>::infinity();
|
||||
|
||||
TorrentID Torrent::id() const
|
||||
{
|
||||
|
@@ -132,8 +132,6 @@ namespace BitTorrent
|
||||
static const int NO_INACTIVE_SEEDING_TIME_LIMIT;
|
||||
|
||||
static const qreal MAX_RATIO;
|
||||
static const int MAX_SEEDING_TIME;
|
||||
static const int MAX_INACTIVE_SEEDING_TIME;
|
||||
|
||||
using TorrentContentHandler::TorrentContentHandler;
|
||||
|
||||
|
@@ -1549,7 +1549,8 @@ qreal TorrentImpl::realRatio() const
|
||||
|
||||
const qreal ratio = upload / static_cast<qreal>(download);
|
||||
Q_ASSERT(ratio >= 0);
|
||||
return (ratio > MAX_RATIO) ? MAX_RATIO : ratio;
|
||||
|
||||
return ratio;
|
||||
}
|
||||
|
||||
int TorrentImpl::uploadPayloadRate() const
|
||||
@@ -1615,18 +1616,20 @@ bool TorrentImpl::setCategory(const QString &category)
|
||||
if (!category.isEmpty() && !m_session->categories().contains(category))
|
||||
return false;
|
||||
|
||||
if (m_session->isDisableAutoTMMWhenCategoryChanged())
|
||||
{
|
||||
// This should be done before changing the category name
|
||||
// to prevent the torrent from being moved at the path of new category.
|
||||
setAutoTMMEnabled(false);
|
||||
}
|
||||
|
||||
const QString oldCategory = m_category;
|
||||
m_category = category;
|
||||
deferredRequestResumeData();
|
||||
m_session->handleTorrentCategoryChanged(this, oldCategory);
|
||||
|
||||
if (m_useAutoTMM)
|
||||
{
|
||||
if (!m_session->isDisableAutoTMMWhenCategoryChanged())
|
||||
adjustStorageLocation();
|
||||
else
|
||||
setAutoTMMEnabled(false);
|
||||
}
|
||||
adjustStorageLocation();
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -2710,8 +2713,6 @@ void TorrentImpl::setRatioLimit(qreal limit)
|
||||
{
|
||||
if (limit < USE_GLOBAL_RATIO)
|
||||
limit = NO_RATIO_LIMIT;
|
||||
else if (limit > MAX_RATIO)
|
||||
limit = MAX_RATIO;
|
||||
|
||||
if (m_ratioLimit != limit)
|
||||
{
|
||||
@@ -2725,8 +2726,6 @@ void TorrentImpl::setSeedingTimeLimit(int limit)
|
||||
{
|
||||
if (limit < USE_GLOBAL_SEEDING_TIME)
|
||||
limit = NO_SEEDING_TIME_LIMIT;
|
||||
else if (limit > MAX_SEEDING_TIME)
|
||||
limit = MAX_SEEDING_TIME;
|
||||
|
||||
if (m_seedingTimeLimit != limit)
|
||||
{
|
||||
@@ -2740,8 +2739,6 @@ void TorrentImpl::setInactiveSeedingTimeLimit(int limit)
|
||||
{
|
||||
if (limit < USE_GLOBAL_INACTIVE_SEEDING_TIME)
|
||||
limit = NO_INACTIVE_SEEDING_TIME_LIMIT;
|
||||
else if (limit > MAX_INACTIVE_SEEDING_TIME)
|
||||
limit = MAX_SEEDING_TIME;
|
||||
|
||||
if (m_inactiveSeedingTimeLimit != limit)
|
||||
{
|
||||
|
@@ -30,8 +30,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QHostAddress>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
#include "base/global.h"
|
||||
|
@@ -2054,6 +2054,19 @@ void Preferences::setAddNewTorrentDialogSavePathHistoryLength(const int value)
|
||||
setValue(u"AddNewTorrentDialog/SavePathHistoryLength"_s, clampedValue);
|
||||
}
|
||||
|
||||
bool Preferences::isAddNewTorrentDialogAttached() const
|
||||
{
|
||||
return value(u"AddNewTorrentDialog/Attached"_s, false);
|
||||
}
|
||||
|
||||
void Preferences::setAddNewTorrentDialogAttached(const bool attached)
|
||||
{
|
||||
if (attached == isAddNewTorrentDialogAttached())
|
||||
return;
|
||||
|
||||
setValue(u"AddNewTorrentDialog/Attached"_s, attached);
|
||||
}
|
||||
|
||||
void Preferences::apply()
|
||||
{
|
||||
if (SettingsStorage::instance()->save())
|
||||
|
@@ -433,6 +433,8 @@ public:
|
||||
void setAddNewTorrentDialogTopLevel(bool value);
|
||||
int addNewTorrentDialogSavePathHistoryLength() const;
|
||||
void setAddNewTorrentDialogSavePathHistoryLength(int value);
|
||||
bool isAddNewTorrentDialogAttached() const;
|
||||
void setAddNewTorrentDialogAttached(bool attached);
|
||||
|
||||
public slots:
|
||||
void setStatusFilterState(bool checked);
|
||||
|
@@ -48,16 +48,16 @@ const QString Article::KeyIsRead = u"isRead"_s;
|
||||
|
||||
Article::Article(Feed *feed, const QVariantHash &varHash)
|
||||
: QObject(feed)
|
||||
, m_feed(feed)
|
||||
, m_guid(varHash.value(KeyId).toString())
|
||||
, m_date(varHash.value(KeyDate).toDateTime())
|
||||
, m_title(varHash.value(KeyTitle).toString())
|
||||
, m_author(varHash.value(KeyAuthor).toString())
|
||||
, m_description(varHash.value(KeyDescription).toString())
|
||||
, m_torrentURL(varHash.value(KeyTorrentURL).toString())
|
||||
, m_link(varHash.value(KeyLink).toString())
|
||||
, m_isRead(varHash.value(KeyIsRead, false).toBool())
|
||||
, m_data(varHash)
|
||||
, m_feed {feed}
|
||||
, m_guid {varHash.value(KeyId).toString()}
|
||||
, m_date {varHash.value(KeyDate).toDateTime()}
|
||||
, m_title {varHash.value(KeyTitle).toString()}
|
||||
, m_author {varHash.value(KeyAuthor).toString()}
|
||||
, m_description {varHash.value(KeyDescription).toString()}
|
||||
, m_torrentURL {varHash.value(KeyTorrentURL).toString()}
|
||||
, m_link {varHash.value(KeyLink).toString()}
|
||||
, m_isRead {varHash.value(KeyIsRead, false).toBool()}
|
||||
, m_data {varHash}
|
||||
{
|
||||
}
|
||||
|
||||
|
@@ -375,10 +375,24 @@ void AutoDownloader::handleTorrentAdded(const QString &source)
|
||||
}
|
||||
}
|
||||
|
||||
void AutoDownloader::handleAddTorrentFailed(const QString &source)
|
||||
void AutoDownloader::handleAddTorrentFailed(const QString &source, const BitTorrent::AddTorrentError &error)
|
||||
{
|
||||
m_waitingJobs.remove(source);
|
||||
// TODO: Re-schedule job here.
|
||||
const auto job = m_waitingJobs.take(source);
|
||||
if (!job)
|
||||
return;
|
||||
|
||||
if (error.kind == BitTorrent::AddTorrentError::DuplicateTorrent)
|
||||
{
|
||||
if (Feed *feed = Session::instance()->feedByURL(job->feedURL))
|
||||
{
|
||||
if (Article *article = feed->articleByGUID(job->articleData.value(Article::KeyId).toString()))
|
||||
article->markAsRead();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Re-schedule job here.
|
||||
}
|
||||
}
|
||||
|
||||
void AutoDownloader::handleNewArticle(const Article *article)
|
||||
|
@@ -37,6 +37,7 @@
|
||||
#include <QSharedPointer>
|
||||
|
||||
#include "base/applicationcomponent.h"
|
||||
#include "base/bittorrent/addtorrenterror.h"
|
||||
#include "base/exceptions.h"
|
||||
#include "base/settingvalue.h"
|
||||
#include "base/utils/thread.h"
|
||||
@@ -111,7 +112,7 @@ namespace RSS
|
||||
private slots:
|
||||
void process();
|
||||
void handleTorrentAdded(const QString &source);
|
||||
void handleAddTorrentFailed(const QString &url);
|
||||
void handleAddTorrentFailed(const QString &url, const BitTorrent::AddTorrentError &error);
|
||||
void handleNewArticle(const Article *article);
|
||||
void handleFeedURLChanged(Feed *feed, const QString &oldURL);
|
||||
|
||||
|
@@ -487,14 +487,14 @@ void SearchPluginManager::updateNova()
|
||||
const Path enginePath = engineLocation();
|
||||
|
||||
QFile packageFile {(enginePath / Path(u"__init__.py"_s)).data()};
|
||||
packageFile.open(QIODevice::WriteOnly);
|
||||
packageFile.close();
|
||||
if (packageFile.open(QIODevice::WriteOnly))
|
||||
packageFile.close();
|
||||
|
||||
Utils::Fs::mkdir(enginePath / Path(u"engines"_s));
|
||||
|
||||
QFile packageFile2 {(enginePath / Path(u"engines/__init__.py"_s)).data()};
|
||||
packageFile2.open(QIODevice::WriteOnly);
|
||||
packageFile2.close();
|
||||
if (packageFile2.open(QIODevice::WriteOnly))
|
||||
packageFile2.close();
|
||||
|
||||
// Copy search plugin files (if necessary)
|
||||
const auto updateFile = [&enginePath](const Path &filename, const bool compareVersion)
|
||||
|
@@ -42,7 +42,7 @@
|
||||
|
||||
uint32_t Utils::Random::rand(const uint32_t min, const uint32_t max)
|
||||
{
|
||||
static RandomLayer layer;
|
||||
static const RandomLayer layer;
|
||||
|
||||
// new distribution is cheap: https://stackoverflow.com/a/19036349
|
||||
std::uniform_int_distribution<uint32_t> uniform(min, max);
|
||||
|
@@ -27,6 +27,7 @@
|
||||
*/
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
@@ -44,6 +45,27 @@ namespace
|
||||
|
||||
RandomLayer()
|
||||
{
|
||||
if (::getrandom(nullptr, 0, 0) < 0)
|
||||
{
|
||||
if (errno == ENOSYS)
|
||||
{
|
||||
// underlying kernel does not implement this system call
|
||||
// fallback to `urandom`
|
||||
m_randDev = fopen("/dev/urandom", "rb");
|
||||
if (!m_randDev)
|
||||
qFatal("Failed to open /dev/urandom. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
else
|
||||
{
|
||||
qFatal("getrandom() error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~RandomLayer()
|
||||
{
|
||||
if (m_randDev)
|
||||
fclose(m_randDev);
|
||||
}
|
||||
|
||||
static constexpr result_type min()
|
||||
@@ -56,7 +78,15 @@ namespace
|
||||
return std::numeric_limits<result_type>::max();
|
||||
}
|
||||
|
||||
result_type operator()()
|
||||
result_type operator()() const
|
||||
{
|
||||
if (!m_randDev)
|
||||
return getRandomViaAPI();
|
||||
return getRandomViaFile();
|
||||
}
|
||||
|
||||
private:
|
||||
result_type getRandomViaAPI() const
|
||||
{
|
||||
const int RETRY_MAX = 3;
|
||||
|
||||
@@ -68,10 +98,21 @@ namespace
|
||||
return buf;
|
||||
|
||||
if (result < 0)
|
||||
qFatal("getrandom() error. Reason: %s. Error code: %d.", std::strerror(errno), errno);
|
||||
qFatal("getrandom() error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
|
||||
qFatal("getrandom() failed. Reason: too many retries.");
|
||||
}
|
||||
|
||||
result_type getRandomViaFile() const
|
||||
{
|
||||
result_type buf = 0;
|
||||
if (fread(&buf, sizeof(buf), 1, m_randDev) == 1)
|
||||
return buf;
|
||||
|
||||
qFatal("Read /dev/urandom error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
|
||||
FILE *m_randDev = nullptr;
|
||||
};
|
||||
}
|
||||
|
@@ -46,7 +46,7 @@ namespace
|
||||
: m_randDev {fopen("/dev/urandom", "rb")}
|
||||
{
|
||||
if (!m_randDev)
|
||||
qFatal("Failed to open /dev/urandom. Reason: %s. Error code: %d.", std::strerror(errno), errno);
|
||||
qFatal("Failed to open /dev/urandom. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
|
||||
~RandomLayer()
|
||||
@@ -67,10 +67,10 @@ namespace
|
||||
result_type operator()() const
|
||||
{
|
||||
result_type buf = 0;
|
||||
if (fread(&buf, sizeof(buf), 1, m_randDev) != 1)
|
||||
qFatal("Read /dev/urandom error. Reason: %s. Error code: %d.", std::strerror(errno), errno);
|
||||
if (fread(&buf, sizeof(buf), 1, m_randDev) == 1)
|
||||
return buf;
|
||||
|
||||
return buf;
|
||||
qFatal("Read /dev/urandom error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||
}
|
||||
|
||||
private:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user