qBitrr2 5.8.3__tar.gz → 5.8.5__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 (83) hide show
  1. {qbitrr2-5.8.3/qBitrr2.egg-info → qbitrr2-5.8.5}/PKG-INFO +1 -1
  2. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/pyproject.toml +1 -1
  3. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/arss.py +102 -45
  4. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/bundled_data.py +2 -2
  5. qbitrr2-5.8.5/qBitrr/static/assets/ProcessesView.js +2 -0
  6. qbitrr2-5.8.5/qBitrr/static/assets/ProcessesView.js.map +1 -0
  7. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/webui.py +17 -7
  8. {qbitrr2-5.8.3 → qbitrr2-5.8.5/qBitrr2.egg-info}/PKG-INFO +1 -1
  9. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/setup.cfg +1 -1
  10. qbitrr2-5.8.3/qBitrr/static/assets/ProcessesView.js +0 -2
  11. qbitrr2-5.8.3/qBitrr/static/assets/ProcessesView.js.map +0 -1
  12. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/LICENSE +0 -0
  13. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/MANIFEST.in +0 -0
  14. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/README.md +0 -0
  15. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/config.example.toml +0 -0
  16. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/__init__.py +0 -0
  17. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/auto_update.py +0 -0
  18. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/config.py +0 -0
  19. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/config_version.py +0 -0
  20. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/database.py +0 -0
  21. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/db_lock.py +0 -0
  22. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/db_recovery.py +0 -0
  23. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/env_config.py +0 -0
  24. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/errors.py +0 -0
  25. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/ffprobe.py +0 -0
  26. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/gen_config.py +0 -0
  27. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/home_path.py +0 -0
  28. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/logger.py +0 -0
  29. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/main.py +0 -0
  30. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/search_activity_store.py +0 -0
  31. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/ArrView.js +0 -0
  32. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/ArrView.js.map +0 -0
  33. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/ConfigView.js +0 -0
  34. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/ConfigView.js.map +0 -0
  35. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/LogsView.js +0 -0
  36. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/LogsView.js.map +0 -0
  37. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/app.css +0 -0
  38. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/app.js +0 -0
  39. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/app.js.map +0 -0
  40. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/build.svg +0 -0
  41. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/check-mark.svg +0 -0
  42. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/close.svg +0 -0
  43. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/download.svg +0 -0
  44. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/gear.svg +0 -0
  45. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/lidarr.svg +0 -0
  46. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/live-streaming.svg +0 -0
  47. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/log.svg +0 -0
  48. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/logo.svg +0 -0
  49. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/plus.svg +0 -0
  50. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/process.svg +0 -0
  51. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/react-select.esm.js +0 -0
  52. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/react-select.esm.js.map +0 -0
  53. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/refresh-arrow.svg +0 -0
  54. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/table.js +0 -0
  55. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/table.js.map +0 -0
  56. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/trash.svg +0 -0
  57. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/up-arrow.svg +0 -0
  58. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/useInterval.js +0 -0
  59. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/useInterval.js.map +0 -0
  60. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/vendor.js +0 -0
  61. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/vendor.js.map +0 -0
  62. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/assets/visibility.svg +0 -0
  63. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/favicon-16x16.png +0 -0
  64. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/favicon-32x32.png +0 -0
  65. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/favicon-48x48.png +0 -0
  66. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/favicon.ico +0 -0
  67. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/icon-192.png +0 -0
  68. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/icon-512.png +0 -0
  69. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/index.html +0 -0
  70. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/logov2-clean.png +0 -0
  71. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/logov2-clean.svg +0 -0
  72. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/manifest.json +0 -0
  73. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/sw.js +0 -0
  74. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/static/vite.svg +0 -0
  75. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/tables.py +0 -0
  76. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/utils.py +0 -0
  77. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr/versioning.py +0 -0
  78. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr2.egg-info/SOURCES.txt +0 -0
  79. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr2.egg-info/dependency_links.txt +0 -0
  80. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr2.egg-info/entry_points.txt +0 -0
  81. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr2.egg-info/requires.txt +0 -0
  82. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/qBitrr2.egg-info/top_level.txt +0 -0
  83. {qbitrr2-5.8.3 → qbitrr2-5.8.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qBitrr2
3
- Version: 5.8.3
3
+ Version: 5.8.5
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
@@ -28,7 +28,7 @@ target-version = ['py311']
28
28
 
29
29
  [tool.poetry]
30
30
  name = "pypi-public"
31
- version = "5.8.3"
31
+ version = "5.8.5"
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"
@@ -1756,11 +1756,8 @@ class Arr:
1756
1756
  try:
1757
1757
  commands = self.client.get_command()
1758
1758
  for command in commands:
1759
- if (
1760
- command["name"].endswith("Search")
1761
- and command["status"] != "completed"
1762
- and "Missing" not in command["name"]
1763
- ):
1759
+ # Count all active search commands (including MissingEpisodeSearch)
1760
+ if command["name"].endswith("Search") and command["status"] != "completed":
1764
1761
  search_commands = search_commands + 1
1765
1762
  break
1766
1763
  except (
@@ -1838,7 +1835,10 @@ class Arr:
1838
1835
  # Create a series entry for searching
1839
1836
  series_model = (
1840
1837
  self.series_file_model.select()
1841
- .where(self.series_file_model.EntryId == series_id)
1838
+ .where(
1839
+ (self.series_file_model.EntryId == series_id)
1840
+ & (self.series_file_model.ArrInstance == self._name)
1841
+ )
1842
1842
  .first()
1843
1843
  )
1844
1844
  if series_model:
@@ -1888,7 +1888,8 @@ class Arr:
1888
1888
  self.loop_completed and self.reset_on_completion and self.series_search
1889
1889
  ): # Only wipe if a loop completed was tagged
1890
1890
  self.series_file_model.update(Searched=False, Upgrade=False).where(
1891
- self.series_file_model.Searched == True
1891
+ (self.series_file_model.Searched == True)
1892
+ & (self.series_file_model.ArrInstance == self._name)
1892
1893
  ).execute()
1893
1894
  while True:
1894
1895
  try:
@@ -1904,7 +1905,8 @@ class Arr:
1904
1905
  ):
