qBitrr2 5.5.2__tar.gz → 5.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {qbitrr2-5.5.2/qBitrr2.egg-info → qbitrr2-5.5.3}/PKG-INFO +2 -2
  2. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/README.md +1 -1
  3. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/pyproject.toml +1 -1
  4. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/arss.py +206 -84
  5. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/bundled_data.py +2 -2
  6. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/main.py +12 -0
  7. qbitrr2-5.5.3/qBitrr/static/assets/ArrView.js +2 -0
  8. qbitrr2-5.5.3/qBitrr/static/assets/ArrView.js.map +1 -0
  9. qbitrr2-5.5.3/qBitrr/static/assets/ConfigView.js +4 -0
  10. qbitrr2-5.5.3/qBitrr/static/assets/ConfigView.js.map +1 -0
  11. qbitrr2-5.5.3/qBitrr/static/assets/LogsView.js +2 -0
  12. qbitrr2-5.5.3/qBitrr/static/assets/LogsView.js.map +1 -0
  13. qbitrr2-5.5.3/qBitrr/static/assets/ProcessesView.js +2 -0
  14. qbitrr2-5.5.3/qBitrr/static/assets/ProcessesView.js.map +1 -0
  15. qbitrr2-5.5.3/qBitrr/static/assets/app.css +1 -0
  16. qbitrr2-5.5.3/qBitrr/static/assets/app.js +11 -0
  17. qbitrr2-5.5.3/qBitrr/static/assets/app.js.map +1 -0
  18. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/react-select.esm.js +4 -4
  19. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/react-select.esm.js.map +1 -1
  20. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/table.js +1 -1
  21. qbitrr2-5.5.3/qBitrr/static/assets/vendor.js +2 -0
  22. qbitrr2-5.5.3/qBitrr/static/assets/vendor.js.map +1 -0
  23. qbitrr2-5.5.3/qBitrr/static/index.html +33 -0
  24. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/sw.js +6 -29
  25. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/webui.py +2 -0
  26. {qbitrr2-5.5.2 → qbitrr2-5.5.3/qBitrr2.egg-info}/PKG-INFO +2 -2
  27. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr2.egg-info/SOURCES.txt +0 -1
  28. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/setup.cfg +1 -1
  29. qbitrr2-5.5.2/qBitrr/static/assets/ArrView.js +0 -2
  30. qbitrr2-5.5.2/qBitrr/static/assets/ArrView.js.map +0 -1
  31. qbitrr2-5.5.2/qBitrr/static/assets/ConfigView.js +0 -5
  32. qbitrr2-5.5.2/qBitrr/static/assets/ConfigView.js.map +0 -1
  33. qbitrr2-5.5.2/qBitrr/static/assets/LogsView.js +0 -208
  34. qbitrr2-5.5.2/qBitrr/static/assets/LogsView.js.map +0 -1
  35. qbitrr2-5.5.2/qBitrr/static/assets/ProcessesView.js +0 -2
  36. qbitrr2-5.5.2/qBitrr/static/assets/ProcessesView.js.map +0 -1
  37. qbitrr2-5.5.2/qBitrr/static/assets/app.css +0 -1
  38. qbitrr2-5.5.2/qBitrr/static/assets/app.js +0 -3
  39. qbitrr2-5.5.2/qBitrr/static/assets/app.js.map +0 -1
  40. qbitrr2-5.5.2/qBitrr/static/assets/lidarr.svg +0 -1
  41. qbitrr2-5.5.2/qBitrr/static/assets/vendor.js +0 -9
  42. qbitrr2-5.5.2/qBitrr/static/assets/vendor.js.map +0 -1
  43. qbitrr2-5.5.2/qBitrr/static/index.html +0 -47
  44. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/LICENSE +0 -0
  45. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/MANIFEST.in +0 -0
  46. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/config.example.toml +0 -0
  47. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/__init__.py +0 -0
  48. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/auto_update.py +0 -0
  49. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/config.py +0 -0
  50. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/config_version.py +0 -0
  51. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/db_lock.py +0 -0
  52. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/db_recovery.py +0 -0
  53. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/env_config.py +0 -0
  54. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/errors.py +0 -0
  55. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/ffprobe.py +0 -0
  56. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/gen_config.py +0 -0
  57. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/home_path.py +0 -0
  58. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/logger.py +0 -0
  59. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/search_activity_store.py +0 -0
  60. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/build.svg +0 -0
  61. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/check-mark.svg +0 -0
  62. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/close.svg +0 -0
  63. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/download.svg +0 -0
  64. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/gear.svg +0 -0
  65. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/live-streaming.svg +0 -0
  66. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/log.svg +0 -0
  67. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/plus.svg +0 -0
  68. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/process.svg +0 -0
  69. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/refresh-arrow.svg +0 -0
  70. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/table.js.map +0 -0
  71. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/trash.svg +0 -0
  72. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/up-arrow.svg +0 -0
  73. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/useInterval.js +0 -0
  74. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/useInterval.js.map +0 -0
  75. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/assets/visibility.svg +0 -0
  76. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/manifest.json +0 -0
  77. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/static/vite.svg +0 -0
  78. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/tables.py +0 -0
  79. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/utils.py +0 -0
  80. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr/versioning.py +0 -0
  81. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr2.egg-info/dependency_links.txt +0 -0
  82. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr2.egg-info/entry_points.txt +0 -0
  83. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr2.egg-info/requires.txt +0 -0
  84. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/qBitrr2.egg-info/top_level.txt +0 -0
  85. {qbitrr2-5.5.2 → qbitrr2-5.5.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qBitrr2
3
- Version: 5.5.2
3
+ Version: 5.5.3
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
@@ -985,7 +985,7 @@ Manually move torrents here to trigger a proper recheck operation.
985
985
  [Settings]
986
986
  Tagless = true
987
987
  ```
988
- Run qBitrr without requiring Arr instances to tag their torrents. Less precise but works without Arr configuration changes.
988
+ Disables qBitrr from tagging torrents in qBittorrent. Use this if you prefer to manage qBittorrent tags manually or avoid tag clutter.
989
989
 
990
990
  ---
991
991
 
@@ -900,7 +900,7 @@ Manually move torrents here to trigger a proper recheck operation.
900
900
  [Settings]
901
901
  Tagless = true
902
902
  ```
903
- Run qBitrr without requiring Arr instances to tag their torrents. Less precise but works without Arr configuration changes.
903
+ Disables qBitrr from tagging torrents in qBittorrent. Use this if you prefer to manage qBittorrent tags manually or avoid tag clutter.
904
904
 
905
905
  ---
906
906
 
@@ -28,7 +28,7 @@ target-version = ['py311']
28
28
 
29
29
  [tool.poetry]
30
30
  name = "pypi-public"
31
- version = "5.5.2"
31
+ version = "5.5.3"
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"
@@ -620,16 +620,32 @@ class Arr:
620
620
  self.search_api_command = "MissingEpisodeSearch"
621
621
 
622
622
  if not QBIT_DISABLED and not TAGLESS:
623
- self.manager.qbit_manager.client.torrents_create_tags(
624
- [
625
- "qBitrr-allowed_seeding",
626
- "qBitrr-ignored",
627
- "qBitrr-imported",
628
- "qBitrr-allowed_stalled",
629
- ]
630
- )
623
+ try:
624
+ self.manager.qbit_manager.client.torrents_create_tags(
625
+ [
626
+ "qBitrr-allowed_seeding",
627
+ "qBitrr-ignored",
628
+ "qBitrr-imported",
629
+ "qBitrr-allowed_stalled",
630
+ ]
631
+ )
632
+ except qbittorrentapi.exceptions.APIConnectionError as e:
633
+ self.logger.warning(
634
+ "Could not connect to qBittorrent during initialization for %s: %s. "
635
+ "Will retry when process starts.",
636
+ self._name,
637
+ str(e).split("\n")[0], # Only log first line of error
638
+ )
631
639
  elif not QBIT_DISABLED and TAGLESS:
632
- self.manager.qbit_manager.client.torrents_create_tags(["qBitrr-ignored"])
640
+ try:
641
+ self.manager.qbit_manager.client.torrents_create_tags(["qBitrr-ignored"])
642
+ except qbittorrentapi.exceptions.APIConnectionError as e:
643
+ self.logger.warning(
644
+ "Could not connect to qBittorrent during initialization for %s: %s. "
645
+ "Will retry when process starts.",
646
+ self._name,
647
+ str(e).split("\n")[0], # Only log first line of error
648
+ )
633
649
  self.search_setup_completed = False
634
650
  self.model_file: Model | None = None
635
651
  self.series_file_model: Model | None = None
@@ -1257,6 +1273,7 @@ class Arr:
1257
1273
  object_ids = list(object_id)
1258
1274
  self.logger.trace("Requeue cache entry list: %s", object_ids)
1259
1275
  if self.series_search:
1276
+ series_id = None
1260
1277
  while True:
1261
1278
  try:
1262
1279
  data = self.client.get_series(object_ids[0])
@@ -1283,27 +1300,35 @@ class Arr:
1283
1300
  ):
