kryten-webqueue 0.9.12__tar.gz → 0.9.13__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 (90) hide show
  1. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/CHANGELOG.md +10 -0
  2. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/config.example.json +3 -0
  4. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/app.py +5 -1
  5. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/config.py +7 -0
  6. kryten_webqueue-0.9.13/kryten_webqueue/playlists/bulk_add.py +52 -0
  7. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/playlists/fire.py +27 -5
  8. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/playlists/importer.py +15 -3
  9. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/playlists/scheduler.py +5 -1
  10. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/admin_playlists.py +3 -0
  11. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/admin_schedules.py +3 -0
  12. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/pyproject.toml +1 -1
  13. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/.github/workflows/python-publish.yml +0 -0
  14. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/.github/workflows/release.yml +0 -0
  15. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/.gitignore +0 -0
  16. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/README.md +0 -0
  17. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/deploy/kryten-webqueue.service +0 -0
  18. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/deploy/nginx-queue.conf +0 -0
  19. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/IMPLEMENTATION_SPEC.md +0 -0
  20. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/IMPL_API_GATE.md +0 -0
  21. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/IMPL_ECONOMY.md +0 -0
  22. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/IMPL_KRYTEN_PY.md +0 -0
  23. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/IMPL_ROBOT.md +0 -0
  24. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/PRE_PLAN_GAPS.md +0 -0
  25. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/PRODUCT_PLAN.md +0 -0
  26. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  27. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/__init__.py +0 -0
  28. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/__main__.py +0 -0
  29. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/api_gate/__init__.py +0 -0
  30. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/api_gate/client.py +0 -0
  31. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/auth/__init__.py +0 -0
  32. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/auth/otp.py +0 -0
  33. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/auth/rate_limit.py +0 -0
  34. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/auth/session.py +0 -0
  35. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/catalog/__init__.py +0 -0
  36. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/catalog/db.py +0 -0
  37. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/catalog/images.py +0 -0
  38. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/catalog/mediacms.py +0 -0
  39. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/catalog/sync.py +0 -0
  40. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/__init__.py +0 -0
  41. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  42. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  43. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  44. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  45. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  46. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  47. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  48. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  49. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/jobs/__init__.py +0 -0
  50. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  51. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/jobs/manager.py +0 -0
  52. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/jobs/tasks.py +0 -0
  53. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/playlists/__init__.py +0 -0
  54. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/queue/__init__.py +0 -0
  55. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/queue/ordering.py +0 -0
  56. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/queue/poller.py +0 -0
  57. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/queue/shadow.py +0 -0
  58. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/__init__.py +0 -0
  59. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/admin_catalog.py +0 -0
  60. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/admin_jobs.py +0 -0
  61. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/admin_queue.py +0 -0
  62. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/auth.py +0 -0
  63. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/catalog.py +0 -0
  64. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/pages.py +0 -0
  65. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/queue.py +0 -0
  66. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/user.py +0 -0
  67. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/static/css/main.css +0 -0
  68. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/static/js/main.js +0 -0
  69. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/index.html +0 -0
  70. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/playlists.html +0 -0
  71. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  72. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/schedules.html +0 -0
  73. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/auth/login.html +0 -0
  74. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/base.html +0 -0
  75. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/catalog/browse.html +0 -0
  76. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  77. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  78. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/queue/index.html +0 -0
  79. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/user/dashboard.html +0 -0
  80. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/ws/__init__.py +0 -0
  81. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/ws/handler.py +0 -0
  82. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/ws/manager.py +0 -0
  83. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/__init__.py +0 -0
  84. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_fetchurls_sharepoint.py +0 -0
  85. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_phase1.py +0 -0
  86. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_phase2_jobs.py +0 -0
  87. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_phase3_jobs.py +0 -0
  88. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_phase4_live_fixes.py +0 -0
  89. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_playlist_import.py +0 -0
  90. {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_queue_announce.py +0 -0
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.13] — 2026-06-12
10
+
11
+ ### Fixed
12
+
13
+ - **Bulk playlist loads no longer fail with spurious `422 Unprocessable Entity`.** When importing a saved playlist into the live queue or firing a scheduled playlist, items were added back-to-back with no pacing. CyTube validates each queued item server-side (fetching the custom MediaCMS manifest), and adding faster than it can validate triggers a transient `queueFail` — surfaced by api-gate as HTTP 422 — even for perfectly valid URLs. The importer and scheduled-fire loops now throttle consecutive adds and retry the transient 422 with a short backoff.
14
+
15
+ ### Added
16
+
17
+ - Config: `playlist_bulk_add_delay_sec` (default `0.5`) — pause between consecutive CyTube adds during bulk loads — and `playlist_bulk_add_max_retries` (default `2`) — retries on a transient 422.
18
+
9
19
  ## [0.9.12] — 2026-06-12
