kryten-webqueue 0.34.0__tar.gz → 0.34.2__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 (139) hide show
  1. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/CHANGELOG.md +24 -0
  2. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/catalog/db/_catalog.py +3 -5
  4. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/jobs/job_scheduler.py +44 -49
  5. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/jobs/manager.py +0 -17
  6. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/admin_job_schedules.py +23 -19
  7. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/static/js/admin.js +0 -13
  8. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/pyproject.toml +1 -1
  9. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/.github/workflows/gitleaks.yml +0 -0
  10. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/.github/workflows/python-publish.yml +0 -0
  11. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/.github/workflows/release.yml +0 -0
  12. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/.gitignore +0 -0
  13. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/.gitleaks.toml +0 -0
  14. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/.pre-commit-config.yaml +0 -0
  15. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/AGENT-WORKFLOW-GUIDE.md +0 -0
  16. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/README.md +0 -0
  17. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/config.example.json +0 -0
  18. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/deploy/kryten-webqueue.service +0 -0
  19. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/deploy/nginx-queue.conf +0 -0
  20. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/IMPLEMENTATION_SPEC.md +0 -0
  21. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/IMPL_API_GATE.md +0 -0
  22. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/IMPL_ECONOMY.md +0 -0
  23. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/IMPL_KRYTEN_PY.md +0 -0
  24. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/IMPL_ROBOT.md +0 -0
  25. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  26. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  27. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/Log for incorrect promo insertion problem.txt +0 -0
  28. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  29. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/PRE_PLAN_GAPS.md +0 -0
  30. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/PRODUCT_PLAN.md +0 -0
  31. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/SPEC_EMOTE_REHOST.md +0 -0
  32. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  33. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/docs/UX_POLISH_PLAN.md +0 -0
  34. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/__init__.py +0 -0
  35. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/__main__.py +0 -0
  36. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/api_gate/__init__.py +0 -0
  37. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/api_gate/client.py +0 -0
  38. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/app.py +0 -0
  39. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/auth/__init__.py +0 -0
  40. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/auth/otp.py +0 -0
  41. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/auth/rate_limit.py +0 -0
  42. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/auth/session.py +0 -0
  43. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/catalog/__init__.py +0 -0
  44. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/catalog/db/__init__.py +0 -0
  45. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/catalog/db/_connection.py +0 -0
  46. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  47. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  48. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/catalog/db/_queue.py +0 -0
  49. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/catalog/images.py +0 -0
  50. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/catalog/mediacms.py +0 -0
  51. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/catalog/sync.py +0 -0
  52. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/config.py +0 -0
  53. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/integrations/__init__.py +0 -0
  54. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  55. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  56. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  57. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  58. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  59. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  60. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  61. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  62. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  63. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  64. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/jobs/__init__.py +0 -0
  65. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  66. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  67. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/jobs/tasks.py +0 -0
  68. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/logging_config.py +0 -0
  69. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/playlists/__init__.py +0 -0
  70. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/playlists/bulk_add.py +0 -0
  71. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/playlists/fire.py +0 -0
  72. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/playlists/importer.py +0 -0
  73. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/playlists/ordering.py +0 -0
  74. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/playlists/scheduler.py +0 -0
  75. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/promos/__init__.py +0 -0
  76. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/promos/director.py +0 -0
  77. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/queue/__init__.py +0 -0
  78. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/queue/completion.py +0 -0
  79. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/queue/ordering.py +0 -0
  80. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/queue/poller.py +0 -0
  81. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/queue/presence.py +0 -0
  82. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/queue/race_poller.py +0 -0
  83. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/queue/shadow.py +0 -0
  84. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/__init__.py +0 -0
  85. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/admin_catalog.py +0 -0
  86. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/admin_feedback.py +0 -0
  87. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/admin_jobs.py +0 -0
  88. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/admin_moderation.py +0 -0
  89. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/admin_playlists.py +0 -0
  90. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/admin_promos.py +0 -0
  91. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/admin_queue.py +0 -0
  92. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/admin_schedules.py +0 -0
  93. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/auth.py +0 -0
  94. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/catalog.py +0 -0
  95. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/feedback.py +0 -0
  96. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/pages.py +0 -0
  97. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/queue.py +0 -0
  98. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/routes/user.py +0 -0
  99. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/static/css/main.css +0 -0
  100. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  101. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/static/js/admin-promos.js +0 -0
  102. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  103. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  104. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/static/js/main.js +0 -0
  105. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/admin/index.html +0 -0
  106. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/admin/playlists.html +0 -0
  107. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/admin/promos.html +0 -0
  108. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  109. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/admin/schedules.html +0 -0
  110. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/auth/login.html +0 -0
  111. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/base.html +0 -0
  112. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/catalog/browse.html +0 -0
  113. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  114. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  115. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/feedback/index.html +0 -0
  116. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/queue/index.html +0 -0
  117. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/race.html +0 -0
  118. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/templates/user/dashboard.html +0 -0
  119. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/ws/__init__.py +0 -0
  120. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/ws/handler.py +0 -0
  121. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/kryten_webqueue/ws/manager.py +0 -0
  122. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/__init__.py +0 -0
  123. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_config_persistence.py +0 -0
  124. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_feedback.py +0 -0
  125. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_fetchurls_sharepoint.py +0 -0
  126. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_phase1.py +0 -0
  127. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_phase2_jobs.py +0 -0
  128. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_phase3_jobs.py +0 -0
  129. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_phase4_live_fixes.py +0 -0
  130. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_playlist_import.py +0 -0
  131. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_presence_refund.py +0 -0
  132. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_promo_director.py +0 -0
  133. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_promo_pool_exclusion.py +0 -0
  134. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_queue_announce.py +0 -0
  135. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_race_poller.py +0 -0
  136. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_recently_played_hide.py +0 -0
  137. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_save_results_to_playlist.py +0 -0
  138. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_schedule_lock.py +0 -0
  139. {kryten_webqueue-0.34.0 → kryten_webqueue-0.34.2}/tests/test_search_facets.py +0 -0
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.34.2] - 2026-08-12
4
+
5
+ ### Removed
6
+
7
+ - Job chaining: removed the "Run next" chain feature entirely.
8
+ Removed `_on_job_complete()` from `JobScheduler`, `set_post_run_hook()` and
9
+ `_post_run_hook` from `JobManager`, `run_next_job` from `upsert_job_schedule()`,
10
+ the `run_next_job` field from POST/PUT API bodies, and the "Run next"
11
+ dropdown from the schedule edit modal. The `run_next_job` DB column is
12
+ preserved (unused) for schema compatibility.
13
+
14
+ ## [0.34.1] - 2026-08-12
15
+
16
+ ### Fixed
17
+
18
+ - `JobScheduler._register()`: bad cron expressions in the DB (e.g. hour=27)
19
+ are now logged at WARNING and skipped rather than raising `ValueError` during
20
+ startup and preventing the service from starting.
21
+ - `JobScheduler.upsert()`: validates the cron expression via APScheduler before
22
+ persisting; raises `ValueError` with a clear message so the API route can
23
+ return HTTP 400 instead of saving an unrunnable schedule.
24
+ - `/admin/job-schedules` POST and PUT: APScheduler validation errors now surface
25
+ as HTTP 400 with the rejection reason.
26
+
3
27
  ## [0.34.0] - 2026-08-11