1905
1906
  continue
1906
1907
  self.series_file_model.delete().where(
1907
- self.series_file_model.EntryId.not_in(ids)
1908
+ (self.series_file_model.EntryId.not_in(ids))
1909
+ & (self.series_file_model.ArrInstance == self._name)
1908
1910
  ).execute()
1909
1911
  self.loop_completed = False
1910
1912
 
@@ -1932,7 +1934,9 @@ class Arr:
1932
1934
  JSONDecodeError,
1933
1935
  ) as e:
1934
1936
  continue
1935
- self.model_file.delete().where(self.model_file.EntryId.not_in(ids)).execute()
1937
+ self.model_file.delete().where(
1938
+ (self.model_file.EntryId.not_in(ids)) & (self.model_file.ArrInstance == self._name)
1939
+ ).execute()
1936
1940
  self.loop_completed = False
1937
1941
 
1938
1942
  def db_reset__movie_searched_state(self):
@@ -1942,7 +1946,7 @@ class Arr:
1942
1946
  self.loop_completed is True and self.reset_on_completion
1943
1947
  ): # Only wipe if a loop completed was tagged
1944
1948
  self.model_file.update(Searched=False, Upgrade=False).where(
1945
- self.model_file.Searched == True
1949
+ (self.model_file.Searched == True) & (self.model_file.ArrInstance == self._name)
1946
1950
  ).execute()
1947
1951
  while True:
1948
1952
  try:
@@ -1957,7 +1961,9 @@ class Arr:
1957
1961
  JSONDecodeError,
1958
1962
  ):
1959
1963
  continue
1960
- self.model_file.delete().where(self.model_file.EntryId.not_in(ids)).execute()
1964
+ self.model_file.delete().where(
1965
+ (self.model_file.EntryId.not_in(ids)) & (self.model_file.ArrInstance == self._name)
1966
+ ).execute()
1961
1967
  self.loop_completed = False
1962
1968
 
1963
1969
  def db_reset__album_searched_state(self):
@@ -1967,7 +1973,7 @@ class Arr:
1967
1973
  self.loop_completed is True and self.reset_on_completion
1968
1974
  ): # Only wipe if a loop completed was tagged