10
20
 
11
21
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kryten-webqueue
3
- Version: 0.9.12
3
+ Version: 0.9.13
4
4
  Summary: Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube
5
5
  Author: grobertson
6
6
  License-Expression: MIT
@@ -32,5 +32,8 @@
32
32
  "pre_fire_lock_minutes_default": 15,
33
33
  "state_poll_interval_sec": 3.0,
34
34
 
35
+ "playlist_bulk_add_delay_sec": 0.5,
36
+ "playlist_bulk_add_max_retries": 2,
37
+
35
38
  "prometheus_port": 28292
36
39
  }
@@ -131,7 +131,11 @@ async def lifespan(app: FastAPI):
131
131
  app.state.rate_limiter = RateLimiter()
132
132
 
133
133
  # Playlist scheduler
134
- scheduler = PlaylistScheduler(db=db, api_gate=api_gate, shadow=shadow, ws_manager=ws_manager)
134
+ scheduler = PlaylistScheduler(
135
+ db=db, api_gate=api_gate, shadow=shadow, ws_manager=ws_manager,
136
+ add_delay_sec=config.playlist_bulk_add_delay_sec,
137
+ add_max_retries=config.playlist_bulk_add_max_retries,
138
+ )
135
139
  await scheduler.start()
136
140
  app.state.scheduler = scheduler
137
141
 
@@ -60,6 +60,13 @@ class Config(BaseModel):
60
60
  pre_fire_lock_minutes_default: int = 15
61
61
  state_poll_interval_sec: float = 3.0
62
62
 
63
+ # Bulk playlist loading (manual import + scheduled fire). CyTube validates
64
+ # each queued item server-side (fetching custom manifests); adding faster
65
+ # than it can validate triggers a transient queueFail (surfaced by api-gate
66
+ # as HTTP 422). Throttle consecutive adds and retry the transient 422.
67
+ playlist_bulk_add_delay_sec: float = 0.5 # pause between consecutive adds
68
+ playlist_bulk_add_max_retries: int = 2 # retries on transient 422
69
+
63
70
  # Monitoring
64
71
  prometheus_port: int = 28292
65
72
 
@@ -0,0 +1,52 @@
1
+ """Throttled, retrying single-item add used by bulk playlist loaders.
2
+
3
+ CyTube validates each queued item server-side (e.g. fetching a custom MediaCMS
4
+ manifest). Adding items faster than CyTube can validate them produces a
5
+ transient ``queueFail`` — which api-gate surfaces as HTTP 422 on
6
+ ``/playlist/add``. These rejections are not about a bad URL; spacing the calls
7
+ out (and retrying the 422 a couple of times) lets the queue settle.
8
+ """
9
+
10
+ import asyncio
11
+ import logging
12
+
13
+ import httpx
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ async def add_item_throttled(
19
+ api_gate,
20
+ *,
21
+ media_type: str,
22
+ media_id: str,
23
+ position: str = "end",
24
+ max_retries: int = 0,
25
+ retry_delay_sec: float = 0.5,
26
+ ) -> dict:
27
+ """Add one item to the live queue, retrying transient CyTube rejections.
28
+
29
+ A 422 (CyTube ``queueFail``) is retried up to ``max_retries`` times with a
30
+ linear backoff (``retry_delay_sec`` × attempt). Any other HTTP error is not
31
+ retried. Returns the api-gate result dict; re-raises the final exception
32
+ when retries are exhausted so callers can count/log the failure.
33
+ """
34
+ attempt = 0
35
+ while True:
36
+ try:
37
+ return await api_gate.playlist_add(
38
+ media_type=media_type,
39
+ media_id=media_id,
40
+ position=position,
41
+ )
42
+ except httpx.HTTPStatusError as e:
43
+ is_transient = e.response is not None and e.response.status_code == 422
44
+ if not is_transient or attempt >= max_retries:
45
+ raise
46
+ attempt += 1
47
+ backoff = retry_delay_sec * attempt
48
+ logger.info(
49
+ "CyTube rejected %s (422 queueFail); retry %d/%d after %.1fs",
50
+ media_id, attempt, max_retries, backoff,
51
+ )
52
+ await asyncio.sleep(backoff)
@@ -3,13 +3,23 @@ import logging
3
3
  from datetime import datetime, timedelta, UTC