4
28
 
5
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.34.0
3
+ Version: 0.34.2
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
@@ -926,23 +926,21 @@ class _CatalogMixin:
926
926
  label: str | None = None,
927
927
  params_json: str | None = None,
928
928
  is_active: bool = True,
929
- run_next_job: str | None = None,
930
929
  created_by: str | None = None,
931
930
  ) -> None:
932
931
  await self._db.execute(
933
932
  """
934
933
  INSERT INTO job_schedules
935
- (job_name, label, cron_expression, params_json, is_active, run_next_job, created_by)
936
- VALUES (?, ?, ?, ?, ?, ?, ?)
934
+ (job_name, label, cron_expression, params_json, is_active, created_by)
935
+ VALUES (?, ?, ?, ?, ?, ?)
937
936
  ON CONFLICT(job_name) DO UPDATE SET
938
937
  label = excluded.label,
939
938
  cron_expression = excluded.cron_expression,
940
939
  params_json = excluded.params_json,
941
940
  is_active = excluded.is_active,
942
- run_next_job = excluded.run_next_job,
943
941
  updated_at = datetime('now')
944
942
  """,
945
- [job_name, label, cron_expression, params_json, int(is_active), run_next_job, created_by],
943
+ [job_name, label, cron_expression, params_json, int(is_active), created_by],
946
944
  )
