kryten-webqueue 0.17.0__tar.gz → 0.18.0__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.17.0 → kryten_webqueue-0.18.0}/CHANGELOG.md +12 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/PKG-INFO +1 -1
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/queue/shadow.py +60 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/admin/index.html +48 -5
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/admin/schedules.html +15 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/pyproject.toml +1 -1
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_phase4_live_fixes.py +46 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/.gitignore +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/README.md +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/config.example.json +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/catalog/db.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/__init__.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/tests/test_search_facets.py +0 -0
|
@@ -6,6 +6,18 @@ 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.18.0] — 2026-06-17
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Active scheduled event lingered on the admin page after it ended.** The `active_schedule` row was only removed by the manual "Clear Active Schedule" button — the lock auto-lifted but the row (and its banner) stayed. The queue shadow now clears it automatically on the next poll once the event is genuinely over, via two signals: the last scheduled item has left the queue (event temp items auto-remove after playing), or the estimated end is more than 5 minutes in the past (safety net for a missed boundary or a restart mid-event). The schedules page also hides a well-past banner immediately and re-checks every 15s.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Live admin dashboard.** The admin page now subscribes to the same `/ws` feed as the public queue, so the queue item count and now-playing update without a reload. Job status refreshes every 5s while the tab is visible (jobs are DB-polled, not broadcast), and a fired schedule triggers an immediate jobs refresh.
|
|
18
|
+
|
|
19
|
+
[0.18.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.18.0
|
|
20
|
+
|
|
9
21
|
## [0.17.0] — 2026-06-17
|
|
10
22
|
|
|
11
23
|
### Added
|
|
@@ -23,6 +23,19 @@ def _to_seconds(value) -> float:
|
|
|
23
23
|
return 0.0
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _parse_iso(value) -> datetime | None:
|
|
27
|
+
"""Parse an ISO-8601 timestamp to an aware UTC datetime, or None."""
|
|
28
|
+
if not value:
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
dt = datetime.fromisoformat(str(value))
|
|
32
|
+
except (ValueError, TypeError):
|
|
33
|
+
return None
|
|
34
|
+
if dt.tzinfo is None:
|
|
35
|
+
dt = dt.replace(tzinfo=UTC)
|
|
36
|
+
return dt
|
|
37
|
+
|
|
38
|
+
|
|
26
39
|
class QueueShadow:
|
|
27
40
|
"""Local mirror of the CyTube playlist state."""
|
|
28
41
|
|
|
@@ -116,6 +129,7 @@ class QueueShadow:
|
|
|
116
129
|
self._items = new_items
|
|
117
130
|
await self._recalculate_estimated_starts()
|
|
118
131
|
await self._maybe_lift_event_lock()
|
|
132
|
+
await self._maybe_expire_active_schedule()
|
|
119
133
|
|
|
120
134
|
async def _maybe_lift_event_lock(self):
|
|
121
135
|
"""Auto-lift a scheduled-event lock once its last item begins playing.
|
|
@@ -142,6 +156,52 @@ class QueueShadow:
|
|
|
142
156
|
except (TypeError, ValueError):
|
|
143
157
|
return
|
|
144
158
|
|
|
159
|
+
async def _maybe_expire_active_schedule(self):
|
|
160
|
+
"""Clear the active-schedule row once the event is genuinely over.
|
|
161
|
+
|
|
162
|
+
The lock auto-lifts when the last scheduled item *starts* (see
|
|
163
|
+
:meth:`_maybe_lift_event_lock`), but the row itself used to linger so the
|
|
164
|
+
admin banner kept showing a finished event. This clears it via two
|
|
165
|
+
signals:
|
|
166
|
+
|
|
167
|
+
* **Event-driven (primary):** the last scheduled item has left the queue
|
|
168
|
+
(event items are temp and auto-remove after playing), and something is
|
|
169
|
+
still playing — so the event has played out.
|
|
170
|
+
* **Time safety net:** the estimated end is well in the past (covers a
|
|
171
|
+
missed boundary, a restart mid-event, or ``last_item_uid`` never being
|
|
172
|
+
captured).
|
|
173
|
+
"""
|
|
174
|
+
active = await self._db.get_active_schedule()
|
|
175
|
+
if not active:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# Event-driven: last scheduled item is gone from the queue.
|
|
179
|
+
last_uid = active.get("last_item_uid")
|
|
180
|
+
if last_uid is not None and self._now_playing is not None:
|
|
181
|
+
try:
|
|
182
|
+
last_uid_int = int(last_uid)
|
|
183
|
+
except (TypeError, ValueError):
|
|
184
|
+
last_uid_int = None
|
|
185
|
+
if last_uid_int is not None:
|
|
186
|
+
present = False
|
|
187
|
+
for it in self._items:
|
|
188
|
+
try:
|
|
189
|
+
if int(it.get("uid")) == last_uid_int:
|
|
190
|
+
present = True
|
|
191
|
+
break
|
|
192
|
+
except (TypeError, ValueError):
|
|
193
|
+
continue
|
|
194
|
+
if not present:
|
|
195
|
+
await self._db.clear_active_schedule()
|
|
196
|
+
logger.info("Active schedule cleared: last scheduled item has played out")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
# Time safety net: estimated end well in the past.
|
|
200
|
+
end_dt = _parse_iso(active.get("estimated_end_at"))
|
|
201
|
+
if end_dt is not None and datetime.now(UTC) > end_dt + timedelta(minutes=5):
|
|
202
|
+
await self._db.clear_active_schedule()
|
|
203
|
+
logger.info("Active schedule cleared: estimated end passed (stale row)")
|
|
204
|
+
|
|
145
205
|
def _now_playing_index(self) -> int | None:
|
|
146
206
|
"""Index of the currently-playing item within ``self._items``.
|
|
147
207
|
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/admin/index.html
RENAMED
|
@@ -234,11 +234,7 @@ async function loadAdminData() {
|
|
|
234
234
|
// Queue status
|
|
235
235
|
const qResp = await fetch('/queue/state');
|
|
236
236
|
if (qResp.ok) {
|
|
237
|
-
|
|
238
|
-
document.getElementById('queue-status').innerHTML = `
|
|
239
|
-
<p>Items in queue: ${(state.items || []).length}</p>
|
|
240
|
-
<p>Now playing: ${state.now_playing ? escapeHtml(state.now_playing.title || 'Unknown') : 'Nothing'}</p>
|
|
241
|
-
`;
|
|
237
|
+
renderQueueStatus(await qResp.json());
|
|
242
238
|
}
|
|
243
239
|
|
|
244
240
|
// Sync logs
|
|
@@ -265,6 +261,46 @@ async function loadAdminData() {
|
|
|
265
261
|
}
|
|
266
262
|
}
|
|
267
263
|
|
|
264
|
+
// Render the live queue-status block. Shared by the initial load and the
|
|
265
|
+
// WebSocket so the count + now-playing stay current without a reload.
|
|
266
|
+
function renderQueueStatus(state) {
|
|
267
|
+
const el = document.getElementById('queue-status');
|
|
268
|
+
if (!el) return;
|
|
269
|
+
const np = state && state.now_playing;
|
|
270
|
+
el.innerHTML = `
|
|
271
|
+
<p>Items in queue: ${((state && state.items) || []).length}</p>
|
|
272
|
+
<p>Now playing: ${np ? escapeHtml(np.title || 'Unknown') : 'Nothing'}</p>
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Subscribe to the same /ws feed the public queue page uses, so queue size and
|
|
277
|
+
// now-playing update live. Jobs are DB-polled (below) since they aren't
|
|
278
|
+
// broadcast.
|
|
279
|
+
let adminWs = null;
|
|
280
|
+
let adminWsReconnect = null;
|
|
281
|
+
function connectAdminWebSocket() {
|
|
282
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
283
|
+
adminWs = new WebSocket(`${proto}//${location.host}/ws`);
|
|
284
|
+
adminWs.onmessage = (event) => {
|
|
285
|
+
let msg;
|
|
286
|
+
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
287
|
+
if (msg.type === 'queue_state' || msg.type === 'queue_update') {
|
|
288
|
+
renderQueueStatus(msg.data);
|
|
289
|
+
} else if (msg.type === 'schedule_fired') {
|
|
290
|
+
showToast(`Scheduled playlist loaded: ${msg.data.playlist_name}`);
|
|
291
|
+
loadJobs();
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
adminWs.onclose = () => {
|
|
295
|
+
adminWsReconnect = setTimeout(connectAdminWebSocket, 3000);
|
|
296
|
+
};
|
|
297
|
+
setInterval(() => {
|
|
298
|
+
if (adminWs && adminWs.readyState === WebSocket.OPEN) {
|
|
299
|
+
adminWs.send(JSON.stringify({type: 'ping'}));
|
|
300
|
+
}
|
|
301
|
+
}, 30000);
|
|
302
|
+
}
|
|
303
|
+
|
|
268
304
|
function escapeHtml(str) {
|
|
269
305
|
const div = document.createElement('div');
|
|
270
306
|
div.textContent = str;
|
|
@@ -294,5 +330,12 @@ function summarizeRunDetail(detail) {
|
|
|
294
330
|
|
|
295
331
|
loadAdminData();
|
|
296
332
|
loadJobs();
|
|
333
|
+
connectAdminWebSocket();
|
|
334
|
+
|
|
335
|
+
// Jobs are DB-polled (not broadcast), so refresh them periodically while the
|
|
336
|
+
// tab is visible to reflect running/finished status without a reload.
|
|
337
|
+
setInterval(() => {
|
|
338
|
+
if (document.visibilityState === 'visible') loadJobs();
|
|
339
|
+
}, 5000);
|
|
297
340
|
</script>
|
|
298
341
|
{% endblock %}
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
@@ -42,6 +42,15 @@ async function loadActive() {
|
|
|
42
42
|
if (!resp.ok) { banner.classList.add('hidden'); return; }
|
|
43
43
|
const a = await resp.json();
|
|
44
44
|
if (!a || !a.schedule_id) { banner.classList.add('hidden'); return; }
|
|
45
|
+
// Treat a well-past estimated end as ended even before the backend clears
|
|
46
|
+
// the row on its next poll, so the banner never lingers.
|
|
47
|
+
if (a.estimated_end_at) {
|
|
48
|
+
const end = new Date(a.estimated_end_at);
|
|
49
|
+
if (!isNaN(end) && (Date.now() - end.getTime()) > 5 * 60 * 1000) {
|
|
50
|
+
banner.classList.add('hidden');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
45
54
|
const name = playlistMap[a.playlist_id] || `Playlist #${a.playlist_id}`;
|
|
46
55
|
banner.classList.remove('hidden');
|
|
47
56
|
const locked = a.is_immutable && !a.lock_disabled;
|
|
@@ -203,5 +212,11 @@ function showModal(html) {
|
|
|
203
212
|
function closeModal() { const m = document.getElementById('admin-modal'); if (m) m.remove(); }
|
|
204
213
|
|
|
205
214
|
loadSchedules();
|
|
215
|
+
|
|
216
|
+
// Keep the active-event banner fresh without a reload: re-check every 15s while
|
|
217
|
+
// the tab is visible (the backend clears the row once the event plays out).
|
|
218
|
+
setInterval(() => {
|
|
219
|
+
if (document.visibilityState === 'visible') loadActive();
|
|
220
|
+
}, 15000);
|
|
206
221
|
</script>
|
|
207
222
|
{% endblock %}
|
|
@@ -152,6 +152,52 @@ async def test_event_lock_stays_until_last_item(db):
|
|
|
152
152
|
assert await db.is_event_lock_active() is True
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
# --- active-schedule auto-expiry (v0.18.0) ---
|
|
156
|
+
|
|
157
|
+
async def test_active_schedule_cleared_when_last_item_plays_out(db):
|
|
158
|
+
await _make_event(db, last_item_uid=3)
|
|
159
|
+
shadow = QueueShadow(db)
|
|
160
|
+
# The last scheduled item (uid=3) is still in the queue -> row kept.
|
|
161
|
+
await shadow.apply_poll_result(
|
|
162
|
+
[_polled(1), _polled(2), _polled(3)], {"uid": 2, "seconds": 100, "currentTime": 0}
|
|
163
|
+
)
|
|
164
|
+
assert (await db.get_active_schedule()) is not None
|
|
165
|
+
# uid=3 has now left the queue (played out, temp item removed) -> row cleared.
|
|
166
|
+
await shadow.apply_poll_result(
|
|
167
|
+
[_polled(4), _polled(5)], {"uid": 4, "seconds": 100, "currentTime": 0}
|
|
168
|
+
)
|
|
169
|
+
assert (await db.get_active_schedule()) is None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def test_active_schedule_not_cleared_when_nothing_playing(db):
|
|
173
|
+
await _make_event(db, last_item_uid=3)
|
|
174
|
+
shadow = QueueShadow(db)
|
|
175
|
+
# Last item absent but nothing is playing (transient empty poll) -> keep row.
|
|
176
|
+
await shadow.apply_poll_result([], None)
|
|
177
|
+
assert (await db.get_active_schedule()) is not None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def test_active_schedule_cleared_when_estimated_end_passed(db):
|
|
181
|
+
# last_item_uid is None so the event-driven path can't fire; rely on the
|
|
182
|
+
# time safety net with an estimated end well in the past.
|
|
183
|
+
pid = await db.create_saved_playlist(
|
|
184
|
+
name="Stale", description=None, is_immutable=True, created_by="admin"
|
|
185
|
+
)
|
|
186
|
+
sid = await db.create_schedule(
|
|
187
|
+
playlist_id=pid, label="Stale", fire_at=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S"),
|
|
188
|
+
is_active=1, created_by="admin",
|
|
189
|
+
)
|
|
190
|
+
past = datetime.now(UTC) - timedelta(hours=1)
|
|
191
|
+
await db.set_active_schedule(
|
|
192
|
+
schedule_id=sid, playlist_id=pid, is_immutable=True,
|
|
193
|
+
started_at=(past - timedelta(hours=1)).isoformat(),
|
|
194
|
+
estimated_end_at=past.isoformat(), last_item_uid=None,
|
|
195
|
+
)
|
|
196
|
+
shadow = QueueShadow(db)
|
|
197
|
+
await shadow.apply_poll_result([_polled(1)], {"uid": 1, "seconds": 100, "currentTime": 0})
|
|
198
|
+
assert (await db.get_active_schedule()) is None
|
|
199
|
+
|
|
200
|
+
|
|
155
201
|
# --- #4 pre-fire lock override ---
|
|
156
202
|
|
|
157
203
|
async def test_pre_fire_lock_can_be_disabled(db):
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/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
|
|
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.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/admin/promos.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/kryten_webqueue/templates/queue/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.17.0 → kryten_webqueue-0.18.0}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|