kryten-webqueue 0.28.0__tar.gz → 0.30.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.28.0 → kryten_webqueue-0.30.0}/CHANGELOG.md +21 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/PKG-INFO +1 -1
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/app.py +3 -1
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/sync.py +13 -2
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/jobs/manager.py +30 -7
- kryten_webqueue-0.28.0/kryten_webqueue/templates/admin/playlists.html → kryten_webqueue-0.30.0/kryten_webqueue/static/js/admin-playlists.js +1 -89
- kryten_webqueue-0.28.0/kryten_webqueue/templates/admin/promos.html → kryten_webqueue-0.30.0/kryten_webqueue/static/js/admin-promos.js +1 -49
- kryten_webqueue-0.28.0/kryten_webqueue/templates/admin/queue_mgmt.html → kryten_webqueue-0.30.0/kryten_webqueue/static/js/admin-queue-mgmt.js +1 -55
- kryten_webqueue-0.28.0/kryten_webqueue/templates/admin/schedules.html → kryten_webqueue-0.30.0/kryten_webqueue/static/js/admin-schedules.js +1 -39
- kryten_webqueue-0.28.0/kryten_webqueue/templates/admin/index.html → kryten_webqueue-0.30.0/kryten_webqueue/static/js/admin.js +10 -193
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/static/js/main.js +19 -0
- kryten_webqueue-0.30.0/kryten_webqueue/templates/admin/index.html +184 -0
- kryten_webqueue-0.30.0/kryten_webqueue/templates/admin/playlists.html +69 -0
- kryten_webqueue-0.30.0/kryten_webqueue/templates/admin/promos.html +45 -0
- kryten_webqueue-0.30.0/kryten_webqueue/templates/admin/queue_mgmt.html +35 -0
- kryten_webqueue-0.30.0/kryten_webqueue/templates/admin/schedules.html +25 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/pyproject.toml +1 -1
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/.gitignore +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/README.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/config.example.json +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/Log for incorrect promo insertion problem.txt +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/_catalog.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/_connection.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/_feedback.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/_playlists.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/_queue.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/race_poller.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_feedback.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_moderation.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/feedback.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/feedback/index.html +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/race.html +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/__init__.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_feedback.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_race_poller.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_schedule_lock.py +0 -0
- {kryten_webqueue-0.28.0 → kryten_webqueue-0.30.0}/tests/test_search_facets.py +0 -0
|
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
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
|
|
|
8
|
+
## [0.30.0] — 2026-07-04
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Clean shutdown when a catalog sync (or other job) is in progress.** Previously, shutting the server down while a background job was running caused a cascade of WARNING/ERROR log lines as the database connection and HTTP client were closed underneath the still-running job task. Three changes fix this:
|
|
13
|
+
- `JobManager` gains a `stop()` method that cancels all in-flight task and awaits them.
|
|
14
|
+
- The lifespan shutdown now calls `job_manager.stop()` *before* closing the database, HTTP clients, or any other shared resource, so the cancelled tasks can still record their final status cleanly.
|
|
15
|
+
- Background loop tasks (`bg_tasks`) are now properly awaited after cancellation, preventing "Task exception was never retrieved" warnings.
|
|
16
|
+
- `JobManager._execute` wraps every database write in a defensive `try/except` so that a race between task teardown and resource teardown never produces an unhandled exception or spurious ERROR log.
|
|
17
|
+
- `CatalogSync.sync` adds an explicit `asyncio.CancelledError` handler that logs a clean info message and re-raises (letting `JobManager` record the `cancelled` status), and wraps the error-path `finish_sync_log` calls defensively for the same reason.
|
|
18
|
+
|
|
19
|
+
[0.30.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.30.0
|
|
20
|
+
|
|
21
|
+
## [0.29.0] — 2026-07-04
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **Admin JavaScript moved to external static files.** All inline `{% block scripts %}` JavaScript has been extracted from the five admin templates into individual per-page files under `static/js/` (`admin.js`, `admin-playlists.js`, `admin-promos.js`, `admin-schedules.js`, `admin-queue-mgmt.js`). Shared utilities `showModal`/`closeModal` and `fmtDur` were promoted to `main.js` (previously only defined per-page). Duplicate `escapeHtml`/`fmtDur`/`showModal`/`closeModal` definitions are removed from per-page files. Each template now references its JS via a single `<script src="/static/js/...">` tag — no functional changes, better cacheability and maintainability.
|
|
26
|
+
|
|
27
|
+
[0.29.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.29.0
|
|
28
|
+
|
|
8
29
|
## [0.28.0] — 2026-07-04
|
|
9
30
|
|
|
10
31
|
### Added
|
|
@@ -227,9 +227,11 @@ async def lifespan(app: FastAPI):
|
|
|
227
227
|
|
|
228
228
|
yield
|
|
229
229
|
|
|
230
|
-
# Shutdown
|
|
230
|
+
# Shutdown — order matters: cancel in-flight work before closing resources.
|
|
231
231
|
for task in bg_tasks:
|
|
232
232
|
task.cancel()
|
|
233
|
+
await asyncio.gather(*bg_tasks, return_exceptions=True)
|
|
234
|
+
await job_manager.stop() # cancel running jobs while DB/client still open
|
|
233
235
|
await poller.stop()
|
|
234
236
|
await race_poller.stop()
|
|
235
237
|
await scheduler.stop()
|
|
@@ -96,14 +96,25 @@ class CatalogSync:
|
|
|
96
96
|
|
|
97
97
|
await self._db.finish_sync_log(log_id, stats, "completed")
|
|
98
98
|
logger.info(f"Catalog sync complete: {stats} ({page} pages)")
|
|
99
|
+
except asyncio.CancelledError:
|
|
100
|
+
# Server is shutting down — propagate cleanly so JobManager can
|
|
101
|
+
# record the cancelled status while the DB is still open.
|
|
102
|
+
logger.info("Catalog sync cancelled (shutdown in progress)")
|
|
103
|
+
raise
|
|
99
104
|
except httpx.TransportError as exc:
|
|
100
105
|
logger.error(_describe_httpx_error(exc, f"{self._url}/api/v1/media"))
|
|
101
106
|
stats["errors"] += 1
|
|
102
|
-
|
|
107
|
+
try:
|
|
108
|
+
await self._db.finish_sync_log(log_id, stats, "error")
|
|
109
|
+
except Exception: # noqa: BLE001
|
|
110
|
+
logger.debug("Could not persist sync error status (DB unavailable)")
|
|
103
111
|
except Exception as exc:
|
|
104
112
|
logger.exception(f"Catalog sync failed: {type(exc).__name__}: {exc}")
|
|
105
113
|
stats["errors"] += 1
|
|
106
|
-
|
|
114
|
+
try:
|
|
115
|
+
await self._db.finish_sync_log(log_id, stats, "error")
|
|
116
|
+
except Exception: # noqa: BLE001
|
|
117
|
+
logger.debug("Could not persist sync error status (DB unavailable)")
|
|
107
118
|
|
|
108
119
|
async def _process_item(self, media: dict, stats: dict):
|
|
109
120
|
token = media.get("friendly_token")
|
|
@@ -158,6 +158,20 @@ class JobManager:
|
|
|
158
158
|
def is_running(self, name: str) -> bool:
|
|
159
159
|
return name in self._running
|
|
160
160
|
|
|
161
|
+
async def stop(self) -> None:
|
|
162
|
+
"""Cancel all running job tasks and wait for them to finish.
|
|
163
|
+
|
|
164
|
+
Call this during application shutdown *before* closing shared resources
|
|
165
|
+
(database, HTTP clients) so that in-progress tasks receive
|
|
166
|
+
``CancelledError`` while those resources are still usable.
|
|
167
|
+
"""
|
|
168
|
+
if not self._running:
|
|
169
|
+
return
|
|
170
|
+
logger.info("Stopping %d running job(s) for shutdown", len(self._running))
|
|
171
|
+
for task in list(self._running.values()):
|
|
172
|
+
task.cancel()
|
|
173
|
+
await asyncio.gather(*self._running.values(), return_exceptions=True)
|
|
174
|
+
|
|
161
175
|
async def run(self, name: str, *, triggered_by: str | None = None,
|
|
162
176
|
params: dict | None = None) -> dict:
|
|
163
177
|
"""Start a job in the background. Returns immediately.
|
|
@@ -191,19 +205,28 @@ class JobManager:
|
|
|
191
205
|
detail = json.dumps(result) if result is not None else None
|
|
192
206
|
await self._db.finish_job_run(run_id, "completed", detail)
|
|
193
207
|
except asyncio.CancelledError:
|
|
194
|
-
|
|
208
|
+
try:
|
|
209
|
+
await self._db.finish_job_run(run_id, "cancelled", None)
|
|
210
|
+
except Exception: # noqa: BLE001
|
|
211
|
+
logger.debug("Could not persist 'cancelled' status for job '%s' (DB unavailable)", name)
|
|
195
212
|
raise
|
|
196
213
|
except JobError as exc:
|
|
197
214
|
# Expected, user-facing failure (bad input/config): record a clean
|
|
198
215
|
# message and log without a stack trace.
|
|
199
216
|
logger.warning("Job '%s' failed: %s", name, exc)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
217
|
+
try:
|
|
218
|
+
await self._db.finish_job_run(
|
|
219
|
+
run_id, "failed", json.dumps({"error": str(exc)})
|
|
220
|
+
)
|
|
221
|
+
except Exception: # noqa: BLE001
|
|
222
|
+
logger.debug("Could not persist 'failed' status for job '%s' (DB unavailable)", name)
|
|
203
223
|
except Exception as exc: # noqa: BLE001 - record any failure
|
|
204
224
|
logger.exception("Job '%s' failed", name)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
225
|
+
try:
|
|
226
|
+
await self._db.finish_job_run(
|
|
227
|
+
run_id, "failed", json.dumps({"error": f"{type(exc).__name__}: {exc}"})
|
|
228
|
+
)
|
|
229
|
+
except Exception: # noqa: BLE001
|
|
230
|
+
logger.debug("Could not persist 'failed' status for job '%s' (DB unavailable)", name)
|
|
208
231
|
finally:
|
|
209
232
|
self._running.pop(name, None)
|
|
@@ -1,83 +1,7 @@
|
|
|
1
|
-
{% extends "base.html" %}
|
|
2
|
-
{% block title %}Playlists - Admin{% endblock %}
|
|
3
|
-
{% block body_class %}admin-page{% endblock %}
|
|
4
|
-
|
|
5
|
-
{% block content %}
|
|
6
|
-
<div class="admin-dashboard">
|
|
7
|
-
<h1>Playlists</h1>
|
|
8
|
-
<p><a href="/admin">← Back to Admin</a></p>
|
|
9
|
-
|
|
10
|
-
<!-- LIST VIEW -->
|
|
11
|
-
<div id="list-view">
|
|
12
|
-
<div class="admin-section">
|
|
13
|
-
<div class="section-head">
|
|
14
|
-
<h2>Saved Playlists</h2>
|
|
15
|
-
<button class="btn btn-primary" onclick="showCreateModal()">+ New Playlist</button>
|
|
16
|
-
</div>
|
|
17
|
-
<div id="playlists-list">Loading…</div>
|
|
18
|
-
</div>
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
<!-- EDITOR VIEW -->
|
|
22
|
-
<div id="editor-view" class="hidden">
|
|
23
|
-
<div class="admin-section">
|
|
24
|
-
<div class="section-head">
|
|
25
|
-
<h2 id="editor-title">Edit Playlist</h2>
|
|
26
|
-
<div class="btn-group">
|
|
27
|
-
<button class="btn btn-secondary" onclick="closeEditor()">← Back</button>
|
|
28
|
-
<button class="btn" onclick="editMeta()">Rename</button>
|
|
29
|
-
<button class="btn btn-primary" onclick="saveItems()">Save Items</button>
|
|
30
|
-
<button class="btn" onclick="importToLive()" title="Load this playlist into the live CyTube queue">Import to Live</button>
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
<p id="editor-meta" class="muted"></p>
|
|
34
|
-
|
|
35
|
-
<div class="editor-grid">
|
|
36
|
-
<!-- Working item list -->
|
|
37
|
-
<div>
|
|
38
|
-
<h3>Items (<span id="item-count">0</span>)</h3>
|
|
39
|
-
<ul id="editor-items" class="editor-list"></ul>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<!-- Add panel -->
|
|
43
|
-
<div>
|
|
44
|
-
<h3>Add from Catalog</h3>
|
|
45
|
-
<div class="search-form">
|
|
46
|
-
<input type="text" id="cat-search" placeholder="Search catalog…" onkeydown="if(event.key==='Enter')catalogSearch()">
|
|
47
|
-
<button class="btn btn-sm" onclick="catalogSearch()">Search</button>
|
|
48
|
-
</div>
|
|
49
|
-
<div id="cat-results" class="cat-results"></div>
|
|
50
|
-
|
|
51
|
-
<h3 style="margin-top:1.5rem;">Bulk Text Import</h3>
|
|
52
|
-
<p class="muted" style="font-size:0.8rem;">One entry per line: a <strong>dropsugar.co</strong> link, a <strong>YouTube</strong>/<strong>youtu.be</strong> link (playlist & start-time args are stripped), <code>cm:token</code>, <code>yt:id</code>, or a bare catalog token. <code>#</code> starts a comment (whole-line or trailing). Unknown sites are skipped.</p>
|
|
53
|
-
<textarea id="import-text" class="import-text" rows="5" placeholder="https://www.dropsugar.co/view?m=abc123def https://youtu.be/dQw4w9WgXcQ?t=42 # start time is stripped yt:dQw4w9WgXcQ abc123def"></textarea>
|
|
54
|
-
<div class="btn-group" style="margin-top:0.25rem;">
|
|
55
|
-
<button class="btn btn-sm" onclick="parseImport()">Append Parsed Items</button>
|
|
56
|
-
<input type="file" id="import-file" accept=".txt,.csv,text/plain" style="display:none" onchange="loadImportFile(event)">
|
|
57
|
-
<button class="btn btn-sm" onclick="document.getElementById('import-file').click()">Choose text file…</button>
|
|
58
|
-
</div>
|
|
59
|
-
<div id="import-errors" class="import-errors"></div>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
{% endblock %}
|
|
66
|
-
|
|
67
|
-
{% block scripts %}
|
|
68
|
-
<script>
|
|
69
1
|
let editorId = null;
|
|
70
2
|
let editorItems = []; // working list: {media_type, media_id, title, duration_sec}
|
|
71
3
|
let editorImmutable = false;
|
|
72
4
|
|
|
73
|
-
function fmtDur(sec) {
|
|
74
|
-
sec = Math.floor(sec || 0);
|
|
75
|
-
const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
|
|
76
|
-
if (h > 0) return `${h}:${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`;
|
|
77
|
-
return `${m}:${s.toString().padStart(2,'0')}`;
|
|
78
|
-
}
|
|
79
|
-
function escapeHtml(str) { const d = document.createElement('div'); d.textContent = str == null ? '' : str; return d.innerHTML; }
|
|
80
|
-
|
|
81
5
|
// ---------- LIST ----------
|
|
82
6
|
async function loadPlaylists() {
|
|
83
7
|
const resp = await fetch('/admin/playlists/');
|
|
@@ -327,16 +251,4 @@ async function saveMeta() {
|
|
|
327
251
|
}
|
|
328
252
|
|
|
329
253
|
// ---------- generic modal ----------
|
|
330
|
-
|
|
331
|
-
closeModal();
|
|
332
|
-
const o = document.createElement('div');
|
|
333
|
-
o.className = 'modal-overlay'; o.id = 'admin-modal';
|
|
334
|
-
o.innerHTML = `<div class="modal-box">${html}</div>`;
|
|
335
|
-
o.addEventListener('click', e => { if (e.target === o) closeModal(); });
|
|
336
|
-
document.body.appendChild(o);
|
|
337
|
-
}
|
|
338
|
-
function closeModal() { const m = document.getElementById('admin-modal'); if (m) m.remove(); }
|
|
339
|
-
|
|
340
|
-
loadPlaylists();
|
|
341
|
-
</script>
|
|
342
|
-
{% endblock %}
|
|
254
|
+
loadPlaylists();
|
|
@@ -1,47 +1,3 @@
|
|
|
1
|
-
{% extends "base.html" %}
|
|
2
|
-
{% block title %}Promos - Admin{% endblock %}
|
|
3
|
-
{% block body_class %}admin-page{% endblock %}
|
|
4
|
-
|
|
5
|
-
{% block content %}
|
|
6
|
-
<div class="admin-dashboard">
|
|
7
|
-
<h1>Promos</h1>
|
|
8
|
-
<p><a href="/admin">← Back to Admin</a></p>
|
|
9
|
-
|
|
10
|
-
<!-- POOL DESIGNATION -->
|
|
11
|
-
<div class="admin-section">
|
|
12
|
-
<div class="section-head">
|
|
13
|
-
<h2>Promo Pools</h2>
|
|
14
|
-
<a href="/admin/playlists" class="btn btn-sm">Manage Playlist Items →</a>
|
|
15
|
-
</div>
|
|
16
|
-
<p class="muted">
|
|
17
|
-
Designate a saved playlist as a promo pool by choosing its type. Pool
|
|
18
|
-
items are hidden from the public catalog and excluded from pay-to-play,
|
|
19
|
-
and are drawn from automatically by the promo director. Edit a pool's
|
|
20
|
-
clips on the <a href="/admin/playlists">Playlists</a> page.
|
|
21
|
-
</p>
|
|
22
|
-
<div id="pools-list">Loading…</div>
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
<!-- SETTINGS (editable) -->
|
|
26
|
-
<div class="admin-section">
|
|
27
|
-
<div class="section-head">
|
|
28
|
-
<h2>Promo Settings</h2>
|
|
29
|
-
<span class="badge badge-accent" title="Saved to the service config file and applied live">editable</span>
|
|
30
|
-
</div>
|
|
31
|
-
<p class="muted">
|
|
32
|
-
Promo cadence and per-type behaviour. Changes are saved to the service
|
|
33
|
-
config file and applied to the running promo director immediately — no
|
|
34
|
-
restart required.
|
|
35
|
-
</p>
|
|
36
|
-
<form id="settings-form">
|
|
37
|
-
<div id="settings-view">Loading…</div>
|
|
38
|
-
</form>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
{% endblock %}
|
|
42
|
-
|
|
43
|
-
{% block scripts %}
|
|
44
|
-
<script>
|
|
45
1
|
const PROMO_TYPE_LABELS = {
|
|
46
2
|
channel_identity: 'Channel Identity',
|
|
47
3
|
event: 'Event',
|
|
@@ -53,8 +9,6 @@ const PROMO_TYPE_LABELS = {
|
|
|
53
9
|
let ALL_PLAYLISTS = [];
|
|
54
10
|
let PROMO_META = { promo_types: [], general_types: [], lead_in_types: [] };
|
|
55
11
|
|
|
56
|
-
function escapeHtml(str) { const d = document.createElement('div'); d.textContent = str == null ? '' : str; return d.innerHTML; }
|
|
57
|
-
|
|
58
12
|
function typeLabel(t) { return PROMO_TYPE_LABELS[t] || t; }
|
|
59
13
|
|
|
60
14
|
// ---------- POOLS ----------
|
|
@@ -207,6 +161,4 @@ document.getElementById('settings-form').addEventListener('submit', async (e) =>
|
|
|
207
161
|
});
|
|
208
162
|
|
|
209
163
|
loadPools();
|
|
210
|
-
loadSettings();
|
|
211
|
-
</script>
|
|
212
|
-
{% endblock %}
|
|
164
|
+
loadSettings();
|
|
@@ -1,47 +1,5 @@
|
|
|
1
|
-
{% extends "base.html" %}
|
|
2
|
-
{% block title %}Queue Management - Admin{% endblock %}
|
|
3
|
-
{% block body_class %}admin-page{% endblock %}
|
|
4
|
-
|
|
5
|
-
{% block content %}
|
|
6
|
-
<div class="admin-dashboard">
|
|
7
|
-
<h1>Queue Management</h1>
|
|
8
|
-
<p><a href="/admin">← Back to Admin</a></p>
|
|
9
|
-
|
|
10
|
-
<div class="admin-section">
|
|
11
|
-
<div class="section-head">
|
|
12
|
-
<h2>Live Queue</h2>
|
|
13
|
-
<div class="btn-group">
|
|
14
|
-
<button class="btn btn-sm" onclick="showAddModal()">+ Add Item</button>
|
|
15
|
-
<button class="btn btn-sm" onclick="loadQueue()">Refresh</button>
|
|
16
|
-
<button class="btn btn-sm btn-danger" onclick="clearAll()">Clear Queue</button>
|
|
17
|
-
</div>
|
|
18
|
-
</div>
|
|
19
|
-
<div id="np-line" class="muted" style="margin-bottom:0.75rem;"></div>
|
|
20
|
-
<div id="queue-table">Loading…</div>
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div class="admin-section">
|
|
24
|
-
<div class="section-head">
|
|
25
|
-
<h2>Catalog Sync</h2>
|
|
26
|
-
<button class="btn btn-sm" onclick="syncNow()">Sync Now</button>
|
|
27
|
-
</div>
|
|
28
|
-
<div id="sync-logs">Loading…</div>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
{% endblock %}
|
|
32
|
-
|
|
33
|
-
{% block scripts %}
|
|
34
|
-
<script>
|
|
35
1
|
let refreshTimer = null;
|
|
36
2
|
|
|
37
|
-
function escapeHtml(str) { const d = document.createElement('div'); d.textContent = str == null ? '' : str; return d.innerHTML; }
|
|
38
|
-
function fmtDur(sec) {
|
|
39
|
-
sec = Math.floor(sec || 0);
|
|
40
|
-
const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
|
|
41
|
-
if (h > 0) return `${h}:${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`;
|
|
42
|
-
return `${m}:${s.toString().padStart(2,'0')}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
3
|
async function loadQueue() {
|
|
46
4
|
const resp = await fetch('/queue/state');
|
|
47
5
|
const el = document.getElementById('queue-table');
|
|
@@ -168,19 +126,7 @@ async function loadSyncLogs() {
|
|
|
168
126
|
</table>`;
|
|
169
127
|
}
|
|
170
128
|
|
|
171
|
-
function showModal(html) {
|
|
172
|
-
closeModal();
|
|
173
|
-
const o = document.createElement('div');
|
|
174
|
-
o.className = 'modal-overlay'; o.id = 'admin-modal';
|
|
175
|
-
o.innerHTML = `<div class="modal-box">${html}</div>`;
|
|
176
|
-
o.addEventListener('click', e => { if (e.target === o) closeModal(); });
|
|
177
|
-
document.body.appendChild(o);
|
|
178
|
-
}
|
|
179
|
-
function closeModal() { const m = document.getElementById('admin-modal'); if (m) m.remove(); }
|
|
180
|
-
|
|
181
129
|
loadQueue();
|
|
182
130
|
loadSyncLogs();
|
|
183
131
|
refreshTimer = setInterval(loadQueue, 5000);
|
|
184
|
-
window.addEventListener('beforeunload', () => clearInterval(refreshTimer));
|
|
185
|
-
</script>
|
|
186
|
-
{% endblock %}
|
|
132
|
+
window.addEventListener('beforeunload', () => clearInterval(refreshTimer));
|
|
@@ -1,32 +1,6 @@
|
|
|
1
|
-
{% extends "base.html" %}
|
|
2
|
-
{% block title %}Schedules - Admin{% endblock %}
|
|
3
|
-
{% block body_class %}admin-page{% endblock %}
|
|
4
|
-
|
|
5
|
-
{% block content %}
|
|
6
|
-
<div class="admin-dashboard">
|
|
7
|
-
<h1>Schedules</h1>
|
|
8
|
-
<p><a href="/admin">← Back to Admin</a></p>
|
|
9
|
-
|
|
10
|
-
<div id="lock-banner" class="lock-banner hidden"></div>
|
|
11
|
-
<div id="active-banner" class="schedule-info hidden" style="margin-bottom:1.5rem;"></div>
|
|
12
|
-
|
|
13
|
-
<div class="admin-section">
|
|
14
|
-
<div class="section-head">
|
|
15
|
-
<h2>Scheduled Events</h2>
|
|
16
|
-
<button class="btn btn-primary" onclick="showScheduleModal()">+ New Schedule</button>
|
|
17
|
-
</div>
|
|
18
|
-
<div id="schedules-list">Loading…</div>
|
|
19
|
-
</div>
|
|
20
|
-
</div>
|
|
21
|
-
{% endblock %}
|
|
22
|
-
|
|
23
|
-
{% block scripts %}
|
|
24
|
-
<script>
|
|
25
1
|
let playlistMap = {};
|
|
26
2
|
let playlistRows = [];
|
|
27
3
|
|
|
28
|
-
function escapeHtml(str) { const d = document.createElement('div'); d.textContent = str == null ? '' : str; return d.innerHTML; }
|
|
29
|
-
|
|
30
4
|
async function loadPlaylistsForSelect() {
|
|
31
5
|
const resp = await fetch('/admin/playlists/');
|
|
32
6
|
if (!resp.ok) return [];
|
|
@@ -257,16 +231,6 @@ async function deleteSchedule(id, label) {
|
|
|
257
231
|
loadSchedules();
|
|
258
232
|
}
|
|
259
233
|
|
|
260
|
-
function showModal(html) {
|
|
261
|
-
closeModal();
|
|
262
|
-
const o = document.createElement('div');
|
|
263
|
-
o.className = 'modal-overlay'; o.id = 'admin-modal';
|
|
264
|
-
o.innerHTML = `<div class="modal-box">${html}</div>`;
|
|
265
|
-
o.addEventListener('click', e => { if (e.target === o) closeModal(); });
|
|
266
|
-
document.body.appendChild(o);
|
|
267
|
-
}
|
|
268
|
-
function closeModal() { const m = document.getElementById('admin-modal'); if (m) m.remove(); }
|
|
269
|
-
|
|
270
234
|
loadSchedules();
|
|
271
235
|
|
|
272
236
|
// Keep the lock indicator and active-event banner fresh without a reload:
|
|
@@ -277,6 +241,4 @@ setInterval(() => {
|
|
|
277
241
|
loadLockStatus();
|
|
278
242
|
loadActive();
|
|
279
243
|
}
|
|
280
|
-
}, 15000);
|
|
281
|
-
</script>
|
|
282
|
-
{% endblock %}
|
|
244
|
+
}, 15000);
|