947
945
 
948
946
  async def delete_job_schedule(self, job_name: str) -> None:
@@ -3,10 +3,6 @@
3
3
  Persists schedules to the ``job_schedules`` table and fires them via
4
4
  APScheduler's ``CronTrigger``. Completely separate from ``PlaylistScheduler``,
5
5
  which fires playlists onto the live queue.
6
-
7
- Chain support: when a job finishes with status ``completed`` and was triggered
8
- by the scheduler, the scheduler checks ``run_next_job`` on that schedule and
9
- immediately launches the next job using its own saved params.
10
6
  """
11
7
 
12
8
  from __future__ import annotations
@@ -30,7 +26,6 @@ class JobScheduler:
30
26
 
31
27
  async def start(self) -> None:
32
28
  self._aps.start()
33
- self._jm.set_post_run_hook(self._on_job_complete)
34
29
  await self._reload()
35
30
  logger.info("JobScheduler started")
36
31
 
@@ -57,21 +52,30 @@ class JobScheduler:
57
52
  )
58
53
  return False
59
54
  minute, hour, day, month, day_of_week = parts
60
- trigger = CronTrigger(
61
- minute=minute,
62
- hour=hour,
63
- day=day,
64
- month=month,
65
- day_of_week=day_of_week,
66
- timezone="UTC",
67
- )
68
- self._aps.add_job(
69
- self._fire,
70
- trigger=trigger,
71
- args=[job_name],
72
- id=f"jsch_{job_name}",
73
- replace_existing=True,
74
- )
55
+ try:
56
+ trigger = CronTrigger(
57
+ minute=minute,
58
+ hour=hour,
59
+ day=day,
60
+ month=month,
61
+ day_of_week=day_of_week,
62
+ timezone="UTC",
63
+ )
64
+ self._aps.add_job(
65
+ self._fire,
66
+ trigger=trigger,
67
+ args=[job_name],
68
+ id=f"jsch_{job_name}",
69
+ replace_existing=True,
70
+ )
71
+ except (ValueError, Exception) as exc: # noqa: BLE001 - bad stored cron must not crash startup
72
+ logger.warning(
73
+ "JobScheduler: skipping invalid cron %r for %r: %s",
74
+ cron_expression,
75
+ job_name,
76
+ exc,
77
+ )
78
+ return False
75
79
  logger.info("JobScheduler: registered %r cron=%r", job_name, cron_expression)
76
80
  return True
77
81
 
@@ -95,32 +99,6 @@ class JobScheduler:
95
99
  except ValueError as exc:
96
100
  logger.warning("JobScheduler: %r invalid params: %s", job_name, exc)
97
101
 
98
- async def _on_job_complete(
99
- self, job_name: str, status: str, triggered_by: str | None
100
- ) -> None:
101
- """Post-run hook: chain to run_next_job if the scheduler triggered this run."""
102
- if triggered_by != "scheduler" or status != "completed":
103
- return
104
- sched = await self._db.get_job_schedule(job_name)
105
- if not sched or not sched.get("run_next_job"):
106
- return
107
- next_job: str = sched["run_next_job"]
108
- next_sched = await self._db.get_job_schedule(next_job)
109
- params = (
110
- json.loads(next_sched["params_json"])
111
- if next_sched and next_sched.get("params_json")
112
- else None
113
- )
114
- try:
115
- await self._jm.run(next_job, triggered_by="scheduler", params=params)
116
- logger.info("JobScheduler: chained %r → %r", job_name, next_job)
117
- except KeyError:
118
- logger.warning("JobScheduler: chain target %r is not registered", next_job)
119
- except ValueError as exc:
120
- logger.warning(
121
- "JobScheduler: chain %r → %r invalid params: %s", job_name, next_job, exc
122
- )
123
-
124
102
  # ── Public API ─────────────────────────────────────────────────────────────
125
103
 
126
104
  async def upsert(
@@ -130,11 +108,29 @@ class JobScheduler:
130
108
  *,
131
109
  params: dict | None = None,
132
110
  label: str | None = None,
133
- run_next_job: str | None = None,
134
111
  is_active: bool = True,
135
112
  created_by: str | None = None,
136
113
  ) -> None:
137
- """Persist a schedule and (re)register it with APScheduler."""
114
+ """Persist a schedule and (re)register it with APScheduler.
115
+
116
+ Raises ValueError if cron_expression is rejected by APScheduler.
117
+ """
118
+ # Validate before persisting so a bad expression is rejected at save time.
119
+ if is_active:
120
+ parts = (cron_expression or "").strip().split()
121
+ if len(parts) != 5:
122
+ raise ValueError(
123
+ f"cron_expression must have exactly 5 fields (min hour dom mon dow), got: {cron_expression!r}"
124
+ )
125
+ minute, hour, day, month, day_of_week = parts
126
+ try:
127
+ CronTrigger(
128
+ minute=minute, hour=hour, day=day,
129
+ month=month, day_of_week=day_of_week, timezone="UTC",
130
+ )
131
+ except Exception as exc:
132
+ raise ValueError(f"Invalid cron expression {cron_expression!r}: {exc}") from exc
133
+
138
134
  params_json = json.dumps(params) if params is not None else None
139
135
  await self._db.upsert_job_schedule(
140
136
  job_name,
@@ -142,7 +138,6 @@ class JobScheduler:
142
138
  label=label,
143
139
  params_json=params_json,
144
140
  is_active=is_active,
145
- run_next_job=run_next_job,
146
141
  created_by=created_by,
147
142
  )
148
143
  self._unregister(job_name)
@@ -139,14 +139,6 @@ class JobManager:
139
139
  self._config = config
140
140
  self._jobs: dict[str, dict] = {}
141
141
  self._running: dict[str, asyncio.Task] = {}
142
- self._post_run_hook: Callable | None = None
143
-
144
- def set_post_run_hook(self, hook: "Callable[[str, str, str | None], Any]") -> None:
145
- """Register a coroutine called after each job finishes.
146
-
147
- Signature: async hook(job_name, status, triggered_by) -> None
148
- """
149
- self._post_run_hook = hook
150
142
 
151
143
  def register(
152
144
  self,
@@ -232,14 +224,11 @@ class JobManager:
232
224
  run_id=run_id,
233
225
  triggered_by=triggered_by,
234
226
  )
235
- _final_status = "failed"
236
227
  try:
237
228
  result = await func(params, ctx)
238
229
  detail = json.dumps(result) if result is not None else None
239
230
  await self._db.finish_job_run(run_id, "completed", detail)
240
- _final_status = "completed"
241
231
  except asyncio.CancelledError:
242
- _final_status = "cancelled"
243
232
  try:
244
233
  await self._db.finish_job_run(run_id, "cancelled", None)
245
234
  except Exception: # noqa: BLE001
@@ -276,9 +265,3 @@ class JobManager:
276
265
  )
277
266
  finally:
278
267
  self._running.pop(name, None)
279
- hook = self._post_run_hook
280
- if hook:
281
- try:
282
- await hook(name, _final_status, triggered_by)
283
- except Exception: # noqa: BLE001
284
- logger.debug("post_run_hook failed for '%s'", name, exc_info=True)
@@ -26,7 +26,7 @@ async def get_job_schedule(
26
26
  async def upsert_job_schedule(request: Request, user: dict = Depends(require_admin)):
27
27
  """Create or update a job schedule.
