qBitrr2 5.7.1__tar.gz → 5.8.0__tar.gz
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.
- {qbitrr2-5.7.1/qBitrr2.egg-info → qbitrr2-5.8.0}/PKG-INFO +19 -1
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/README.md +18 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/pyproject.toml +1 -1
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/arss.py +25 -141
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/bundled_data.py +2 -2
- qbitrr2-5.8.0/qBitrr/database.py +79 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/main.py +14 -8
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/search_activity_store.py +9 -39
- qbitrr2-5.8.0/qBitrr/static/assets/ConfigView.js +6 -0
- qbitrr2-5.8.0/qBitrr/static/assets/ConfigView.js.map +1 -0
- qbitrr2-5.8.0/qBitrr/static/assets/LogsView.js +208 -0
- qbitrr2-5.8.0/qBitrr/static/assets/LogsView.js.map +1 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/tables.py +11 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0/qBitrr2.egg-info}/PKG-INFO +19 -1
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr2.egg-info/SOURCES.txt +1 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/setup.cfg +1 -1
- qbitrr2-5.7.1/qBitrr/static/assets/ConfigView.js +0 -6
- qbitrr2-5.7.1/qBitrr/static/assets/ConfigView.js.map +0 -1
- qbitrr2-5.7.1/qBitrr/static/assets/LogsView.js +0 -208
- qbitrr2-5.7.1/qBitrr/static/assets/LogsView.js.map +0 -1
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/LICENSE +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/MANIFEST.in +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/config.example.toml +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/__init__.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/auto_update.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/config.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/config_version.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/db_lock.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/db_recovery.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/env_config.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/errors.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/ffprobe.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/gen_config.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/home_path.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/logger.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/ArrView.js +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/ArrView.js.map +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/ProcessesView.js +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/ProcessesView.js.map +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/app.css +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/app.js +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/app.js.map +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/build.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/check-mark.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/close.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/download.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/gear.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/lidarr.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/live-streaming.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/log.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/logo.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/plus.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/process.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/react-select.esm.js +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/react-select.esm.js.map +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/refresh-arrow.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/table.js +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/table.js.map +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/trash.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/up-arrow.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/useInterval.js +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/useInterval.js.map +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/vendor.js +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/vendor.js.map +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/visibility.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/favicon-16x16.png +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/favicon-32x32.png +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/favicon-48x48.png +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/favicon.ico +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/icon-192.png +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/icon-512.png +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/index.html +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/logov2-clean.png +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/logov2-clean.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/manifest.json +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/sw.js +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/vite.svg +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/utils.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/versioning.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/webui.py +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr2.egg-info/dependency_links.txt +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr2.egg-info/entry_points.txt +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr2.egg-info/requires.txt +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr2.egg-info/top_level.txt +0 -0
- {qbitrr2-5.7.1 → qbitrr2-5.8.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qBitrr2
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.8.0
|
|
4
4
|
Summary: Intelligent automation for qBittorrent and *Arr apps (Radarr/Sonarr/Lidarr) - health monitoring, instant imports, quality upgrades, request integration
|
|
5
5
|
Home-page: https://github.com/Feramance/qBitrr
|
|
6
6
|
Author: Feramance
|
|
@@ -169,6 +169,24 @@ services:
|
|
|
169
169
|
|
|
170
170
|
Access the WebUI at `http://<host>:6969/ui` after startup.
|
|
171
171
|
|
|
172
|
+
## 🆕 What's New in v5.8.0
|
|
173
|
+
|
|
174
|
+
### Single Consolidated Database
|
|
175
|
+
qBitrr now uses a **single `qbitrr.db` file** for all Arr instances, replacing the previous per-instance database approach.
|
|
176
|
+
|
|
177
|
+
**Benefits:**
|
|
178
|
+
- ✅ Single file to backup instead of 9+ separate databases
|
|
179
|
+
- ✅ 78% code reduction in database initialization
|
|
180
|
+
- ✅ Better performance with shared connection pool
|
|
181
|
+
- ✅ Simplified database management
|
|
182
|
+
|
|
183
|
+
**Migration:**
|
|
184
|
+
- Automatic on first upgrade (5-30 minutes re-sync from Arr APIs)
|
|
185
|
+
- Old databases deleted automatically
|
|
186
|
+
- No manual intervention required
|
|
187
|
+
|
|
188
|
+
[Full Migration Guide →](https://feramance.github.io/qBitrr/getting-started/migration/)
|
|
189
|
+
|
|
172
190
|
## ✨ Key Features
|
|
173
191
|
|
|
174
192
|
- **🚀 Multi-qBittorrent Support (v3.0+)** – Manage torrents across multiple qBittorrent instances for load balancing, redundancy, and VPN isolation
|
|
@@ -65,6 +65,24 @@ services:
|
|
|
65
65
|
|
|
66
66
|
Access the WebUI at `http://<host>:6969/ui` after startup.
|
|
67
67
|
|
|
68
|
+
## 🆕 What's New in v5.8.0
|
|
69
|
+
|
|
70
|
+
### Single Consolidated Database
|
|
71
|
+
qBitrr now uses a **single `qbitrr.db` file** for all Arr instances, replacing the previous per-instance database approach.
|
|
72
|
+
|
|
73
|
+
**Benefits:**
|
|
74
|
+
- ✅ Single file to backup instead of 9+ separate databases
|
|
75
|
+
- ✅ 78% code reduction in database initialization
|
|
76
|
+
- ✅ Better performance with shared connection pool
|
|
77
|
+
- ✅ Simplified database management
|
|
78
|
+
|
|
79
|
+
**Migration:**
|
|
80
|
+
- Automatic on first upgrade (5-30 minutes re-sync from Arr APIs)
|
|
81
|
+
- Old databases deleted automatically
|
|
82
|
+
- No manual intervention required
|
|
83
|
+
|
|
84
|
+
[Full Migration Guide →](https://feramance.github.io/qBitrr/getting-started/migration/)
|
|
85
|
+
|
|
68
86
|
## ✨ Key Features
|
|
69
87
|
|
|
70
88
|
- **🚀 Multi-qBittorrent Support (v3.0+)** – Manage torrents across multiple qBittorrent instances for load balancing, redundancy, and VPN isolation
|
|
@@ -28,7 +28,7 @@ target-version = ['py311']
|
|
|
28
28
|
|
|
29
29
|
[tool.poetry]
|
|
30
30
|
name = "pypi-public"
|
|
31
|
-
version = "5.
|
|
31
|
+
version = "5.8.0"
|
|
32
32
|
description = "Intelligent automation for qBittorrent and *Arr apps (Radarr/Sonarr/Lidarr) - health monitoring, instant imports, quality upgrades, request integration"
|
|
33
33
|
authors = ["Drapersniper", "Feramance"]
|
|
34
34
|
readme = "README.md"
|
|
@@ -6615,159 +6615,43 @@ class Arr:
|
|
|
6615
6615
|
self.logger.error(f"Error checking temp profile timeouts: {e}", exc_info=True)
|
|
6616
6616
|
|
|
6617
6617
|
def register_search_mode(self):
|
|
6618
|
+
"""Initialize database models using the single shared database."""
|
|
6618
6619
|
if self.search_setup_completed:
|
|
6619
6620
|
return
|
|
6620
6621
|
|
|
6621
|
-
|
|
6622
|
+
# Import the shared database
|
|
6623
|
+
from qBitrr.database import get_database
|
|
6622
6624
|
|
|
6623
|
-
|
|
6624
|
-
self.search_missing
|
|
6625
|
-
or self.do_upgrade_search
|
|
6626
|
-
or self.quality_unmet_search
|
|
6627
|
-
or self.custom_format_unmet_search
|
|
6628
|
-
or self.ombi_search_requests
|
|
6629
|
-
or self.overseerr_requests
|
|
6630
|
-
):
|
|
6631
|
-
if db5 and getattr(self, "torrents", None) is None:
|
|
6632
|
-
self.torrent_db = SqliteDatabase(None)
|
|
6633
|
-
self.torrent_db.init(
|
|
6634
|
-
str(self._app_data_folder.joinpath("Torrents.db")),
|
|
6635
|
-
pragmas={
|
|
6636
|
-
"journal_mode": "wal",
|
|
6637
|
-
"cache_size": -64_000,
|
|
6638
|
-
"foreign_keys": 1,
|
|
6639
|
-
"ignore_check_constraints": 0,
|
|
6640
|
-
"synchronous": 0,
|
|
6641
|
-
"read_uncommitted": 1,
|
|
6642
|
-
},
|
|
6643
|
-
timeout=15,
|
|
6644
|
-
)
|
|
6645
|
-
|
|
6646
|
-
class Torrents(db5):
|
|
6647
|
-
class Meta:
|
|
6648
|
-
database = self.torrent_db
|
|
6649
|
-
|
|
6650
|
-
# Connect with retry logic for transient I/O errors
|
|
6651
|
-
with_database_retry(
|
|
6652
|
-
lambda: self.torrent_db.connect(),
|
|
6653
|
-
logger=self.logger,
|
|
6654
|
-
)
|
|
6655
|
-
self.torrent_db.create_tables([Torrents])
|
|
6656
|
-
self.torrents = Torrents
|
|
6657
|
-
self.search_setup_completed = True
|
|
6658
|
-
return
|
|
6659
|
-
|
|
6660
|
-
self.search_db_file.parent.mkdir(parents=True, exist_ok=True)
|
|
6661
|
-
self.db = SqliteDatabase(None)
|
|
6662
|
-
self.db.init(
|
|
6663
|
-
str(self.search_db_file),
|
|
6664
|
-
pragmas={
|
|
6665
|
-
"journal_mode": "wal",
|
|
6666
|
-
"cache_size": -64_000,
|
|
6667
|
-
"foreign_keys": 1,
|
|
6668
|
-
"ignore_check_constraints": 0,
|
|
6669
|
-
"synchronous": 0,
|
|
6670
|
-
"read_uncommitted": 1,
|
|
6671
|
-
},
|
|
6672
|
-
timeout=15,
|
|
6673
|
-
)
|
|
6674
|
-
|
|
6675
|
-
class Files(db1):
|
|
6676
|
-
class Meta:
|
|
6677
|
-
database = self.db
|
|
6678
|
-
|
|
6679
|
-
class Queue(db2):
|
|
6680
|
-
class Meta:
|
|
6681
|
-
database = self.db
|
|
6682
|
-
|
|
6683
|
-
class PersistingQueue(FilesQueued):
|
|
6684
|
-
class Meta:
|
|
6685
|
-
database = self.db
|
|
6625
|
+
self.db = get_database()
|
|
6686
6626
|
|
|
6687
|
-
#
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
logger=self.logger,
|
|
6627
|
+
# Get the appropriate model classes for this Arr type
|
|
6628
|
+
file_model, queue_model, series_or_artist_model, track_model, torrent_model = (
|
|
6629
|
+
self._get_models()
|
|
6691
6630
|
)
|
|
6692
6631
|
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
database = self.db
|
|
6698
|
-
|
|
6699
|
-
self.track_file_model = Tracks
|
|
6700
|
-
else:
|
|
6701
|
-
self.track_file_model = None
|
|
6702
|
-
|
|
6703
|
-
if db3 and self.type == "sonarr":
|
|
6704
|
-
|
|
6705
|
-
class Series(db3):
|
|
6706
|
-
class Meta:
|
|
6707
|
-
database = self.db
|
|
6632
|
+
# Set model references for this instance
|
|
6633
|
+
self.model_file = file_model
|
|
6634
|
+
self.model_queue = queue_model
|
|
6635
|
+
self.persistent_queue = FilesQueued
|
|
6708
6636
|
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
self.logger.error("Failed to create database tables for Sonarr: %s", e)
|
|
6713
|
-
raise
|
|
6714
|
-
self.series_file_model = Series
|
|
6715
|
-
self.artists_file_model = None
|
|
6716
|
-
elif db3 and self.type == "lidarr":
|
|
6717
|
-
|
|
6718
|
-
class Artists(db3):
|
|
6719
|
-
class Meta:
|
|
6720
|
-
database = self.db
|
|
6721
|
-
|
|
6722
|
-
try:
|
|
6723
|
-
self.db.create_tables([Files, Queue, PersistingQueue, Artists, Tracks], safe=True)
|
|
6724
|
-
except Exception as e:
|
|
6725
|
-
self.logger.error("Failed to create database tables for Lidarr: %s", e)
|
|
6726
|
-
raise
|
|
6727
|
-
self.artists_file_model = Artists
|
|
6728
|
-
self.series_file_model = None # Lidarr uses artists, not series
|
|
6729
|
-
else:
|
|
6730
|
-
# Radarr or any type without db3/db4 (series/artists/tracks models)
|
|
6731
|
-
try:
|
|
6732
|
-
self.db.create_tables([Files, Queue, PersistingQueue], safe=True)
|
|
6733
|
-
except Exception as e:
|
|
6734
|
-
self.logger.error("Failed to create database tables for Radarr: %s", e)
|
|
6735
|
-
raise
|
|
6637
|
+
# Set type-specific models
|
|
6638
|
+
if self.type == "sonarr":
|
|
6639
|
+
self.series_file_model = series_or_artist_model
|
|
6736
6640
|
self.artists_file_model = None
|
|
6641
|
+
self.track_file_model = None
|
|
6642
|
+
elif self.type == "lidarr":
|
|
6737
6643
|
self.series_file_model = None
|
|
6644
|
+
self.artists_file_model = series_or_artist_model
|
|
6645
|
+
self.track_file_model = track_model
|
|
6646
|
+
else: # radarr
|
|
6647
|
+
self.series_file_model = None
|
|
6648
|
+
self.artists_file_model = None
|
|
6649
|
+
self.track_file_model = None
|
|
6738
6650
|
|
|
6739
|
-
if
|
|
6740
|
-
|
|
6741
|
-
self.torrent_db.init(
|
|
6742
|
-
str(self._app_data_folder.joinpath("Torrents.db")),
|
|
6743
|
-
pragmas={
|
|
6744
|
-
"journal_mode": "wal",
|
|
6745
|
-
"cache_size": -64_000,
|
|
6746
|
-
"foreign_keys": 1,
|
|
6747
|
-
"ignore_check_constraints": 0,
|
|
6748
|
-
"synchronous": 0,
|
|
6749
|
-
"read_uncommitted": 1,
|
|
6750
|
-
},
|
|
6751
|
-
timeout=15,
|
|
6752
|
-
)
|
|
6753
|
-
|
|
6754
|
-
class Torrents(db5):
|
|
6755
|
-
class Meta:
|
|
6756
|
-
database = self.torrent_db
|
|
6757
|
-
|
|
6758
|
-
# Connect with retry logic for transient I/O errors
|
|
6759
|
-
with_database_retry(
|
|
6760
|
-
lambda: self.torrent_db.connect(),
|
|
6761
|
-
logger=self.logger,
|
|
6762
|
-
)
|
|
6763
|
-
self.torrent_db.create_tables([Torrents])
|
|
6764
|
-
self.torrents = Torrents
|
|
6765
|
-
else:
|
|
6766
|
-
self.torrents = None
|
|
6651
|
+
# Set torrents model if TAGLESS is enabled
|
|
6652
|
+
self.torrents = torrent_model if TAGLESS else None
|
|
6767
6653
|
|
|
6768
|
-
self.
|
|
6769
|
-
self.model_queue = Queue
|
|
6770
|
-
self.persistent_queue = PersistingQueue
|
|
6654
|
+
self.logger.debug("Database initialization completed for %s", self._name)
|
|
6771
6655
|
self.search_setup_completed = True
|
|
6772
6656
|
|
|
6773
6657
|
def _get_models(
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Single consolidated database for all Arr instances."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from peewee import SqliteDatabase
|
|
9
|
+
|
|
10
|
+
from qBitrr.config import APPDATA_FOLDER
|
|
11
|
+
from qBitrr.db_lock import with_database_retry
|
|
12
|
+
from qBitrr.tables import (
|
|
13
|
+
AlbumFilesModel,
|
|
14
|
+
AlbumQueueModel,
|
|
15
|
+
ArtistFilesModel,
|
|
16
|
+
EpisodeFilesModel,
|
|
17
|
+
EpisodeQueueModel,
|
|
18
|
+
FilesQueued,
|
|
19
|
+
MovieQueueModel,
|
|
20
|
+
MoviesFilesModel,
|
|
21
|
+
SeriesFilesModel,
|
|
22
|
+
TorrentLibrary,
|
|
23
|
+
TrackFilesModel,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("qBitrr.database")
|
|
27
|
+
|
|
28
|
+
# Global database instance
|
|
29
|
+
_db: SqliteDatabase | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_database() -> SqliteDatabase:
|
|
33
|
+
"""Get or create the global database instance."""
|
|
34
|
+
global _db
|
|
35
|
+
if _db is None:
|
|
36
|
+
db_path = Path(APPDATA_FOLDER) / "qbitrr.db"
|
|
37
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
_db = SqliteDatabase(
|
|
40
|
+
str(db_path),
|
|
41
|
+
pragmas={
|
|
42
|
+
"journal_mode": "wal",
|
|
43
|
+
"cache_size": -64_000,
|
|
44
|
+
"foreign_keys": 1,
|
|
45
|
+
"ignore_check_constraints": 0,
|
|
46
|
+
"synchronous": 0,
|
|
47
|
+
"read_uncommitted": 1,
|
|
48
|
+
},
|
|
49
|
+
timeout=15,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Connect with retry logic
|
|
53
|
+
with_database_retry(
|
|
54
|
+
lambda: _db.connect(reuse_if_open=True),
|
|
55
|
+
logger=logger,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Bind models to database
|
|
59
|
+
models = [
|
|
60
|
+
MoviesFilesModel,
|
|
61
|
+
EpisodeFilesModel,
|
|
62
|
+
AlbumFilesModel,
|
|
63
|
+
SeriesFilesModel,
|
|
64
|
+
ArtistFilesModel,
|
|
65
|
+
TrackFilesModel,
|
|
66
|
+
MovieQueueModel,
|
|
67
|
+
EpisodeQueueModel,
|
|
68
|
+
AlbumQueueModel,
|
|
69
|
+
FilesQueued,
|
|
70
|
+
TorrentLibrary,
|
|
71
|
+
]
|
|
72
|
+
_db.bind(models)
|
|
73
|
+
|
|
74
|
+
# Create all tables
|
|
75
|
+
_db.create_tables(models, safe=True)
|
|
76
|
+
|
|
77
|
+
logger.info("Initialized single database: %s", db_path)
|
|
78
|
+
|
|
79
|
+
return _db
|
|
@@ -53,28 +53,34 @@ def _mask_secret(value: str | None) -> str:
|
|
|
53
53
|
|
|
54
54
|
def _delete_all_databases() -> None:
|
|
55
55
|
"""
|
|
56
|
-
Delete
|
|
56
|
+
Delete old per-instance database files from the APPDATA_FOLDER on startup.
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
- All .db-wal files (Write-Ahead Log files)
|
|
61
|
-
- All .db-shm files (Shared Memory files)
|
|
58
|
+
Preserves the consolidated database (qbitrr.db) and Torrents.db.
|
|
59
|
+
Deletes old per-instance databases and their WAL/SHM files.
|
|
62
60
|
"""
|
|
63
61
|
db_patterns = ["*.db", "*.db-wal", "*.db-shm"]
|
|
64
62
|
deleted_files = []
|
|
63
|
+
# Files to preserve (consolidated database)
|
|
64
|
+
preserve_files = {"qbitrr.db", "Torrents.db"}
|
|
65
65
|
|
|
66
66
|
for pattern in db_patterns:
|
|
67
67
|
for db_file in glob.glob(str(APPDATA_FOLDER.joinpath(pattern))):
|
|
68
|
+
base_name = os.path.basename(db_file)
|
|
69
|
+
# Preserve consolidated database and its WAL/SHM files
|
|
70
|
+
should_preserve = any(base_name.startswith(f) for f in preserve_files)
|
|
71
|
+
if should_preserve:
|
|
72
|
+
continue
|
|
73
|
+
|
|
68
74
|
try:
|
|
69
75
|
os.remove(db_file)
|
|
70
|
-
deleted_files.append(
|
|
76
|
+
deleted_files.append(base_name)
|
|
71
77
|
except Exception as e:
|
|
72
78
|
logger.error("Failed to delete database file %s: %s", db_file, e)
|
|
73
79
|
|
|
74
80
|
if deleted_files:
|
|
75
|
-
logger.info("Deleted database files on startup: %s", ", ".join(deleted_files))
|
|
81
|
+
logger.info("Deleted old database files on startup: %s", ", ".join(deleted_files))
|
|
76
82
|
else:
|
|
77
|
-
logger.debug("No database files found to delete on startup")
|
|
83
|
+
logger.debug("No old database files found to delete on startup")
|
|
78
84
|
|
|
79
85
|
|
|
80
86
|
class qBitManager:
|
|
@@ -3,52 +3,25 @@ from __future__ import annotations
|
|
|
3
3
|
from threading import RLock
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from peewee import Model,
|
|
6
|
+
from peewee import Model, TextField
|
|
7
7
|
|
|
8
|
-
from qBitrr.
|
|
9
|
-
from qBitrr.home_path import APPDATA_FOLDER
|
|
8
|
+
from qBitrr.database import get_database
|
|
10
9
|
|
|
11
10
|
_DB_LOCK = RLock()
|
|
12
|
-
_DB_INSTANCE: SqliteDatabase | None = None
|
|
13
11
|
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
global _DB_INSTANCE
|
|
17
|
-
if _DB_INSTANCE is None:
|
|
18
|
-
path = APPDATA_FOLDER.joinpath("webui_activity.db")
|
|
19
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
-
_DB_INSTANCE = SqliteDatabase(
|
|
21
|
-
str(path),
|
|
22
|
-
pragmas={
|
|
23
|
-
"journal_mode": "wal",
|
|
24
|
-
"cache_size": -64_000,
|
|
25
|
-
"foreign_keys": 1,
|
|
26
|
-
"ignore_check_constraints": 0,
|
|
27
|
-
"synchronous": 0,
|
|
28
|
-
"read_uncommitted": 1,
|
|
29
|
-
},
|
|
30
|
-
timeout=15,
|
|
31
|
-
check_same_thread=False,
|
|
32
|
-
)
|
|
33
|
-
return _DB_INSTANCE
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class BaseModel(Model):
|
|
37
|
-
class Meta:
|
|
38
|
-
database = _get_database()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class SearchActivity(BaseModel):
|
|
13
|
+
class SearchActivity(Model):
|
|
42
14
|
category = TextField(primary_key=True)
|
|
43
15
|
summary = TextField(null=True)
|
|
44
16
|
timestamp = TextField(null=True)
|
|
45
17
|
|
|
46
18
|
|
|
47
19
|
def _ensure_tables() -> None:
|
|
48
|
-
db =
|
|
20
|
+
db = get_database()
|
|
49
21
|
with _DB_LOCK:
|
|
50
|
-
#
|
|
51
|
-
|
|
22
|
+
# Bind model to database if not already bound
|
|
23
|
+
if not SearchActivity._meta.database:
|
|
24
|
+
db.bind([SearchActivity])
|
|
52
25
|
db.create_tables([SearchActivity], safe=True)
|
|
53
26
|
|
|
54
27
|
|
|
@@ -59,7 +32,7 @@ def record_search_activity(category: str, summary: str | None, timestamp: str |
|
|
|
59
32
|
if timestamp is not None and not isinstance(timestamp, str):
|
|
60
33
|
timestamp = str(timestamp)
|
|
61
34
|
data: dict[str, Any] = {"summary": summary, "timestamp": timestamp}
|
|
62
|
-
with
|
|
35
|
+
with get_database().atomic():
|
|
63
36
|
SearchActivity.insert(category=category, **data).on_conflict(
|
|
64
37
|
conflict_target=[SearchActivity.category],
|
|
65
38
|
update=data,
|
|
@@ -69,9 +42,6 @@ def record_search_activity(category: str, summary: str | None, timestamp: str |
|
|
|
69
42
|
def fetch_search_activities() -> dict[str, dict[str, str | None]]:
|
|
70
43
|
_ensure_tables()
|
|
71
44
|
activities: dict[str, dict[str, str | None]] = {}
|
|
72
|
-
db = _get_database()
|
|
73
|
-
# Connect with retry logic for transient I/O errors
|
|
74
|
-
with_database_retry(lambda: db.connect(reuse_if_open=True))
|
|
75
45
|
try:
|
|
76
46
|
query = SearchActivity.select()
|
|
77
47
|
except Exception:
|
|
@@ -88,5 +58,5 @@ def clear_search_activity(category: str) -> None:
|
|
|
88
58
|
if not category:
|
|
89
59
|
return
|
|
90
60
|
_ensure_tables()
|
|
91
|
-
with
|
|
61
|
+
with get_database().atomic():
|
|
92
62
|
SearchActivity.delete().where(SearchActivity.category == category).execute()
|