qBitrr2 5.8.1__tar.gz → 5.8.4__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 (84) hide show
  1. {qbitrr2-5.8.1/qBitrr2.egg-info → qbitrr2-5.8.4}/PKG-INFO +4 -22
  2. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/README.md +3 -21
  3. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/pyproject.toml +1 -1
  4. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/arss.py +30 -17
  5. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/bundled_data.py +2 -2
  6. qbitrr2-5.8.4/qBitrr/database.py +150 -0
  7. qbitrr2-5.8.4/qBitrr/static/assets/ProcessesView.js +2 -0
  8. qbitrr2-5.8.4/qBitrr/static/assets/ProcessesView.js.map +1 -0
  9. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/webui.py +69 -21
  10. {qbitrr2-5.8.1 → qbitrr2-5.8.4/qBitrr2.egg-info}/PKG-INFO +4 -22
  11. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/setup.cfg +1 -1
  12. qbitrr2-5.8.1/qBitrr/database.py +0 -79
  13. qbitrr2-5.8.1/qBitrr/static/assets/ProcessesView.js +0 -2
  14. qbitrr2-5.8.1/qBitrr/static/assets/ProcessesView.js.map +0 -1
  15. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/LICENSE +0 -0
  16. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/MANIFEST.in +0 -0
  17. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/config.example.toml +0 -0
  18. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/__init__.py +0 -0
  19. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/auto_update.py +0 -0
  20. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/config.py +0 -0
  21. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/config_version.py +0 -0
  22. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/db_lock.py +0 -0
  23. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/db_recovery.py +0 -0
  24. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/env_config.py +0 -0
  25. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/errors.py +0 -0
  26. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/ffprobe.py +0 -0
  27. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/gen_config.py +0 -0
  28. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/home_path.py +0 -0
  29. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/logger.py +0 -0
  30. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/main.py +0 -0
  31. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/search_activity_store.py +0 -0
  32. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/ArrView.js +0 -0
  33. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/ArrView.js.map +0 -0
  34. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/ConfigView.js +0 -0
  35. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/ConfigView.js.map +0 -0
  36. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/LogsView.js +0 -0
  37. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/LogsView.js.map +0 -0
  38. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/app.css +0 -0
  39. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/app.js +0 -0
  40. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/app.js.map +0 -0
  41. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/build.svg +0 -0
  42. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/check-mark.svg +0 -0
  43. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/close.svg +0 -0
  44. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/download.svg +0 -0
  45. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/gear.svg +0 -0
  46. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/lidarr.svg +0 -0
  47. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/live-streaming.svg +0 -0
  48. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/log.svg +0 -0
  49. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/logo.svg +0 -0
  50. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/plus.svg +0 -0
  51. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/process.svg +0 -0
  52. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/react-select.esm.js +0 -0
  53. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/react-select.esm.js.map +0 -0
  54. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/refresh-arrow.svg +0 -0
  55. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/table.js +0 -0
  56. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/table.js.map +0 -0
  57. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/trash.svg +0 -0
  58. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/up-arrow.svg +0 -0
  59. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/useInterval.js +0 -0
  60. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/useInterval.js.map +0 -0
  61. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/vendor.js +0 -0
  62. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/vendor.js.map +0 -0
  63. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/assets/visibility.svg +0 -0
  64. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/favicon-16x16.png +0 -0
  65. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/favicon-32x32.png +0 -0
  66. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/favicon-48x48.png +0 -0
  67. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/favicon.ico +0 -0
  68. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/icon-192.png +0 -0
  69. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/icon-512.png +0 -0
  70. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/index.html +0 -0
  71. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/logov2-clean.png +0 -0
  72. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/logov2-clean.svg +0 -0
  73. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/manifest.json +0 -0
  74. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/sw.js +0 -0
  75. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/static/vite.svg +0 -0
  76. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/tables.py +0 -0
  77. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/utils.py +0 -0
  78. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr/versioning.py +0 -0
  79. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr2.egg-info/SOURCES.txt +0 -0
  80. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr2.egg-info/dependency_links.txt +0 -0
  81. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr2.egg-info/entry_points.txt +0 -0
  82. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr2.egg-info/requires.txt +0 -0
  83. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/qBitrr2.egg-info/top_level.txt +0 -0
  84. {qbitrr2-5.8.1 → qbitrr2-5.8.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qBitrr2
3
- Version: 5.8.1
3
+ Version: 5.8.4
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,24 +169,6 @@ 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
-
190
172
  ## ✨ Key Features
191
173
 
192
174
  - **🚀 Multi-qBittorrent Support (v3.0+)** – Manage torrents across multiple qBittorrent instances for load balancing, redundancy, and VPN isolation
@@ -254,8 +236,8 @@ See [Configuration Guide](https://feramance.github.io/qBitrr/configuration/) and
254
236
  - **PyPI Package:** https://pypi.org/project/qBitrr2/
255
237
  - **Docker Hub:** https://hub.docker.com/r/feramance/qbitrr
256
238
  - **Example Config:** [config.example.toml](config.example.toml)
257
- - **API Documentation:** [API_DOCUMENTATION.md](API_DOCUMENTATION.md)
258
- - **Systemd Setup:** [SYSTEMD_SERVICE.md](SYSTEMD_SERVICE.md)
239
+ - **API Documentation:** [docs/reference/api.md](docs/reference/api.md)
240
+ - **Systemd Setup:** [docs/getting-started/installation/systemd.md](docs/getting-started/installation/systemd.md)
259
241
 
260
242
  ## 🐛 Issues & Support
261
243
 
@@ -266,7 +248,7 @@ See [Configuration Guide](https://feramance.github.io/qBitrr/configuration/) and
266
248
 
267
249
  ## 🤝 Contributing
268
250
 
269
- Contributions welcome! See [CONTRIBUTION.md](CONTRIBUTION.md) for coding guidelines and development setup.
251
+ Contributions welcome! See [docs/development/contributing.md](docs/development/contributing.md) for coding guidelines and development setup.
270
252
 
271
253
  **Development setup:**
272
254
  ```bash
@@ -65,24 +65,6 @@ 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
-
86
68
  ## ✨ Key Features
87
69
 
88
70
  - **🚀 Multi-qBittorrent Support (v3.0+)** – Manage torrents across multiple qBittorrent instances for load balancing, redundancy, and VPN isolation
@@ -150,8 +132,8 @@ See [Configuration Guide](https://feramance.github.io/qBitrr/configuration/) and
150
132
  - **PyPI Package:** https://pypi.org/project/qBitrr2/
151
133
  - **Docker Hub:** https://hub.docker.com/r/feramance/qbitrr
152
134
  - **Example Config:** [config.example.toml](config.example.toml)
153
- - **API Documentation:** [API_DOCUMENTATION.md](API_DOCUMENTATION.md)
154
- - **Systemd Setup:** [SYSTEMD_SERVICE.md](SYSTEMD_SERVICE.md)
135
+ - **API Documentation:** [docs/reference/api.md](docs/reference/api.md)
136
+ - **Systemd Setup:** [docs/getting-started/installation/systemd.md](docs/getting-started/installation/systemd.md)
155
137
 
156
138
  ## 🐛 Issues & Support
157
139
 
@@ -162,7 +144,7 @@ See [Configuration Guide](https://feramance.github.io/qBitrr/configuration/) and
162
144
 
163
145
  ## 🤝 Contributing
164
146
 
165
- Contributions welcome! See [CONTRIBUTION.md](CONTRIBUTION.md) for coding guidelines and development setup.
147
+ Contributions welcome! See [docs/development/contributing.md](docs/development/contributing.md) for coding guidelines and development setup.
166
148
 
167
149
  **Development setup:**
168
150
  ```bash
@@ -28,7 +28,7 @@ target-version = ['py311']
28
28
 
29
29
  [tool.poetry]
30
30
  name = "pypi-public"
31
- version = "5.8.1"
31
+ version = "5.8.4"
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"
@@ -1434,7 +1434,9 @@ class Arr:
1434
1434
  ):
1435
1435
  continue
1436
1436
  if self.persistent_queue:
1437
- self.persistent_queue.insert(EntryId=series_id).on_conflict_ignore()
1437
+ self.persistent_queue.insert(
1438
+ EntryId=series_id, ArrInstance=self._name
1439
+ ).on_conflict_ignore()
1438
1440
  else:
1439
1441
  for object_id in object_ids:
1440
1442
  episode_found = False
@@ -1503,7 +1505,7 @@ class Arr:
1503
1505
  continue
1504
1506
  if self.persistent_queue:
1505
1507
  self.persistent_queue.insert(
1506
- EntryId=object_id
1508
+ EntryId=object_id, ArrInstance=self._name
1507
1509
  ).on_conflict_ignore()
1508
1510
  elif self.type == "radarr":
1509
1511
  self.logger.trace("Requeue cache entry: %s", object_id)
@@ -1554,7 +1556,9 @@ class Arr:
1554
1556
  ):
1555
1557
  continue
1556
1558
  if self.persistent_queue:
1557
- self.persistent_queue.insert(EntryId=object_id).on_conflict_ignore()
1559
+ self.persistent_queue.insert(
1560
+ EntryId=object_id, ArrInstance=self._name
1561
+ ).on_conflict_ignore()
1558
1562
  elif self.type == "lidarr":
1559
1563
  self.logger.trace("Requeue cache entry: %s", object_id)
1560
1564
  album_found = False
@@ -1604,7 +1608,9 @@ class Arr:
1604
1608
  ):
