kryten-webqueue 0.29.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.
Files changed (127) hide show
  1. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/CHANGELOG.md +13 -0
  2. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/app.py +3 -1
  4. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/sync.py +13 -2
  5. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/jobs/manager.py +30 -7
  6. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/pyproject.toml +1 -1
  7. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/.github/workflows/python-publish.yml +0 -0
  8. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/.github/workflows/release.yml +0 -0
  9. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/.gitignore +0 -0
  10. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/README.md +0 -0
  11. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/config.example.json +0 -0
  12. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/deploy/kryten-webqueue.service +0 -0
  13. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/deploy/nginx-queue.conf +0 -0
  14. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
  15. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/IMPL_API_GATE.md +0 -0
  16. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/IMPL_ECONOMY.md +0 -0
  17. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/IMPL_KRYTEN_PY.md +0 -0
  18. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/IMPL_ROBOT.md +0 -0
  19. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  20. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  21. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/Log for incorrect promo insertion problem.txt +0 -0
  22. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  23. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/PRE_PLAN_GAPS.md +0 -0
  24. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/PRODUCT_PLAN.md +0 -0
  25. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  26. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/docs/UX_POLISH_PLAN.md +0 -0
  27. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/__init__.py +0 -0
  28. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/__main__.py +0 -0
  29. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/api_gate/__init__.py +0 -0
  30. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/api_gate/client.py +0 -0
  31. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/auth/__init__.py +0 -0
  32. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/auth/otp.py +0 -0
  33. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/auth/rate_limit.py +0 -0
  34. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/auth/session.py +0 -0
  35. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/__init__.py +0 -0
  36. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/__init__.py +0 -0
  37. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/_catalog.py +0 -0
  38. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/_connection.py +0 -0
  39. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  40. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  41. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/db/_queue.py +0 -0
  42. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/images.py +0 -0
  43. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/catalog/mediacms.py +0 -0
  44. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/config.py +0 -0
  45. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/__init__.py +0 -0
  46. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  47. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  48. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  49. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  50. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  51. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  52. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  53. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  54. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/jobs/__init__.py +0 -0
  55. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  56. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/jobs/tasks.py +0 -0
  57. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/logging_config.py +0 -0
  58. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/__init__.py +0 -0
  59. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
  60. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/fire.py +0 -0
  61. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/importer.py +0 -0
  62. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/ordering.py +0 -0
  63. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/playlists/scheduler.py +0 -0
  64. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/promos/__init__.py +0 -0
  65. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/promos/director.py +0 -0
  66. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/__init__.py +0 -0
  67. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/ordering.py +0 -0
  68. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/poller.py +0 -0
  69. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/presence.py +0 -0
  70. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/race_poller.py +0 -0
  71. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/queue/shadow.py +0 -0
  72. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/__init__.py +0 -0
  73. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
  74. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_feedback.py +0 -0
  75. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
  76. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_moderation.py +0 -0
  77. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
  78. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_promos.py +0 -0
  79. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_queue.py +0 -0
  80. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
  81. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/auth.py +0 -0
  82. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/catalog.py +0 -0
  83. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/feedback.py +0 -0
  84. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/pages.py +0 -0
  85. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/queue.py +0 -0
  86. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/routes/user.py +0 -0
  87. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/static/css/main.css +0 -0
  88. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  89. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/static/js/admin-promos.js +0 -0
  90. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  91. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  92. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/static/js/admin.js +0 -0
  93. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/static/js/main.js +0 -0
  94. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/admin/index.html +0 -0
  95. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
  96. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/admin/promos.html +0 -0
  97. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  98. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
  99. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/auth/login.html +0 -0
  100. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/base.html +0 -0
  101. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
  102. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  103. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  104. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/feedback/index.html +0 -0
  105. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/queue/index.html +0 -0
  106. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/race.html +0 -0
  107. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
  108. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/ws/__init__.py +0 -0
  109. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/ws/handler.py +0 -0
  110. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/kryten_webqueue/ws/manager.py +0 -0
  111. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/__init__.py +0 -0
  112. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_config_persistence.py +0 -0
  113. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_feedback.py +0 -0
  114. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_fetchurls_sharepoint.py +0 -0
  115. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_phase1.py +0 -0
  116. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_phase2_jobs.py +0 -0
  117. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_phase3_jobs.py +0 -0
  118. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_phase4_live_fixes.py +0 -0
  119. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_playlist_import.py +0 -0
  120. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_presence_refund.py +0 -0
  121. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_promo_director.py +0 -0
  122. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_promo_pool_exclusion.py +0 -0
  123. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_queue_announce.py +0 -0
  124. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_race_poller.py +0 -0
  125. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_save_results_to_playlist.py +0 -0
  126. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_schedule_lock.py +0 -0
  127. {kryten_webqueue-0.29.0 → kryten_webqueue-0.30.0}/tests/test_search_facets.py +0 -0
@@ -5,6 +5,19 @@ 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
+
8
21
  ## [0.29.0] — 2026-07-04
9
22
 
10
23
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kryten-webqueue
3
- Version: 0.29.0
3
+ Version: 0.30.0
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
@@ -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
- await self._db.finish_sync_log(log_id, stats, "error")
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
- await self._db.finish_sync_log(log_id, stats, "error")
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
- await self._db.finish_job_run(run_id, "cancelled", None)
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
- await self._db.finish_job_run(
201
- run_id, "failed", json.dumps({"error": str(exc)})
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
- await self._db.finish_job_run(
206
- run_id, "failed", json.dumps({"error": f"{type(exc).__name__}: {exc}"})
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.29.0"
3
+ version = "0.30.0"
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"