1969
1975
  self.model_file.update(Searched=False, Upgrade=False).where(
1970
- self.model_file.Searched == True
1976
+ (self.model_file.Searched == True) & (self.model_file.ArrInstance == self._name)
1971
1977
  ).execute()
1972
1978
  while True:
1973
1979
  try:
@@ -1984,7 +1990,9 @@ class Arr:
1984
1990
  JSONDecodeError,
1985
1991
  ):
1986
1992
  continue
1987
- self.model_file.delete().where(self.model_file.EntryId.not_in(ids)).execute()
1993
+ self.model_file.delete().where(
1994
+ (self.model_file.EntryId.not_in(ids)) & (self.model_file.ArrInstance == self._name)
1995
+ ).execute()
1988
1996
  self.loop_completed = False
1989
1997
 
1990
1998
  def db_get_files_series(self) -> list[list[SeriesFilesModel, bool, bool]] | None:
@@ -2040,9 +2048,13 @@ class Arr:
2040
2048
  if i1 is not None:
2041
2049
  entries.append([i1, i2, i3])
2042
2050
  if not self.do_upgrade_search:
2043
- condition = self.series_file_model.Searched == False
2051
+ condition = (self.series_file_model.Searched == False) & (
2052
+ self.series_file_model.ArrInstance == self._name
2053
+ )
2044
2054
  else:
2045
- condition = self.series_file_model.Upgrade == False
2055
+ condition = (self.series_file_model.Upgrade == False) & (
2056
+ self.series_file_model.ArrInstance == self._name
2057
+ )
2046
2058
 
2047
2059
  # Collect series entries with their priority based on episode reasons
2048
2060
  # Missing > CustomFormat > Quality > Upgrade
@@ -2082,7 +2094,9 @@ class Arr:
2082
2094
  if not (self.search_missing or self.do_upgrade_search):
2083
2095
  return None
2084
2096
  elif self.type == "sonarr":
2085
- condition = self.model_file.AirDateUtc.is_null(False)
2097
+ condition = (self.model_file.AirDateUtc.is_null(False)) & (
2098
+ self.model_file.ArrInstance == self._name
2099
+ )
2086
2100
  if not self.search_specials:
2087
2101
  condition &= self.model_file.SeasonNumber != 0
2088
2102
  if self.do_upgrade_search:
@@ -2163,7 +2177,9 @@ class Arr:
2163
2177
  if not (self.search_missing or self.do_upgrade_search):
2164
2178
  return None
2165
2179
  if self.type == "radarr":
2166
- condition = self.model_file.Year.is_null(False)
2180
+ condition = (self.model_file.Year.is_null(False)) & (
2181
+ self.model_file.ArrInstance == self._name
2182
+ )
2167
2183
  if self.do_upgrade_search:
2168
2184
  condition &= self.model_file.Upgrade == False
2169
2185
  else:
@@ -2214,7 +2230,7 @@ class Arr:
2214
2230
  entries.append([entry, False, False])
2215
2231
  return entries
2216
2232
  elif self.type == "lidarr":
2217
- condition = True # Placeholder, will be refined
2233
+ condition = self.model_file.ArrInstance == self._name
2218
2234
  if self.do_upgrade_search:
2219
2235
  condition &= self.model_file.Upgrade == False
2220
2236
  else:
@@ -2267,7 +2283,9 @@ class Arr:
2267
2283
  entries = []
2268
2284
  self.logger.trace("Getting request files")
2269
2285
  if self.type == "sonarr":
2270
- condition = self.model_file.IsRequest == True
2286
+ condition = (self.model_file.IsRequest == True) & (
2287
+ self.model_file.ArrInstance == self._name
2288
+ )
2271
2289
  condition &= self.model_file.AirDateUtc.is_null(False)
2272
2290
  condition &= self.model_file.EpisodeFileId == 0
2273
2291
  condition &= self.model_file.Searched == False
@@ -2285,7 +2303,9 @@ class Arr:
2285
2303
  .execute()
2286
2304
  )
2287
2305
  elif self.type == "radarr":
2288
- condition = self.model_file.IsRequest == True
2306
+ condition = (self.model_file.IsRequest == True) & (
2307
+ self.model_file.ArrInstance == self._name
2308
+ )
2289
2309
  condition &= self.model_file.Year.is_null(False)
