kryten-webqueue 0.34.4__tar.gz → 0.34.7__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.34.4 → kryten_webqueue-0.34.7}/CHANGELOG.md +32 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/PKG-INFO +1 -1
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/catalog/db/_connection.py +11 -4
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/catalog/db/_playlists.py +5 -2
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/catalog/images.py +1 -1
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/fetchurls.py +3 -1
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/motdposters.py +15 -4
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/playedmovies.py +18 -4
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/jobs/job_scheduler.py +13 -4
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/playlists/fire.py +5 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/queue/shadow.py +27 -8
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/admin_job_schedules.py +6 -2
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/admin_schedules.py +17 -7
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/pyproject.toml +1 -1
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_phase4_live_fixes.py +132 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/.github/workflows/gitleaks.yml +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/.gitignore +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/.gitleaks.toml +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/.pre-commit-config.yaml +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/AGENT-WORKFLOW-GUIDE.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/README.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/config.example.json +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/Log for incorrect promo insertion problem.txt +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/SPEC_EMOTE_REHOST.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/catalog/db/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/catalog/db/_catalog.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/catalog/db/_feedback.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/catalog/db/_queue.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/queue/completion.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/queue/race_poller.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/admin_feedback.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/admin_moderation.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/feedback.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/static/js/admin-playlists.js +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/static/js/admin-promos.js +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/static/js/admin-schedules.js +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/static/js/admin.js +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/feedback/index.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/race.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/__init__.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_feedback.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_race_poller.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_recently_played_hide.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_schedule_lock.py +0 -0
- {kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/tests/test_search_facets.py +0 -0
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.34.7] - 2026-08-13
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Event lock lifts at the correct time (Bug #1).** The scheduled-event
|
|
10
|
+
pay-to-play lock was lifting ~30 minutes late when `last_item_uid` was never
|
|
11
|
+
captured (e.g. a CyTube `queue`-event timeout during bulk add left the field
|
|
12
|
+
`NULL`). With no uid anchor `_maybe_lift_event_lock` returned early and the
|
|
13
|
+
only fallback was the `estimated_end_at + 5 min` time safety net, which fires
|
|
14
|
+
roughly when the *last* item finishes rather than when it *starts*.
|
|
15
|
+
Fix: `fire_schedule` now always records `last_item_media_id` (the manifest
|
|
16
|
+
URL of the last scheduled item) even when the uid is unavailable. The lock
|
|
17
|
+
lift logic now matches on `media_id` first and falls back to `last_item_uid`,
|
|
18
|
+
so pay-to-play reopens as soon as the last scheduled item begins playing
|
|
19
|
+
regardless of whether the uid was captured. DB migration v12 adds the new
|
|
20
|
+
`last_item_media_id TEXT` column to `active_schedule`.
|
|
21
|
+
|
|
22
|
+
- **Schedule modifications apply immediately (Bug #2).** When a schedule was
|
|
23
|
+
updated via the admin UI, the APScheduler job was re-registered only if
|
|
24
|
+
`fire_at` was present in the request body. The `update_schedule` route now
|
|
25
|
+
always re-registers the job after any modification, and it reads the
|
|
26
|
+
authoritative `fire_at` from the database (not from the request body) to
|
|
27
|
+
avoid stale or mismatched values. If the updated `fire_at` is already in the
|
|
28
|
+
past, the job is not re-armed (preventing an accidental immediate re-fire).
|
|
29
|
+
|
|
30
|
+
- Cover art downloads for movies: corrected TMDB API parameter from `year` to
|
|
31
|
+
`primary_release_year` in movie search. TV searches already used the correct
|
|
32
|
+
`first_air_date_year` parameter, which is why cover art worked for TV but
|
|
33
|
+
not movies.
|
|
34
|
+
|
|
3
35
|
## [0.34.4] - 2026-08-12
|
|
4
36
|
|
|
5
37
|
### Changed
|
|
@@ -227,7 +227,14 @@ MIGRATIONS = [
|
|
|
227
227
|
ALTER TABLE queue_shadow ADD COLUMN promo_type TEXT;
|
|
228
228
|
ALTER TABLE queue_shadow ADD COLUMN lead_in_for_uid INTEGER;
|
|
229
229
|
""",
|
|
230
|
-
# v12:
|
|
230
|
+
# v12: Media-id fallback for the event-lock auto-lift. Stores the media_id
|
|
231
|
+
# of the last scheduled item so _maybe_lift_event_lock can detect when it
|
|
232
|
+
# begins playing even when last_item_uid was never captured (e.g. a CyTube
|
|
233
|
+
# queue-event timeout during bulk add left the uid null).
|
|
234
|
+
"""
|
|
235
|
+
ALTER TABLE active_schedule ADD COLUMN last_item_media_id TEXT;
|
|
236
|
+
""",
|
|
237
|
+
# v13: Viewer feedback + movie-title suggestions, each with a simple
|
|
231
238
|
# admin-triage queue (status new|read). Title suggestions record the
|
|
232
239
|
# resolved match against TMDB/OMDB (NULL when unresolved) and whether we
|
|
233
240
|
# already have the title in the catalog (resolution + catalog_token).
|
|
@@ -257,7 +264,7 @@ MIGRATIONS = [
|
|
|
257
264
|
);
|
|
258
265
|
CREATE INDEX IF NOT EXISTS idx_title_suggestions_status ON title_suggestions(status, created_at);
|
|
259
266
|
""",
|
|
260
|
-
#
|
|
267
|
+
# v14: Play-completion tracking that drives hiding recently-played catalog
|
|
261
268
|
# items from regular users.
|
|
262
269
|
#
|
|
263
270
|
# play_completions records a *genuine* completion (an item that reached the
|
|
@@ -290,7 +297,7 @@ MIGRATIONS = [
|
|
|
290
297
|
);
|
|
291
298
|
CREATE INDEX IF NOT EXISTS idx_playlist_item_played_media ON playlist_item_played(media_id);
|
|
292
299
|
""",
|
|
293
|
-
#
|
|
300
|
+
# v15: One-time cleanup of recently-played hide state wrongly recorded for
|
|
294
301
|
# promo-pool clips. Promos are excluded from the public catalog and must
|
|
295
302
|
# never be treated like normal playlist items, so any hide state recorded for
|
|
296
303
|
# a promo clip (before the exemption in record_play_completion) is purged.
|
|
@@ -308,7 +315,7 @@ MIGRATIONS = [
|
|
|
308
315
|
WHERE sp.promo_type IS NOT NULL AND spi.media_type = 'cm'
|
|
309
316
|
);
|
|
310
317
|
""",
|
|
311
|
-
#
|
|
318
|
+
# v16: Cron-based job schedule persistence (separate from playlist_schedules,
|
|
312
319
|
# which fires playlists onto the queue). One row per job; UNIQUE on job_name.
|
|
313
320
|
# run_next_job chains another job to run immediately after this one completes.
|
|
314
321
|
"""
|
|
@@ -242,11 +242,13 @@ class _PlaylistsMixin:
|
|
|
242
242
|
started_at: str,
|
|
243
243
|
estimated_end_at: str,
|
|
244
244
|
last_item_uid: int | None = None,
|
|
245
|
+
last_item_media_id: str | None = None,
|
|
245
246
|
):
|
|
246
247
|
await self._execute(
|
|
247
248
|
"INSERT OR REPLACE INTO active_schedule "
|
|
248
|
-
"(id, schedule_id, playlist_id, is_immutable, started_at, estimated_end_at,
|
|
249
|
-
"
|
|
249
|
+
"(id, schedule_id, playlist_id, is_immutable, started_at, estimated_end_at, "
|
|
250
|
+
"last_item_uid, last_item_media_id, lock_disabled) "
|
|
251
|
+
"VALUES (1, ?, ?, ?, ?, ?, ?, ?, 0)",
|
|
250
252
|
[
|
|
251
253
|
schedule_id,
|
|
252
254
|
playlist_id,
|
|
@@ -254,6 +256,7 @@ class _PlaylistsMixin:
|
|
|
254
256
|
started_at,
|
|
255
257
|
estimated_end_at,
|
|
256
258
|
last_item_uid,
|
|
259
|
+
last_item_media_id,
|
|
257
260
|
],
|
|
258
261
|
)
|
|
259
262
|
|
|
@@ -180,7 +180,7 @@ class CoverArtResolver:
|
|
|
180
180
|
"""Search a specific TMDB media type. Returns (poster_url_or_None, popularity)."""
|
|
181
181
|
params: dict = {"api_key": self._tmdb_key, "query": title}
|
|
182
182
|
if year and media_type == "movie":
|
|
183
|
-
params["
|
|
183
|
+
params["primary_release_year"] = year
|
|
184
184
|
elif year and media_type == "tv":
|
|
185
185
|
params["first_air_date_year"] = year
|
|
186
186
|
try:
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
@@ -1627,7 +1627,9 @@ def run(params: dict, *, config, progress=None) -> dict:
|
|
|
1627
1627
|
dry_run=dry_run,
|
|
1628
1628
|
progress=_emit,
|
|
1629
1629
|
)
|
|
1630
|
-
except
|
|
1630
|
+
except (
|
|
1631
|
+
Exception
|
|
1632
|
+
) as _pm_exc: # noqa: BLE001 - non-fatal; keep fetchurls result intact
|
|
1631
1633
|
logger.warning("played_movies sync failed: %s", _pm_exc, exc_info=True)
|
|
1632
1634
|
|
|
1633
1635
|
_emit(
|
|
@@ -210,7 +210,9 @@ def _pick_placeholder(placeholder_dir: str) -> bytes | None:
|
|
|
210
210
|
p = Path(placeholder_dir).expanduser()
|
|
211
211
|
if not p.is_dir():
|
|
212
212
|
return None
|
|
213
|
-
candidates = [
|
|
213
|
+
candidates = [
|
|
214
|
+
f for f in p.iterdir() if f.is_file() and f.suffix.lower() in _IMAGE_EXTS
|
|
215
|
+
]
|
|
214
216
|
if not candidates:
|
|
215
217
|
return None
|
|
216
218
|
return random.choice(candidates).read_bytes()
|
|
@@ -239,7 +241,11 @@ def _generate_html(night_entries: list[dict], poster_base_url: str) -> str:
|
|
|
239
241
|
|
|
240
242
|
img_url = f"{base}/{entry['filename']}"
|
|
241
243
|
imdb_id = entry.get("imdb_id") or ""
|
|
242
|
-
href =
|
|
244
|
+
href = (
|
|
245
|
+
f"https://www.imdb.com/title/{imdb_id}/"
|
|
246
|
+
if imdb_id
|
|
247
|
+
else "https://www.imdb.com/"
|
|
248
|
+
)
|
|
243
249
|
|
|
244
250
|
lines.append(
|
|
245
251
|
f' <a href="{escape(href)}" target="_blank" rel="noopener noreferrer">'
|
|
@@ -393,7 +399,10 @@ def run(params: dict, *, config, progress=None) -> dict:
|
|
|
393
399
|
entry["imdb_id"] = info["imdb_id"]
|
|
394
400
|
if dry_run:
|
|
395
401
|
logger.info(
|
|
396
|
-
"motd_posters [dry-run]: %s → %s (%s)",
|
|
402
|
+
"motd_posters [dry-run]: %s → %s (%s)",
|
|
403
|
+
title,
|
|
404
|
+
info["imdb_id"],
|
|
405
|
+
filename,
|
|
397
406
|
)
|
|
398
407
|
resolved += 1
|
|
399
408
|
else:
|
|
@@ -431,7 +440,9 @@ def run(params: dict, *, config, progress=None) -> dict:
|
|
|
431
440
|
output_path = output_dir / f"motd-{sheet_name}.html"
|
|
432
441
|
|
|
433
442
|
if dry_run:
|
|
434
|
-
logger.info(
|
|
443
|
+
logger.info(
|
|
444
|
+
"motd_posters [dry-run]: HTML snippet (not written):\n%s", html_snippet
|
|
445
|
+
)
|
|
435
446
|
else:
|
|
436
447
|
output_path.write_text(html_snippet, encoding="utf-8")
|
|
437
448
|
logger.info("motd_posters: wrote HTML snippet → %s", output_path)
|
|
@@ -161,7 +161,10 @@ def _write_rows(
|
|
|
161
161
|
|
|
162
162
|
sess_resp = requests.post(
|
|
163
163
|
f"{wb_base}/createSession",
|
|
164
|
-
headers={
|
|
164
|
+
headers={
|
|
165
|
+
"Authorization": f"Bearer {token}",
|
|
166
|
+
"Content-Type": "application/json",
|
|
167
|
+
},
|
|
165
168
|
data=json.dumps({"persistChanges": True}),
|
|
166
169
|
timeout=WRITEBACK_TIMEOUT,
|
|
167
170
|
)
|
|
@@ -169,7 +172,10 @@ def _write_rows(
|
|
|
169
172
|
if sess_resp.status_code in (200, 201):
|
|
170
173
|
sid = sess_resp.json().get("id", "")
|
|
171
174
|
|
|
172
|
-
headers = {
|
|
175
|
+
headers = {
|
|
176
|
+
"Authorization": f"Bearer {token}",
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
}
|
|
173
179
|
if sid:
|
|
174
180
|
headers["workbook-session-id"] = sid
|
|
175
181
|
|
|
@@ -261,7 +267,13 @@ def sync_played_movies(
|
|
|
261
267
|
|
|
262
268
|
if not new_rows:
|
|
263
269
|
logger.info("played_movies: all %d movie(s) already recorded", len(movies))
|
|
264
|
-
return {
|
|
270
|
+
return {
|
|
271
|
+
"found": len(movies),
|
|
272
|
+
"added": 0,
|
|
273
|
+
"skipped": skipped,
|
|
274
|
+
"failed": 0,
|
|
275
|
+
"dry_run": dry_run,
|
|
276
|
+
}
|
|
265
277
|
|
|
266
278
|
start_row = last_played_row(wb_bytes) + 1
|
|
267
279
|
|
|
@@ -282,7 +294,9 @@ def sync_played_movies(
|
|
|
282
294
|
if progress:
|
|
283
295
|
progress({"phase": "played_movies_write", "count": len(new_rows)})
|
|
284
296
|
|
|
285
|
-
ok, fail = _write_rows(
|
|
297
|
+
ok, fail = _write_rows(
|
|
298
|
+
graph_token, drive_id, item_id, new_rows, start_row, progress=progress
|
|
299
|
+
)
|
|
286
300
|
logger.info(
|
|
287
301
|
"played_movies: added %d/%d, skipped %d, failed %d",
|
|
288
302
|
ok,
|
|
@@ -68,7 +68,10 @@ class JobScheduler:
|
|
|
68
68
|
id=f"jsch_{job_name}",
|
|
69
69
|
replace_existing=True,
|
|
70
70
|
)
|
|
71
|
-
except (
|
|
71
|
+
except (
|
|
72
|
+
ValueError,
|
|
73
|
+
Exception,
|
|
74
|
+
) as exc: # noqa: BLE001 - bad stored cron must not crash startup
|
|
72
75
|
logger.warning(
|
|
73
76
|
"JobScheduler: skipping invalid cron %r for %r: %s",
|
|
74
77
|
cron_expression,
|
|
@@ -125,11 +128,17 @@ class JobScheduler:
|
|
|
125
128
|
minute, hour, day, month, day_of_week = parts
|
|
126
129
|
try:
|
|
127
130
|
CronTrigger(
|
|
128
|
-
minute=minute,
|
|
129
|
-
|
|
131
|
+
minute=minute,
|
|
132
|
+
hour=hour,
|
|
133
|
+
day=day,
|
|
134
|
+
month=month,
|
|
135
|
+
day_of_week=day_of_week,
|
|
136
|
+
timezone="UTC",
|
|
130
137
|
)
|
|
131
138
|
except Exception as exc:
|
|
132
|
-
raise ValueError(
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"Invalid cron expression {cron_expression!r}: {exc}"
|
|
141
|
+
) from exc
|
|
133
142
|
|
|
134
143
|
params_json = json.dumps(params) if params is not None else None
|
|
135
144
|
await self._db.upsert_job_schedule(
|
|
@@ -78,6 +78,7 @@ async def fire_schedule(
|
|
|
78
78
|
)
|
|
79
79
|
total_duration = 0
|
|
80
80
|
last_item_uid = None
|
|
81
|
+
last_item_media_id = None
|
|
81
82
|
for index, item in enumerate(items):
|
|
82
83
|
# Throttle consecutive adds so CyTube can validate each item before
|
|
83
84
|
# the next arrives (avoids transient queueFail/422 under load).
|
|
@@ -97,6 +98,9 @@ async def fire_schedule(
|
|
|
97
98
|
and add_result.get("uid") is not None
|
|
98
99
|
):
|
|
99
100
|
last_item_uid = add_result["uid"]
|
|
101
|
+
# Always track the last scheduled item's media_id so the
|
|
102
|
+
# event-lock can lift even when the uid was not captured.
|
|
103
|
+
last_item_media_id = item.get("media_id")
|
|
100
104
|
total_duration += item.get("duration_sec", 0) or 0
|
|
101
105
|
except Exception as e:
|
|
102
106
|
logger.warning(
|
|
@@ -150,6 +154,7 @@ async def fire_schedule(
|
|
|
150
154
|
started_at=now.isoformat(),
|
|
151
155
|
estimated_end_at=(now + timedelta(seconds=total_duration)).isoformat(),
|
|
152
156
|
last_item_uid=last_item_uid,
|
|
157
|
+
last_item_media_id=last_item_media_id,
|
|
153
158
|
)
|
|
154
159
|
|
|
155
160
|
# Mark schedule as fired
|
|
@@ -147,25 +147,44 @@ class QueueShadow:
|
|
|
147
147
|
scheduled fire, the curated event is effectively over (only the final
|
|
148
148
|
item remains), so pay-to-play should reopen for content that plays after
|
|
149
149
|
it. Idempotent: a no-op once the lock is already lifted.
|
|
150
|
+
|
|
151
|
+
Matching uses two anchors in priority order:
|
|
152
|
+
1. ``last_item_uid`` — the CyTube playlist uid assigned at add time.
|
|
153
|
+
Most reliable when the bot hasn't reconnected between fire and now.
|
|
154
|
+
2. ``last_item_media_id`` — the media id (manifest URL) of the last
|
|
155
|
+
scheduled item, tracked even when the uid was never captured (e.g.
|
|
156
|
+
the CyTube ``queue`` event timed out in the robot during bulk add).
|
|
150
157
|
"""
|
|
151
158
|
active = await self._db.get_active_schedule()
|
|
152
159
|
if not active or active.get("lock_disabled"):
|
|
153
160
|
return
|
|
154
161
|
last_uid = active.get("last_item_uid")
|
|
155
|
-
|
|
162
|
+
last_media_id = active.get("last_item_media_id")
|
|
163
|
+
if last_uid is None and last_media_id is None:
|
|
156
164
|
return
|
|
157
165
|
idx = self._now_playing_index()
|
|
158
166
|
if idx is None:
|
|
159
167
|
return
|
|
160
|
-
|
|
168
|
+
cur_item = self._items[idx]
|
|
169
|
+
cur_uid = cur_item.get("uid")
|
|
170
|
+
cur_media_id = cur_item.get("media_id")
|
|
161
171
|
try:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
uid_match = (
|
|
173
|
+
last_uid is not None
|
|
174
|
+
and cur_uid is not None
|
|
175
|
+
and int(cur_uid) == int(last_uid)
|
|
176
|
+
)
|
|
167
177
|
except (TypeError, ValueError):
|
|
168
|
-
|
|
178
|
+
uid_match = False
|
|
179
|
+
media_match = last_media_id is not None and cur_media_id == last_media_id
|
|
180
|
+
if uid_match or media_match:
|
|
181
|
+
await self._db.disable_active_lock()
|
|
182
|
+
logger.info(
|
|
183
|
+
"Scheduled-event lock lifted: last scheduled item now playing"
|
|
184
|
+
" (uid_match=%s media_match=%s)",
|
|
185
|
+
uid_match,
|
|
186
|
+
media_match,
|
|
187
|
+
)
|
|
169
188
|
|
|
170
189
|
async def _maybe_expire_active_schedule(self):
|
|
171
190
|
"""Clear the active-schedule row once the event is genuinely over.
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/admin_job_schedules.py
RENAMED
|
@@ -37,7 +37,9 @@ async def upsert_job_schedule(request: Request, user: dict = Depends(require_adm
|
|
|
37
37
|
if not cron_expression:
|
|
38
38
|
raise HTTPException(400, "cron_expression is required")
|
|
39
39
|
if len(cron_expression.split()) != 5:
|
|
40
|
-
raise HTTPException(
|
|
40
|
+
raise HTTPException(
|
|
41
|
+
400, "cron_expression must have exactly 5 fields (min hour dom mon dow)"
|
|
42
|
+
)
|
|
41
43
|
|
|
42
44
|
job_scheduler = request.app.state.job_scheduler
|
|
43
45
|
try:
|
|
@@ -64,7 +66,9 @@ async def update_job_schedule(
|
|
|
64
66
|
if not cron_expression:
|
|
65
67
|
raise HTTPException(400, "cron_expression is required")
|
|
66
68
|
if len(cron_expression.split()) != 5:
|
|
67
|
-
raise HTTPException(
|
|
69
|
+
raise HTTPException(
|
|
70
|
+
400, "cron_expression must have exactly 5 fields (min hour dom mon dow)"
|
|
71
|
+
)
|
|
68
72
|
|
|
69
73
|
job_scheduler = request.app.state.job_scheduler
|
|
70
74
|
try:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from fastapi import APIRouter, Request, Depends, HTTPException
|
|
2
|
-
from datetime import datetime
|
|
2
|
+
from datetime import datetime, UTC
|
|
3
3
|
|
|
4
4
|
from ..auth.session import require_admin
|
|
5
5
|
|
|
@@ -71,7 +71,13 @@ async def create_schedule(request: Request, user: dict = Depends(require_admin))
|
|
|
71
71
|
async def update_schedule(
|
|
72
72
|
request: Request, schedule_id: int, user: dict = Depends(require_admin)
|
|
73
73
|
):
|
|
74
|
-
"""Update a schedule.
|
|
74
|
+
"""Update a schedule.
|
|
75
|
+
|
|
76
|
+
Always re-registers the APScheduler job from the authoritative DB value
|
|
77
|
+
after the update, regardless of which fields changed. This ensures that
|
|
78
|
+
modifications made close to the fire time (including playlist-only changes)
|
|
79
|
+
take effect for the immediate scheduled item.
|
|
80
|
+
"""
|
|
75
81
|
body = await request.json()
|
|
76
82
|
db = request.app.state.db
|
|
77
83
|
scheduler = request.app.state.scheduler
|
|
@@ -82,11 +88,15 @@ async def update_schedule(
|
|
|
82
88
|
|
|
83
89
|
await db.update_schedule(schedule_id, **body)
|
|
84
90
|
|
|
85
|
-
# Re-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
# Re-read from DB so the scheduler always uses the authoritative fire_at
|
|
92
|
+
# (not the body value, which may differ due to serialization or race with
|
|
93
|
+
# a recurring re-arm). Only arm a future job; past schedules won't re-fire.
|
|
94
|
+
updated = await db.get_schedule(schedule_id)
|
|
95
|
+
await scheduler.remove_schedule(schedule_id)
|
|
96
|
+
if updated and updated.get("is_active"):
|
|
97
|
+
fire_at = scheduler._parse_fire_at(updated["fire_at"])
|
|
98
|
+
if fire_at > datetime.now(UTC):
|
|
99
|
+
await scheduler.add_schedule(schedule_id, fire_at)
|
|
90
100
|
|
|
91
101
|
return {"success": True}
|
|
92
102
|
|
|
@@ -338,3 +338,135 @@ async def test_fire_without_fallback_only_adds_event(db):
|
|
|
338
338
|
schedule_id=sid, api_gate=api, db=db, shadow=shadow, ws_manager=_FakeWs()
|
|
339
339
|
)
|
|
340
340
|
assert [a["media_id"] for a in api.added] == ["e1"]
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# --- Bug #1 regression: media_id fallback for event-lock lift ---
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
async def test_event_lock_lifts_via_media_id_when_uid_absent(db):
|
|
347
|
+
"""Lock lifts when now-playing matches last_item_media_id, even if
|
|
348
|
+
last_item_uid is None (uid not captured during bulk add)."""
|
|
349
|
+
pid = await db.create_saved_playlist(
|
|
350
|
+
name="E", description=None, is_immutable=True, created_by="admin"
|
|
351
|
+
)
|
|
352
|
+
sid = await db.create_schedule(
|
|
353
|
+
playlist_id=pid,
|
|
354
|
+
label="E",
|
|
355
|
+
fire_at=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S"),
|
|
356
|
+
is_active=1,
|
|
357
|
+
created_by="admin",
|
|
358
|
+
)
|
|
359
|
+
now = datetime.now(UTC)
|
|
360
|
+
await db.set_active_schedule(
|
|
361
|
+
schedule_id=sid,
|
|
362
|
+
playlist_id=pid,
|
|
363
|
+
is_immutable=True,
|
|
364
|
+
started_at=now.isoformat(),
|
|
365
|
+
estimated_end_at=(now + timedelta(hours=1)).isoformat(),
|
|
366
|
+
last_item_uid=None, # uid never captured
|
|
367
|
+
last_item_media_id="https://cms.example/media/last.json",
|
|
368
|
+
)
|
|
369
|
+
assert await db.is_event_lock_active() is True
|
|
370
|
+
|
|
371
|
+
shadow = QueueShadow(db)
|
|
372
|
+
# now_playing has no uid — CyTube's changeMedia payload — only id/type.
|
|
373
|
+
items = [_polled(1), _polled(2)]
|
|
374
|
+
# Override the second item's media_id to match last_item_media_id.
|
|
375
|
+
items[1] = {
|
|
376
|
+
"uid": 2,
|
|
377
|
+
"queueby": None,
|
|
378
|
+
"media": {
|
|
379
|
+
"id": "https://cms.example/media/last.json",
|
|
380
|
+
"type": "cm",
|
|
381
|
+
"title": "Last Item",
|
|
382
|
+
"seconds": 100,
|
|
383
|
+
},
|
|
384
|
+
}
|
|
385
|
+
np_no_uid = {
|
|
386
|
+
"id": "https://cms.example/media/last.json",
|
|
387
|
+
"type": "cm",
|
|
388
|
+
"seconds": 100,
|
|
389
|
+
"currentTime": 0,
|
|
390
|
+
}
|
|
391
|
+
await shadow.apply_poll_result(items, np_no_uid)
|
|
392
|
+
assert await db.is_event_lock_active() is False
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
async def test_event_lock_stays_when_earlier_item_plays_by_media_id(db):
|
|
396
|
+
"""Lock must NOT lift when an earlier item (not the last) is playing."""
|
|
397
|
+
pid = await db.create_saved_playlist(
|
|
398
|
+
name="E", description=None, is_immutable=True, created_by="admin"
|
|
399
|
+
)
|
|
400
|
+
sid = await db.create_schedule(
|
|
401
|
+
playlist_id=pid,
|
|
402
|
+
label="E",
|
|
403
|
+
fire_at=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S"),
|
|
404
|
+
is_active=1,
|
|
405
|
+
created_by="admin",
|
|
406
|
+
)
|
|
407
|
+
now = datetime.now(UTC)
|
|
408
|
+
await db.set_active_schedule(
|
|
409
|
+
schedule_id=sid,
|
|
410
|
+
playlist_id=pid,
|
|
411
|
+
is_immutable=True,
|
|
412
|
+
started_at=now.isoformat(),
|
|
413
|
+
estimated_end_at=(now + timedelta(hours=1)).isoformat(),
|
|
414
|
+
last_item_uid=None,
|
|
415
|
+
last_item_media_id="https://cms.example/media/last.json",
|
|
416
|
+
)
|
|
417
|
+
shadow = QueueShadow(db)
|
|
418
|
+
items = [_polled(1), _polled(2)]
|
|
419
|
+
items[1] = {
|
|
420
|
+
"uid": 2,
|
|
421
|
+
"queueby": None,
|
|
422
|
+
"media": {
|
|
423
|
+
"id": "https://cms.example/media/last.json",
|
|
424
|
+
"type": "cm",
|
|
425
|
+
"title": "Last",
|
|
426
|
+
"seconds": 100,
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
# Earlier item (uid=1 / id=m1) is playing — lock must stay on.
|
|
430
|
+
np_earlier = {"id": "m1", "type": "cm", "seconds": 100, "currentTime": 0}
|
|
431
|
+
await shadow.apply_poll_result(items, np_earlier)
|
|
432
|
+
assert await db.is_event_lock_active() is True
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
async def test_fire_stores_last_item_media_id(db):
|
|
436
|
+
"""fire_schedule must store last_item_media_id on the active_schedule row."""
|
|
437
|
+
from kryten_webqueue.playlists.fire import fire_schedule
|
|
438
|
+
|
|
439
|
+
pid = await db.create_saved_playlist(
|
|
440
|
+
name="Event", description=None, is_immutable=True, created_by="admin"
|
|
441
|
+
)
|
|
442
|
+
await db.replace_playlist_items(
|
|
443
|
+
pid,
|
|
444
|
+
[
|
|
445
|
+
{
|
|
446
|
+
"media_type": "cm",
|
|
447
|
+
"media_id": "https://cms/a.json",
|
|
448
|
+
"title": "A",
|
|
449
|
+
"duration_sec": 60,
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
"media_type": "cm",
|
|
453
|
+
"media_id": "https://cms/b.json",
|
|
454
|
+
"title": "B",
|
|
455
|
+
"duration_sec": 60,
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
)
|
|
459
|
+
sid = await db.create_schedule(
|
|
460
|
+
playlist_id=pid,
|
|
461
|
+
label="Test",
|
|
462
|
+
fire_at=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S"),
|
|
463
|
+
is_active=1,
|
|
464
|
+
created_by="admin",
|
|
465
|
+
)
|
|
466
|
+
api = _FakeApiGate()
|
|
467
|
+
shadow = QueueShadow(db)
|
|
468
|
+
await fire_schedule(
|
|
469
|
+
schedule_id=sid, api_gate=api, db=db, shadow=shadow, ws_manager=_FakeWs()
|
|
470
|
+
)
|
|
471
|
+
active = await db.get_active_schedule()
|
|
472
|
+
assert active["last_item_media_id"] == "https://cms/b.json"
|
|
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.34.4 → kryten_webqueue-0.34.7}/docs/Log for incorrect promo insertion problem.txt
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
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/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
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/routes/admin_moderation.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
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/static/js/admin-playlists.js
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/static/js/admin-queue-mgmt.js
RENAMED
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/static/js/admin-schedules.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/admin/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/admin/promos.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/feedback/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/kryten_webqueue/templates/queue/index.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.34.4 → kryten_webqueue-0.34.7}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|