qBitrr2 5.6.2__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 +237 -88
- qBitrr/bundled_data.py +2 -2
- 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.js +2 -2
- 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.6.2.dist-info → qbitrr2-5.7.0.dist-info}/METADATA +24 -2
- {qbitrr2-5.6.2.dist-info → qbitrr2-5.7.0.dist-info}/RECORD +23 -23
- {qbitrr2-5.6.2.dist-info → qbitrr2-5.7.0.dist-info}/WHEEL +0 -0
- {qbitrr2-5.6.2.dist-info → qbitrr2-5.7.0.dist-info}/entry_points.txt +0 -0
- {qbitrr2-5.6.2.dist-info → qbitrr2-5.7.0.dist-info}/licenses/LICENSE +0 -0
- {qbitrr2-5.6.2.dist-info → qbitrr2-5.7.0.dist-info}/top_level.txt +0 -0
qBitrr/bundled_data.py
CHANGED
qBitrr/main.py
CHANGED
|
@@ -102,6 +102,11 @@ class qBitManager:
|
|
|
102
102
|
self._validated_version = False
|
|
103
103
|
self.client = None
|
|
104
104
|
self.current_qbit_version = None
|
|
105
|
+
# Multi-instance support
|
|
106
|
+
self.clients: dict[str, qbittorrentapi.Client] = {}
|
|
107
|
+
self.qbit_versions: dict[str, VersionClass] = {}
|
|
108
|
+
self.instance_metadata: dict[str, dict] = {}
|
|
109
|
+
self.instance_health: dict[str, bool] = {}
|
|
105
110
|
if not (QBIT_DISABLED or SEARCH_ONLY):
|
|
106
111
|
self.client = qbittorrentapi.Client(
|
|
107
112
|
host=self.qBit_Host,
|
|
@@ -120,6 +125,15 @@ class qBitManager:
|
|
|
120
125
|
e,
|
|
121
126
|
)
|
|
122
127
|
self._version_validator()
|
|
128
|
+
# Register default instance in multi-instance dictionaries
|
|
129
|
+
self.clients["default"] = self.client
|
|
130
|
+
self.qbit_versions["default"] = self.current_qbit_version
|
|
131
|
+
self.instance_metadata["default"] = {
|
|
132
|
+
"host": self.qBit_Host,
|
|
133
|
+
"port": self.qBit_Port,
|
|
134
|
+
"username": self.qBit_UserName,
|
|
135
|
+
}
|
|
136
|
+
self.instance_health["default"] = self._validated_version
|
|
123
137
|
self.expiring_bool = ExpiringSet(max_age_seconds=10)
|
|
124
138
|
self.cache = {}
|
|
125
139
|
self.name_cache = {}
|
|
@@ -383,6 +397,8 @@ class qBitManager:
|
|
|
383
397
|
def _complete_startup(self) -> None:
|
|
384
398
|
started_at = monotonic()
|
|
385
399
|
try:
|
|
400
|
+
# Initialize all qBit instances before Arr managers
|
|
401
|
+
self._initialize_qbit_instances()
|
|
386
402
|
arr_manager = ArrManager(self)
|
|
387
403
|
self.arr_manager = arr_manager
|
|
388
404
|
arr_manager.build_arr_instances()
|
|
@@ -427,10 +443,195 @@ class qBitManager:
|
|
|
427
443
|
)
|
|
428
444
|
sys.exit(1)
|
|
429
445
|
|
|
446
|
+
def _initialize_qbit_instances(self) -> None:
|
|
447
|
+
"""
|
|
448
|
+
Initialize all qBittorrent instances from config.
|
|
449
|
+
|
|
450
|
+
Scans config for [qBit] and [qBit-XXX] sections, initializes clients,
|
|
451
|
+
and populates multi-instance dictionaries. The default [qBit] section
|
|
452
|
+
is registered as "default" instance.
|
|
453
|
+
"""
|
|
454
|
+
if QBIT_DISABLED or SEARCH_ONLY:
|
|
455
|
+
self.logger.debug("qBit disabled or search-only mode; skipping instance init")
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
# Default instance already initialized in __init__
|
|
459
|
+
self.logger.info("Initialized qBit instance: default")
|
|
460
|
+
|
|
461
|
+
# Scan for additional instances (qBit-XXX sections)
|
|
462
|
+
for section in CONFIG.sections():
|
|
463
|
+
if section.startswith("qBit-") and section != "qBit":
|
|
464
|
+
instance_name = section.replace("qBit-", "", 1)
|
|
465
|
+
try:
|
|
466
|
+
self._init_instance(section, instance_name)
|
|
467
|
+
self.logger.info("Initialized qBit instance: %s", instance_name)
|
|
468
|
+
except Exception as e:
|
|
469
|
+
self.logger.error(
|
|
470
|
+
"Failed to initialize qBit instance '%s': %s", instance_name, e
|
|
471
|
+
)
|
|
472
|
+
self.instance_health[instance_name] = False
|
|
473
|
+
|
|
474
|
+
self.logger.info("Total qBit instances initialized: %d", len(self.clients))
|
|
475
|
+
|
|
476
|
+
def _init_instance(self, section_name: str, instance_name: str) -> None:
|
|
477
|
+
"""
|
|
478
|
+
Initialize a single qBittorrent instance.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
section_name: Config section name (e.g., "qBit-Seedbox")
|
|
482
|
+
instance_name: Short instance identifier (e.g., "Seedbox")
|
|
483
|
+
|
|
484
|
+
Raises:
|
|
485
|
+
Exception: If connection fails or version is unsupported
|
|
486
|
+
"""
|
|
487
|
+
host = CONFIG.get(f"{section_name}.Host", fallback="localhost")
|
|
488
|
+
port = CONFIG.get(f"{section_name}.Port", fallback=8105)
|
|
489
|
+
username = CONFIG.get(f"{section_name}.UserName", fallback=None)
|
|
490
|
+
password = CONFIG.get(f"{section_name}.Password", fallback=None)
|
|
491
|
+
|
|
492
|
+
self.logger.debug(
|
|
493
|
+
"Connecting to qBit instance '%s': %s:%s (user: %s)",
|
|
494
|
+
instance_name,
|
|
495
|
+
host,
|
|
496
|
+
port,
|
|
497
|
+
username,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
client = qbittorrentapi.Client(
|
|
501
|
+
host=host,
|
|
502
|
+
port=port,
|
|
503
|
+
username=username,
|
|
504
|
+
password=password,
|
|
505
|
+
SIMPLE_RESPONSES=False,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Test connection and get version
|
|
509
|
+
try:
|
|
510
|
+
version = version_parser.parse(client.app_version())
|
|
511
|
+
self.logger.debug("Instance '%s' version: %s", instance_name, version)
|
|
512
|
+
except Exception as e:
|
|
513
|
+
self.logger.error(
|
|
514
|
+
"Could not connect to qBit instance '%s' at %s:%s: %s",
|
|
515
|
+
instance_name,
|
|
516
|
+
host,
|
|
517
|
+
port,
|
|
518
|
+
e,
|
|
519
|
+
)
|
|
520
|
+
raise
|
|
521
|
+
|
|
522
|
+
# Validate version
|
|
523
|
+
if version < self.min_supported_version:
|
|
524
|
+
self.logger.critical(
|
|
525
|
+
"Instance '%s' version %s is below minimum supported %s",
|
|
526
|
+
instance_name,
|
|
527
|
+
version,
|
|
528
|
+
self.min_supported_version,
|
|
529
|
+
)
|
|
530
|
+
raise ValueError(
|
|
531
|
+
f"Unsupported qBittorrent version {version} for instance {instance_name}"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# Register instance
|
|
535
|
+
self.clients[instance_name] = client
|
|
536
|
+
self.qbit_versions[instance_name] = version
|
|
537
|
+
self.instance_metadata[instance_name] = {
|
|
538
|
+
"host": host,
|
|
539
|
+
"port": port,
|
|
540
|
+
"username": username,
|
|
541
|
+
}
|
|
542
|
+
self.instance_health[instance_name] = True
|
|
543
|
+
|
|
544
|
+
def is_instance_alive(self, instance_name: str = "default") -> bool:
|
|
545
|
+
"""
|
|
546
|
+
Check if a specific qBittorrent instance is alive and responding.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
instance_name: The instance identifier (default: "default")
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
bool: True if instance is healthy and responding, False otherwise
|
|
553
|
+
"""
|
|
554
|
+
if instance_name not in self.clients:
|
|
555
|
+
self.logger.warning("Instance '%s' not found in clients", instance_name)
|
|
556
|
+
return False
|
|
557
|
+
|
|
558
|
+
client = self.clients[instance_name]
|
|
559
|
+
if client is None:
|
|
560
|
+
return False
|
|
561
|
+
|
|
562
|
+
try:
|
|
563
|
+
# Quick health check - just get app version
|
|
564
|
+
client.app_version()
|
|
565
|
+
self.instance_health[instance_name] = True
|
|
566
|
+
return True
|
|
567
|
+
except Exception as e:
|
|
568
|
+
self.logger.debug("Instance '%s' health check failed: %s", instance_name, e)
|
|
569
|
+
self.instance_health[instance_name] = False
|
|
570
|
+
return False
|
|
571
|
+
|
|
572
|
+
def get_all_instances(self) -> list[str]:
|
|
573
|
+
"""
|
|
574
|
+
Get list of all configured qBittorrent instance names.
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
list[str]: List of instance identifiers (e.g., ["default", "Seedbox"])
|
|
578
|
+
"""
|
|
579
|
+
return list(self.clients.keys())
|
|
580
|
+
|
|
581
|
+
def get_healthy_instances(self) -> list[str]:
|
|
582
|
+
"""
|
|
583
|
+
Get list of all healthy (responding) qBittorrent instances.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
list[str]: List of healthy instance identifiers
|
|
587
|
+
"""
|
|
588
|
+
return [name for name in self.clients.keys() if self.is_instance_alive(name)]
|
|
589
|
+
|
|
590
|
+
def get_instance_info(self, instance_name: str = "default") -> dict:
|
|
591
|
+
"""
|
|
592
|
+
Get metadata about a specific qBittorrent instance.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
instance_name: The instance identifier (default: "default")
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
dict: Instance metadata including host, port, version, health status
|
|
599
|
+
"""
|
|
600
|
+
if instance_name not in self.clients:
|
|
601
|
+
return {"error": f"Instance '{instance_name}' not found"}
|
|
602
|
+
|
|
603
|
+
metadata = self.instance_metadata.get(instance_name, {})
|
|
604
|
+
return {
|
|
605
|
+
"name": instance_name,
|
|
606
|
+
"host": metadata.get("host"),
|
|
607
|
+
"port": metadata.get("port"),
|
|
608
|
+
"version": str(self.qbit_versions.get(instance_name, "unknown")),
|
|
609
|
+
"healthy": self.instance_health.get(instance_name, False),
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
def get_client(self, instance_name: str = "default") -> qbittorrentapi.Client | None:
|
|
613
|
+
"""
|
|
614
|
+
Get qBittorrent client for a specific instance.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
instance_name: The instance identifier (default: "default")
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
qbittorrentapi.Client | None: Client instance, or None if not found/unhealthy
|
|
621
|
+
"""
|
|
622
|
+
if instance_name not in self.clients:
|
|
623
|
+
self.logger.warning("Instance '%s' not found in clients", instance_name)
|
|
624
|
+
return None
|
|
625
|
+
return self.clients[instance_name]
|
|
626
|
+
|
|
430
627
|
# @response_text(str)
|
|
431
628
|
# @login_required
|
|
432
|
-
def app_version(self, **kwargs):
|
|
433
|
-
|
|
629
|
+
def app_version(self, instance_name: str = "default", **kwargs):
|
|
630
|
+
"""Get qBittorrent app version for a specific instance."""
|
|
631
|
+
client = self.get_client(instance_name)
|
|
632
|
+
if client is None:
|
|
633
|
+
return None
|
|
634
|
+
return client._get(
|
|
434
635
|
_name=APINames.Application,
|
|
435
636
|
_method="version",
|
|
436
637
|
_retries=0,
|
|
@@ -438,23 +639,44 @@ class qBitManager:
|
|
|
438
639
|
**kwargs,
|
|
439
640
|
)
|
|
440
641
|
|
|
441
|
-
def transfer_info(self, **kwargs):
|
|
442
|
-
"""
|
|
443
|
-
|
|
642
|
+
def transfer_info(self, instance_name: str = "default", **kwargs):
|
|
643
|
+
"""
|
|
644
|
+
Proxy transfer info requests to a specific qBittorrent instance.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
instance_name: The instance identifier (default: "default")
|
|
648
|
+
**kwargs: Additional arguments to pass to transfer_info
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
dict: Transfer info or connection status
|
|
652
|
+
"""
|
|
653
|
+
client = self.get_client(instance_name)
|
|
654
|
+
if client is None:
|
|
444
655
|
return {"connection_status": "disconnected"}
|
|
445
|
-
return
|
|
656
|
+
return client.transfer_info(**kwargs)
|
|
446
657
|
|
|
447
658
|
@property
|
|
448
659
|
def is_alive(self) -> bool:
|
|
660
|
+
"""
|
|
661
|
+
Check if the default qBittorrent instance is alive.
|
|
662
|
+
|
|
663
|
+
Backward-compatible property that delegates to is_instance_alive("default").
|
|
664
|
+
Uses caching via expiring_bool to avoid excessive health checks.
|
|
665
|
+
"""
|
|
449
666
|
try:
|
|
450
667
|
if self.client is None:
|
|
451
668
|
return False
|
|
452
669
|
if 1 in self.expiring_bool:
|
|
453
670
|
return True
|
|
454
|
-
|
|
455
|
-
self.
|
|
456
|
-
|
|
457
|
-
|
|
671
|
+
# Delegate to instance health check
|
|
672
|
+
alive = self.is_instance_alive("default")
|
|
673
|
+
if alive:
|
|
674
|
+
self.logger.trace(
|
|
675
|
+
"Successfully connected to %s:%s", self.qBit_Host, self.qBit_Port
|
|
676
|
+
)
|
|
677
|
+
self.expiring_bool.add(1)
|
|
678
|
+
return True
|
|
679
|
+
self.logger.warning("Could not connect to %s:%s", self.qBit_Host, self.qBit_Port)
|
|
458
680
|
except requests.RequestException:
|
|
459
681
|
self.logger.warning("Could not connect to %s:%s", self.qBit_Host, self.qBit_Port)
|
|
460
682
|
self.should_delay_torrent_scan = True
|