2290
2310
  condition &= self.model_file.MovieFileId == 0
2291
2311
  condition &= self.model_file.Searched == False
@@ -2829,7 +2849,8 @@ class Arr:
2829
2849
  if not series:
2830
2850
  self.model_file: EpisodeFilesModel
2831
2851
  episodeData = self.model_file.get_or_none(
2832
- self.model_file.EntryId == db_entry["id"]
2852
+ (self.model_file.EntryId == db_entry["id"])
2853
+ & (self.model_file.ArrInstance == self._name)
2833
2854
  )
2834
2855
  while True:
2835
2856
  try:
@@ -2948,7 +2969,8 @@ class Arr:
2948
2969
  ):
2949
2970
  searched = True
2950
2971
  self.model_queue.update(Completed=True).where(
2951
- self.model_queue.EntryId == episode["id"]
2972
+ (self.model_queue.EntryId == episode["id"])
2973
+ & (self.model_queue.ArrInstance == self._name)
2952
2974
  ).execute()
2953
2975
 
2954
2976
  if self.use_temp_for_missing:
@@ -3157,14 +3179,16 @@ class Arr:
3157
3179
  db_commands.execute()
3158
3180
  else:
3159
3181
  db_commands = self.model_file.delete().where(
3160
- self.model_file.EntryId == episode["id"]
3182
+ (self.model_file.EntryId == episode["id"])
3183
+ & (self.model_file.ArrInstance == self._name)
3161
3184
  )
3162
3185
  db_commands.execute()
3163
3186
  else:
3164
3187
  self.series_file_model: SeriesFilesModel
3165
3188
  EntryId = db_entry["id"]
3166
3189
  seriesData = self.series_file_model.get_or_none(
3167
- self.series_file_model.EntryId == EntryId
3190
+ (self.series_file_model.EntryId == EntryId)
3191
+ & (self.series_file_model.ArrInstance == self._name)
3168
3192
  )
3169
3193
  if db_entry["monitored"] or self.search_unmonitored:
3170
3194
  while True:
@@ -3342,14 +3366,18 @@ class Arr:
3342
3366
  # No need to recursively process episodes here to avoid duplication
3343
3367
  else:
3344
3368
  db_commands = self.series_file_model.delete().where(
3345
- self.series_file_model.EntryId == EntryId
3369
+ (self.series_file_model.EntryId == EntryId)
3370
+ & (self.series_file_model.ArrInstance == self._name)
3346
3371
  )
3347
3372
  db_commands.execute()
3348
3373
 
3349
3374
  elif self.type == "radarr":
3350
3375
  self.model_file: MoviesFilesModel
3351
3376
  searched = False
3352
- movieData = self.model_file.get_or_none(self.model_file.EntryId == db_entry["id"])
3377
+ movieData = self.model_file.get_or_none(
3378
+ (self.model_file.EntryId == db_entry["id"])
3379
+ & (self.model_file.ArrInstance == self._name)
3380
+ )
3353
3381
  if self.minimum_availability_check(db_entry) and (
3354
3382
  db_entry["monitored"] or self.search_unmonitored
3355
3383
  ):
@@ -3409,7 +3437,8 @@ class Arr:
3409
3437
  ):
3410
3438
  searched = True
3411
3439
  self.model_queue.update(Completed=True).where(
3412
- self.model_queue.EntryId == db_entry["id"]
3440
+ (self.model_queue.EntryId == db_entry["id"])
3441
+ & (self.model_queue.ArrInstance == self._name)
3413
3442
  ).execute()
3414
3443
 
3415
3444
  profile_switch_timestamp = None
@@ -3578,7 +3607,8 @@ class Arr:
3578
3607
  db_commands.execute()
3579
3608
  else:
3580
3609
  db_commands = self.model_file.delete().where(
3581
- self.model_file.EntryId == db_entry["id"]
3610
+ (self.model_file.EntryId == db_entry["id"])
3611
+ & (self.model_file.ArrInstance == self._name)
3582
3612
  )
3583
3613
  db_commands.execute()
3584
3614
  elif self.type == "lidarr":