1605
1609
  continue
1606
1610
  if self.persistent_queue:
1607
- self.persistent_queue.insert(EntryId=object_id).on_conflict_ignore()
1611
+ self.persistent_queue.insert(
1612
+ EntryId=object_id, ArrInstance=self._name
1613
+ ).on_conflict_ignore()
1608
1614
 
1609
1615
  def _process_errored(self) -> None:
1610
1616
  # Recheck all torrents marked for rechecking.
@@ -1750,11 +1756,8 @@ class Arr:
1750
1756
  try:
1751
1757
  commands = self.client.get_command()
1752
1758
  for command in commands:
1753
- if (
1754
- command["name"].endswith("Search")
1755
- and command["status"] != "completed"
1756
- and "Missing" not in command["name"]
1757
- ):
1759
+ # Count all active search commands (including MissingEpisodeSearch)
1760
+ if command["name"].endswith("Search") and command["status"] != "completed":
1758
1761
  search_commands = search_commands + 1
1759
1762
  break
1760
1763
  except (
@@ -3146,6 +3149,7 @@ class Arr:
3146
3149
  CustomFormatScore=customFormat,
3147
3150
  CustomFormatMet=customFormatMet,
3148
3151
  Reason=reason,
3152
+ ArrInstance=self._name,
3149
3153
  ).on_conflict(conflict_target=[self.model_file.EntryId], update=to_update)
3150
3154
  db_commands.execute()
3151
3155
  else:
@@ -3325,6 +3329,7 @@ class Arr:
3325
3329
  MinCustomFormatScore=minCustomFormat,
3326
3330
  QualityProfileId=quality_profile_id,
3327
3331
  QualityProfileName=qualityProfileName,
3332
+ ArrInstance=self._name,
3328
3333
  ).on_conflict(
3329
3334
  conflict_target=[self.series_file_model.EntryId], update=to_update
3330
3335
  )
@@ -3565,6 +3570,7 @@ class Arr:
3565
3570
  Reason=reason,
3566
3571
  QualityProfileId=qualityProfileId,
3567
3572
  QualityProfileName=qualityProfileName,
3573
+ ArrInstance=self._name,
3568
3574
  ).on_conflict(conflict_target=[self.model_file.EntryId], update=to_update)
3569
3575
  db_commands.execute()
3570
3576
  else:
@@ -3866,6 +3872,7 @@ class Arr:
3866
3872
  Reason=reason,
3867
3873
  QualityProfileId=qualityProfileId,
3868
3874
  QualityProfileName=qualityProfileName,
3875
+ ArrInstance=self._name,
3869
3876
  ).on_conflict(conflict_target=[self.model_file.EntryId], update=to_update)
3870
3877
  db_commands.execute()
3871
3878
 
@@ -3902,6 +3909,7 @@ class Arr:
3902
3909
  HasFile=track.get("hasFile", False),
3903
3910
  TrackFileId=track.get("trackFileId", 0),