4
4
 
5
5
  from ..queue.ordering import refund_item
6
+ from .bulk_add import add_item_throttled
6
7
 
7
8
  logger = logging.getLogger(__name__)
8
9
 
9
10
  _queue_lock = asyncio.Lock()
10
11
 
11
12
 
12
- async def fire_schedule(*, schedule_id: int, api_gate, db, shadow, ws_manager):
13
+ async def fire_schedule(
14
+ *,
15
+ schedule_id: int,
16
+ api_gate,
17
+ db,
18
+ shadow,
19
+ ws_manager,
20
+ add_delay_sec: float = 0.0,
21
+ add_max_retries: int = 0,
22
+ ):
13
23
  """Fire a scheduled playlist: clear queue, refund displaced pay items, load playlist."""
14
24
  async with _queue_lock:
15
25
  schedule = await db.get_schedule(schedule_id)
@@ -35,12 +45,19 @@ async def fire_schedule(*, schedule_id: int, api_gate, db, shadow, ws_manager):
35
45
  items = await db.get_saved_playlist_items(playlist_id)
36
46
  total_duration = 0
37
47
  last_item_uid = None
38
- for item in items:
48
+ for index, item in enumerate(items):
49
+ # Throttle consecutive adds so CyTube can validate each item before
50
+ # the next arrives (avoids transient queueFail/422 under load).
51
+ if index and add_delay_sec:
52
+ await asyncio.sleep(add_delay_sec)
39
53
  try:
