qBitrr2 5.6.1__py3-none-any.whl → 5.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qBitrr/arss.py +390 -89
- qBitrr/bundled_data.py +2 -2
- qBitrr/db_lock.py +24 -10
- qBitrr/main.py +232 -10
- qBitrr/static/assets/ArrView.js +1 -1
- qBitrr/static/assets/ArrView.js.map +1 -1
- qBitrr/static/assets/ConfigView.js +5 -4
- qBitrr/static/assets/ConfigView.js.map +1 -1
- qBitrr/static/assets/LogsView.js +1 -1
- qBitrr/static/assets/LogsView.js.map +1 -1
- qBitrr/static/assets/ProcessesView.js +1 -1
- qBitrr/static/assets/ProcessesView.js.map +1 -1
- qBitrr/static/assets/app.css +1 -1
- qBitrr/static/assets/app.js +5 -5
- qBitrr/static/assets/app.js.map +1 -1
- qBitrr/static/assets/vendor.js +1 -1
- qBitrr/static/assets/vendor.js.map +1 -1
- qBitrr/tables.py +7 -0
- qBitrr/webui.py +48 -1
- qbitrr2-5.7.0.dist-info/METADATA +282 -0
- {qbitrr2-5.6.1.dist-info → qbitrr2-5.7.0.dist-info}/RECORD +25 -25
- qbitrr2-5.6.1.dist-info/METADATA +0 -1210
- {qbitrr2-5.6.1.dist-info → qbitrr2-5.7.0.dist-info}/WHEEL +0 -0
- {qbitrr2-5.6.1.dist-info → qbitrr2-5.7.0.dist-info}/entry_points.txt +0 -0
- {qbitrr2-5.6.1.dist-info → qbitrr2-5.7.0.dist-info}/licenses/LICENSE +0 -0
- {qbitrr2-5.6.1.dist-info → qbitrr2-5.7.0.dist-info}/top_level.txt +0 -0
qBitrr/arss.py
CHANGED
|
@@ -20,7 +20,7 @@ import qbittorrentapi
|
|
|
20
20
|
import qbittorrentapi.exceptions
|
|
21
21
|
import requests
|
|
22
22
|
from packaging import version as version_parser
|
|
23
|
-
from peewee import Model, SqliteDatabase
|
|
23
|
+
from peewee import DatabaseError, Model, OperationalError, SqliteDatabase
|
|
24
24
|
from pyarr import LidarrAPI, RadarrAPI, SonarrAPI
|
|
25
25
|
from pyarr.exceptions import PyarrResourceNotFound, PyarrServerError
|
|
26
26
|
from pyarr.types import JsonObject
|
|
@@ -147,36 +147,36 @@ class Arr:
|
|
|
147
147
|
self.logger = logging.getLogger(f"qBitrr.{self._name}")
|
|
148
148
|
run_logs(self.logger, self._name)
|
|
149
149
|
|
|
150
|
+
# Set completed_folder path (used for category creation and file monitoring)
|
|
150
151
|
if not QBIT_DISABLED:
|
|
151
152
|
try:
|
|
153
|
+
# Check default instance for existing category configuration
|
|
152
154
|
categories = self.manager.qbit_manager.client.torrent_categories.categories
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
path
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
else:
|
|
160
|
-
self.logger.trace("Category exists without save path")
|
|
161
|
-
self.completed_folder = pathlib.Path(COMPLETED_DOWNLOAD_FOLDER).joinpath(
|
|
162
|
-
self.category
|
|
163
|
-
)
|
|
164
|
-
except KeyError:
|
|
155
|
+
categ = categories.get(self.category)
|
|
156
|
+
if categ and categ.get("savePath"):
|
|
157
|
+
self.logger.trace("Category exists with save path [%s]", categ["savePath"])
|
|
158
|
+
self.completed_folder = pathlib.Path(categ["savePath"])
|
|
159
|
+
else:
|
|
160
|
+
self.logger.trace("Category does not exist or lacks save path")
|
|
165
161
|
self.completed_folder = pathlib.Path(COMPLETED_DOWNLOAD_FOLDER).joinpath(
|
|
166
162
|
self.category
|
|
167
163
|
)
|
|
168
|
-
self.manager.qbit_manager.client.torrent_categories.create_category(
|
|
169
|
-
self.category, save_path=self.completed_folder
|
|
170
|
-
)
|
|
171
164
|
except Exception as e:
|
|
172
165
|
self.logger.warning(
|
|
173
|
-
"Could not connect to qBittorrent during initialization for %s: %s.
|
|
166
|
+
"Could not connect to qBittorrent during initialization for %s: %s. Using default path.",
|
|
174
167
|
self._name,
|
|
175
|
-
str(e).split("\n")[0] if "\n" in str(e) else str(e),
|
|
168
|
+
str(e).split("\n")[0] if "\n" in str(e) else str(e),
|
|
176
169
|
)
|
|
177
170
|
self.completed_folder = pathlib.Path(COMPLETED_DOWNLOAD_FOLDER).joinpath(
|
|
178
171
|
self.category
|
|
179
172
|
)
|
|
173
|
+
# Ensure category exists on ALL instances (deferred to avoid __init__ failures)
|
|
174
|
+
try:
|
|
175
|
+
self._ensure_category_on_all_instances()
|
|
176
|
+
except Exception as e:
|
|
177
|
+
self.logger.warning(
|
|
178
|
+
"Could not ensure category on all instances during init: %s", e
|
|
179
|
+
)
|
|
180
180
|
else:
|
|
181
181
|
self.completed_folder = pathlib.Path(COMPLETED_DOWNLOAD_FOLDER).joinpath(self.category)
|
|
182
182
|
|
|
@@ -673,6 +673,57 @@ class Arr:
|
|
|
673
673
|
)
|
|
674
674
|
self.logger.hnotice("Starting %s monitor", self._name)
|
|
675
675
|
|
|
676
|
+
def _ensure_category_on_all_instances(self) -> None:
|
|
677
|
+
"""
|
|
678
|
+
Ensure the Arr category exists on ALL qBittorrent instances.
|
|
679
|
+
|
|
680
|
+
Creates the category with the completed_folder save path on each instance.
|
|
681
|
+
Logs errors but continues if individual instances fail.
|
|
682
|
+
"""
|
|
683
|
+
if QBIT_DISABLED:
|
|
684
|
+
return
|
|
685
|
+
|
|
686
|
+
qbit_manager = self.manager.qbit_manager
|
|
687
|
+
all_instances = qbit_manager.get_all_instances()
|
|
688
|
+
|
|
689
|
+
self.logger.debug(
|
|
690
|
+
"Ensuring category '%s' exists on %d qBit instance(s)",
|
|
691
|
+
self.category,
|
|
692
|
+
len(all_instances),
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
for instance_name in all_instances:
|
|
696
|
+
try:
|
|
697
|
+
client = qbit_manager.get_client(instance_name)
|
|
698
|
+
if client is None:
|
|
699
|
+
self.logger.warning(
|
|
700
|
+
"Skipping category creation on instance '%s' (client unavailable)",
|
|
701
|
+
instance_name,
|
|
702
|
+
)
|
|
703
|
+
continue
|
|
704
|
+
|
|
705
|
+
categories = client.torrent_categories.categories
|
|
706
|
+
if self.category not in categories:
|
|
707
|
+
client.torrent_categories.create_category(
|
|
708
|
+
self.category, save_path=str(self.completed_folder)
|
|
709
|
+
)
|
|
710
|
+
self.logger.info(
|
|
711
|
+
"Created category '%s' on instance '%s'", self.category, instance_name
|
|
712
|
+
)
|
|
713
|
+
else:
|
|
714
|
+
self.logger.debug(
|
|
715
|
+
"Category '%s' already exists on instance '%s'",
|
|
716
|
+
self.category,
|
|
717
|
+
instance_name,
|
|
718
|
+
)
|
|
719
|
+
except Exception as e:
|
|
720
|
+
self.logger.error(
|
|
721
|
+
"Failed to ensure category '%s' on instance '%s': %s",
|
|
722
|
+
self.category,
|
|
723
|
+
instance_name,
|
|
724
|
+
str(e).split("\n")[0] if "\n" in str(e) else str(e),
|
|
725
|
+
)
|
|
726
|
+
|
|
676
727
|
@staticmethod
|
|
677
728
|
def _humanize_request_tag(tag: str) -> str | None:
|
|
678
729
|
if not tag:
|
|
@@ -785,7 +836,9 @@ class Arr:
|
|
|
785
836
|
"""Returns True if the State is categorized as Downloading."""
|
|
786
837
|
return torrent.state_enum in (TorrentStates.DOWNLOADING, TorrentStates.PAUSED_DOWNLOAD)
|
|
787
838
|
|
|
788
|
-
def in_tags(
|
|
839
|
+
def in_tags(
|
|
840
|
+
self, torrent: TorrentDictionary, tag: str, instance_name: str = "default"
|
|
841
|
+
) -> bool:
|
|
789
842
|
return_value = False
|
|
790
843
|
if TAGLESS:
|
|
791
844
|
if tag == "qBitrr-ignored":
|
|
@@ -796,15 +849,20 @@ class Arr:
|
|
|
796
849
|
.where(
|
|
797
850
|
(self.torrents.Hash == torrent.hash)
|
|
798
851
|
& (self.torrents.Category == torrent.category)
|
|
852
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
799
853
|
)
|
|
800
854
|
.execute()
|
|
801
855
|
)
|
|
802
856
|
if not query:
|
|
803
857
|
self.torrents.insert(
|
|
804
|
-
Hash=torrent.hash,
|
|
858
|
+
Hash=torrent.hash,
|
|
859
|
+
Category=torrent.category,
|
|
860
|
+
QbitInstance=instance_name,
|
|
805
861
|
).on_conflict_ignore().execute()
|
|
806
|
-
condition = (
|
|
807
|
-
self.torrents.
|
|
862
|
+
condition = (
|
|
863
|
+
(self.torrents.Hash == torrent.hash)
|
|
864
|
+
& (self.torrents.Category == torrent.category)
|
|
865
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
808
866
|
)
|
|
809
867
|
if tag == "qBitrr-allowed_seeding":
|
|
810
868
|
condition &= self.torrents.AllowedSeeding == True
|
|
@@ -832,7 +890,9 @@ class Arr:
|
|
|
832
890
|
self.logger.trace("Tag %s not in %s", tag, torrent.name)
|
|
833
891
|
return False
|
|
834
892
|
|
|
835
|
-
def remove_tags(
|
|
893
|
+
def remove_tags(
|
|
894
|
+
self, torrent: TorrentDictionary, tags: list, instance_name: str = "default"
|
|
895
|
+
) -> None:
|
|
836
896
|
for tag in tags:
|
|
837
897
|
self.logger.trace("Removing tag %s from %s", tag, torrent.name)
|
|
838
898
|
if TAGLESS:
|
|
@@ -842,32 +902,39 @@ class Arr:
|
|
|
842
902
|
.where(
|
|
843
903
|
(self.torrents.Hash == torrent.hash)
|
|
844
904
|
& (self.torrents.Category == torrent.category)
|
|
905
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
845
906
|
)
|
|
846
907
|
.execute()
|
|
847
908
|
)
|
|
848
909
|
if not query:
|
|
849
910
|
self.torrents.insert(
|
|
850
|
-
Hash=torrent.hash,
|
|
911
|
+
Hash=torrent.hash,
|
|
912
|
+
Category=torrent.category,
|
|
913
|
+
QbitInstance=instance_name,
|
|
851
914
|
).on_conflict_ignore().execute()
|
|
852
915
|
if tag == "qBitrr-allowed_seeding":
|
|
853
916
|
self.torrents.update(AllowedSeeding=False).where(
|
|
854
917
|
(self.torrents.Hash == torrent.hash)
|
|
855
918
|
& (self.torrents.Category == torrent.category)
|
|
919
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
856
920
|
).execute()
|
|
857
921
|
elif tag == "qBitrr-imported":
|
|
858
922
|
self.torrents.update(Imported=False).where(
|
|
859
923
|
(self.torrents.Hash == torrent.hash)
|
|
860
924
|
& (self.torrents.Category == torrent.category)
|
|
925
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
861
926
|
).execute()
|
|
862
927
|
elif tag == "qBitrr-allowed_stalled":
|
|
863
928
|
self.torrents.update(AllowedStalled=False).where(
|
|
864
929
|
(self.torrents.Hash == torrent.hash)
|
|
865
930
|
& (self.torrents.Category == torrent.category)
|
|
931
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
866
932
|
).execute()
|
|
867
933
|
elif tag == "qBitrr-free_space_paused":
|
|
868
934
|
self.torrents.update(FreeSpacePaused=False).where(
|
|
869
935
|
(self.torrents.Hash == torrent.hash)
|
|
870
936
|
& (self.torrents.Category == torrent.category)
|
|
937
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
871
938
|
).execute()
|
|
872
939
|
else:
|
|
873
940
|
with contextlib.suppress(Exception):
|
|
@@ -883,7 +950,9 @@ class Arr:
|
|
|
883
950
|
),
|
|
884
951
|
)
|
|
885
952
|
|
|
886
|
-
def add_tags(
|
|
953
|
+
def add_tags(
|
|
954
|
+
self, torrent: TorrentDictionary, tags: list, instance_name: str = "default"
|
|
955
|
+
) -> None:
|
|
887
956
|
for tag in tags:
|
|
888
957
|
self.logger.trace("Adding tag %s from %s", tag, torrent.name)
|
|
889
958
|
if TAGLESS:
|
|
@@ -893,32 +962,39 @@ class Arr:
|
|
|
893
962
|
.where(
|
|
894
963
|
(self.torrents.Hash == torrent.hash)
|
|
895
964
|
& (self.torrents.Category == torrent.category)
|
|
965
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
896
966
|
)
|
|
897
967
|
.execute()
|
|
898
968
|
)
|
|
899
969
|
if not query:
|
|
900
970
|
self.torrents.insert(
|
|
901
|
-
Hash=torrent.hash,
|
|
971
|
+
Hash=torrent.hash,
|
|
972
|
+
Category=torrent.category,
|
|
973
|
+
QbitInstance=instance_name,
|
|
902
974
|
).on_conflict_ignore().execute()
|
|
903
975
|
if tag == "qBitrr-allowed_seeding":
|
|
904
976
|
self.torrents.update(AllowedSeeding=True).where(
|
|
905
977
|
(self.torrents.Hash == torrent.hash)
|
|
906
978
|
& (self.torrents.Category == torrent.category)
|
|
979
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
907
980
|
).execute()
|
|
908
981
|
elif tag == "qBitrr-imported":
|
|
909
982
|
self.torrents.update(Imported=True).where(
|
|
910
983
|
(self.torrents.Hash == torrent.hash)
|
|
911
984
|
& (self.torrents.Category == torrent.category)
|
|
985
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
912
986
|
).execute()
|
|
913
987
|
elif tag == "qBitrr-allowed_stalled":
|
|
914
988
|
self.torrents.update(AllowedStalled=True).where(
|
|
915
989
|
(self.torrents.Hash == torrent.hash)
|
|
916
990
|
& (self.torrents.Category == torrent.category)
|
|
991
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
917
992
|
).execute()
|
|
918
993
|
elif tag == "qBitrr-free_space_paused":
|
|
919
994
|
self.torrents.update(FreeSpacePaused=True).where(
|
|
920
995
|
(self.torrents.Hash == torrent.hash)
|
|
921
996
|
& (self.torrents.Category == torrent.category)
|
|
997
|
+
& (self.torrents.QbitInstance == instance_name)
|
|
922
998
|
).execute()
|
|
923
999
|
else:
|
|
924
1000
|
with contextlib.suppress(Exception):
|
|
@@ -1159,7 +1235,7 @@ class Arr:
|
|
|
1159
1235
|
def _process_imports(self) -> None:
|
|
1160
1236
|
if self.import_torrents:
|
|
1161
1237
|
self.needs_cleanup = True
|
|
1162
|
-
for torrent in self.import_torrents:
|
|
1238
|
+
for torrent, instance_name in self.import_torrents:
|
|
1163
1239
|
if torrent.hash in self.sent_to_scan:
|
|
1164
1240
|
continue
|
|
1165
1241
|
path = validate_and_return_torrent_file(torrent.content_path)
|
|
@@ -1245,7 +1321,7 @@ class Arr:
|
|
|
1245
1321
|
self.import_mode,
|
|
1246
1322
|
ex,
|
|
1247
1323
|
)
|
|
1248
|
-
self.add_tags(torrent, ["qBitrr-imported"])
|
|
1324
|
+
self.add_tags(torrent, ["qBitrr-imported"], instance_name)
|
|
1249
1325
|
self.sent_to_scan.add(path)
|
|
1250
1326
|
self.import_torrents.clear()
|
|
1251
1327
|
|
|
@@ -4448,33 +4524,96 @@ class Arr:
|
|
|
4448
4524
|
|
|
4449
4525
|
return payload
|
|
4450
4526
|
|
|
4527
|
+
def _get_torrents_from_all_instances(
|
|
4528
|
+
self,
|
|
4529
|
+
) -> list[tuple[str, qbittorrentapi.TorrentDictionary]]:
|
|
4530
|
+
"""
|
|
4531
|
+
Get torrents from ALL qBittorrent instances for this Arr's category.
|
|
4532
|
+
|
|
4533
|
+
Returns:
|
|
4534
|
+
list[tuple[str, TorrentDictionary]]: List of (instance_name, torrent) tuples
|
|
4535
|
+
"""
|
|
4536
|
+
all_torrents = []
|
|
4537
|
+
qbit_manager = self.manager.qbit_manager
|
|
4538
|
+
|
|
4539
|
+
for instance_name in qbit_manager.get_all_instances():
|
|
4540
|
+
if not qbit_manager.is_instance_alive(instance_name):
|
|
4541
|
+
self.logger.debug(
|
|
4542
|
+
"Skipping unhealthy instance '%s' during torrent scan", instance_name
|
|
4543
|
+
)
|
|
4544
|
+
continue
|
|
4545
|
+
|
|
4546
|
+
client = qbit_manager.get_client(instance_name)
|
|
4547
|
+
if client is None:
|
|
4548
|
+
continue
|
|
4549
|
+
|
|
4550
|
+
try:
|
|
4551
|
+
torrents = client.torrents.info(
|
|
4552
|
+
status_filter="all",
|
|
4553
|
+
category=self.category,
|
|
4554
|
+
sort="added_on",
|
|
4555
|
+
reverse=False,
|
|
4556
|
+
)
|
|
4557
|
+
# Tag each torrent with its instance name
|
|
4558
|
+
for torrent in torrents:
|
|
4559
|
+
if hasattr(torrent, "category"):
|
|
4560
|
+
all_torrents.append((instance_name, torrent))
|
|
4561
|
+
|
|
4562
|
+
self.logger.trace(
|
|
4563
|
+
"Retrieved %d torrents from instance '%s' for category '%s'",
|
|
4564
|
+
len(torrents),
|
|
4565
|
+
instance_name,
|
|
4566
|
+
self.category,
|
|
4567
|
+
)
|
|
4568
|
+
except (qbittorrentapi.exceptions.APIError, JSONDecodeError) as e:
|
|
4569
|
+
self.logger.warning(
|
|
4570
|
+
"Failed to get torrents from instance '%s': %s", instance_name, e
|
|
4571
|
+
)
|
|
4572
|
+
continue
|
|
4573
|
+
|
|
4574
|
+
self.logger.debug(
|
|
4575
|
+
"Total torrents across %d instances: %d",
|
|
4576
|
+
len(qbit_manager.get_all_instances()),
|
|
4577
|
+
len(all_torrents),
|
|
4578
|
+
)
|
|
4579
|
+
return all_torrents
|
|
4580
|
+
|
|
4451
4581
|
def process_torrents(self):
|
|
4452
4582
|
try:
|
|
4453
4583
|
try:
|
|
4454
4584
|
while True:
|
|
4455
4585
|
try:
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
category=self.category,
|
|
4459
|
-
sort="added_on",
|
|
4460
|
-
reverse=False,
|
|
4461
|
-
)
|
|
4586
|
+
# Multi-instance: Scan all qBit instances for category-matching torrents
|
|
4587
|
+
torrents_with_instances = self._get_torrents_from_all_instances()
|
|
4462
4588
|
break
|
|
4463
4589
|
except (qbittorrentapi.exceptions.APIError, JSONDecodeError) as e:
|
|
4464
4590
|
if "JSONDecodeError" in str(e):
|
|
4465
4591
|
continue
|
|
4466
4592
|
else:
|
|
4467
4593
|
raise qbittorrentapi.exceptions.APIError
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4594
|
+
|
|
4595
|
+
# Filter torrents that have category attribute
|
|
4596
|
+
torrents_with_instances = [
|
|
4597
|
+
(instance, t)
|
|
4598
|
+
for instance, t in torrents_with_instances
|
|
4599
|
+
if hasattr(t, "category")
|
|
4600
|
+
]
|
|
4601
|
+
self.category_torrent_count = len(torrents_with_instances)
|
|
4602
|
+
if not len(torrents_with_instances):
|
|
4471
4603
|
raise DelayLoopException(length=LOOP_SLEEP_TIMER, type="no_downloads")
|
|
4604
|
+
|
|
4605
|
+
# Internet check: Use default instance for backward compatibility
|
|
4472
4606
|
if not has_internet(self.manager.qbit_manager.client):
|
|
4473
4607
|
self.manager.qbit_manager.should_delay_torrent_scan = True
|
|
4474
4608
|
raise DelayLoopException(length=NO_INTERNET_SLEEP_TIMER, type="internet")
|
|
4475
4609
|
if self.manager.qbit_manager.should_delay_torrent_scan:
|
|
4476
4610
|
raise DelayLoopException(length=NO_INTERNET_SLEEP_TIMER, type="delay")
|
|
4477
4611
|
|
|
4612
|
+
# Initialize database error tracking for exponential backoff
|
|
4613
|
+
if not hasattr(self, "_db_error_count"):
|
|
4614
|
+
self._db_error_count = 0
|
|
4615
|
+
self._db_last_error_time = 0
|
|
4616
|
+
|
|
4478
4617
|
# Periodic database health check (every 10th iteration)
|
|
4479
4618
|
if not hasattr(self, "_health_check_counter"):
|
|
4480
4619
|
self._health_check_counter = 0
|
|
@@ -4502,9 +4641,10 @@ class Arr:
|
|
|
4502
4641
|
|
|
4503
4642
|
self.api_calls()
|
|
4504
4643
|
self.refresh_download_queue()
|
|
4505
|
-
|
|
4644
|
+
# Multi-instance: Process torrents from all instances
|
|
4645
|
+
for instance_name, torrent in torrents_with_instances:
|
|
4506
4646
|
with contextlib.suppress(qbittorrentapi.NotFound404Error):
|
|
4507
|
-
self._process_single_torrent(torrent)
|
|
4647
|
+
self._process_single_torrent(torrent, instance_name=instance_name)
|
|
4508
4648
|
self.process()
|
|
4509
4649
|
except NoConnectionrException as e:
|
|
4510
4650
|
self.logger.error(e.message)
|
|
@@ -4516,6 +4656,76 @@ class Arr:
|
|
|
4516
4656
|
self.logger.error("The qBittorrent API returned an unexpected error")
|
|
4517
4657
|
self.logger.debug("Unexpected APIError from qBitTorrent") # , exc_info=e)
|
|
4518
4658
|
raise DelayLoopException(length=300, type="qbit")
|
|
4659
|
+
except (OperationalError, DatabaseError) as e:
|
|
4660
|
+
# Database errors after retry exhaustion - implement automatic recovery with backoff
|
|
4661
|
+
error_msg = str(e).lower()
|
|
4662
|
+
current_time = time.time()
|
|
4663
|
+
|
|
4664
|
+
# Track consecutive database errors for exponential backoff
|
|
4665
|
+
if (
|
|
4666
|
+
current_time - self._db_last_error_time > 300
|
|
4667
|
+
): # Reset if >5min since last error
|
|
4668
|
+
self._db_error_count = 0
|
|
4669
|
+
self._db_error_count += 1
|
|
4670
|
+
self._db_last_error_time = current_time
|
|
4671
|
+
|
|
4672
|
+
# Calculate exponential backoff: 2min, 5min, 10min, 20min, 30min (max)
|
|
4673
|
+
delay_seconds = min(120 * (2 ** (self._db_error_count - 1)), 1800)
|
|
4674
|
+
|
|
4675
|
+
# Log detailed error information based on error type
|
|
4676
|
+
if "disk i/o error" in error_msg:
|
|
4677
|
+
self.logger.critical(
|
|
4678
|
+
"Persistent database I/O error detected (consecutive error #%d). "
|
|
4679
|
+
"This indicates disk issues, filesystem corruption, or resource exhaustion. "
|
|
4680
|
+
"Attempting automatic recovery and retrying in %d seconds...",
|
|
4681
|
+
self._db_error_count,
|
|
4682
|
+
delay_seconds,
|
|
4683
|
+
)
|
|
4684
|
+
elif "database is locked" in error_msg:
|
|
4685
|
+
self.logger.error(
|
|
4686
|
+
"Database locked error (consecutive error #%d). "
|
|
4687
|
+
"Retrying in %d seconds...",
|
|
4688
|
+
self._db_error_count,
|
|
4689
|
+
delay_seconds,
|
|
4690
|
+
)
|
|
4691
|
+
elif "disk image is malformed" in error_msg:
|
|
4692
|
+
self.logger.critical(
|
|
4693
|
+
"Database corruption detected (consecutive error #%d). "
|
|
4694
|
+
"Attempting automatic recovery and retrying in %d seconds...",
|
|
4695
|
+
self._db_error_count,
|
|
4696
|
+
delay_seconds,
|
|
4697
|
+
)
|
|
4698
|
+
else:
|
|
4699
|
+
self.logger.error(
|
|
4700
|
+
"Database error (consecutive error #%d): %s. " "Retrying in %d seconds...",
|
|
4701
|
+
self._db_error_count,
|
|
4702
|
+
error_msg,
|
|
4703
|
+
delay_seconds,
|
|
4704
|
+
)
|
|
4705
|
+
|
|
4706
|
+
# Attempt automatic recovery for critical errors
|
|
4707
|
+
if "disk i/o error" in error_msg or "disk image is malformed" in error_msg:
|
|
4708
|
+
try:
|
|
4709
|
+
self.logger.warning(
|
|
4710
|
+
"Attempting enhanced database recovery (WAL checkpoint, repair, and verification)..."
|
|
4711
|
+
)
|
|
4712
|
+
self._enhanced_database_recovery()
|
|
4713
|
+
self.logger.info(
|
|
4714
|
+
"Database recovery completed successfully - will retry operation after delay"
|
|
4715
|
+
)
|
|
4716
|
+
# Reduce error count on successful recovery (but don't reset completely)
|
|
4717
|
+
self._db_error_count = max(0, self._db_error_count - 1)
|
|
4718
|
+
except Exception as recovery_error:
|
|
4719
|
+
self.logger.critical(
|
|
4720
|
+
"Automatic database recovery failed: %s. "
|
|
4721
|
+
"MANUAL INTERVENTION REQUIRED: Check disk health (smartctl), "
|
|
4722
|
+
"filesystem integrity (fsck), available space (df -h), "
|
|
4723
|
+
"Docker volume mounts, permissions, and system logs (dmesg).",
|
|
4724
|
+
recovery_error,
|
|
4725
|
+
)
|
|
4726
|
+
|
|
4727
|
+
# Delay processing to avoid hammering failing database
|
|
4728
|
+
raise DelayLoopException(length=delay_seconds, type="database")
|
|
4519
4729
|
except DelayLoopException:
|
|
4520
4730
|
raise
|
|
4521
4731
|
except KeyboardInterrupt:
|
|
@@ -4566,6 +4776,83 @@ class Arr:
|
|
|
4566
4776
|
"Manual intervention may be required. Continuing with caution..."
|
|
4567
4777
|
)
|
|
4568
4778
|
|
|
4779
|
+
def _enhanced_database_recovery(self):
|
|
4780
|
+
"""
|
|
4781
|
+
Enhanced automatic database recovery with additional filesystem checks.
|
|
4782
|
+
|
|
4783
|
+
This method is called when disk I/O errors persist after retry logic has been exhausted.
|
|
4784
|
+
It implements a comprehensive recovery strategy:
|
|
4785
|
+
1. Try WAL checkpoint (least invasive)
|
|
4786
|
+
2. Try VACUUM to reclaim space and fix minor corruption
|
|
4787
|
+
3. Try full database repair (dump/restore) if needed
|
|
4788
|
+
4. Verify database integrity after recovery
|
|
4789
|
+
"""
|
|
4790
|
+
from qBitrr.db_recovery import (
|
|
4791
|
+
DatabaseRecoveryError,
|
|
4792
|
+
checkpoint_wal,
|
|
4793
|
+
repair_database,
|
|
4794
|
+
vacuum_database,
|
|
4795
|
+
)
|
|
4796
|
+
from qBitrr.home_path import APPDATA_FOLDER
|
|
4797
|
+
|
|
4798
|
+
db_path = APPDATA_FOLDER / "qbitrr.db"
|
|
4799
|
+
|
|
4800
|
+
self.logger.info("Starting enhanced database recovery procedure...")
|
|
4801
|
+
|
|
4802
|
+
# Step 1: Try WAL checkpoint
|
|
4803
|
+
self.logger.info("Step 1/3: Attempting WAL checkpoint...")
|
|
4804
|
+
if checkpoint_wal(db_path, self.logger):
|
|
4805
|
+
self.logger.info("WAL checkpoint successful")
|
|
4806
|
+
# Try a quick health check
|
|
4807
|
+
from qBitrr.db_lock import check_database_health
|
|
4808
|
+
|
|
4809
|
+
healthy, msg = check_database_health(db_path, self.logger)
|
|
4810
|
+
if healthy:
|
|
4811
|
+
self.logger.info("Database health verified - recovery complete")
|
|
4812
|
+
return
|
|
4813
|
+
else:
|
|
4814
|
+
self.logger.warning(
|
|
4815
|
+
"WAL checkpoint completed but database still unhealthy: %s", msg
|
|
4816
|
+
)
|
|
4817
|
+
|
|
4818
|
+
# Step 2: Try VACUUM (only if WAL didn't fully fix it)
|
|
4819
|
+
self.logger.info("Step 2/3: Attempting VACUUM to reclaim space and fix minor issues...")
|
|
4820
|
+
if vacuum_database(db_path, self.logger):
|
|
4821
|
+
self.logger.info("VACUUM completed successfully")
|
|
4822
|
+
from qBitrr.db_lock import check_database_health
|
|
4823
|
+
|
|
4824
|
+
healthy, msg = check_database_health(db_path, self.logger)
|
|
4825
|
+
if healthy:
|
|
4826
|
+
self.logger.info("Database health verified after VACUUM - recovery complete")
|
|
4827
|
+
return
|
|
4828
|
+
else:
|
|
4829
|
+
self.logger.warning("VACUUM completed but database still unhealthy: %s", msg)
|
|
4830
|
+
|
|
4831
|
+
# Step 3: Try full repair (most invasive)
|
|
4832
|
+
self.logger.warning("Step 3/3: Attempting full database repair (dump/restore)...")
|
|
4833
|
+
try:
|
|
4834
|
+
if repair_database(db_path, backup=True, logger_override=self.logger):
|
|
4835
|
+
self.logger.info("Database repair successful")
|
|
4836
|
+
# Final health check
|
|
4837
|
+
from qBitrr.db_lock import check_database_health
|
|
4838
|
+
|
|
4839
|
+
healthy, msg = check_database_health(db_path, self.logger)
|
|
4840
|
+
if healthy:
|
|
4841
|
+
self.logger.info("Database health verified after repair - recovery complete")
|
|
4842
|
+
return
|
|
4843
|
+
else:
|
|
4844
|
+
self.logger.error("Repair completed but database still unhealthy: %s", msg)
|
|
4845
|
+
raise DatabaseRecoveryError(f"Database unhealthy after repair: {msg}")
|
|
4846
|
+
except DatabaseRecoveryError as e:
|
|
4847
|
+
self.logger.error("Database repair failed: %s", e)
|
|
4848
|
+
raise
|
|
4849
|
+
except Exception as e:
|
|
4850
|
+
self.logger.error("Unexpected error during database repair: %s", e)
|
|
4851
|
+
raise
|
|
4852
|
+
|
|
4853
|
+
# If we reach here, all recovery methods failed
|
|
4854
|
+
raise DatabaseRecoveryError("All automatic recovery methods failed")
|
|
4855
|
+
|
|
4569
4856
|
def _process_single_torrent_failed_cat(self, torrent: qbittorrentapi.TorrentDictionary):
|
|
4570
4857
|
self.logger.notice(
|
|
4571
4858
|
"Deleting manually failed torrent: "
|
|
@@ -4825,7 +5112,10 @@ class Arr:
|
|
|
4825
5112
|
self.recheck.add(torrent.hash)
|
|
4826
5113
|
|
|
4827
5114
|
def _process_single_torrent_fully_completed_torrent(
|
|
4828
|
-
self,
|
|
5115
|
+
self,
|
|
5116
|
+
torrent: qbittorrentapi.TorrentDictionary,
|
|
5117
|
+
leave_alone: bool,
|
|
5118
|
+
instance_name: str = "default",
|
|
4829
5119
|
):
|
|
4830
5120
|
if leave_alone or torrent.state_enum == TorrentStates.FORCED_UPLOAD:
|
|
4831
5121
|
self.logger.trace(
|
|
@@ -4843,7 +5133,7 @@ class Arr:
|
|
|
4843
5133
|
torrent.name,
|
|
4844
5134
|
torrent.hash,
|
|
4845
5135
|
)
|
|
4846
|
-
elif not self.in_tags(torrent, "qBitrr-imported"):
|
|
5136
|
+
elif not self.in_tags(torrent, "qBitrr-imported", instance_name):
|
|
4847
5137
|
self.logger.info(
|
|
4848
5138
|
"Importing Completed torrent: "
|
|
4849
5139
|
"[Progress: %s%%][Added On: %s]"
|
|
@@ -4868,7 +5158,7 @@ class Arr:
|
|
|
4868
5158
|
else:
|
|
4869
5159
|
torrent_folder = content_path
|
|
4870
5160
|
self.files_to_cleanup.add((torrent.hash, torrent_folder))
|
|
4871
|
-
self.import_torrents.append(torrent)
|
|
5161
|
+
self.import_torrents.append((torrent, instance_name))
|
|
4872
5162
|
|
|
4873
5163
|
def _process_single_torrent_missing_files(self, torrent: qbittorrentapi.TorrentDictionary):
|
|
4874
5164
|
# Sometimes Sonarr/Radarr does not automatically remove the
|
|
@@ -5236,7 +5526,7 @@ class Arr:
|
|
|
5236
5526
|
return data_settings, data_torrent
|
|
5237
5527
|
|
|
5238
5528
|
def _should_leave_alone(
|
|
5239
|
-
self, torrent: qbittorrentapi.TorrentDictionary
|
|
5529
|
+
self, torrent: qbittorrentapi.TorrentDictionary, instance_name: str = "default"
|
|
5240
5530
|
) -> tuple[bool, int, bool]:
|
|
5241
5531
|
return_value = True
|
|
5242
5532
|
remove_torrent = False
|
|
@@ -5262,18 +5552,18 @@ class Arr:
|
|
|
5262
5552
|
return_value = not self.torrent_limit_check(torrent, seeding_time_limit, ratio_limit)
|
|
5263
5553
|
if data_settings.get("super_seeding", False) or data_torrent.get("super_seeding", False):
|
|
5264
5554
|
return_value = True
|
|
5265
|
-
if self.in_tags(torrent, "qBitrr-free_space_paused"):
|
|
5555
|
+
if self.in_tags(torrent, "qBitrr-free_space_paused", instance_name):
|
|
5266
5556
|
return_value = True
|
|
5267
5557
|
if (
|
|
5268
5558
|
return_value
|
|
5269
|
-
and not self.in_tags(torrent, "qBitrr-allowed_seeding")
|
|
5270
|
-
and not self.in_tags(torrent, "qBitrr-free_space_paused")
|
|
5559
|
+
and not self.in_tags(torrent, "qBitrr-allowed_seeding", instance_name)
|
|
5560
|
+
and not self.in_tags(torrent, "qBitrr-free_space_paused", instance_name)
|
|
5271
5561
|
):
|
|
5272
|
-
self.add_tags(torrent, ["qBitrr-allowed_seeding"])
|
|
5562
|
+
self.add_tags(torrent, ["qBitrr-allowed_seeding"], instance_name)
|
|
5273
5563
|
elif (
|
|
5274
|
-
not return_value and self.in_tags(torrent, "qBitrr-allowed_seeding")
|
|
5275
|
-
) or self.in_tags(torrent, "qBitrr-free_space_paused"):
|
|
5276
|
-
self.remove_tags(torrent, ["qBitrr-allowed_seeding"])
|
|
5564
|
+
not return_value and self.in_tags(torrent, "qBitrr-allowed_seeding", instance_name)
|
|
5565
|
+
) or self.in_tags(torrent, "qBitrr-free_space_paused", instance_name):
|
|
5566
|
+
self.remove_tags(torrent, ["qBitrr-allowed_seeding"], instance_name)
|
|
5277
5567
|
|
|
5278
5568
|
self.logger.trace("Config Settings returned [%s]: %r", torrent.name, data_settings)
|
|
5279
5569
|
return (
|
|
@@ -5439,9 +5729,14 @@ class Arr:
|
|
|
5439
5729
|
current_tags = set(torrent.tags.split(", "))
|
|
5440
5730
|
add_tags = unique_tags.difference(current_tags)
|
|
5441
5731
|
if add_tags:
|
|
5442
|
-
self.add_tags(torrent, add_tags)
|
|
5732
|
+
self.add_tags(torrent, add_tags, instance_name)
|
|
5443
5733
|
|
|
5444
|
-
def _stalled_check(
|
|
5734
|
+
def _stalled_check(
|
|
5735
|
+
self,
|
|
5736
|
+
torrent: qbittorrentapi.TorrentDictionary,
|
|
5737
|
+
time_now: float,
|
|
5738
|
+
instance_name: str = "default",
|
|
5739
|
+
) -> bool:
|
|
5445
5740
|
stalled_ignore = True
|
|
5446
5741
|
if not self.allowed_stalled:
|
|
5447
5742
|
self.logger.trace("Stalled check: Stalled delay disabled")
|
|
@@ -5478,15 +5773,15 @@ class Arr:
|
|
|
5478
5773
|
(
|
|
5479
5774
|
torrent.state_enum
|
|
5480
5775
|
in (TorrentStates.METADATA_DOWNLOAD, TorrentStates.STALLED_DOWNLOAD)
|
|
5481
|
-
and not self.in_tags(torrent, "qBitrr-ignored")
|
|
5482
|
-
and not self.in_tags(torrent, "qBitrr-free_space_paused")
|
|
5776
|
+
and not self.in_tags(torrent, "qBitrr-ignored", instance_name)
|
|
5777
|
+
and not self.in_tags(torrent, "qBitrr-free_space_paused", instance_name)
|
|
5483
5778
|
)
|
|
5484
5779
|
or (
|
|
5485
5780
|
torrent.availability < 1
|
|
5486
5781
|
and torrent.hash in self.cleaned_torrents
|
|
5487
5782
|
and torrent.state_enum in (TorrentStates.DOWNLOADING)
|
|
5488
|
-
and not self.in_tags(torrent, "qBitrr-ignored")
|
|
5489
|
-
and not self.in_tags(torrent, "qBitrr-free_space_paused")
|
|
5783
|
+
and not self.in_tags(torrent, "qBitrr-ignored", instance_name)
|
|
5784
|
+
and not self.in_tags(torrent, "qBitrr-free_space_paused", instance_name)
|
|
5490
5785
|
)
|
|
5491
5786
|
) and self.allowed_stalled:
|
|
5492
5787
|
if (
|
|
@@ -5496,8 +5791,8 @@ class Arr:
|
|
|
5496
5791
|
):
|
|
5497
5792
|
stalled_ignore = False
|
|
5498
5793
|
self.logger.trace("Process stalled, delay expired: %s", torrent.name)
|
|
5499
|
-
elif not self.in_tags(torrent, "qBitrr-allowed_stalled"):
|
|
5500
|
-
self.add_tags(torrent, ["qBitrr-allowed_stalled"])
|
|
5794
|
+
elif not self.in_tags(torrent, "qBitrr-allowed_stalled", instance_name):
|
|
5795
|
+
self.add_tags(torrent, ["qBitrr-allowed_stalled"], instance_name)
|
|
5501
5796
|
if self.re_search_stalled:
|
|
5502
5797
|
self.logger.trace(
|
|
5503
5798
|
"Stalled, adding tag, blocklosting and re-searching: %s", torrent.name
|
|
@@ -5514,7 +5809,7 @@ class Arr:
|
|
|
5514
5809
|
)
|
|
5515
5810
|
else:
|
|
5516
5811
|
self.logger.trace("Stalled, adding tag: %s", torrent.name)
|
|
5517
|
-
elif self.in_tags(torrent, "qBitrr-allowed_stalled"):
|
|
5812
|
+
elif self.in_tags(torrent, "qBitrr-allowed_stalled", instance_name):
|
|
5518
5813
|
self.logger.trace(
|
|
5519
5814
|
"Stalled: %s [Current:%s][Last Activity:%s][Limit:%s]",
|
|
5520
5815
|
torrent.name,
|
|
@@ -5525,8 +5820,8 @@ class Arr:
|
|
|
5525
5820
|
),
|
|
5526
5821
|
)
|
|
5527
5822
|
|
|
5528
|
-
elif self.in_tags(torrent, "qBitrr-allowed_stalled"):
|
|
5529
|
-
self.remove_tags(torrent, ["qBitrr-allowed_stalled"])
|
|
5823
|
+
elif self.in_tags(torrent, "qBitrr-allowed_stalled", instance_name):
|
|
5824
|
+
self.remove_tags(torrent, ["qBitrr-allowed_stalled"], instance_name)
|
|
5530
5825
|
stalled_ignore = False
|
|
5531
5826
|
self.logger.trace("Not stalled, removing tag: %s", torrent.name)
|
|
5532
5827
|
else:
|
|
@@ -5534,13 +5829,17 @@ class Arr:
|
|
|
5534
5829
|
self.logger.trace("Not stalled: %s", torrent.name)
|
|
5535
5830
|
return stalled_ignore
|
|
5536
5831
|
|
|
5537
|
-
def _process_single_torrent(
|
|
5832
|
+
def _process_single_torrent(
|
|
5833
|
+
self, torrent: qbittorrentapi.TorrentDictionary, instance_name: str = "default"
|
|
5834
|
+
):
|
|
5538
5835
|
if torrent.category != RECHECK_CATEGORY:
|
|
5539
5836
|
self.manager.qbit_manager.cache[torrent.hash] = torrent.category
|
|
5540
5837
|
self._process_single_torrent_trackers(torrent)
|
|
5541
5838
|
self.manager.qbit_manager.name_cache[torrent.hash] = torrent.name
|
|
5542
5839
|
time_now = time.time()
|
|
5543
|
-
leave_alone, _tracker_max_eta, remove_torrent = self._should_leave_alone(
|
|
5840
|
+
leave_alone, _tracker_max_eta, remove_torrent = self._should_leave_alone(
|
|
5841
|
+
torrent, instance_name
|
|
5842
|
+
)
|
|
5544
5843
|
self.logger.trace(
|
|
5545
5844
|
"Torrent [%s]: Leave Alone (allow seeding): %s, Max ETA: %s, State[%s]",
|
|
5546
5845
|
torrent.name,
|
|
@@ -5555,20 +5854,22 @@ class Arr:
|
|
|
5555
5854
|
TorrentStates.STALLED_DOWNLOAD,
|
|
5556
5855
|
TorrentStates.DOWNLOADING,
|
|
5557
5856
|
):
|
|
5558
|
-
stalled_ignore = self._stalled_check(torrent, time_now)
|
|
5857
|
+
stalled_ignore = self._stalled_check(torrent, time_now, instance_name)
|
|
5559
5858
|
else:
|
|
5560
5859
|
stalled_ignore = False
|
|
5561
5860
|
|
|
5562
|
-
if self.in_tags(torrent, "qBitrr-ignored"):
|
|
5563
|
-
self.remove_tags(
|
|
5861
|
+
if self.in_tags(torrent, "qBitrr-ignored", instance_name):
|
|
5862
|
+
self.remove_tags(
|
|
5863
|
+
torrent, ["qBitrr-allowed_seeding", "qBitrr-free_space_paused"], instance_name
|
|
5864
|
+
)
|
|
5564
5865
|
|
|
5565
5866
|
if (
|
|
5566
5867
|
self.custom_format_unmet_search
|
|
5567
5868
|
and self.custom_format_unmet_check(torrent)
|
|
5568
|
-
and not self.in_tags(torrent, "qBitrr-ignored")
|
|
5569
|
-
and not self.in_tags(torrent, "qBitrr-free_space_paused")
|
|
5869
|
+
and not self.in_tags(torrent, "qBitrr-ignored", instance_name)
|
|
5870
|
+
and not self.in_tags(torrent, "qBitrr-free_space_paused", instance_name)
|
|
5570
5871
|
):
|
|
5571
|
-
self._process_single_torrent_delete_cfunmet(torrent)
|
|
5872
|
+
self._process_single_torrent_delete_cfunmet(torrent, instance_name)
|
|
5572
5873
|
elif remove_torrent and not leave_alone and torrent.amount_left == 0:
|
|
5573
5874
|
self._process_single_torrent_delete_ratio_seed(torrent)
|
|
5574
5875
|
elif torrent.category == FAILED_CATEGORY:
|
|
@@ -5581,8 +5882,8 @@ class Arr:
|
|
|
5581
5882
|
self._process_single_torrent_ignored(torrent)
|
|
5582
5883
|
elif (
|
|
5583
5884
|
torrent.state_enum in (TorrentStates.METADATA_DOWNLOAD, TorrentStates.STALLED_DOWNLOAD)
|
|
5584
|
-
and not self.in_tags(torrent, "qBitrr-ignored")
|
|
5585
|
-
and not self.in_tags(torrent, "qBitrr-free_space_paused")
|
|
5885
|
+
and not self.in_tags(torrent, "qBitrr-ignored", instance_name)
|
|
5886
|
+
and not self.in_tags(torrent, "qBitrr-free_space_paused", instance_name)
|
|
5586
5887
|
and not stalled_ignore
|
|
5587
5888
|
):
|
|
5588
5889
|
self._process_single_torrent_stalled_torrent(torrent, "Stalled State")
|
|
@@ -5603,15 +5904,15 @@ class Arr:
|
|
|
5603
5904
|
elif (
|
|
5604
5905
|
torrent.state_enum == TorrentStates.PAUSED_DOWNLOAD
|
|
5605
5906
|
and torrent.amount_left != 0
|
|
5606
|
-
and not self.in_tags(torrent, "qBitrr-free_space_paused")
|
|
5607
|
-
and not self.in_tags(torrent, "qBitrr-ignored")
|
|
5907
|
+
and not self.in_tags(torrent, "qBitrr-free_space_paused", instance_name)
|
|
5908
|
+
and not self.in_tags(torrent, "qBitrr-ignored", instance_name)
|
|
5608
5909
|
):
|
|
5609
5910
|
self._process_single_torrent_paused(torrent)
|
|
5610
5911
|
elif (
|
|
5611
5912
|
torrent.progress <= self.maximum_deletable_percentage
|
|
5612
5913
|
and not self.is_complete_state(torrent)
|
|
5613
|
-
and not self.in_tags(torrent, "qBitrr-ignored")
|
|
5614
|
-
and not self.in_tags(torrent, "qBitrr-free_space_paused")
|
|
5914
|
+
and not self.in_tags(torrent, "qBitrr-ignored", instance_name)
|
|
5915
|
+
and not self.in_tags(torrent, "qBitrr-free_space_paused", instance_name)
|
|
5615
5916
|
and not stalled_ignore
|
|
5616
5917
|
) and torrent.hash in self.cleaned_torrents:
|
|
5617
5918
|
self._process_single_torrent_percentage_threshold(torrent, maximum_eta)
|
|
@@ -5658,8 +5959,8 @@ class Arr:
|
|
|
5658
5959
|
and time_now > torrent.added_on + self.ignore_torrents_younger_than
|
|
5659
5960
|
and 0 < maximum_eta < torrent.eta
|
|
5660
5961
|
and not self.do_not_remove_slow
|
|
5661
|
-
and not self.in_tags(torrent, "qBitrr-ignored")
|
|
5662
|
-
and not self.in_tags(torrent, "qBitrr-free_space_paused")
|
|
5962
|
+
and not self.in_tags(torrent, "qBitrr-ignored", instance_name)
|
|
5963
|
+
and not self.in_tags(torrent, "qBitrr-free_space_paused", instance_name)
|
|
5663
5964
|
and not stalled_ignore
|
|
5664
5965
|
):
|
|
5665
5966
|
self._process_single_torrent_delete_slow(torrent)
|
|
@@ -5674,8 +5975,8 @@ class Arr:
|
|
|
5674
5975
|
)
|
|
5675
5976
|
and torrent.hash in self.cleaned_torrents
|
|
5676
5977
|
and self.is_downloading_state(torrent)
|
|
5677
|
-
and not self.in_tags(torrent, "qBitrr-ignored")
|
|
5678
|
-
and not self.in_tags(torrent, "qBitrr-free_space_paused")
|
|
5978
|
+
and not self.in_tags(torrent, "qBitrr-ignored", instance_name)
|
|
5979
|
+
and not self.in_tags(torrent, "qBitrr-free_space_paused", instance_name)
|
|
5679
5980
|
and not stalled_ignore
|
|
5680
5981
|
):
|
|
5681
5982
|
self._process_single_torrent_stalled_torrent(torrent, "Unavailable")
|
|
@@ -6898,7 +7199,7 @@ class FreeSpaceManager(Arr):
|
|
|
6898
7199
|
)
|
|
6899
7200
|
self.pause.add(torrent.hash)
|
|
6900
7201
|
|
|
6901
|
-
def _process_single_torrent(self, torrent):
|
|
7202
|
+
def _process_single_torrent(self, torrent, instance_name: str = "default"):
|
|
6902
7203
|
if self.is_downloading_state(torrent):
|
|
6903
7204
|
free_space_test = self.current_free_space
|
|
6904
7205
|
free_space_test -= torrent["amount_left"]
|
|
@@ -6917,8 +7218,8 @@ class FreeSpaceManager(Arr):
|
|
|
6917
7218
|
format_bytes(torrent.amount_left),
|
|
6918
7219
|
format_bytes(-free_space_test),
|
|
6919
7220
|
)
|
|
6920
|
-
self.add_tags(torrent, ["qBitrr-free_space_paused"])
|
|
6921
|
-
self.remove_tags(torrent, ["qBitrr-allowed_seeding"])
|
|
7221
|
+
self.add_tags(torrent, ["qBitrr-free_space_paused"], instance_name)
|
|
7222
|
+
self.remove_tags(torrent, ["qBitrr-allowed_seeding"], instance_name)
|
|
6922
7223
|
self._process_single_torrent_pause_disk_space(torrent)
|
|
6923
7224
|
elif torrent.state_enum == TorrentStates.PAUSED_DOWNLOAD and free_space_test < 0:
|
|
6924
7225
|
self.logger.info(
|
|
@@ -6928,8 +7229,8 @@ class FreeSpaceManager(Arr):
|
|
|
6928
7229
|
format_bytes(torrent.amount_left),
|
|
6929
7230
|
format_bytes(-free_space_test),
|
|
6930
7231
|
)
|
|
6931
|
-
self.add_tags(torrent, ["qBitrr-free_space_paused"])
|
|
6932
|
-
self.remove_tags(torrent, ["qBitrr-allowed_seeding"])
|
|
7232
|
+
self.add_tags(torrent, ["qBitrr-free_space_paused"], instance_name)
|
|
7233
|
+
self.remove_tags(torrent, ["qBitrr-allowed_seeding"], instance_name)
|
|
6933
7234
|
elif torrent.state_enum != TorrentStates.PAUSED_DOWNLOAD and free_space_test > 0:
|
|
6934
7235
|
self.logger.info(
|
|
6935
7236
|
"Continuing download (sufficient space) | Torrent: %s | Available: %s | Space after: %s",
|
|
@@ -6938,7 +7239,7 @@ class FreeSpaceManager(Arr):
|
|
|
6938
7239
|
format_bytes(free_space_test + self._min_free_space_bytes),
|
|
6939
7240
|
)
|
|
6940
7241
|
self.current_free_space = free_space_test
|
|
6941
|
-
self.remove_tags(torrent, ["qBitrr-free_space_paused"])
|
|
7242
|
+
self.remove_tags(torrent, ["qBitrr-free_space_paused"], instance_name)
|
|
6942
7243
|
elif torrent.state_enum == TorrentStates.PAUSED_DOWNLOAD and free_space_test > 0:
|
|
6943
7244
|
self.logger.info(
|
|
6944
7245
|
"Resuming download (space available) | Torrent: %s | Available: %s | Space after: %s",
|
|
@@ -6947,16 +7248,16 @@ class FreeSpaceManager(Arr):
|
|
|
6947
7248
|
format_bytes(free_space_test + self._min_free_space_bytes),
|
|
6948
7249
|
)
|
|
6949
7250
|
self.current_free_space = free_space_test
|
|
6950
|
-
self.remove_tags(torrent, ["qBitrr-free_space_paused"])
|
|
7251
|
+
self.remove_tags(torrent, ["qBitrr-free_space_paused"], instance_name)
|
|
6951
7252
|
elif not self.is_downloading_state(torrent) and self.in_tags(
|
|
6952
|
-
torrent, "qBitrr-free_space_paused"
|
|
7253
|
+
torrent, "qBitrr-free_space_paused", instance_name
|
|
6953
7254
|
):
|
|
6954
7255
|
self.logger.info(
|
|
6955
7256
|
"Torrent completed, removing free space tag | Torrent: %s | Available: %s",
|
|
6956
7257
|
torrent.name,
|
|
6957
7258
|
format_bytes(self.current_free_space + self._min_free_space_bytes),
|
|
6958
7259
|
)
|
|
6959
|
-
self.remove_tags(torrent, ["qBitrr-free_space_paused"])
|
|
7260
|
+
self.remove_tags(torrent, ["qBitrr-free_space_paused"], instance_name)
|
|
6960
7261
|
|
|
6961
7262
|
def process(self):
|
|
6962
7263
|
self._process_paused()
|
|
@@ -6986,7 +7287,7 @@ class FreeSpaceManager(Arr):
|
|
|
6986
7287
|
torrents = [t for t in torrents if "qBitrr-ignored" not in t.tags]
|
|
6987
7288
|
self.category_torrent_count = len(torrents)
|
|
6988
7289
|
self.free_space_tagged_count = sum(
|
|
6989
|
-
1 for t in torrents if self.in_tags(t, "qBitrr-free_space_paused")
|
|
7290
|
+
1 for t in torrents if self.in_tags(t, "qBitrr-free_space_paused", "default")
|
|
6990
7291
|
)
|
|
6991
7292
|
if not len(torrents):
|
|
6992
7293
|
raise DelayLoopException(length=LOOP_SLEEP_TIMER, type="no_downloads")
|