1284
1301
  continue
1285
1302
  except PyarrResourceNotFound as e:
1286
- self.logger.debug(e)
1287
- self.logger.error("PyarrResourceNotFound: %s", object_ids[0])
1303
+ self.logger.warning(
1304
+ "Series %s not found in Sonarr (likely removed): %s",
1305
+ object_ids[0],
1306
+ str(e),
1307
+ )
1308
+ break
1288
1309
  for object_id in object_ids:
1289
1310
  if object_id in self.queue_file_ids:
1290
1311
  self.queue_file_ids.remove(object_id)
1291
- self.logger.trace("Research series id: %s", series_id)
1292
- while True:
1293
- try:
1294
- self.client.post_command(self.search_api_command, seriesId=series_id)
1295
- break
1296
- except (
1297
- requests.exceptions.ChunkedEncodingError,
1298
- requests.exceptions.ContentDecodingError,
1299
- requests.exceptions.ConnectionError,
1300
- JSONDecodeError,
1301
- ):
1302
- continue
1303
- if self.persistent_queue and series_id:
1304
- self.persistent_queue.insert(EntryId=series_id).on_conflict_ignore()
1312
+ if series_id:
1313
+ self.logger.trace("Research series id: %s", series_id)
1314
+ while True:
1315
+ try:
1316
+ self.client.post_command(
1317
+ self.search_api_command, seriesId=series_id
1318
+ )
1319
+ break
1320
+ except (
1321
+ requests.exceptions.ChunkedEncodingError,
1322
+ requests.exceptions.ContentDecodingError,
1323
+ requests.exceptions.ConnectionError,
1324
+ JSONDecodeError,
1325
+ ):
1326
+ continue
1327
+ if self.persistent_queue:
1328
+ self.persistent_queue.insert(EntryId=series_id).on_conflict_ignore()
1305
1329
  else:
1306
1330
  for object_id in object_ids:
1331
+ episode_found = False
1307
1332
  while True:
1308
1333
  try:
1309
1334
  data = self.client.get_episode(object_id)
@@ -1333,6 +1358,7 @@ class Arr:
1333
1358
  )
1334
1359
  else:
1335
1360
  self.logger.notice("Re-Searching episode: %s", object_id)
1361
+ episode_found = True
1336
1362
  break
1337
1363
  except (
1338
1364
  requests.exceptions.ChunkedEncodingError,
@@ -1342,24 +1368,37 @@ class Arr:
1342
1368
  AttributeError,
1343
1369
  ):
1344
1370
  continue
1371
+ except PyarrResourceNotFound as e:
1372
+ self.logger.warning(
1373
+ "Episode %s not found in Sonarr (likely removed): %s",
1374
+ object_id,
1375
+ str(e),
1376
+ )
1377
+ break
1345
1378
 
1346
1379
  if object_id in self.queue_file_ids:
1347
1380
  self.queue_file_ids.remove(object_id)
1348
- while True:
1349
- try:
1350
- self.client.post_command("EpisodeSearch", episodeIds=[object_id])
1351
- break
1352
- except (
1353
- requests.exceptions.ChunkedEncodingError,
1354
- requests.exceptions.ContentDecodingError,
1355
- requests.exceptions.ConnectionError,
1356
- JSONDecodeError,
1357
- ):
1358
- continue
1359
- if self.persistent_queue:
1360
- self.persistent_queue.insert(EntryId=object_id).on_conflict_ignore()
1381
+ if episode_found:
1382
+ while True:
1383
+ try:
1384
+ self.client.post_command(
1385
+ "EpisodeSearch", episodeIds=[object_id]
1386
+ )
1387
+ break
1388
+ except (
1389
+ requests.exceptions.ChunkedEncodingError,
1390
+ requests.exceptions.ContentDecodingError,
1391
+ requests.exceptions.ConnectionError,
1392
+ JSONDecodeError,
1393
+ ):
1394
+ continue
1395
+ if self.persistent_queue:
1396
+ self.persistent_queue.insert(
1397
+ EntryId=object_id
1398
+ ).on_conflict_ignore()
1361
1399
  elif self.type == "radarr":
1362
1400
  self.logger.trace("Requeue cache entry: %s", object_id)
1401
+ movie_found = False
1363
1402
  while True:
1364
1403
  try:
1365
1404
  data = self.client.get_movie(object_id)
@@ -1376,6 +1415,7 @@ class Arr:
1376
1415
  )
1377
1416
  else:
1378
1417
  self.logger.notice("Re-Searching movie: %s", object_id)
1418
+ movie_found = True
1379
1419
  break
1380
1420
  except (
1381
1421
  requests.exceptions.ChunkedEncodingError,
@@ -1385,23 +1425,30 @@ class Arr:
1385
1425
  AttributeError,
1386
1426
  ):
1387
1427
  continue
1428
+ except PyarrResourceNotFound as e:
1429
+ self.logger.warning(
1430
+ "Movie %s not found in Radarr (likely removed): %s", object_id, str(e)
1431
+ )
1432
+ break
1388
1433
  if object_id in self.queue_file_ids:
1389
1434
  self.queue_file_ids.remove(object_id)
1390
- while True:
1391
- try:
1392
- self.client.post_command("MoviesSearch", movieIds=[object_id])
1393
- break
1394
- except (
1395
- requests.exceptions.ChunkedEncodingError,
1396
- requests.exceptions.ContentDecodingError,
1397
- requests.exceptions.ConnectionError,
1398
- JSONDecodeError,
1399
- ):
1400
- continue
1401
- if self.persistent_queue:
1402
- self.persistent_queue.insert(EntryId=object_id).on_conflict_ignore()
1435
+ if movie_found:
1436
+ while True:
1437
+ try:
1438
+ self.client.post_command("MoviesSearch", movieIds=[object_id])
1439
+ break
1440
+ except (
1441
+ requests.exceptions.ChunkedEncodingError,
1442
+ requests.exceptions.ContentDecodingError,
1443
+ requests.exceptions.ConnectionError,
1444
+ JSONDecodeError,
1445
+ ):
1446
+ continue
1447
+ if self.persistent_queue:
1448
+ self.persistent_queue.insert(EntryId=object_id).on_conflict_ignore()
1403
1449
  elif self.type == "lidarr":
1404
1450
  self.logger.trace("Requeue cache entry: %s", object_id)
1451
+ album_found = False
1405
1452
  while True:
1406
1453
  try:
1407
1454
  data = self.client.get_album(object_id)
@@ -1418,6 +1465,7 @@ class Arr:
1418
1465
  )
1419
1466
  else:
1420
1467
  self.logger.notice("Re-Searching album: %s", object_id)
1468
+ album_found = True
1421
1469
  break
1422
1470
  except (
1423
1471
  requests.exceptions.ChunkedEncodingError,
@@ -1427,21 +1475,27 @@ class Arr:
1427
1475
  AttributeError,
1428
1476
  ):
1429
1477
  continue
1478
+ except PyarrResourceNotFound as e:
1479
+ self.logger.warning(
1480
+ "Album %s not found in Lidarr (likely removed): %s", object_id, str(e)
1481
+ )
1482
+ break
1430
1483
  if object_id in self.queue_file_ids:
1431
1484
  self.queue_file_ids.remove(object_id)
1432
- while True:
1433
- try:
1434
- self.client.post_command("AlbumSearch", albumIds=[object_id])
1435
- break
1436
- except (
1437
- requests.exceptions.ChunkedEncodingError,
1438
- requests.exceptions.ContentDecodingError,
1439
- requests.exceptions.ConnectionError,
1440
- JSONDecodeError,
1441
- ):
1442
- continue
1443
- if self.persistent_queue:
1444
- self.persistent_queue.insert(EntryId=object_id).on_conflict_ignore()
1485
+ if album_found:
1486
+ while True:
1487
+ try:
1488
+ self.client.post_command("AlbumSearch", albumIds=[object_id])
1489
+ break
1490
+ except (
1491
+ requests.exceptions.ChunkedEncodingError,
1492
+ requests.exceptions.ContentDecodingError,
1493
+ requests.exceptions.ConnectionError,
1494
+ JSONDecodeError,
1495
+ ):
1496
+ continue
1497
+ if self.persistent_queue:
1498
+ self.persistent_queue.insert(EntryId=object_id).on_conflict_ignore()
1445
1499
 
1446
1500
  def _process_errored(self) -> None:
1447
1501
  # Recheck all torrents marked for rechecking.
@@ -2673,6 +2727,35 @@ class Arr:
2673
2727
  JSONDecodeError,
2674
2728
  ):
2675
2729
  continue
2730
+
2731
+ # Validate episode object has required fields
2732
+ if not episode or not isinstance(episode, dict):
2733
+ self.logger.warning(
2734
+ "Invalid episode object returned from API for episode ID %s: %s",
2735
+ db_entry.get("id"),
2736
+ type(episode).__name__,
2737
+ )
2738
+ return
2739
+
2740
+ required_fields = [
2741
+ "id",
2742
+ "seriesId",
2743
+ "seasonNumber",
2744
+ "episodeNumber",
2745
+ "title",
2746
+ "airDateUtc",
2747
+ "episodeFileId",
2748
+ ]
2749
+ missing_fields = [field for field in required_fields if field not in episode]
2750
+ if missing_fields:
2751
+ self.logger.warning(
2752
+ "Episode %s missing required fields %s. Episode data: %s",
2753
+ db_entry.get("id"),
2754
+ missing_fields,
2755
+ episode,
2756
+ )
2757
+ return
2758
+
2676
2759
  if episode.get("monitored", True) or self.search_unmonitored:
2677
2760
  while True:
2678
2761
  try:
@@ -2820,24 +2903,24 @@ class Arr:
2820
2903
  ):
2821
2904
  continue
2822
2905
 
2823
- EntryId = episode["id"]
2906
+ EntryId = episode.get("id")
2824
2907
  SeriesTitle = episode.get("series", {}).get("title")
2825
- SeasonNumber = episode["seasonNumber"]
2826
- Title = episode["title"]
2827
- SeriesId = episode["seriesId"]
2828
- EpisodeFileId = episode["episodeFileId"]
2829
- EpisodeNumber = episode["episodeNumber"]
2908
+ SeasonNumber = episode.get("seasonNumber")
2909
+ Title = episode.get("title")
2910
+ SeriesId = episode.get("seriesId")
2911
+ EpisodeFileId = episode.get("episodeFileId")
2912
+ EpisodeNumber = episode.get("episodeNumber")
2830
2913
  AbsoluteEpisodeNumber = (
2831
- episode["absoluteEpisodeNumber"]
2914
+ episode.get("absoluteEpisodeNumber")
2832
2915
  if "absoluteEpisodeNumber" in episode
2833
2916
  else None
2834
2917
  )
2835
2918
  SceneAbsoluteEpisodeNumber = (
2836
- episode["sceneAbsoluteEpisodeNumber"]
2919
+ episode.get("sceneAbsoluteEpisodeNumber")
2837
2920
  if "sceneAbsoluteEpisodeNumber" in episode
2838
2921
  else None
2839
2922
  )
2840
- AirDateUtc = episode["airDateUtc"]
2923
+ AirDateUtc = episode.get("airDateUtc")
2841
2924
  Monitored = episode.get("monitored", True)
2842
2925
  QualityMet = not QualityUnmet if db_entry["hasFile"] else False
2843
2926
  customFormatMet = customFormat >= minCustomFormat
@@ -3110,9 +3193,13 @@ class Arr:
3110
3193
  try:
3111
3194
  if movieData:
3112
3195
  if not movieData.MinCustomFormatScore:
3113
- minCustomFormat = self.client.get_quality_profile(
3114
- db_entry["qualityProfileId"]
3115
- )["minFormatScore"]
3196
+ profile = (
3197
+ self.client.get_quality_profile(
3198
+ db_entry["qualityProfileId"]
3199
+ )
3200
+ or {}
3201
+ )
3202
+ minCustomFormat = profile.get("minFormatScore", 0)
3116
3203
  else:
3117
3204
  minCustomFormat = movieData.MinCustomFormatScore
3118
3205
  if db_entry["hasFile"]:
@@ -3125,9 +3212,11 @@ class Arr:
3125
3212
  else:
3126
3213
  customFormat = 0
3127
3214
  else:
3128
- minCustomFormat = self.client.get_quality_profile(
3129
- db_entry["qualityProfileId"]
3130
- )["minFormatScore"]
3215
+ profile = (
3216
+ self.client.get_quality_profile(db_entry["qualityProfileId"])
3217
+ or {}
3218
+ )
3219
+ minCustomFormat = profile.get("minFormatScore", 0)
3131
3220
  if db_entry["hasFile"]:
3132
3221
  customFormat = self.client.get_movie_file(
3133
3222
  db_entry["movieFile"]["id"]
@@ -3825,8 +3914,29 @@ class Arr:
3825
3914
  ):
3826
3915
  continue
3827
3916
  except PyarrResourceNotFound as e:
3828
- self.logger.error("Connection Error: %s", str(e))
3829
- raise DelayLoopException(length=300, type=self._name)
3917
+ # Queue item not found - this is expected when Arr has already auto-imported
3918
+ # and removed the item, or if it was manually removed. Clean up internal tracking.
3919
+ self.logger.warning(
3920
+ "Queue item %s not found in Arr (likely already imported/removed): %s",
3921
+ id_,
3922
+ str(e),
3923
+ )
3924
+ # Clean up internal tracking data for this queue entry
3925
+ if id_ in self.requeue_cache:
3926
+ # Remove associated media IDs from queue_file_ids
3927
+ media_ids = self.requeue_cache[id_]
3928
+ if isinstance(media_ids, set):
3929
+ self.queue_file_ids.difference_update(media_ids)
3930
+ elif media_ids in self.queue_file_ids:
3931
+ self.queue_file_ids.discard(media_ids)
3932
+ # Remove from requeue_cache
3933
+ del self.requeue_cache[id_]
3934
+ # Remove from cache (downloadId -> queue entry ID mapping)
3935
+ # We need to find and remove the cache entry by value (queue ID)
3936
+ cache_keys_to_remove = [k for k, v in self.cache.items() if v == id_]
3937
+ for key in cache_keys_to_remove:
3938
+ del self.cache[key]
3939
+ return None
3830
3940
  return res
3831
3941
 
3832
3942
  def file_is_probeable(self, file: pathlib.Path) -> bool:
@@ -5978,7 +6088,11 @@ class Arr:
5978
6088
  class Meta:
5979
6089
  database = self.db
5980
6090
 
5981
- self.db.create_tables([Files, Queue, PersistingQueue, Series])
6091
+ try:
6092
+ self.db.create_tables([Files, Queue, PersistingQueue, Series], safe=True)
6093
+ except Exception as e:
6094
+ self.logger.error("Failed to create database tables for Sonarr: %s", e)
6095
+ raise
5982
6096
  self.series_file_model = Series
5983
6097
  self.artists_file_model = None
5984
6098
  elif db3 and self.type == "lidarr":
@@ -5987,12 +6101,20 @@ class Arr:
5987
6101
  class Meta:
5988
6102
  database = self.db
5989
6103
 
5990
- self.db.create_tables([Files, Queue, PersistingQueue, Artists, Tracks])
6104
+ try:
6105
+ self.db.create_tables([Files, Queue, PersistingQueue, Artists, Tracks], safe=True)
6106
+ except Exception as e:
6107
+ self.logger.error("Failed to create database tables for Lidarr: %s", e)
6108
+ raise
5991
6109
  self.artists_file_model = Artists
5992
6110
  self.series_file_model = None # Lidarr uses artists, not series
5993
6111
  else:
5994
6112
  # Radarr or any type without db3/db4 (series/artists/tracks models)
5995
- self.db.create_tables([Files, Queue, PersistingQueue])
6113
+ try:
6114
+ self.db.create_tables([Files, Queue, PersistingQueue], safe=True)
6115
+ except Exception as e:
6116
+ self.logger.error("Failed to create database tables for Radarr: %s", e)
6117
+ raise
5996
6118
  self.artists_file_model = None
5997
6119
  self.series_file_model = None
5998
6120
 
@@ -1,5 +1,5 @@
1
- version = "5.5.2"
2
- git_hash = "01d27209"
1
+ version = "5.5.3"
2
+ git_hash = "0af67a21"
3
3
  license_text = (
4
4
  "Licence can be found on:\n\nhttps://github.com/Feramance/qBitrr/blob/master/LICENSE"
5
5
  )
@@ -476,6 +476,18 @@ class qBitManager:
476
476
  )
477
477
  for proc in list(self.child_processes):
478
478
  try:
479
+ # Check if process has already been started
480
+ if proc.is_alive() or proc.exitcode is not None:
481
+ meta = self._process_registry.get(proc, {})
482
+ self.logger.warning(
483
+ "Skipping start of already-started %s worker for category '%s' (alive=%s, exitcode=%s)",
484
+ meta.get("role", "worker"),
485
+ meta.get("category", "unknown"),
486
+ proc.is_alive(),
487
+ proc.exitcode,
488
+ )
489
+ continue
490
+
479
491
  proc.start()