3904
3911
  Monitored=track_monitored,
3912
+ ArrInstance=self._name,
3905
3913
  ).execute()
3906
3914
  track_insert_count += 1
3907
3915
 
@@ -4045,6 +4053,7 @@ class Arr:
4045
4053
  Monitored=Monitored,
4046
4054
  Upgrade=False,
4047
4055
  MinCustomFormatScore=minCustomFormat,
4056
+ ArrInstance=self._name,
4048
4057
  ).on_conflict(
4049
4058
  conflict_target=[self.artists_file_model.EntryId], update=to_update
4050
4059
  )
@@ -4342,10 +4351,10 @@ class Arr:
4342
4351
  )
4343
4352
  return False
4344
4353
  self.persistent_queue.insert(
4345
- EntryId=file_model.EntryId
4354
+ EntryId=file_model.EntryId, ArrInstance=self._name
4346
4355
  ).on_conflict_ignore().execute()
4347
4356
  self.model_queue.insert(
4348
- Completed=False, EntryId=file_model.EntryId
4357
+ Completed=False, EntryId=file_model.EntryId, ArrInstance=self._name
4349
4358
  ).on_conflict_replace().execute()
4350
4359
  if file_model.EntryId not in self.queue_file_ids:
4351
4360
  while True:
@@ -4414,10 +4423,10 @@ class Arr:
4414
4423
  )
4415
4424
  return False
4416
4425
  self.persistent_queue.insert(
4417
- EntryId=file_model.EntryId
4426
+ EntryId=file_model.EntryId, ArrInstance=self._name
4418
4427
  ).on_conflict_ignore().execute()
4419
4428
  self.model_queue.insert(
4420
- Completed=False, EntryId=file_model.EntryId
4429
+ Completed=False, EntryId=file_model.EntryId, ArrInstance=self._name
4421
4430
  ).on_conflict_replace().execute()
4422
4431
  while True:
4423
4432
  try:
@@ -4485,10 +4494,12 @@ class Arr:
4485
4494
  file_model.EntryId,
4486
4495
  )
4487
4496
  return False
4488
- self.persistent_queue.insert(EntryId=file_model.EntryId).on_conflict_ignore().execute()
4497
+ self.persistent_queue.insert(
4498
+ EntryId=file_model.EntryId, ArrInstance=self._name
4499
+ ).on_conflict_ignore().execute()
4489
4500
 
4490
4501
  self.model_queue.insert(
4491
- Completed=False, EntryId=file_model.EntryId
4502
+ Completed=False, EntryId=file_model.EntryId, ArrInstance=self._name
4492
4503
  ).on_conflict_replace().execute()
4493
4504
  if file_model.EntryId:
4494
4505
  while True:
@@ -4569,10 +4580,12 @@ class Arr:
4569
4580
  file_model.EntryId,
4570
4581
  )
4571
4582
  return False
4572
- self.persistent_queue.insert(EntryId=file_model.EntryId).on_conflict_ignore().execute()
4583
+ self.persistent_queue.insert(
4584
+ EntryId=file_model.EntryId, ArrInstance=self._name
4585
+ ).on_conflict_ignore().execute()
4573
4586
 
4574
4587
  self.model_queue.insert(
4575
- Completed=False, EntryId=file_model.EntryId
4588
+ Completed=False, EntryId=file_model.EntryId, ArrInstance=self._name
4576
4589
  ).on_conflict_replace().execute()
4577
4590
  if file_model.EntryId:
4578
4591
  while True:
@@ -1,5 +1,5 @@
1
- version = "5.8.1"
2
- git_hash = "8cc293b9"
1
+ version = "5.8.4"
2
+ git_hash = "5d3a2580"
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,150 @@
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
+ # Run migrations
78
+ _migrate_arrinstance_field(models)
79
+ _create_arrinstance_indexes(_db, models)
80
+
81
+ logger.info("Initialized single database: %s", db_path)
82
+
83
+ return _db
84
+
85
+
86
+ def _migrate_arrinstance_field(models: list) -> None:
87
+ """
88
+ Migration: Remove records with empty ArrInstance field.
89
+
90
+ After database consolidation, old records don't have ArrInstance set.
91
+ Since we can't reliably determine which instance they belong to,
92
+ we delete them and let the application repopulate with correct values.
93
+ """
94
+ try:
95
+ deleted_count = 0
96
+ for model in models:
97
+ # Check if model has ArrInstance field
98
+ if hasattr(model, "ArrInstance"):
99
+ # Delete records where ArrInstance is NULL or empty string
100
+ query = model.delete().where(
101
+ (model.ArrInstance.is_null()) | (model.ArrInstance == "")
102
+ )
103
+ count = query.execute()
104
+ if count > 0:
105
+ logger.info(
106
+ "Migrated %s: deleted %d records with empty ArrInstance",
107
+ model.__name__,
108
+ count,
109
+ )
110
+ deleted_count += count
111
+
112
+ if deleted_count > 0:
113
+ logger.warning(
114
+ "Database migration: Removed %d old records without ArrInstance. "
115
+ "qBitrr will repopulate data from Arr instances.",
116
+ deleted_count,
117
+ )
118
+ except Exception as e:
119
+ logger.error("Error during ArrInstance migration: %s", e)
120
+
121
+
122
+ def _create_arrinstance_indexes(db: SqliteDatabase, models: list) -> None:
123
+ """
124
+ Create database indexes on ArrInstance field for performance.
125
+
126
+ Indexes improve query performance when filtering by ArrInstance,
127
+ which is done on every WebUI page load.
128
+ """
129
+ try:
130
+ cursor = db.cursor()
131
+ for model in models:
132
+ if hasattr(model, "ArrInstance"):
133
+ table_name = model._meta.table_name
134
+ index_name = f"idx_arrinstance_{table_name}"
135
+
136
+ # Check if index already exists
137
+ cursor.execute(
138
+ "SELECT name FROM sqlite_master WHERE type='index' AND name=?",
139
+ (index_name,),
140
+ )
141
+ if cursor.fetchone():
142
+ continue # Index already exists
143
+
144
+ # Create index
145
+ cursor.execute(f"CREATE INDEX {index_name} ON {table_name}(ArrInstance)")
146
+ logger.info("Created index: %s on %s.ArrInstance", index_name, table_name)
147
+
148
+ db.commit()
149
+ except Exception as e:
150
+ logger.error("Error creating ArrInstance indexes: %s", e)
@@ -0,0 +1,2 @@
1
+ import{j as e,I as P,C as V,u as W,g as J,a as Q,r as K,b as Y,c as Z,R as M}from"./app.js";import{r as i}from"./table.js";import{u as ee}from"./useInterval.js";import"./vendor.js";function se({title:r,message:t,confirmLabel:p="Confirm",cancelLabel:f="Cancel",onConfirm:y,onCancel:x,danger:_=!1}){return e.jsx("div",{className:"modal-backdrop",onClick:x,children:e.jsxs("div",{className:"modal",style:{maxWidth:"500px"},onClick:b=>b.stopPropagation(),children:[e.jsxs("div",{className:"modal-header",children:[e.jsx("h2",{children:r}),e.jsx("button",{className:"btn ghost",onClick:x,children:e.jsx(P,{src:V})})]}),e.jsx("div",{className:"modal-body",children:e.jsx("p",{style:{margin:0,lineHeight:1.6},children:t})}),e.jsxs("div",{className:"modal-footer",children:[e.jsx("button",{className:"btn ghost",onClick:x,children:f}),e.jsx("button",{className:`btn ${_?"danger":"primary"}`,onClick:y,children:p})]})]})})}const re="/static/assets/build.svg",te=/\b(480p|576p|720p|1080p|2160p|4k|8k|web[-_. ]?(?:dl|rip)|hdrip|hdtv|bluray|bd(?:rip)?|brrip|webrip|remux|x264|x265|hevc|dts|truehd|atmos|proper|repack|dvdrip|hdr|amzn|nf)\b/i,ae=/\bS\d{1,3}E\d{1,3}\b/i,ne=/\bSeason\s+\d+\b/i;function ce(r){const t=r.trim();if(!t)return"";if(/^\d+\s+queued item/i.test(t))return t;const p=t.replace(/\s+/g," "),f=p.match(/^(?<title>.+?)\s+(?<year>(?:19|20)\d{2})(?:\s+(?<rest>.*))?$/);if(f){const y=f.groups?.rest??"",x=ae.test(y)||ne.test(y);if(y&&!x&&te.test(y)){const b=(f.groups?.title??"").replace(/[-_.]/g," ").replace(/\s{2,}/g," ").trim(),R=f.groups?.year??"";if(b)return R?`${b} (${R})`:b}}return p}function oe(r,t){return r.category===t.category&&r.name===t.name&&r.kind===t.kind&&r.pid===t.pid&&r.alive===t.alive&&(r.rebuilding??!1)===(t.rebuilding??!1)&&(r.searchSummary??"")===(t.searchSummary??"")&&(r.searchTimestamp??"")===(t.searchTimestamp??"")&&(r.queueCount??null)===(t.queueCount??null)&&(r.categoryCount??null)===(t.categoryCount??null)&&(r.metricType??"")===(t.metricType??"")}function le(r,t){if(r===t)return!0;if(r.length!==t.length)return!1;for(let p=0;p<r.length;p+=1)if(!oe(r[p],t[p]))return!1;return!0}function ie(r,t){return r?1e3:null}function ge({active:r}){const[t,p]=i.useState([]),[f,y]=i.useState(!1),[x,_]=i.useState(!1),[b,R]=i.useState(!1),[I,D]=i.useState(null),[S,E]=i.useState(null),{push:u}=W(),F=i.useRef(!1),m=i.useCallback(async(a=!0)=>{if(!F.current){F.current=!0,a&&y(!0);try{const[c,g]=await Promise.all([J(),Q()]),j=(c.processes??[]).map(d=>{if(typeof d.searchSummary=="string"){const N=ce(d.searchSummary);return{...d,searchSummary:N}}return d});p(d=>le(d,j)?d:j),D(g)}catch(c){u(c instanceof Error?c.message:"Failed to load processes list","error")}finally{F.current=!1,a&&y(!1)}}},[u]);i.useEffect(()=>{m()},[m]),i.useEffect(()=>{r&&m()},[r,m]);const G=i.useMemo(()=>ie(r),[r,t]);ee(()=>{m(!1)},G);const z=i.useCallback(async(a,c)=>{try{await K(a,c),u(`Restarted ${a}:${c}`,"success"),m()}catch(g){u(g instanceof Error?g.message:`Failed to restart ${a}:${c}`,"error")}},[m,u]),B=i.useCallback(async()=>{E({title:"Restart All Processes",message:"Are you sure you want to restart all processes? This will temporarily interrupt all operations.",onConfirm:async()=>{E(null),_(!0);try{await Y(),u("Restarted all processes","success"),m()}catch(a){u(a instanceof Error?a.message:"Failed to restart all","error")}finally{_(!1)}}})},[m,u]),X=i.useCallback(async()=>{E({title:"Rebuild Arrs",message:"Are you sure you want to rebuild all Arr instances? This will refresh all connections and may take some time.",onConfirm:async()=>{E(null),R(!0);try{await Z(),u("Requested Arr rebuild","success"),m()}catch(a){u(a instanceof Error?a.message:"Failed to rebuild Arrs","error")}finally{R(!1)}}})},[m,u]),H=i.useMemo(()=>{const a=new Map,c=n=>{const o=(n.category??"").toLowerCase(),l=(n.name??"").toLowerCase();return o.includes("radarr")||l.includes("radarr")?"Radarr":o.includes("sonarr")||l.includes("sonarr")?"Sonarr":o.includes("lidarr")||l.includes("lidarr")?"Lidarr":o.includes("qbit")||o.includes("qbittorrent")||l.includes("qbit")||l.includes("qbittorrent")?"qBittorrent":"Other"},g=I?.arrs??[],j=g.some(n=>n.type==="radarr"),d=g.some(n=>n.type==="sonarr"),N=g.some(n=>n.type==="lidarr");t.forEach(n=>{const o=c(n);if(o==="Radarr"&&!j||o==="Sonarr"&&!d||o==="Lidarr"&&!N)return;a.has(o)||a.set(o,new Map);const l=a.get(o),h=n.name||n.category||`${n.category}:${n.kind}`;l.has(h)||l.set(h,[]),l.get(h).push(n)});const v=["Radarr","Sonarr","Lidarr","qBittorrent","Other"],k=Array.from(a.entries()).map(([n,o])=>{const l=Array.from(o.entries()).map(([h,C])=>({name:h,items:C.sort((w,T)=>w.kind.localeCompare(T.kind))})).sort((h,C)=>h.name.localeCompare(C.name));return{app:n,instances:l}}).filter(n=>n.instances.length);return k.sort((n,o)=>{const l=h=>{const C=v.indexOf(h);return C===-1?Number.MAX_SAFE_INTEGER:C};return l(n.app)-l(o.app)||n.app.localeCompare(o.app)}),k},[t,I]),U=i.useCallback(async a=>{try{await Promise.all(a.map(c=>K(c.category,c.kind))),u(`Restarted ${a[0]?.name??"group"}`,"success"),m()}catch(c){u(c instanceof Error?c.message:"Failed to restart process group","error")}},[m,u]),O=H.map(({app:a,instances:c})=>{const g=c.map(({name:j,items:d})=>{const N=d.filter(s=>s.alive).length,v=d.length,k=v===0?"":N===v?"status-indicator--ok":N===0?"status-indicator--bad":"",n=["status-indicator"];k&&n.push(k);const o=v===0?"No processes":N===v?"All running":N===0?"Stopped":`${N}/${v} running`,l=v===1?"1 process":`${v} processes`,h=j==="FreeSpaceManager"?"Free Space Manager":j,w=Array.from(new Set(d.map(s=>s.kind))).filter(s=>{const A=s.toLowerCase();return A!=="search"&&A!=="torrent"}),T=s=>s&&s.charAt(0).toUpperCase()+s.slice(1);return e.jsxs("div",{className:"process-card",children:[e.jsxs("div",{className:"process-card__header",children:[e.jsxs("div",{className:"process-card__title",children:[e.jsx("div",{className:"process-card__name",children:h}),e.jsx("div",{className:"process-card__summary",children:l}),w.length?e.jsx("div",{className:"process-card__badges",children:w.map(s=>e.jsx("span",{className:"process-card__badge",children:T(s)},`${j}:${s}:badge`))}):null]}),e.jsx("div",{className:n.join(" "),title:o})]}),e.jsx("div",{className:"process-card__list",children:d.map(s=>e.jsxs("div",{className:"process-chip",children:[e.jsxs("div",{className:"process-chip__top",children:[e.jsx("div",{className:"process-chip__name",children:T(s.kind)}),e.jsx("div",{className:`status-pill__dot ${s.alive?"text-success":"text-danger"}`})]}),e.jsx("div",{className:"process-chip__detail",children:(()=>{if(s.rebuilding)return"Rebuilding";const A=s.kind.toLowerCase();if(A==="search")return(s.searchSummary??"")||"No searches recorded";if(A==="torrent"){const $=s.metricType?.toLowerCase(),L=typeof s.categoryCount=="number"?s.categoryCount:null,q=typeof s.queueCount=="number"?s.queueCount:null;return $?$==="category"&&L!==null?`Torrent count ${L}`:$==="free-space"&&q!==null?`Torrent count ${q}`:"Torrent count unavailable":`Torrents in queue ${q!==null?q:"?"} / total ${L!==null?L:"?"}`}return""})()}),e.jsx("div",{className:"process-chip__actions",children:e.jsx("button",{className:"btn small",onClick:()=>z(s.category,s.kind),children:"Restart"})})]},`${s.category}:${s.kind}`))}),e.jsx("div",{className:"process-card__footer",children:e.jsx("button",{className:"btn small outline",onClick:()=>{U(d)},children:"Restart All"})})]},j)});return{app:a,cards:g}});return e.jsxs(e.Fragment,{children:[e.jsxs("section",{className:"card",children:[e.jsx("div",{className:"card-header",children:"Processes"}),e.jsxs("div",{className:"card-body stack",children:[e.jsx("div",{className:"row",children:e.jsxs("div",{className:"col inline",children:[e.jsxs("button",{className:"btn ghost",onClick:()=>{m()},disabled:f,children:[f&&e.jsx("span",{className:"spinner"}),e.jsx(P,{src:M}),f?"Refreshing...":"Refresh"]}),e.jsxs("button",{className:"btn",onClick:()=>{B()},disabled:x,children:[x&&e.jsx("span",{className:"spinner"}),e.jsx(P,{src:M}),x?"Restarting...":"Restart All"]}),e.jsxs("button",{className:"btn",onClick:()=>{X()},disabled:b,children:[b&&e.jsx("span",{className:"spinner"}),e.jsx(P,{src:re}),b?"Rebuilding...":"Rebuild Arrs"]})]})}),O.length?O.map(({app:a,cards:c})=>e.jsxs("div",{className:"process-section",children:[e.jsx("div",{className:"process-section__title",children:a}),e.jsx("div",{className:"process-grid",children:c})]},a)):e.jsx("div",{className:"empty-state",children:"No processes available."})]})]}),S&&e.jsx(se,{title:S.title,message:S.message,confirmLabel:"Confirm",cancelLabel:"Cancel",danger:!0,onConfirm:S.onConfirm,onCancel:()=>E(null)})]})}export{ge as ProcessesView};
2
+ //# sourceMappingURL=ProcessesView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProcessesView.js","sources":["../../../webui/src/components/ConfirmDialog.tsx","../../../webui/src/icons/build.svg","../../../webui/src/pages/ProcessesView.tsx"],"sourcesContent":["import type { JSX } from \"react\";\nimport { IconImage } from \"./IconImage\";\nimport CloseIcon from \"../icons/close.svg\";\n\ninterface ConfirmDialogProps {\n title: string;\n message: string;\n confirmLabel?: string;\n cancelLabel?: string;\n onConfirm: () => void;\n onCancel: () => void;\n danger?: boolean;\n}\n\nexport function ConfirmDialog({\n title,\n message,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n danger = false,\n}: ConfirmDialogProps): JSX.Element {\n return (\n <div className=\"modal-backdrop\" onClick={onCancel}>\n <div\n className=\"modal\"\n style={{ maxWidth: '500px' }}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"modal-header\">\n <h2>{title}</h2>\n <button className=\"btn ghost\" onClick={onCancel}>\n <IconImage src={CloseIcon} />\n </button>\n </div>\n <div className=\"modal-body\">\n <p style={{ margin: 0, lineHeight: 1.6 }}>{message}</p>\n </div>\n <div className=\"modal-footer\">\n <button className=\"btn ghost\" onClick={onCancel}>\n {cancelLabel}\n </button>\n <button\n className={`btn ${danger ? 'danger' : 'primary'}`}\n onClick={onConfirm}\n >\n {confirmLabel}\n </button>\n </div>\n </div>\n </div>\n );\n}\n","export default \"__VITE_ASSET__DznMzWc1__\"","import { useCallback, useEffect, useMemo, useRef, useState, type JSX } from \"react\";\nimport {\n getProcesses,\n getStatus,\n rebuildArrs,\n restartAllProcesses,\n restartProcess,\n} from \"../api/client\";\nimport type { ProcessInfo, StatusResponse } from \"../api/types\";\nimport { useToast } from \"../context/ToastContext\";\nimport { useInterval } from \"../hooks/useInterval\";\nimport { IconImage } from \"../components/IconImage\";\nimport { ConfirmDialog } from \"../components/ConfirmDialog\";\n\nimport RefreshIcon from \"../icons/refresh-arrow.svg\";\nimport RestartIcon from \"../icons/refresh-arrow.svg\";\nimport ToolsIcon from \"../icons/build.svg\";\n\nconst RELEASE_TOKEN_REGEX =\n /\\b(480p|576p|720p|1080p|2160p|4k|8k|web[-_. ]?(?:dl|rip)|hdrip|hdtv|bluray|bd(?:rip)?|brrip|webrip|remux|x264|x265|hevc|dts|truehd|atmos|proper|repack|dvdrip|hdr|amzn|nf)\\b/i;\nconst EPISODE_TOKEN_REGEX = /\\bS\\d{1,3}E\\d{1,3}\\b/i;\nconst SEASON_TOKEN_REGEX = /\\bSeason\\s+\\d+\\b/i;\n\nfunction sanitizeSearchSummary(raw: string): string {\n const trimmed = raw.trim();\n if (!trimmed) return \"\";\n\n // Keep \"X queued items\" messages as-is (don't filter them out)\n if (/^\\d+\\s+queued item/i.test(trimmed)) {\n return trimmed;\n }\n\n const normalized = trimmed.replace(/\\s+/g, \" \");\n const releaseMatch = normalized.match(\n /^(?<title>.+?)\\s+(?<year>(?:19|20)\\d{2})(?:\\s+(?<rest>.*))?$/\n );\n\n if (releaseMatch) {\n const rest = releaseMatch.groups?.rest ?? \"\";\n const looksLikeEpisode =\n EPISODE_TOKEN_REGEX.test(rest) || SEASON_TOKEN_REGEX.test(rest);\n if (rest && !looksLikeEpisode && RELEASE_TOKEN_REGEX.test(rest)) {\n const rawTitle = releaseMatch.groups?.title ?? \"\";\n const cleanedTitle = rawTitle\n .replace(/[-_.]/g, \" \")\n .replace(/\\s{2,}/g, \" \")\n .trim();\n const year = releaseMatch.groups?.year ?? \"\";\n if (cleanedTitle) {\n return year ? `${cleanedTitle} (${year})` : cleanedTitle;\n }\n }\n }\n\n return normalized;\n}\n\nfunction isProcessEqual(a: ProcessInfo, b: ProcessInfo): boolean {\n return (\n a.category === b.category &&\n a.name === b.name &&\n a.kind === b.kind &&\n a.pid === b.pid &&\n a.alive === b.alive &&\n (a.rebuilding ?? false) === (b.rebuilding ?? false) &&\n (a.searchSummary ?? \"\") === (b.searchSummary ?? \"\") &&\n (a.searchTimestamp ?? \"\") === (b.searchTimestamp ?? \"\") &&\n (a.queueCount ?? null) === (b.queueCount ?? null) &&\n (a.categoryCount ?? null) === (b.categoryCount ?? null) &&\n (a.metricType ?? \"\") === (b.metricType ?? \"\")\n );\n}\n\nfunction areProcessListsEqual(a: ProcessInfo[], b: ProcessInfo[]): boolean {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let index = 0; index < a.length; index += 1) {\n if (!isProcessEqual(a[index], b[index])) {\n return false;\n }\n }\n return true;\n}\n\nfunction getRefreshDelay(active: boolean, processes: ProcessInfo[]): number | null {\n if (!active) return null;\n // Refresh every 1 second when active\n return 1000;\n}\n\ninterface ProcessesViewProps {\n active: boolean;\n}\n\nexport function ProcessesView({ active }: ProcessesViewProps): JSX.Element {\n const [processes, setProcesses] = useState<ProcessInfo[]>([]);\n const [loading, setLoading] = useState(false);\n const [restartingAll, setRestartingAll] = useState(false);\n const [rebuildingArrs, setRebuildingArrs] = useState(false);\n const [statusData, setStatusData] = useState<StatusResponse | null>(null);\n const [confirmAction, setConfirmAction] = useState<{\n title: string;\n message: string;\n onConfirm: () => void;\n } | null>(null);\n const { push } = useToast();\n const isFetching = useRef(false);\n\n const load = useCallback(async (showLoading = true) => {\n if (isFetching.current) {\n return;\n }\n isFetching.current = true;\n if (showLoading) {\n setLoading(true);\n }\n try {\n const [processData, status] = await Promise.all([\n getProcesses(),\n getStatus(),\n ]);\n const next = (processData.processes ?? []).map((process) => {\n if (typeof process.searchSummary === \"string\") {\n const sanitized = sanitizeSearchSummary(process.searchSummary);\n return {\n ...process,\n searchSummary: sanitized,\n };\n }\n return process;\n });\n setProcesses((prev) =>\n areProcessListsEqual(prev, next) ? prev : next\n );\n setStatusData(status);\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : \"Failed to load processes list\",\n \"error\"\n );\n } finally {\n isFetching.current = false;\n if (showLoading) {\n setLoading(false);\n }\n }\n }, [push]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n useEffect(() => {\n if (active) {\n void load();\n }\n }, [active, load]);\n\n const refreshDelay = useMemo(\n () => getRefreshDelay(active, processes),\n [active, processes]\n );\n\n useInterval(() => {\n void load(false); // Auto-refresh without showing loading spinner\n }, refreshDelay);\n\n const handleRestart = useCallback(\n async (category: string, kind: string) => {\n try {\n await restartProcess(category, kind);\n push(`Restarted ${category}:${kind}`, \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : `Failed to restart ${category}:${kind}`,\n \"error\"\n );\n }\n },\n [load, push]\n );\n\n const handleRestartAll = useCallback(async () => {\n setConfirmAction({\n title: \"Restart All Processes\",\n message: \"Are you sure you want to restart all processes? This will temporarily interrupt all operations.\",\n onConfirm: async () => {\n setConfirmAction(null);\n setRestartingAll(true);\n try {\n await restartAllProcesses();\n push(\"Restarted all processes\", \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error ? error.message : \"Failed to restart all\",\n \"error\"\n );\n } finally {\n setRestartingAll(false);\n }\n }\n });\n }, [load, push]);\n\n const handleRebuildArrs = useCallback(async () => {\n setConfirmAction({\n title: \"Rebuild Arrs\",\n message: \"Are you sure you want to rebuild all Arr instances? This will refresh all connections and may take some time.\",\n onConfirm: async () => {\n setConfirmAction(null);\n setRebuildingArrs(true);\n try {\n await rebuildArrs();\n push(\"Requested Arr rebuild\", \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error ? error.message : \"Failed to rebuild Arrs\",\n \"error\"\n );\n } finally {\n setRebuildingArrs(false);\n }\n }\n });\n }, [load, push]);\n\n const groupedProcesses = useMemo(() => {\n interface Instance {\n name: string;\n items: ProcessInfo[];\n }\n interface AppGroup {\n app: string;\n instances: Instance[];\n }\n const appBuckets = new Map<string, Map<string, ProcessInfo[]>>();\n\n const classifyApp = (proc: ProcessInfo): string => {\n const category = (proc.category ?? \"\").toLowerCase();\n const name = (proc.name ?? \"\").toLowerCase();\n if (category.includes(\"radarr\") || name.includes(\"radarr\")) return \"Radarr\";\n if (category.includes(\"sonarr\") || name.includes(\"sonarr\")) return \"Sonarr\";\n if (category.includes(\"lidarr\") || name.includes(\"lidarr\")) return \"Lidarr\";\n if (\n category.includes(\"qbit\") ||\n category.includes(\"qbittorrent\") ||\n name.includes(\"qbit\") ||\n name.includes(\"qbittorrent\")\n ) {\n return \"qBittorrent\";\n }\n return \"Other\";\n };\n\n // Check which Arr types are configured\n const arrs = statusData?.arrs ?? [];\n const hasRadarr = arrs.some((arr) => arr.type === \"radarr\");\n const hasSonarr = arrs.some((arr) => arr.type === \"sonarr\");\n const hasLidarr = arrs.some((arr) => arr.type === \"lidarr\");\n\n processes.forEach((proc) => {\n const app = classifyApp(proc);\n\n // Skip Arr processes if that Arr type is not configured\n if (app === \"Radarr\" && !hasRadarr) return;\n if (app === \"Sonarr\" && !hasSonarr) return;\n if (app === \"Lidarr\" && !hasLidarr) return;\n\n if (!appBuckets.has(app)) appBuckets.set(app, new Map());\n const instances = appBuckets.get(app)!;\n const instanceKey =\n proc.name || proc.category || `${proc.category}:${proc.kind}`;\n if (!instances.has(instanceKey)) instances.set(instanceKey, []);\n instances.get(instanceKey)!.push(proc);\n });\n\n const appOrder = [\"Radarr\", \"Sonarr\", \"Lidarr\", \"qBittorrent\", \"Other\"];\n\n const result: AppGroup[] = Array.from(appBuckets.entries())\n .map(([app, instances]) => {\n const sortedInstances = Array.from(instances.entries())\n .map(([name, items]) => ({\n name,\n items: items.sort((a, b) => a.kind.localeCompare(b.kind)),\n }))\n .sort((a, b) => a.name.localeCompare(b.name));\n return { app, instances: sortedInstances };\n })\n .filter((group) => group.instances.length);\n\n result.sort((a, b) => {\n const order = (label: string) => {\n const index = appOrder.indexOf(label);\n return index === -1 ? Number.MAX_SAFE_INTEGER : index;\n };\n return order(a.app) - order(b.app) || a.app.localeCompare(b.app);\n });\n\n return result;\n }, [processes, statusData]);\n\n const handleRestartGroup = useCallback(\n async (items: ProcessInfo[]) => {\n try {\n await Promise.all(\n items.map((item) => restartProcess(item.category, item.kind))\n );\n push(`Restarted ${items[0]?.name ?? \"group\"}`, \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : \"Failed to restart process group\",\n \"error\"\n );\n }\n },\n [load, push]\n );\n\n const cardsByApp = groupedProcesses.map(({ app, instances }) => {\n const cards = instances.map(({ name, items }) => {\n const runningCount = items.filter((item) => item.alive).length;\n const totalCount = items.length;\n const tone =\n totalCount === 0\n ? \"\"\n : runningCount === totalCount\n ? \"status-indicator--ok\"\n : runningCount === 0\n ? \"status-indicator--bad\"\n : \"\";\n const statusClass = [\"status-indicator\"];\n if (tone) statusClass.push(tone);\n const statusLabel =\n totalCount === 0\n ? \"No processes\"\n : runningCount === totalCount\n ? \"All running\"\n : runningCount === 0\n ? \"Stopped\"\n : `${runningCount}/${totalCount} running`;\n const summaryLabel = totalCount === 1 ? \"1 process\" : `${totalCount} processes`;\n const displayName = name === \"FreeSpaceManager\" ? \"Free Space Manager\" : name;\n const uniqueKinds = Array.from(new Set(items.map((item) => item.kind)));\n const filteredKinds = uniqueKinds.filter((kind) => {\n const lower = kind.toLowerCase();\n return lower !== \"search\" && lower !== \"torrent\";\n });\n const formatKind = (kind: string) =>\n kind ? kind.charAt(0).toUpperCase() + kind.slice(1) : kind;\n\n return (\n <div className=\"process-card\" key={name}>\n <div className=\"process-card__header\">\n <div className=\"process-card__title\">\n <div className=\"process-card__name\">{displayName}</div>\n <div className=\"process-card__summary\">{summaryLabel}</div>\n {filteredKinds.length ? (\n <div className=\"process-card__badges\">\n {filteredKinds.map((kind) => (\n <span key={`${name}:${kind}:badge`} className=\"process-card__badge\">\n {formatKind(kind)}\n </span>\n ))}\n </div>\n ) : null}\n </div>\n <div className={statusClass.join(\" \")} title={statusLabel} />\n </div>\n <div className=\"process-card__list\">\n {items.map((item) => (\n <div className=\"process-chip\" key={`${item.category}:${item.kind}`}>\n <div className=\"process-chip__top\">\n <div className=\"process-chip__name\">{formatKind(item.kind)}</div>\n <div className={`status-pill__dot ${item.alive ? \"text-success\" : \"text-danger\"}`} />\n </div>\n <div className=\"process-chip__detail\">\n {(() => {\n if (item.rebuilding) {\n return \"Rebuilding\";\n }\n const kindLower = item.kind.toLowerCase();\n if (kindLower === \"search\") {\n const summary = item.searchSummary ?? \"\";\n return summary || \"No searches recorded\";\n }\n if (kindLower === \"torrent\") {\n const metricType = item.metricType?.toLowerCase();\n const categoryTotal =\n typeof item.categoryCount === \"number\" ? item.categoryCount : null;\n const queueTotal =\n typeof item.queueCount === \"number\" ? item.queueCount : null;\n\n if (!metricType) {\n const queueLabel = queueTotal !== null ? queueTotal : \"?\";\n const categoryLabel = categoryTotal !== null ? categoryTotal : \"?\";\n return `Torrents in queue ${queueLabel} / total ${categoryLabel}`;\n }\n\n if (metricType === \"category\" && categoryTotal !== null) {\n return `Torrent count ${categoryTotal}`;\n }\n\n if (metricType === \"free-space\" && queueTotal !== null) {\n return `Torrent count ${queueTotal}`;\n }\n\n return \"Torrent count unavailable\";\n }\n return \"\";\n })()}\n </div>\n <div className=\"process-chip__actions\">\n <button\n className=\"btn small\"\n onClick={() => handleRestart(item.category, item.kind)}\n >\n Restart\n </button>\n </div>\n </div>\n ))}\n </div>\n <div className=\"process-card__footer\">\n <button\n className=\"btn small outline\"\n onClick={() => void handleRestartGroup(items)}\n >\n Restart All\n </button>\n </div>\n </div>\n );\n });\n return { app, cards };\n });\n\n return (\n <>\n <section className=\"card\">\n <div className=\"card-header\">Processes</div>\n <div className=\"card-body stack\">\n <div className=\"row\">\n <div className=\"col inline\">\n <button className=\"btn ghost\" onClick={() => void load()} disabled={loading}>\n {loading && <span className=\"spinner\" />}\n <IconImage src={RefreshIcon} />\n {loading ? 'Refreshing...' : 'Refresh'}\n </button>\n <button className=\"btn\" onClick={() => void handleRestartAll()} disabled={restartingAll}>\n {restartingAll && <span className=\"spinner\" />}\n <IconImage src={RestartIcon} />\n {restartingAll ? 'Restarting...' : 'Restart All'}\n </button>\n <button className=\"btn\" onClick={() => void handleRebuildArrs()} disabled={rebuildingArrs}>\n {rebuildingArrs && <span className=\"spinner\" />}\n <IconImage src={ToolsIcon} />\n {rebuildingArrs ? 'Rebuilding...' : 'Rebuild Arrs'}\n </button>\n </div>\n </div>\n {cardsByApp.length ? (\n cardsByApp.map(({ app, cards }) => (\n <div className=\"process-section\" key={app}>\n <div className=\"process-section__title\">{app}</div>\n <div className=\"process-grid\">{cards}</div>\n </div>\n ))\n ) : (\n <div className=\"empty-state\">No processes available.</div>\n )}\n </div>\n </section>\n {confirmAction && (\n <ConfirmDialog\n title={confirmAction.title}\n message={confirmAction.message}\n confirmLabel=\"Confirm\"\n cancelLabel=\"Cancel\"\n danger={true}\n onConfirm={confirmAction.onConfirm}\n onCancel={() => setConfirmAction(null)}\n />\n )}\n </>\n );\n}\n"],"names":["ConfirmDialog","title","message","confirmLabel","cancelLabel","onConfirm","onCancel","danger","jsx","jsxs","e","IconImage","CloseIcon","ToolsIcon","RELEASE_TOKEN_REGEX","EPISODE_TOKEN_REGEX","SEASON_TOKEN_REGEX","sanitizeSearchSummary","raw","trimmed","normalized","releaseMatch","rest","looksLikeEpisode","cleanedTitle","year","isProcessEqual","a","b","areProcessListsEqual","index","getRefreshDelay","active","processes","ProcessesView","setProcesses","useState","loading","setLoading","restartingAll","setRestartingAll","rebuildingArrs","setRebuildingArrs","statusData","setStatusData","confirmAction","setConfirmAction","push","useToast","isFetching","useRef","load","useCallback","showLoading","processData","status","getProcesses","getStatus","next","process","sanitized","prev","error","useEffect","refreshDelay","useMemo","useInterval","handleRestart","category","kind","restartProcess","handleRestartAll","restartAllProcesses","handleRebuildArrs","rebuildArrs","groupedProcesses","appBuckets","classifyApp","proc","name","arrs","hasRadarr","arr","hasSonarr","hasLidarr","app","instances","instanceKey","appOrder","result","sortedInstances","items","group","order","label","handleRestartGroup","item","cardsByApp","cards","runningCount","totalCount","tone","statusClass","statusLabel","summaryLabel","displayName","filteredKinds","lower","formatKind","kindLower","metricType","categoryTotal","queueTotal","Fragment","RefreshIcon","RestartIcon"],"mappings":"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"}