28
28
 
29
- Body: {job_name, cron_expression, params?, label?, run_next_job?, is_active?}
29
+ Body: {job_name, cron_expression, params?, label?, is_active?}
30
30
  cron_expression must be a standard 5-field cron string (min hour dom mon dow).
31
31
  """
32
32
  body = await request.json()
@@ -40,15 +40,17 @@ async def upsert_job_schedule(request: Request, user: dict = Depends(require_adm
40
40
  raise HTTPException(400, "cron_expression must have exactly 5 fields (min hour dom mon dow)")
41
41
 
42
42
  job_scheduler = request.app.state.job_scheduler
43
- await job_scheduler.upsert(
44
- job_name,
45
- cron_expression,
46
- params=body.get("params") or None,
47
- label=body.get("label") or None,
48
- run_next_job=body.get("run_next_job") or None,
49
- is_active=bool(body.get("is_active", True)),
50
- created_by=user["username"],
51
- )
43
+ try:
44
+ await job_scheduler.upsert(
45
+ job_name,
46
+ cron_expression,
47
+ params=body.get("params") or None,
48
+ label=body.get("label") or None,
49
+ is_active=bool(body.get("is_active", True)),
50
+ created_by=user["username"],
51
+ )
52
+ except ValueError as exc:
53
+ raise HTTPException(400, str(exc))
52
54
  return {"success": True}
53
55
 
54
56
 
@@ -65,15 +67,17 @@ async def update_job_schedule(
65
67
  raise HTTPException(400, "cron_expression must have exactly 5 fields (min hour dom mon dow)")
66
68
 
67
69
  job_scheduler = request.app.state.job_scheduler
68
- await job_scheduler.upsert(
69
- job_name,
70
- cron_expression,
71
- params=body.get("params") or None,
72
- label=body.get("label") or None,
73
- run_next_job=body.get("run_next_job") or None,
74
- is_active=bool(body.get("is_active", True)),
75
- created_by=user["username"],
76
- )
70
+ try:
71
+ await job_scheduler.upsert(
72
+ job_name,
73
+ cron_expression,
74
+ params=body.get("params") or None,
75
+ label=body.get("label") or None,
76
+ is_active=bool(body.get("is_active", True)),
77
+ created_by=user["username"],
78
+ )
79
+ except ValueError as exc:
80
+ raise HTTPException(400, str(exc))
77
81
  return {"success": True}
78
82
 
79
83
 
@@ -145,19 +145,12 @@ async function openScheduleModal(jobName) {
145
145
 
146
146
  const cronVal = existing ? escapeHtml(existing.cron_expression || '') : '';
147
147
  const isActive = existing ? !!existing.is_active : true;
148
- const runNextVal = existing ? (existing.run_next_job || '') : '';
149
148
 
150
149
  const paramFields = job.schema.map(f => {
151
150
  const savedVal = existingParams[f.name] !== undefined ? existingParams[f.name] : f.default;
152
151
  return _jobFieldHtml({...f, default: savedVal}, 'schedparam-');
153
152
  }).join('');
154
153
 
155
- const otherJobs = JOBS_CACHE.filter(j => j.name !== jobName);
156
- const nextOpts = '<option value="">\u2014 none (no chain) \u2014</option>' +
157
- otherJobs.map(j =>
158
- `<option value="${escapeHtml(j.name)}" ${j.name === runNextVal ? 'selected' : ''}>${escapeHtml(j.label)}</option>`
159
- ).join('');
160
-
161
154
  const overlay = document.createElement('div');
162
155
  overlay.id = 'schedule-modal';
163
156
  overlay.className = 'modal-overlay';
@@ -173,10 +166,6 @@ async function openScheduleModal(jobName) {
173
166
  <span>Active</span>
174
167
  </label>
175
168
  ${paramFields ? `<details ${existing ? 'open' : ''}><summary style="cursor:pointer;margin:.5rem 0">Job parameters</summary><div class="job-params">${paramFields}</div></details>` : ''}
176
- <label class="field">
177
- <span>Run next (chain)</span>
178
- <select id="sched-run-next">${nextOpts}</select>
179
- </label>
180
169
  <div class="modal-actions">
181
170
  <button class="btn btn-secondary" data-action="cancel">Cancel</button>
182
171
  <button class="btn btn-primary" data-action="save">Save</button>
@@ -196,7 +185,6 @@ async function _submitScheduleModal(jobName, job, overlay) {
196
185
  const cron = (document.getElementById('sched-cron').value || '').trim();
197
186
  if (!cron) { showToast('Cron expression is required', 'error'); return; }
198
187
  const isActive = document.getElementById('sched-active').checked;
199
- const runNext = document.getElementById('sched-run-next').value || null;
200
188
 
201
189
  const params = {};
202
190
  for (const f of job.schema) {
@@ -216,7 +204,6 @@ async function _submitScheduleModal(jobName, job, overlay) {
216
204
  job_name: jobName,
217
205
  cron_expression: cron,
218
206
  is_active: isActive,
219
- run_next_job: runNext,
220
207
  params: Object.keys(params).length ? params : null,
221
208
  label: job.label,
222
209
  }),
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.34.0"
3
+ version = "0.34.2"
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"