@@ -3587,7 +3617,8 @@ class Arr:
3587
3617
  self.model_file: AlbumFilesModel
3588
3618
  searched = False
3589
3619
  albumData = self.model_file.get_or_none(
3590
- self.model_file.EntryId == db_entry["id"]
3620
+ (self.model_file.EntryId == db_entry["id"])
3621
+ & (self.model_file.ArrInstance == self._name)
3591
3622
  )
3592
3623
  if db_entry["monitored"] or self.search_unmonitored:
3593
3624
  while True:
@@ -3930,20 +3961,23 @@ class Arr:
3930
3961
  )
3931
3962
  else:
3932
3963
  db_commands = self.model_file.delete().where(
3933
- self.model_file.EntryId == db_entry["id"]
3964
+ (self.model_file.EntryId == db_entry["id"])
3965
+ & (self.model_file.ArrInstance == self._name)
3934
3966
  )
3935
3967
  db_commands.execute()
3936
3968
  # Also delete tracks for this album (Lidarr only)
3937
3969
  if self.track_file_model:
3938
3970
  self.track_file_model.delete().where(
3939
- self.track_file_model.AlbumId == db_entry["id"]
3971
+ (self.track_file_model.AlbumId == db_entry["id"])
3972
+ & (self.track_file_model.ArrInstance == self._name)
3940
3973
  ).execute()
3941
3974
  else:
3942
3975
  # Artist handling
3943
3976
  self.artists_file_model: ArtistFilesModel
3944
3977
  EntryId = db_entry["id"]
3945
3978
  artistData = self.artists_file_model.get_or_none(
3946
- self.artists_file_model.EntryId == EntryId
3979
+ (self.artists_file_model.EntryId == EntryId)
3980
+ & (self.artists_file_model.ArrInstance == self._name)
3947
3981
  )
3948
3982
  if db_entry["monitored"] or self.search_unmonitored:
3949
3983
  while True:
@@ -4331,7 +4365,8 @@ class Arr:
4331
4365
  file_model.AirDateUtc,
4332
4366
  )
4333
4367
  self.model_file.update(Searched=True, Upgrade=True).where(
4334
- file_model.EntryId == file_model.EntryId
4368
+ (self.model_file.EntryId == file_model.EntryId)
4369
+ & (self.model_file.ArrInstance == self._name)
4335
4370
  ).execute()
4336
4371
  return True
4337
4372
  active_commands = self.arr_db_query_commands_count()
@@ -4374,7 +4409,8 @@ class Arr:
4374
4409
  ):
4375
4410
  continue
4376
4411
  self.model_file.update(Searched=True, Upgrade=True).where(
4377
- file_model.EntryId == file_model.EntryId
4412
+ (self.model_file.EntryId == file_model.EntryId)
4413
+ & (self.model_file.ArrInstance == self._name)
4378
4414
  ).execute()
4379
4415
  reason_text = getattr(file_model, "Reason", None) or None
4380
4416
  if reason_text:
@@ -4445,7 +4481,8 @@ class Arr:
4445
4481
  ):
4446
4482
  continue
4447
4483
  self.model_file.update(Searched=True, Upgrade=True).where(
4448
- file_model.EntryId == file_model.EntryId
4484
+ (self.model_file.EntryId == file_model.EntryId)
4485
+ & (self.model_file.ArrInstance == self._name)
4449
4486
  ).execute()
4450
4487
  self.logger.hnotice(
4451
4488
  "%sSearching for: %s | %s | [id=%s]",
@@ -4485,7 +4522,8 @@ class Arr:
4485
4522
  file_model.EntryId,
4486
4523
  )
4487
4524
  self.model_file.update(Searched=True, Upgrade=True).where(
4488
- file_model.EntryId == file_model.EntryId
4525
+ (self.model_file.EntryId == file_model.EntryId)
4526
+ & (self.model_file.ArrInstance == self._name)
4489
4527
  ).execute()
4490
4528
  return True
4491
4529
  active_commands = self.arr_db_query_commands_count()
@@ -4517,7 +4555,8 @@ class Arr:
4517
4555
  ):
4518
4556
  continue
