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.
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/CHANGELOG.md +10 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/PKG-INFO +1 -1
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/config.example.json +3 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/app.py +5 -1
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/config.py +7 -0
- kryten_webqueue-0.9.13/kryten_webqueue/playlists/bulk_add.py +52 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/playlists/fire.py +27 -5
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/playlists/importer.py +15 -3
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/playlists/scheduler.py +5 -1
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/admin_playlists.py +3 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/admin_schedules.py +3 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/pyproject.toml +1 -1
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/.gitignore +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/README.md +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/catalog/db.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/__init__.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/tests/test_playlist_import.py +0 -0
- {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
|
|
@@ -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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/integrations/ytpipe/downloader.py
RENAMED
|
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
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/queue/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.12 → kryten_webqueue-0.9.13}/kryten_webqueue/templates/user/dashboard.html
RENAMED
|
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
|