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.
Files changed (85) hide show
  1. {qbitrr2-5.7.1/qBitrr2.egg-info → qbitrr2-5.8.0}/PKG-INFO +19 -1
  2. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/README.md +18 -0
  3. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/pyproject.toml +1 -1
  4. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/arss.py +25 -141
  5. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/bundled_data.py +2 -2
  6. qbitrr2-5.8.0/qBitrr/database.py +79 -0
  7. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/main.py +14 -8
  8. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/search_activity_store.py +9 -39
  9. qbitrr2-5.8.0/qBitrr/static/assets/ConfigView.js +6 -0
  10. qbitrr2-5.8.0/qBitrr/static/assets/ConfigView.js.map +1 -0
  11. qbitrr2-5.8.0/qBitrr/static/assets/LogsView.js +208 -0
  12. qbitrr2-5.8.0/qBitrr/static/assets/LogsView.js.map +1 -0
  13. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/tables.py +11 -0
  14. {qbitrr2-5.7.1 → qbitrr2-5.8.0/qBitrr2.egg-info}/PKG-INFO +19 -1
  15. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr2.egg-info/SOURCES.txt +1 -0
  16. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/setup.cfg +1 -1
  17. qbitrr2-5.7.1/qBitrr/static/assets/ConfigView.js +0 -6
  18. qbitrr2-5.7.1/qBitrr/static/assets/ConfigView.js.map +0 -1
  19. qbitrr2-5.7.1/qBitrr/static/assets/LogsView.js +0 -208
  20. qbitrr2-5.7.1/qBitrr/static/assets/LogsView.js.map +0 -1
  21. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/LICENSE +0 -0
  22. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/MANIFEST.in +0 -0
  23. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/config.example.toml +0 -0
  24. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/__init__.py +0 -0
  25. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/auto_update.py +0 -0
  26. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/config.py +0 -0
  27. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/config_version.py +0 -0
  28. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/db_lock.py +0 -0
  29. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/db_recovery.py +0 -0
  30. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/env_config.py +0 -0
  31. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/errors.py +0 -0
  32. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/ffprobe.py +0 -0
  33. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/gen_config.py +0 -0
  34. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/home_path.py +0 -0
  35. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/logger.py +0 -0
  36. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/ArrView.js +0 -0
  37. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/ArrView.js.map +0 -0
  38. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/ProcessesView.js +0 -0
  39. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/ProcessesView.js.map +0 -0
  40. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/app.css +0 -0
  41. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/app.js +0 -0
  42. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/app.js.map +0 -0
  43. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/build.svg +0 -0
  44. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/check-mark.svg +0 -0
  45. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/close.svg +0 -0
  46. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/download.svg +0 -0
  47. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/gear.svg +0 -0
  48. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/lidarr.svg +0 -0
  49. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/live-streaming.svg +0 -0
  50. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/log.svg +0 -0
  51. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/logo.svg +0 -0
  52. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/plus.svg +0 -0
  53. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/process.svg +0 -0
  54. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/react-select.esm.js +0 -0
  55. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/react-select.esm.js.map +0 -0
  56. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/refresh-arrow.svg +0 -0
  57. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/table.js +0 -0
  58. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/table.js.map +0 -0
  59. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/trash.svg +0 -0
  60. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/up-arrow.svg +0 -0
  61. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/useInterval.js +0 -0
  62. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/useInterval.js.map +0 -0
  63. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/vendor.js +0 -0
  64. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/vendor.js.map +0 -0
  65. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/assets/visibility.svg +0 -0
  66. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/favicon-16x16.png +0 -0
  67. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/favicon-32x32.png +0 -0
  68. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/favicon-48x48.png +0 -0
  69. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/favicon.ico +0 -0
  70. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/icon-192.png +0 -0
  71. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/icon-512.png +0 -0
  72. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/index.html +0 -0
  73. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/logov2-clean.png +0 -0
  74. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/logov2-clean.svg +0 -0
  75. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/manifest.json +0 -0
  76. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/sw.js +0 -0
  77. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/static/vite.svg +0 -0
  78. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/utils.py +0 -0
  79. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/versioning.py +0 -0
  80. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr/webui.py +0 -0
  81. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr2.egg-info/dependency_links.txt +0 -0
  82. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr2.egg-info/entry_points.txt +0 -0
  83. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr2.egg-info/requires.txt +0 -0
  84. {qbitrr2-5.7.1 → qbitrr2-5.8.0}/qBitrr2.egg-info/top_level.txt +0 -0
  85. {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.7.1
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.7.1"
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
- db1, db2, db3, db4, db5 = self._get_models()
6622
+ # Import the shared database
6623
+ from qBitrr.database import get_database
6622
6624
 
6623
- if not (
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
- # Connect with retry logic for transient I/O errors
6688
- with_database_retry(
6689
- lambda: self.db.connect(),
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
- if db4:
6694
-
6695
- class Tracks(db4):
6696
- class Meta:
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
- try:
6710
- self.db.create_tables([Files, Queue, PersistingQueue, Series], safe=True)
6711
- except Exception as e:
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 db5:
6740
- self.torrent_db = SqliteDatabase(None)
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.model_file = Files
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(
@@ -1,5 +1,5 @@
1
- version = "5.7.1"
2
- git_hash = "a8446ba3"
1
+ version = "5.8.0"
2
+ git_hash = "ae0807b0"
3
3
  license_text = (
4
4
  "Licence can be found on:\n\nhttps://github.com/Feramance/qBitrr/blob/master/LICENSE"
5
5
  )
@@ -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 all database files from the APPDATA_FOLDER on startup.
56
+ Delete old per-instance database files from the APPDATA_FOLDER on startup.
57
57
 
58
- This includes:
59
- - All .db files (SQLite databases)
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(os.path.basename(db_file))
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, SqliteDatabase, TextField
6
+ from peewee import Model, TextField
7
7
 
8
- from qBitrr.db_lock import with_database_retry
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
- def _get_database() -> SqliteDatabase:
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 = _get_database()
20
+ db = get_database()
49
21
  with _DB_LOCK:
50
- # Connect with retry logic for transient I/O errors
51
- with_database_retry(lambda: db.connect(reuse_if_open=True))
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 _get_database().atomic():
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 _get_database().atomic():
61
+ with get_database().atomic():
92
62
  SearchActivity.delete().where(SearchActivity.category == category).execute()