480
492
  meta = self._process_registry.get(proc, {})
481
493
  self.logger.debug(
@@ -0,0 +1,2 @@
1
+ import{u as qe,f as es,h as ss,i as as,k as Ve,l as ts,j as e,I as Je,R as Ge,m as Ue}from"./app.js";import{r as a,u as Le,f as pe,g as Pe,a as ns,b as cs,c as ds}from"./table.js";import{u as Ie}from"./useInterval.js";import"./vendor.js";const Ye=50,Se=50,Be=500;function gs({loading:d,rows:c,total:m,page:f,totalPages:R,onPageChange:M,onRefresh:L,lastUpdated:I,sort:p,onSort:P,summary:i}){const g=a.useMemo(()=>[{accessorKey:"__instance",header:"Instance",size:150},{accessorKey:"title",header:"Title",cell:o=>o.getValue()},{accessorKey:"year",header:"Year",size:80},{accessorKey:"monitored",header:"Monitored",cell:o=>o.getValue()?e.jsx("span",{className:"table-badge",children:"Yes"}):e.jsx("span",{children:"No"}),size:100},{accessorKey:"hasFile",header:"Has File",cell:o=>o.getValue()?e.jsx("span",{className:"table-badge",children:"Yes"}):e.jsx("span",{children:"No"}),size:100},{accessorKey:"reason",header:"Reason",cell:o=>{const v=o.getValue();return v?e.jsx("span",{className:"table-badge table-badge-reason",children:v}):e.jsx("span",{className:"hint",children:"—"})},size:120}],[]),n=Le({data:c,columns:g,getCoreRowModel:Pe()});return e.jsxs("div",{className:"stack animate-fade-in",children:[e.jsxs("div",{className:"row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{className:"hint",children:["Aggregated movies across all instances"," ",I?`(updated ${I})`:"",e.jsx("br",{}),e.jsx("strong",{children:"Available:"})," ",i.available.toLocaleString(void 0,{maximumFractionDigits:0})," •"," ",e.jsx("strong",{children:"Monitored:"})," ",i.monitored.toLocaleString(void 0,{maximumFractionDigits:0})," •"," ",e.jsx("strong",{children:"Missing:"})," ",i.missing.toLocaleString(void 0,{maximumFractionDigits:0})," •"," ",e.jsx("strong",{children:"Total:"})," ",i.total.toLocaleString(void 0,{maximumFractionDigits:0})]}),e.jsxs("button",{className:"btn ghost",onClick:L,disabled:d,children:[e.jsx(Je,{src:Ge}),"Refresh"]})]}),d?e.jsxs("div",{className:"loading",children:[e.jsx("span",{className:"spinner"})," Loading Radarr library…"]}):m?e.jsx("div",{className:"table-wrapper",children:e.jsxs("table",{className:"responsive-table",children:[e.jsx("thead",{children:n.getHeaderGroups().map(o=>e.jsx("tr",{children:o.headers.map(v=>e.jsx("th",{className:v.column.getCanSort()?"sortable":"",onClick:()=>{const h=v.id;P(h)},children:v.isPlaceholder?null:pe(v.column.columnDef.header,v.getContext())},v.id))},o.id))}),e.jsx("tbody",{children:n.getRowModel().rows.map(o=>{const v=o.original,h=`${v.__instance}-${v.title}-${v.year}`;return e.jsx("tr",{children:o.getVisibleCells().map(x=>e.jsx("td",{children:pe(x.column.columnDef.cell,x.getContext())},x.id))},h)})})]})}):e.jsx("div",{className:"hint",children:"No movies found."}),m>0&&e.jsxs("div",{className:"pagination",children:[e.jsxs("div",{children:["Page ",f+1," of ",R," (",m.toLocaleString()," items · page size"," ",Se,")"]}),e.jsxs("div",{className:"inline",children:[e.jsx("button",{className:"btn",onClick:()=>M(Math.max(0,f-1)),disabled:f===0||d,children:"Prev"}),e.jsx("button",{className:"btn",onClick:()=>M(Math.min(R-1,f+1)),disabled:f>=R-1||d,children:"Next"})]})]})]})}function hs({loading:d,data:c,page:m,totalPages:f,pageSize:R,allMovies:M,onlyMissing:L,reasonFilter:I,onPageChange:p,onRestart:P,lastUpdated:i}){const g=a.useMemo(()=>{let h=M;return L&&(h=h.filter(x=>!x.hasFile)),h},[M,L]),n=a.useMemo(()=>I==="all"?g:I==="none"?g.filter(h=>!h.reason):g.filter(h=>h.reason===I),[g,I]),o=a.useMemo(()=>[{accessorKey:"title",header:"Title",cell:h=>h.getValue()},{accessorKey:"year",header:"Year",size:80},{accessorKey:"monitored",header:"Monitored",cell:h=>h.getValue()?e.jsx("span",{className:"table-badge",children:"Yes"}):e.jsx("span",{children:"No"}),size:100},{accessorKey:"hasFile",header:"Has File",cell:h=>h.getValue()?e.jsx("span",{className:"table-badge",children:"Yes"}):e.jsx("span",{children:"No"}),size:100},{accessorKey:"reason",header:"Reason",cell:h=>{const x=h.getValue();return x?e.jsx("span",{className:"table-badge table-badge-reason",children:x}):e.jsx("span",{className:"hint",children:"—"})},size:120}],[]),v=Le({data:n.slice(m*R,m*R+R),columns:o,getCoreRowModel:Pe(),getSortedRowModel:ns()});return e.jsxs("div",{className:"stack animate-fade-in",children:[e.jsxs("div",{className:"row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{className:"hint",children:[c?.counts?`Available: ${c.counts.available??0} • Monitored: ${c.counts.monitored??0}`:"",i?` (updated ${i})`:""]}),e.jsxs("button",{className:"btn ghost",onClick:P,disabled:d,children:[e.jsx(Je,{src:Ge}),"Restart"]})]}),d?e.jsxs("div",{className:"loading",children:[e.jsx("span",{className:"spinner"})," Loading…"]}):M.length?e.jsx("div",{className:"table-wrapper",children:e.jsxs("table",{className:"responsive-table",children:[e.jsx("thead",{children:v.getHeaderGroups().map(h=>e.jsx("tr",{children:h.headers.map(x=>e.jsx("th",{children:x.isPlaceholder?null:pe(x.column.columnDef.header,x.getContext())},x.id))},h.id))}),e.jsx("tbody",{children:v.getRowModel().rows.map(h=>{const x=h.original,G=`${x.title}-${x.year}`;return e.jsx("tr",{children:h.getVisibleCells().map(W=>e.jsx("td",{children:pe(W.column.columnDef.cell,W.getContext())},W.id))},G)})})]})}):e.jsx("div",{className:"hint",children:"No movies found."}),n.length>R&&e.jsxs("div",{className:"pagination",children:[e.jsxs("div",{children:["Page ",m+1," of ",f," (",n.length.toLocaleString()," items · page size"," ",R,")"]}),e.jsxs("div",{className:"inline",children:[e.jsx("button",{className:"btn",onClick:()=>p(Math.max(0,m-1)),disabled:m===0||d,children:"Prev"}),e.jsx("button",{className:"btn",onClick:()=>p(Math.min(f-1,m+1)),disabled:m>=f-1||d,children:"Next"})]})]})]})}function us({active:d}){const{push:c}=qe(),{value:m,setValue:f,register:R,clearHandler:M}=es(),{liveArr:L,setLiveArr:I}=ss(),[p,P]=a.useState([]),[i,g]=a.useState("aggregate"),[n,o]=a.useState(null),[v,h]=a.useState(0),[x,G]=a.useState(""),[W,ie]=a.useState(!1),[re,X]=a.useState(null),[V,se]=a.useState({}),[l,E]=a.useState(Ye),[y,_]=a.useState(1),D=a.useRef(""),K=a.useRef({}),Y=a.useRef(m),H=a.useRef(!1),[le,ue]=a.useState([]),[ye,oe]=a.useState(!1),[ce,me]=a.useState(0),[q,ve]=a.useState(""),[Re,Ne]=a.useState(null),[ee,xe]=a.useState({key:"__instance",direction:"asc"}),[ae,_e]=a.useState(!1),[te,O]=a.useState("all"),[ke,Q]=a.useState({available:0,monitored:0,missing:0,total:0}),Me=a.useCallback(async()=>{try{const t=await as();t.ready===!1&&!H.current?(H.current=!0,c("Radarr backend is still initialising. Check the logs if this persists.","info")):t.ready&&(H.current=!0);const s=(t.arr||[]).filter(r=>r.type==="radarr");if(P(s),!s.length){g("aggregate"),o(null),ue([]),Q({available:0,monitored:0,missing:0,total:0});return}i===""?g("aggregate"):i!=="aggregate"&&!s.some(r=>r.category===i)&&g(s[0].category)}catch(t){c(t instanceof Error?t.message:"Unable to load Radarr instances","error")}},[c,i]),Ae=a.useCallback(async(t,s,r,u,N)=>{if(u.length)try{const $=[];for(const j of u){const b=await Ve(t,j,r,s),S=b.page??j;if($.push({page:S,movies:b.movies??[]}),D.current!==N)return}if(D.current!==N)return;se(j=>{const b={...j};let S=!1;for(const{page:C,movies:F}of $){const w=j[C]??[];JSON.stringify(w)!==JSON.stringify(F)&&(b[C]=F,S=!0)}return K.current=b,S?b:j})}catch($){c($ instanceof Error?$.message:`Failed to load additional pages for ${t}`,"error")}},[c]),J=a.useCallback(async(t,s,r,u={})=>{const N=u.preloadAll!==!1;(u.showLoading??!0)&&ie(!0);try{const j=`${t}::${r}`,b=D.current!==j;b&&(D.current=j,se(()=>(K.current={},{})));const S=await Ve(t,s,Ye,r);o(S);const C=S.page??s;h(C),G(r);const F=S.page_size??Ye,w=S.total??(S.movies??[]).length,T=Math.max(1,Math.ceil((w||0)/F));E(F),_(T);const k=S.movies??[],ge=b?{}:K.current,he=K.current[C]??[],je=JSON.stringify(he)!==JSON.stringify(k);if((b||je)&&(se(z=>{const we={...b?{}:z,[C]:k};return K.current=we,we}),X(new Date().toLocaleTimeString())),N){const z=[];for(let B=0;B<T;B+=1)B!==C&&(ge[B]||z.push(B));Ae(t,r,F,z,j)}}catch(j){c(j instanceof Error?j.message:`Failed to load ${t} movies`,"error")}finally{ie(!1)}},[c,Ae]),de=a.useCallback(async()=>{if(!p.length){ue([]),Q({available:0,monitored:0,missing:0,total:0});return}oe(!0);try{const t=[];let s=0,r=0;for(const N of p){let $=0,j=!1;const b=N.name||N.category;for(;$<100;){const S=await Ve(N.category,$,Be,"");if(!j){const F=S.counts;F&&(s+=F.available??0,r+=F.monitored??0),j=!0}const C=S.movies??[];if(C.forEach(F=>{t.push({...F,__instance:b})}),!C.length||C.length<Be)break;$+=1}}ue(N=>{const $=JSON.stringify(N),j=JSON.stringify(t);return $===j?N:t});const u={available:s,monitored:r,missing:t.length-s,total:t.length};Q(N=>N.available===u.available&&N.monitored===u.monitored&&N.missing===u.missing&&N.total===u.total?N:u),q!==m&&(me(0),ve(m)),Ne(new Date().toLocaleTimeString())}catch(t){ue([]),Q({available:0,monitored:0,missing:0,total:0}),c(t instanceof Error?t.message:"Failed to load aggregated Radarr data","error")}finally{oe(!1)}},[p,m,c]);a.useEffect(()=>{d&&Me()},[d,Me]),a.useEffect(()=>{if(!d||!i||i==="aggregate")return;K.current={},se({}),_(1),h(0);const t=Y.current;J(i,0,t,{preloadAll:!0,showLoading:!0})},[d,i,J]),a.useEffect(()=>{d&&i==="aggregate"&&de()},[d,i,de]),Ie(()=>{i==="aggregate"&&L&&de()},i==="aggregate"&&L?1e4:null),a.useEffect(()=>{if(!d)return;const t=s=>{i==="aggregate"?(ve(s),me(0)):i&&(h(0),J(i,0,s,{preloadAll:!0,showLoading:!0}))};return R(t),()=>{M(t)}},[d,i,R,M,J]),Ie(()=>{if(i&&i!=="aggregate"){if(Y.current?.trim?.()||"")return;J(i,v,x,{preloadAll:!1,showLoading:!1})}},d&&i&&i!=="aggregate"&&L?1e3:null),a.useEffect(()=>{Y.current=m},[m]),a.useEffect(()=>{i==="aggregate"&&ve(m)},[i,m]);const Z=a.useMemo(()=>{let t=le;if(q){const s=q.toLowerCase();t=t.filter(r=>{const u=(r.title??"").toString().toLowerCase(),N=(r.__instance??"").toLowerCase();return u.includes(s)||N.includes(s)})}return ae&&(t=t.filter(s=>!s.hasFile)),te!=="all"&&(te==="none"?t=t.filter(s=>!s.reason):t=t.filter(s=>s.reason===te)),t},[le,q,ae,te]),ne=a.useMemo(()=>{const t=[...Z],s=(r,u)=>{switch(u){case"__instance":return(r.__instance||"").toLowerCase();case"title":return(r.title||"").toLowerCase();case"year":return r.year??0;case"monitored":return r.monitored?1:0;case"hasFile":return r.hasFile?1:0;default:return""}};return t.sort((r,u)=>{const N=s(r,ee.key),$=s(u,ee.key);let j=0;return typeof N=="number"&&typeof $=="number"?j=N-$:typeof N=="string"&&typeof $=="string"?j=N.localeCompare($):j=String(N).localeCompare(String($)),ee.direction==="asc"?j:-j}),t},[Z,ee]),We=Math.max(1,Math.ceil(ne.length/Se)),fe=ne.slice(ce*Se,ce*Se+Se),De=a.useMemo(()=>{const t=Object.keys(V).map(Number).sort((r,u)=>r-u),s=[];return t.forEach(r=>{V[r]&&s.push(...V[r])}),s},[V]),Ke=a.useCallback(async()=>{if(!(!i||i==="aggregate"))try{await ts(i),c(`Restarted ${i}`,"success")}catch(t){c(t instanceof Error?t.message:`Failed to restart ${i}`,"error")}},[i,c]),Te=a.useCallback(t=>{const s=t.target.value||"aggregate";g(s),s!=="aggregate"&&f("")},[g,f]),Ce=i==="aggregate";return e.jsxs("section",{className:"card",children:[e.jsx("div",{className:"card-header",children:"Radarr"}),e.jsx("div",{className:"card-body",children:e.jsxs("div",{className:"split",children:[e.jsxs("aside",{className:"pane sidebar",children:[e.jsx("button",{className:`btn ${Ce?"active":""}`,onClick:()=>g("aggregate"),children:"All Radarr"}),p.map(t=>e.jsx("button",{className:`btn ghost ${i===t.category?"active":""}`,onClick:()=>{g(t.category),f("")},children:t.name||t.category},t.category))]}),e.jsxs("div",{className:"pane",children:[e.jsxs("div",{className:"field mobile-instance-select",children:[e.jsx("label",{children:"Instance"}),e.jsxs("select",{value:i||"aggregate",onChange:Te,disabled:!p.length,children:[e.jsx("option",{value:"aggregate",children:"All Radarr"}),p.map(t=>e.jsx("option",{value:t.category,children:t.name||t.category},t.category))]})]}),e.jsxs("div",{className:"row",style:{alignItems:"flex-end",gap:"12px",flexWrap:"wrap"},children:[e.jsxs("div",{className:"col field",style:{flex:"1 1 200px"},children:[e.jsx("label",{children:"Search"}),e.jsx("input",{placeholder:"Filter movies",value:m,onChange:t=>f(t.target.value)})]}),e.jsxs("div",{className:"field",style:{flex:"0 0 auto",minWidth:"140px"},children:[e.jsx("label",{children:"Status"}),e.jsxs("select",{onChange:t=>{const s=t.target.value;_e(s==="missing")},value:ae?"missing":"all",children:[e.jsx("option",{value:"all",children:"All Movies"}),e.jsx("option",{value:"missing",children:"Missing Only"})]})]}),e.jsxs("div",{className:"field",style:{flex:"0 0 auto",minWidth:"140px"},children:[e.jsx("label",{children:"Search Reason"}),e.jsxs("select",{onChange:t=>O(t.target.value),value:te,children:[e.jsx("option",{value:"all",children:"All Reasons"}),e.jsx("option",{value:"none",children:"Not Being Searched"}),e.jsx("option",{value:"Missing",children:"Missing"}),e.jsx("option",{value:"Quality",children:"Quality"}),e.jsx("option",{value:"CustomFormat",children:"Custom Format"}),e.jsx("option",{value:"Upgrade",children:"Upgrade"}),e.jsx("option",{value:"Scheduled search",children:"Scheduled Search"})]})]})]}),Ce?e.jsx(gs,{loading:ye,rows:fe,total:ne.length,page:ce,totalPages:We,onPageChange:me,onRefresh:()=>void de(),lastUpdated:Re,sort:ee,onSort:t=>xe(s=>s.key===t?{key:t,direction:s.direction==="asc"?"desc":"asc"}:{key:t,direction:"asc"}),summary:ke}):e.jsx(hs,{loading:W,data:n,page:v,totalPages:y,pageSize:l,allMovies:De,onlyMissing:ae,reasonFilter:te,onPageChange:t=>{h(t),J(i,t,x,{preloadAll:!0})},onRestart:()=>void Ke(),lastUpdated:re})]})]})})]})}const He=25,Fe=50,Xe=200;function ms(d,c){if(!c)return d;const m=[];for(const f of d){const R=f.seasons??{},M={};for(const[L,I]of Object.entries(R)){const p=(I.episodes??[]).filter(P=>!P.hasFile);p.length&&(M[L]={...I,episodes:p})}Object.keys(M).length!==0&&m.push({...f,seasons:M})}return m}function Ee(d,c){return JSON.stringify(ms(d,c))}function xs({active:d}){const{push:c}=qe(),{value:m,setValue:f,register:R,clearHandler:M}=es(),{liveArr:L,setLiveArr:I,groupSonarr:p,setGroupSonarr:P}=ss(),[i,g]=a.useState([]),[n,o]=a.useState("aggregate"),[v,h]=a.useState(null),[x,G]=a.useState(0),[W,ie]=a.useState(""),[re,X]=a.useState(!1),[V,se]=a.useState(null),[l,E]=a.useState({}),y=a.useRef({}),_=a.useRef(null),D=a.useRef(""),[K,Y]=a.useState(He),[H,le]=a.useState(1),[ue,ye]=a.useState(0),oe=a.useRef(m),ce=a.useRef(!1),[me,q]=a.useState([]),[ve,Re]=a.useState(!1),[Ne,ee]=a.useState(0),[xe,ae]=a.useState(""),[_e,te]=a.useState(null),[O,ke]=a.useState(!1),[Q,Me]=a.useState("all"),[Ae,J]=a.useState({available:0,monitored:0,missing:0,total:0}),de=a.useCallback(async()=>{try{const s=await as();s.ready===!1&&!ce.current?(ce.current=!0,c("Sonarr backend is still initialising. Check the logs if this persists.","info")):s.ready&&(ce.current=!0);const r=(s.arr||[]).filter(u=>u.type==="sonarr");if(g(r),!r.length){o("aggregate"),h(null),q([]),J({available:0,monitored:0,missing:0,total:0});return}n===""?o("aggregate"):n!=="aggregate"&&!r.some(u=>u.category===n)&&o(r[0].category)}catch(s){c(s instanceof Error?s.message:"Unable to load Sonarr instances","error")}},[c,n]),Z=a.useCallback(async(s,r,u,N={})=>{const{preloadAll:$=!0,showLoading:j=!0,missingOnly:b}=N,S=b??O;j&&X(!0);try{const C=`${s}::${u}::${S?"missing":"all"}`,F=D.current!==C;F&&(D.current=C,E(()=>(y.current={},{})),ye(0),le(1));const w=await Ue(s,r,He,u,{missingOnly:S}),T=w.page??r,k=w.page_size??He,ge=w.total??(w.series??[]).length,he=Math.max(1,Math.ceil((ge||0)/k)),je=w.series??[],z=F?{}:y.current,B={...z,[T]:je},we=Ee(z[T]??[],S),is=Ee(je,S),Oe=F||we!==is;if(y.current=B,Oe&&E(B),h(A=>{const U=A?.counts??null,be=w.counts??null;return!A||A.total!==w.total||A.page!==w.page||A.page_size!==w.page_size||(U?.available??null)!==(be?.available??null)||(U?.monitored??null)!==(be?.monitored??null)||(U?.missing??null)!==(be?.missing??null)||Oe?(_.current=w,w):A}),G(A=>A===T?A:T),ie(A=>A===u?A:u),Y(A=>A===k?A:k),le(A=>A===he?A:he),ye(A=>A===ge?A:ge),Oe&&se(new Date().toLocaleTimeString()),$){const A=[];for(let U=0;U<he;U+=1)U!==T&&(B[U]||A.push(U));for(const U of A)try{const be=await Ue(s,U,k,u,{missingOnly:S});if(D.current!==C)break;const $e=be.page??U,ze=be.series??[],Qe=y.current,rs=Ee(Qe[$e]??[],S),ls=Ee(ze,S);if(rs===ls){y.current={...Qe,[$e]:ze};continue}E(os=>{const Ze={...os,[$e]:ze};return y.current=Ze,Ze})}catch{break}}}catch(C){c(C instanceof Error?C.message:`Failed to load ${s} series`,"error")}finally{j&&X(!1)}},[c,O]),ne=a.useCallback(async()=>{if(!i.length){q([]),J({available:0,monitored:0,missing:0,total:0});return}console.log(`[Sonarr Aggregate] Starting aggregation for ${i.length} instances`),Re(!0);try{const s=[];let r=0,u=0,N=0;for(const b of i){let S=0,C=!1;const F=b.name||b.category;for(console.log(`[Sonarr Aggregate] Processing instance: ${F}`);S<200;){const w=await Ue(b.category,S,Xe,"",{missingOnly:O});if(console.log(`[Sonarr Aggregate] Response for ${F} page ${S}:`,{total:w.total,page:w.page,page_size:w.page_size,series_count:w.series?.length??0,counts:w.counts}),!C){const k=w.counts;k&&(r+=k.available??0,u+=k.monitored??0,N+=k.missing??0),C=!0}const T=w.series??[];if(console.log(`[Sonarr Aggregate] Instance: ${F}, Page: ${S}, Series count: ${T.length}, Total episodes so far: ${s.length}`),T.forEach(k=>{const ge=k.series?.title||"";Object.entries(k.seasons??{}).forEach(([he,je])=>{(je.episodes??[]).forEach(z=>{s.push({__instance:F,series:ge,season:he,episode:z.episodeNumber??"",title:z.title??"",monitored:!!z.monitored,hasFile:!!z.hasFile,airDate:z.airDateUtc??""})})})}),!T.length||T.length<Xe){console.log(`[Sonarr Aggregate] Breaking pagination for ${F} - series.length=${T.length}`);break}S+=1}}const $=new Set(s.map(b=>`${b.__instance}::${b.series}`)).size;console.log("[Sonarr Aggregate] Aggregation complete:",{totalEpisodes:s.length,uniqueSeries:$,instances:i.length}),q(b=>{const S=JSON.stringify(b),C=JSON.stringify(s);return S===C?(console.log("[Sonarr Aggregate] Data unchanged, skipping update"),b):(console.log(`[Sonarr Aggregate] Data changed, updating from ${b.length} to ${s.length} episodes`),s)});const j={available:r,monitored:u,missing:N,total:s.length};J(b=>b.available===j.available&&b.monitored===j.monitored&&b.missing===j.missing&&b.total===j.total?b:j),xe!==m&&(ee(0),ae(m)),te(new Date().toLocaleTimeString())}catch(s){q([]),J({available:0,monitored:0,missing:0,total:0}),c(s instanceof Error?s.message:"Failed to load aggregated Sonarr data","error")}finally{Re(!1)}},[i,m,c,O]);a.useEffect(()=>{d&&de()},[d,de]),a.useEffect(()=>{if(!d||!n||n==="aggregate")return;G(0);const s=oe.current;Z(n,0,s,{preloadAll:!0,showLoading:!0,missingOnly:O})},[d,n,Z]),a.useEffect(()=>{d&&n==="aggregate"&&ne()},[d,n,ne]),Ie(()=>{n==="aggregate"&&L&&ne()},n==="aggregate"&&L?1e4:null),a.useEffect(()=>{if(!d)return;const s=r=>{n==="aggregate"?(ae(r),ee(0)):n&&(G(0),Z(n,0,r,{preloadAll:!0,showLoading:!0,missingOnly:O}))};return R(s),()=>M(s)},[d,n,R,M,Z,O]),Ie(()=>{if(n&&n!=="aggregate"){if(oe.current?.trim?.()||"")return;Z(n,x,W,{preloadAll:!1,showLoading:!1,missingOnly:O})}},d&&n&&n!=="aggregate"&&L?1e3:null),a.useEffect(()=>{oe.current=m},[m]),a.useEffect(()=>{n==="aggregate"&&ae(m)},[n,m]);const fe=a.useMemo(()=>{let s=me;if(xe){const r=xe.toLowerCase();s=s.filter(u=>u.series.toLowerCase().includes(r)||u.title.toLowerCase().includes(r)||u.__instance.toLowerCase().includes(r))}return Q!=="all"&&(Q==="none"?s=s.filter(r=>!r.reason):s=s.filter(r=>r.reason===Q)),s},[me,xe,Q]),De=Math.max(1,Math.ceil(fe.length/Fe));fe.slice(Ne*Fe,Ne*Fe+Fe);const Ke=l[x]??[],Te=a.useCallback(async()=>{if(!(!n||n==="aggregate"))try{await ts(n),c(`Restarted ${n}`,"success")}catch(s){c(s instanceof Error?s.message:`Failed to restart ${n}`,"error")}},[n,c]),Ce=a.useCallback(s=>{const r=s.target.value||"aggregate";o(r),r!=="aggregate"&&f("")},[o,f]),t=n==="aggregate";return e.jsxs("section",{className:"card",children:[e.jsx("div",{className:"card-header",children:"Sonarr"}),e.jsx("div",{className:"card-body",children:e.jsxs("div",{className:"split",children:[e.jsxs("aside",{className:"pane sidebar",children:[e.jsx("button",{className:`btn ${t?"active":""}`,onClick:()=>o("aggregate"),children:"All Sonarr"}),i.map(s=>e.jsx("button",{className:`btn ghost ${n===s.category?"active":""}`,onClick:()=>{o(s.category),f("")},children:s.name||s.category},s.category))]}),e.jsxs("div",{className:"pane",children:[e.jsxs("div",{className:"field mobile-instance-select",children:[e.jsx("label",{children:"Instance"}),e.jsxs("select",{value:n||"aggregate",onChange:Ce,disabled:!i.length,children:[e.jsx("option",{value:"aggregate",children:"All Sonarr"}),i.map(s=>e.jsx("option",{value:s.category,children:s.name||s.category},s.category))]})]}),e.jsxs("div",{className:"row",style:{alignItems:"flex-end",gap:"12px",flexWrap:"wrap"},children:[e.jsxs("div",{className:"col field",style:{flex:"1 1 200px"},children:[e.jsx("label",{children:"Search"}),e.jsx("input",{placeholder:"Filter series or episodes",value:m,onChange:s=>f(s.target.value)})]}),e.jsxs("div",{className:"field",style:{flex:"0 0 auto",minWidth:"140px"},children:[e.jsx("label",{children:"Status"}),e.jsxs("select",{onChange:s=>{const u=s.target.value==="missing";ke(u),n&&n!=="aggregate"&&Z(n,0,oe.current||"",{preloadAll:!0,showLoading:!0,missingOnly:u})},value:O?"missing":"all",children:[e.jsx("option",{value:"all",children:"All Episodes"}),e.jsx("option",{value:"missing",children:"Missing Only"})]})]}),e.jsxs("div",{className:"field",style:{flex:"0 0 auto",minWidth:"140px"},children:[e.jsx("label",{children:"Search Reason"}),e.jsxs("select",{onChange:s=>Me(s.target.value),value:Q,children:[e.jsx("option",{value:"all",children:"All Reasons"}),e.jsx("option",{value:"none",children:"Not Being Searched"}),e.jsx("option",{value:"Missing",children:"Missing"}),e.jsx("option",{value:"Quality",children:"Quality"}),e.jsx("option",{value:"CustomFormat",children:"Custom Format"}),e.jsx("option",{value:"Upgrade",children:"Upgrade"}),e.jsx("option",{value:"Scheduled search",children:"Scheduled Search"})]})]})]}),t?e.jsx(fs,{loading:ve,rows:fe,total:fe.length,page:Ne,totalPages:De,onPageChange:ee,onRefresh:()=>void ne(),lastUpdated:_e,groupSonarr:p,summary:Ae}):e.jsx(js,{loading:re,counts:v?.counts??null,series:Ke,page:x,pageSize:K,totalPages:H,totalItems:ue,onlyMissing:O,onPageChange:s=>{G(s),Z(n,s,W,{preloadAll:!1,showLoading:!0,missingOnly:O})},onRestart:()=>void Te(),lastUpdated:V,groupSonarr:p})]})]})})]})}function fs({loading:d,rows:c,total:m,page:f,totalPages:R,onPageChange:M,onRefresh:L,lastUpdated:I,groupSonarr:p,summary:P}){const i=a.useMemo(()=>{const l=new Map;c.forEach(y=>{const _=y.__instance,D=y.series,K=String(y.season);l.has(_)||l.set(_,new Map);const Y=l.get(_);Y.has(D)||Y.set(D,new Map);const H=Y.get(D);H.has(K)||H.set(K,[]),H.get(K).push(y)});const E=[];return l.forEach((y,_)=>{y.forEach((D,K)=>{E.push({instance:_,series:K,subRows:Array.from(D.entries()).map(([Y,H])=>({seasonNumber:Y,isSeason:!0,subRows:H.map(le=>({...le,isEpisode:!0}))}))})})}),E},[c]),g=a.useMemo(()=>i.slice(f*50,(f+1)*50),[i,f]),n=a.useMemo(()=>c.slice(f*50,(f+1)*50),[c,f]),o=p?g:n,v=a.useMemo(()=>[{accessorKey:"title",header:"Title",cell:({row:l})=>l.original.isEpisode?l.original.title:l.original.isSeason?`Season ${l.original.seasonNumber}`:l.original.series},{accessorKey:"monitored",header:"Monitored",cell:({row:l})=>{const E=(l.original.isEpisode,l.original.monitored);return e.jsx("span",{className:"table-badge",children:E?"Yes":"No"})}},{accessorKey:"hasFile",header:"Has File",cell:({row:l})=>l.original.isEpisode?e.jsx("span",{className:"table-badge",children:l.original.hasFile?"Yes":"No"}):null},{accessorKey:"airDate",header:"Air Date",cell:({row:l})=>l.original.isEpisode?l.original.airDate||"—":null}],[]),h=a.useMemo(()=>[{accessorKey:"__instance",header:"Instance"},{accessorKey:"series",header:"Series"},{accessorKey:"season",header:"Season"},{accessorKey:"episode",header:"Episode"},{accessorKey:"title",header:"Title"},{accessorKey:"monitored",header:"Monitored",cell:({getValue:l})=>e.jsx("span",{className:"table-badge",children:l()?"Yes":"No"})},{accessorKey:"hasFile",header:"Has File",cell:({getValue:l})=>e.jsx("span",{className:"table-badge",children:l()?"Yes":"No"})},{accessorKey:"airDate",header:"Air Date",cell:({getValue:l})=>l()||"—"}],[]),x=p?v:h,G=Le({data:o,columns:x,getCoreRowModel:Pe(),getExpandedRowModel:cs()}),W=Le({data:o,columns:x,getCoreRowModel:Pe(),getSortedRowModel:ns(),getPaginationRowModel:ds(),state:{pagination:{pageIndex:f,pageSize:50}},manualPagination:!0,pageCount:R}),ie=p?G:W,re=50,X=Math.ceil(p?i.length/re:c.length/re),V=Math.min(f,Math.max(0,X-1)),se=p?`${i.length} series`:c.length.toLocaleString();return e.jsxs("div",{className:"stack animate-fade-in",children:[e.jsxs("div",{className:"row",style:{justifyContent:"space-between"},children:[e.jsxs("div",{className:"hint",children:["Aggregated episodes across all instances"," ",I?`(updated ${I})`:"",e.jsx("br",{}),e.jsx("strong",{children:"Available:"})," ",P.available.toLocaleString(void 0,{maximumFractionDigits:0})," •"," ",e.jsx("strong",{children:"Monitored:"})," ",P.monitored.toLocaleString(void 0,{maximumFractionDigits:0})," •"," ",e.jsx("strong",{children:"Missing:"})," ",P.missing.toLocaleString(void 0,{maximumFractionDigits:0})," •"," ",e.jsx("strong",{children:"Total Episodes:"})," ",P.total.toLocaleString(void 0,{maximumFractionDigits:0})]}),e.jsxs("button",{className:"btn ghost",onClick:L,disabled:d,children:[e.jsx(Je,{src:Ge}),"Refresh"]})]}),d?e.jsxs("div",{className:"loading",children:[e.jsx("span",{className:"spinner"})," Loading Sonarr library…"]}):p?e.jsx("div",{className:"sonarr-hierarchical-view",children:g.map(l=>e.jsxs("details",{className:"series-details",children:[e.jsxs("summary",{className:"series-summary",children:[e.jsx("span",{className:"series-title",children:l.series}),e.jsxs("span",{className:"series-instance",children:["(",l.instance,")"]})]}),e.jsx("div",{className:"series-content",children:l.subRows.map(E=>e.jsxs("details",{className:"season-details",children:[e.jsxs("summary",{className:"season-summary",children:[e.jsxs("span",{className:"season-title",children:["Season ",E.seasonNumber]}),e.jsxs("span",{className:"season-count",children:["(",E.subRows.length," episodes)"]})]}),e.jsx("div",{className:"season-content",children:e.jsx("div",{className:"episodes-table-wrapper",children:e.jsxs("table",{className:"episodes-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Episode"}),e.jsx("th",{children:"Title"}),e.jsx("th",{children:"Monitored"}),e.jsx("th",{children:"Has File"}),e.jsx("th",{children:"Air Date"}),e.jsx("th",{children:"Reason"})]})}),e.jsx("tbody",{children:E.subRows.map(y=>e.jsxs("tr",{children:[e.jsx("td",{children:y.episode}),e.jsx("td",{children:y.title}),e.jsx("td",{children:e.jsx("span",{className:"table-badge",children:y.monitored?"Yes":"No"})}),e.jsx("td",{children:e.jsx("span",{className:"table-badge",children:y.hasFile?"Yes":"No"})}),e.jsx("td",{children:y.airDate||"—"}),e.jsx("td",{children:y.reason?e.jsx("span",{className:"table-badge table-badge-reason",children:y.reason}):e.jsx("span",{className:"hint",children:"—"})})]},`${y.__instance}-${y.series}-${y.season}-${y.episode}`))})]})})})]},`${l.instance}-${l.series}-${E.seasonNumber}`))})]},`${l.instance}-${l.series}`))}):o.length?e.jsx("div",{className:"table-wrapper",children:e.jsxs("table",{className:"responsive-table",children:[e.jsx("thead",{children:e.jsx("tr",{children:ie.getFlatHeaders().map(l=>e.jsxs("th",{className:l.column.getCanSort()?"sortable":"",onClick:l.column.getToggleSortingHandler(),children:[l.isPlaceholder?null:pe(l.column.columnDef.header,l.getContext()),l.column.getCanSort()&&e.jsx("span",{className:"sort-arrow",children:{asc:"▲",desc:"▼"}[l.column.getIsSorted()]??null})]},l.id))})}),e.jsx("tbody",{children:ie.getRowModel().rows.map(l=>{const E=l.original,y=`${E.__instance}-${E.series}-${E.season}-${E.episode}`;return e.jsx("tr",{children:l.getVisibleCells().map(_=>e.jsx("td",{"data-label":_.column.columnDef.header,children:pe(_.column.columnDef.cell,_.getContext())},_.id))},y)})})]})}):e.jsx("div",{className:"hint",children:"No series found."}),o.length>0&&e.jsxs("div",{className:"pagination",children:[e.jsxs("div",{children:["Page ",V+1," of ",X," (",se," items · page size ",re,")"]}),e.jsxs("div",{className:"inline",children:[e.jsx("button",{className:"btn",onClick:()=>M(Math.max(0,V-1)),disabled:V===0||d,children:"Prev"}),e.jsx("button",{className:"btn",onClick:()=>M(Math.min(X-1,V+1)),disabled:V>=X-1||d,children:"Next"})]})]})]})}function js({loading:d,counts:c,series:m,page:f,pageSize:R,totalPages:M,onPageChange:L,groupSonarr:I}){const p=Math.min(f,Math.max(0,M-1)),P=a.useMemo(()=>{const g=[];for(const n of m){const o=n.series?.title||"";Object.entries(n.seasons??{}).forEach(([v,h])=>{(h.episodes??[]).forEach(x=>{g.push({__instance:"Instance",series:o,season:v,episode:x.episodeNumber??"",title:x.title??"",monitored:!!x.monitored,hasFile:!!x.hasFile,airDate:x.airDateUtc??""})})})}return g},[m]),i=a.useMemo(()=>{const g=new Map;return P.forEach(n=>{const o=n.series;g.has(o)||g.set(o,new Map);const v=g.get(o),h=String(n.season);v.has(h)||v.set(h,[]),v.get(h).push(n)}),Array.from(g.entries()).map(([n,o])=>({series:n,subRows:Array.from(o.entries()).map(([v,h])=>({seasonNumber:v,isSeason:!0,subRows:h.map(x=>({...x,isEpisode:!0}))}))}))},[P]);return e.jsxs("div",{className:"stack animate-fade-in",children:[e.jsx("div",{className:"row",style:{justifyContent:"space-between"},children:e.jsx("div",{className:"hint",children:c?e.jsxs(e.Fragment,{children:[e.jsx("strong",{children:"Available:"})," ",c.available.toLocaleString()," •"," ",e.jsx("strong",{children:"Monitored:"})," ",c.monitored.toLocaleString()," •"," ",e.jsx("strong",{children:"Missing:"})," ",c.missing?.toLocaleString()??0]}):"Loading series information..."})}),d?e.jsxs("div",{className:"loading",children:[e.jsx("span",{className:"spinner"})," Loading series…"]}):I?e.jsx("div",{className:"sonarr-hierarchical-view",children:i.map(g=>e.jsxs("details",{className:"series-details",children:[e.jsx("summary",{className:"series-summary",children:e.jsx("span",{className:"series-title",children:g.series})}),e.jsx("div",{className:"series-content",children:g.subRows.map(n=>e.jsxs("details",{className:"season-details",children:[e.jsxs("summary",{className:"season-summary",children:[e.jsxs("span",{className:"season-title",children:["Season ",n.seasonNumber]}),e.jsxs("span",{className:"season-count",children:["(",n.subRows.length," episodes)"]})]}),e.jsx("div",{className:"season-content",children:e.jsx("div",{className:"episodes-table-wrapper",children:e.jsxs("table",{className:"episodes-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Episode"}),e.jsx("th",{children:"Title"}),e.jsx("th",{children:"Monitored"}),e.jsx("th",{children:"Has File"}),e.jsx("th",{children:"Air Date"}),e.jsx("th",{children:"Reason"})]})}),e.jsx("tbody",{children:n.subRows.map(o=>e.jsxs("tr",{children:[e.jsx("td",{children:o.episode}),e.jsx("td",{children:o.title}),e.jsx("td",{children:e.jsx("span",{className:"table-badge",children:o.monitored?"Yes":"No"})}),e.jsx("td",{children:e.jsx("span",{className:"table-badge",children:o.hasFile?"Yes":"No"})}),e.jsx("td",{children:o.airDate||"—"}),e.jsx("td",{children:o.reason?e.jsx("span",{className:"table-badge table-badge-reason",children:o.reason}):e.jsx("span",{className:"hint",children:"—"})})]},`${o.series}-${o.season}-${o.episode}`))})]})})})]},`${g.series}-${n.seasonNumber}`))})]},`${g.series}`))}):P.length?e.jsxs("div",{className:"table-wrapper",children:[e.jsxs("table",{className:"responsive-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Series"}),e.jsx("th",{children:"Season"}),e.jsx("th",{children:"Episode"}),e.jsx("th",{children:"Title"}),e.jsx("th",{children:"Monitored"}),e.jsx("th",{children:"Has File"}),e.jsx("th",{children:"Air Date"}),e.jsx("th",{children:"Reason"})]})}),e.jsx("tbody",{children:P.slice(p*R,p*R+R).map((g,n)=>e.jsxs("tr",{children:[e.jsx("td",{children:g.series}),e.jsx("td",{children:g.season}),e.jsx("td",{children:g.episode}),e.jsx("td",{children:g.title}),e.jsx("td",{children:e.jsx("span",{className:"table-badge",children:g.monitored?"Yes":"No"})}),e.jsx("td",{children:e.jsx("span",{className:"table-badge",children:g.hasFile?"Yes":"No"})}),e.jsx("td",{children:g.airDate||"—"}),e.jsx("td",{children:g.reason?e.jsx("span",{className:"table-badge table-badge-reason",children:g.reason}):e.jsx("span",{className:"hint",children:"—"})})]},`${g.series}-${g.season}-${g.episode}-${n}`))})]}),e.jsxs("div",{className:"pagination",children:[e.jsxs("div",{children:["Page ",p+1," of ",M," (",P.length.toLocaleString()," items · page size ",R,")"]}),e.jsxs("div",{className:"inline",children:[e.jsx("button",{className:"btn",onClick:()=>L(Math.max(0,p-1)),disabled:p===0||d,children:"Prev"}),e.jsx("button",{className:"btn",onClick:()=>L(Math.min(M-1,p+1)),disabled:p>=M-1||d,children:"Next"})]})]})]}):e.jsx("div",{className:"hint",children:"No series found."})]})}function Ss({type:d,active:c}){return d==="radarr"?e.jsx(us,{active:c}):e.jsx(xs,{active:c})}export{Ss as ArrView};
2
+ //# sourceMappingURL=ArrView.js.map