4519
4557
  self.model_file.update(Searched=True, Upgrade=True).where(
4520
- file_model.EntryId == file_model.EntryId
4558
+ (self.model_file.EntryId == file_model.EntryId)
4559
+ & (self.model_file.ArrInstance == self._name)
4521
4560
  ).execute()
4522
4561
  reason_text = getattr(file_model, "Reason", None)
4523
4562
  if reason_text:
@@ -4570,7 +4609,8 @@ class Arr:
4570
4609
  file_model.EntryId,
4571
4610
  )
4572
4611
  self.model_file.update(Searched=True, Upgrade=True).where(
4573
- file_model.EntryId == file_model.EntryId
4612
+ (self.model_file.EntryId == file_model.EntryId)
4613
+ & (self.model_file.ArrInstance == self._name)
4574
4614
  ).execute()
4575
4615
  return True
4576
4616
  active_commands = self.arr_db_query_commands_count()
@@ -4603,7 +4643,8 @@ class Arr:
4603
4643
  ):
4604
4644
  continue
4605
4645
  self.model_file.update(Searched=True, Upgrade=True).where(
4606
- file_model.EntryId == file_model.EntryId
4646
+ (self.model_file.EntryId == file_model.EntryId)
4647
+ & (self.model_file.ArrInstance == self._name)
4607
4648
  ).execute()
4608
4649
  reason_text = getattr(file_model, "Reason", None)
4609
4650
  if reason_text:
@@ -6198,7 +6239,12 @@ class Arr:
6198
6239
 
6199
6240
  # Retrieve the model entry from the database
6200
6241
  model_entry = (
6201
- self.model_file.select().where(self.model_file.EntryId == entry_id).first()
6242
+ self.model_file.select()
6243
+ .where(
6244
+ (self.model_file.EntryId == entry_id)
6245
+ & (self.model_file.ArrInstance == self._name)
6246
+ )
6247
+ .first()
6202
6248
  )
6203
6249
  if not model_entry:
6204
6250
  return False
@@ -6271,7 +6317,10 @@ class Arr:
6271
6317
  if self.model_queue:
6272
6318
  with_database_retry(
6273
6319
  lambda: self.model_queue.delete()
6274
- .where(self.model_queue.EntryId.not_in(list(self.queue_file_ids)))
6320
+ .where(
6321
+ (self.model_queue.EntryId.not_in(list(self.queue_file_ids)))
6322
+ & (self.model_queue.ArrInstance == self._name)
6323
+ )
6275
6324
  .execute(),
6276
6325
  logger=self.logger,
6277
6326
  )
@@ -6285,7 +6334,10 @@ class Arr:
6285
6334
  if self.model_queue:
6286
6335
  with_database_retry(
6287
6336
  lambda: self.model_queue.delete()
6288
- .where(self.model_queue.EntryId.not_in(list(self.queue_file_ids)))
6337
+ .where(
6338
+ (self.model_queue.EntryId.not_in(list(self.queue_file_ids)))
6339
+ & (self.model_queue.ArrInstance == self._name)
6340
+ )
6289
6341
  .execute(),
6290
6342
  logger=self.logger,
6291
6343
  )
@@ -6570,7 +6622,8 @@ class Arr:
6570
6622
 
6571
6623
  # Find items with temp profiles that have exceeded timeout
6572
6624
  expired_items = model.select().where(
6573
- (model.LastProfileSwitchTime.is_null(False))
6625
+ (model.ArrInstance == self._name)
6626
+ & (model.LastProfileSwitchTime.is_null(False))
6574
6627
  & (model.LastProfileSwitchTime < timeout_threshold)
6575
6628
  & (model.CurrentProfileId.is_null(False))
6576
6629
  & (model.OriginalProfileId.is_null(False))
@@ -6586,7 +6639,9 @@ class Arr:
6586
6639
  # Not a temp profile anymore, clear tracking
6587
6640
  model.update(
6588
6641
  LastProfileSwitchTime=None, CurrentProfileId=None, OriginalProfileId=None
6589
- ).where(model.EntryId == entry_id).execute()
6642
+ ).where(
6643
+ (model.EntryId == entry_id) & (model.ArrInstance == self._name)
6644
+ ).execute()
6590
6645
  continue
6591
6646
 