40
- add_result = await api_gate.playlist_add(
54
+ add_result = await add_item_throttled(
55
+ api_gate,
41
56
  media_type=item["media_type"],
42
57
  media_id=item["media_id"],
43
58
  position="end",
59
+ max_retries=add_max_retries,
60
+ retry_delay_sec=add_delay_sec or 0.5,
44
61
  )
45
62
  if isinstance(add_result, dict) and add_result.get("uid") is not None:
46
63
  last_item_uid = add_result["uid"]
@@ -56,12 +73,17 @@ async def fire_schedule(*, schedule_id: int, api_gate, db, shadow, ws_manager):
56
73
  fallback_id = schedule.get("fallback_playlist_id")
57
74
  if fallback_id:
58
75
  fallback_items = await db.get_saved_playlist_items(fallback_id)
59
- for item in fallback_items:
76
+ for index, item in enumerate(fallback_items):
77
+ if index and add_delay_sec:
78
+ await asyncio.sleep(add_delay_sec)
60
79
  try:
61
- await api_gate.playlist_add(
80
+ await add_item_throttled(
81
+ api_gate,
62
82
  media_type=item["media_type"],
63
83
  media_id=item["media_id"],
64
84
  position="end",
85
+ max_retries=add_max_retries,
86
+ retry_delay_sec=add_delay_sec or 0.5,
65
87
  )
66
88
  except Exception as e:
67
89
  logger.warning(f"Schedule fire: failed to add fallback {item['media_id']}: {e}")
@@ -1,6 +1,9 @@
1
+ import asyncio
1
2
  import logging
2
3
  import re
3
4
 
5
+ from .bulk_add import add_item_throttled
6
+
4
7
  logger = logging.getLogger(__name__)
5
8
 
6
9
 
@@ -59,10 +62,12 @@ def _manifest_url_for_token(token: str, mediacms_url: str | None) -> str | None:
59
62
  class PlaylistImporter:
60
63
  """Imports items from a saved playlist into the live CyTube queue."""
61
64
 
62
- def __init__(self, *, api_gate, db, shadow):
65
+ def __init__(self, *, api_gate, db, shadow, add_delay_sec: float = 0.0, add_max_retries: int = 0):
63
66
  self._api_gate = api_gate
64
67
  self._db = db
65
68
  self._shadow = shadow
69
+ self._add_delay_sec = add_delay_sec
70
+ self._add_max_retries = add_max_retries
66
71
 
67
72
  async def import_playlist(self, playlist_id: int) -> dict:
68
73
  """Import all items from a saved playlist into the live queue."""
@@ -72,12 +77,19 @@ class PlaylistImporter:
72
77
 
73
78
  added = 0
74
79
  errors = 0
75
- for item in items:
80
+ for index, item in enumerate(items):
81
+ # Throttle consecutive adds so CyTube can validate each item before
82
+ # the next arrives (avoids transient queueFail/422 under load).
83
+ if index and self._add_delay_sec:
84
+ await asyncio.sleep(self._add_delay_sec)
76
85
  try:
77
- result = await self._api_gate.playlist_add(
86
+ result = await add_item_throttled(
87
+ self._api_gate,
78
88
  media_type=item["media_type"],
79
89
  media_id=item["media_id"],
80
90
  position="end",
91
+ max_retries=self._add_max_retries,
92
+ retry_delay_sec=self._add_delay_sec or 0.5,
81
93
  )
82
94
  if result.get("success"):
83
95
  added += 1
@@ -27,11 +27,13 @@ def _next_occurrence(rrule_str: str, dtstart: datetime, after: datetime) -> date
27
27
  class PlaylistScheduler:
28
28
  """APScheduler-based scheduler for playlist fire events."""
29
29
 
30
- def __init__(self, *, db, api_gate, shadow, ws_manager):
30
+ def __init__(self, *, db, api_gate, shadow, ws_manager, add_delay_sec: float = 0.0, add_max_retries: int = 0):
31
31
  self._db = db
32
32
  self._api_gate = api_gate
33
33
  self._shadow = shadow
34
34
  self._ws_manager = ws_manager
35
+ self._add_delay_sec = add_delay_sec
36
+ self._add_max_retries = add_max_retries
35
37
  self._scheduler = AsyncIOScheduler()
36
38
 
37
39
  async def start(self):
@@ -96,6 +98,8 @@ class PlaylistScheduler:
96
98
  db=self._db,
97
99
  shadow=self._shadow,
98
100
  ws_manager=self._ws_manager,
101
+ add_delay_sec=self._add_delay_sec,
102
+ add_max_retries=self._add_max_retries,
99
103
  )
100
104
  # After an automatic timed fire, advance recurring schedules to their
101
105
  # next occurrence and re-arm. (Manual "Fire Now" does NOT advance the
@@ -89,10 +89,13 @@ async def import_to_live(request: Request, playlist_id: int, user: dict = Depend
89
89
  if not playlist:
90
90
  raise HTTPException(404, "Playlist not found")
91
91
 
92
+ config = request.app.state.config
92
93
  importer = PlaylistImporter(
93
94
  api_gate=request.app.state.api_gate,
94
95
  db=db,
95
96
  shadow=request.app.state.shadow,
97
+ add_delay_sec=config.playlist_bulk_add_delay_sec,
98
+ add_max_retries=config.playlist_bulk_add_max_retries,
96
99
  )
97
100
  result = await importer.import_playlist(playlist_id)
98
101
  return result
@@ -112,12 +112,15 @@ async def fire_now(request: Request, schedule_id: int, user: dict = Depends(requ
112
112
  if not sched:
113
113
  raise HTTPException(404, "Schedule not found")
114
114
 
115
+ config = request.app.state.config
115
116
  await fire_schedule(
116
117
  schedule_id=schedule_id,
117
118
  api_gate=request.app.state.api_gate,
118
119
  db=db,
119
120
  shadow=request.app.state.shadow,
120
121
  ws_manager=request.app.state.ws_manager,
122
+ add_delay_sec=config.playlist_bulk_add_delay_sec,
123
+ add_max_retries=config.playlist_bulk_add_max_retries,
121
124
  )
122
125
  return {"success": True}
123
126
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.9.12"
3
+ version = "0.9.13"
4
4
  description = "Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube"
5
5
  readme = "README.md"
6
6
  license = "MIT"