6592
6647
  # Reset to original profile via Arr API
@@ -6609,7 +6664,9 @@ class Arr:
6609
6664
  # Clear tracking fields in database
6610
6665
  model.update(
6611
6666
  LastProfileSwitchTime=None, CurrentProfileId=None, OriginalProfileId=None
6612
- ).where(model.EntryId == entry_id).execute()
6667
+ ).where(
6668
+ (model.EntryId == entry_id) & (model.ArrInstance == self._name)
6669
+ ).execute()
6613
6670
 
6614
6671
  reset_count += 1
6615
6672
  self.logger.info(
@@ -1,5 +1,5 @@
1
- version = "5.8.3"
2
- git_hash = "48b8e83c"
1
+ version = "5.8.5"
2
+ git_hash = "4f8ef826"
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,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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"}
@@ -461,7 +461,8 @@ class WebUI:
461
461
  if is_request is not None:
462
462
  query = query.where(model.IsRequest == is_request)
463
463
 
464
- total = query.count()
464
+ # Total should be ALL items for this instance, not filtered results
465
+ total = base_query.count()
465
466
  page_items = query.order_by(model.Title.asc()).paginate(page + 1, page_size).iterator()
466
467
  movies = []
467
468
  for movie in page_items:
@@ -622,6 +623,9 @@ class WebUI:
622
623
 
623
624
  albums = []
624
625
 
626
+ # Total should be ALL albums for this instance, not filtered results
627
+ total = base_query.count()
628
+
625
629
  if group_by_artist:
626
630
  # Paginate by artists: Two-pass approach with Peewee
627
631
  # First, get all distinct artist names from the filtered query
@@ -632,7 +636,7 @@ class WebUI:
632
636
 
633
637
  # Convert to list to avoid multiple iterations
634
638
  all_artists = [row.ArtistTitle for row in artists_subquery]
635
- total = len(all_artists)
639
+ len(all_artists)
636
640
 
637
641
  # Paginate the artist list in Python
638
642
  start_idx = page * page_size
@@ -650,7 +654,7 @@ class WebUI:
650
654
  album_results = []
651
655
  else:
652
656
  # Flat mode: paginate by albums as before
653
- total = query.count()
657
+ # Note: total is already set to base_query.count() above
654
658
  album_results = list(query.order_by(model.Title).paginate(page + 1, page_size))
655
659
 
656
660
  for album in album_results:
@@ -982,16 +986,22 @@ class WebUI:
982
986
  total_series = 0
983
987
 
984
988
  if series_model is not None:
985
- series_query = series_model.select().where(
989
+ # Base query for ALL series in this instance (unfiltered)
990
+ base_series_query = series_model.select().where(
986
991
  series_model.ArrInstance == arr_instance
987
992
  )
993
+ # Total should be ALL series for this instance, not filtered results
994
+ total_series = base_series_query.count()
995
+
996
+ # Now build the filtered query for pagination
997
+ series_query = base_series_query
988
998
  if search:
989
999
  series_query = series_query.where(series_model.Title.contains(search))
990
1000
  if missing_only and missing_series_ids:
991
1001
  series_query = series_query.where(series_model.EntryId.in_(missing_series_ids))
992
- total_series = series_query.count()
993
- if total_series:
994
- max_pages = (total_series + page_size - 1) // page_size
1002
+ filtered_series_count = series_query.count()
1003
+ if filtered_series_count:
1004
+ max_pages = (filtered_series_count + page_size - 1) // page_size
995
1005
  if max_pages:
996
1006
  resolved_page = min(resolved_page, max_pages - 1)
997
1007
  resolved_page = max(resolved_page, 0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qBitrr2
3
- Version: 5.8.3
3
+ Version: 5.8.5
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
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = qBitrr2
3
- version = 5.8.3
3
+ version = 5.8.5
4
4
  description = Intelligent automation for qBittorrent and *Arr apps (Radarr/Sonarr/Lidarr) - health monitoring, instant imports, quality upgrades, request integration
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
@@ -1,2 +0,0 @@
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||/^\d+\s+queued item/i.test(t))return"";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
@@ -1 +0,0 @@
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 if (/^\\d+\\s+queued item/i.test(trimmed)) return \"\";\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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"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes