kryten-webqueue 0.32.1__tar.gz → 0.32.3__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.32.1 → kryten_webqueue-0.32.3}/CHANGELOG.md +14 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/PKG-INFO +1 -1
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/app.py +10 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/catalog/db/_catalog.py +62 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/catalog/db/_connection.py +18 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/pyproject.toml +1 -1
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_recently_played_hide.py +81 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/.gitignore +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/README.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/config.example.json +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/Log for incorrect promo insertion problem.txt +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/catalog/db/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/catalog/db/_feedback.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/catalog/db/_playlists.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/catalog/db/_queue.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/queue/completion.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/queue/race_poller.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/admin_feedback.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/admin_moderation.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/feedback.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/static/js/admin-playlists.js +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/static/js/admin-promos.js +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/static/js/admin-schedules.js +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/static/js/admin.js +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/feedback/index.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/race.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/__init__.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_feedback.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_race_poller.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_schedule_lock.py +0 -0
- {kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/tests/test_search_facets.py +0 -0
|
@@ -5,6 +5,18 @@ 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.32.3] — 2026-07-09
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Tag/category-classified promos are now also exempt from recently-played hiding.** v0.32.2 only exempted promo-*pool* clips, but most station promos/bumpers (e.g. “CHANNEL Z …”) are classified by hidden category (`Z Channel Promos`) or tag (`channelz`, `bumpers`, …) instead, so they were still being recorded and appeared in the recently-played debug list. `record_play_completion` now skips any item hidden from the public catalog by a promo pool **or** a hidden category/tag. On startup the service self-heals by purging any such rows written by earlier builds (`purge_promo_hide_state`, now covering the tag/category case).
|
|
13
|
+
|
|
14
|
+
## [0.32.2] — 2026-07-09
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Promo clips are exempt from recently-played hiding.** Playing a promo-pool clip no longer records any hide state — promos are already excluded from the public catalog and must not be treated like normal mutable/immutable playlist items. `record_play_completion` now skips promo-pool members entirely. A one-time migration (v14) purges any promo hide state recorded before this fix; the same cleanup is also available on-demand via `purge_promo_hide_state()`.
|
|
19
|
+
|
|
8
20
|
## [0.32.1] — 2026-07-09
|
|
9
21
|
|
|
10
22
|
### Added
|
|
@@ -28,6 +40,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
28
40
|
|
|
29
41
|
[0.32.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.32.0
|
|
30
42
|
[0.32.1]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.32.1
|
|
43
|
+
[0.32.2]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.32.2
|
|
44
|
+
[0.32.3]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.32.3
|
|
31
45
|
|
|
32
46
|
## [0.31.0] — 2026-07-04
|
|
33
47
|
|
|
@@ -51,6 +51,16 @@ async def lifespan(app: FastAPI):
|
|
|
51
51
|
db = Database(config.db_path)
|
|
52
52
|
await db.connect()
|
|
53
53
|
await db.run_migrations()
|
|
54
|
+
# Self-heal: purge any recently-played hide state recorded for promos/bumpers
|
|
55
|
+
# (they are excluded from the public catalog and must never be hidden by the
|
|
56
|
+
# recently-played rules). Cheap and idempotent; also cleans rows written by
|
|
57
|
+
# older builds that classified promos only by pool membership.
|
|
58
|
+
purged = await db.purge_promo_hide_state()
|
|
59
|
+
if purged["completions"] or purged["playlist_pass"]:
|
|
60
|
+
logger.info(
|
|
61
|
+
"Purged promo recently-played state: %d completion(s), %d pass row(s)",
|
|
62
|
+
purged["completions"], purged["playlist_pass"],
|
|
63
|
+
)
|
|
54
64
|
# Any job run still marked 'running' is an orphan from a prior crash/restart
|
|
55
65
|
# (the running flag is in-memory only). Reconcile before registering jobs.
|
|
56
66
|
orphaned = await db.reconcile_orphaned_job_runs()
|
|
@@ -72,6 +72,33 @@ def _hidden_exclusion(alias: str = "c") -> tuple[str, list]:
|
|
|
72
72
|
return sql, [*HIDDEN_CATEGORY_NAMES, *HIDDEN_TAG_NAMES]
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
def _promo_hidden_media_subquery() -> tuple[str, list]:
|
|
76
|
+
"""Subquery (+ params) yielding media_ids that are promos/bumpers.
|
|
77
|
+
|
|
78
|
+
"Promo/bumper" content is anything hidden from the public catalog by a promo
|
|
79
|
+
pool OR a hidden category OR a hidden tag (e.g. ``Z Channel Promos`` /
|
|
80
|
+
``channelz``). These items never appear to regular users, so they must never
|
|
81
|
+
be recorded as recently-played. Immutable *event* playlists are intentionally
|
|
82
|
+
excluded here — they are their own class, not promos.
|
|
83
|
+
"""
|
|
84
|
+
cat_ph = ",".join("?" * len(HIDDEN_CATEGORY_NAMES))
|
|
85
|
+
tag_ph = ",".join("?" * len(HIDDEN_TAG_NAMES))
|
|
86
|
+
sql = f"""
|
|
87
|
+
SELECT spi.media_id FROM saved_playlist_items spi
|
|
88
|
+
JOIN saved_playlists sp ON sp.id = spi.playlist_id
|
|
89
|
+
WHERE sp.promo_type IS NOT NULL AND spi.media_type = 'cm'
|
|
90
|
+
UNION
|
|
91
|
+
SELECT cc.friendly_token FROM catalog_categories cc
|
|
92
|
+
JOIN categories cat ON cc.category_id = cat.id
|
|
93
|
+
WHERE cat.name IN ({cat_ph})
|
|
94
|
+
UNION
|
|
95
|
+
SELECT ct.friendly_token FROM catalog_tags ct
|
|
96
|
+
JOIN tags t ON ct.tag_id = t.id
|
|
97
|
+
WHERE t.name IN ({tag_ph})
|
|
98
|
+
"""
|
|
99
|
+
return sql, [*HIDDEN_CATEGORY_NAMES, *HIDDEN_TAG_NAMES]
|
|
100
|
+
|
|
101
|
+
|
|
75
102
|
def _recently_played_exclusion(alias: str, days: int) -> tuple[str, list]:
|
|
76
103
|
"""SQL fragment (+ params) hiding recently-played items from regular users.
|
|
77
104
|
|
|
@@ -360,7 +387,20 @@ class _CatalogMixin:
|
|
|
360
387
|
collection releases together. Everything else (movies, standalone clips,
|
|
361
388
|
long items even inside a mutable playlist) gets a time-boxed
|
|
362
389
|
``play_completions`` row instead.
|
|
390
|
+
|
|
391
|
+
Promo-pool clips are exempt entirely: they are already excluded from the
|
|
392
|
+
public catalog and must never be treated like normal (mutable/immutable)
|
|
393
|
+
playlist items, so playing one records nothing. The same exemption covers
|
|
394
|
+
any promo/bumper hidden by category or tag (e.g. ``Z Channel Promos`` /
|
|
395
|
+
``channelz``), which is how most station promos are actually classified.
|
|
363
396
|
"""
|
|
397
|
+
sub, sub_params = _promo_hidden_media_subquery()
|
|
398
|
+
promo = await self._fetch_one(
|
|
399
|
+
f"SELECT 1 WHERE ? IN ({sub})",
|
|
400
|
+
[friendly_token, *sub_params],
|
|
401
|
+
)
|
|
402
|
+
if promo:
|
|
403
|
+
return
|
|
364
404
|
rows = await self._fetch_all(
|
|
365
405
|
"""
|
|
366
406
|
SELECT spi.playlist_id AS playlist_id, spi.position AS position,
|
|
@@ -435,6 +475,28 @@ class _CatalogMixin:
|
|
|
435
475
|
)
|
|
436
476
|
return {"completions": pc["c"] if pc else 0, "playlist_pass": pip["c"] if pip else 0}
|
|
437
477
|
|
|
478
|
+
async def purge_promo_hide_state(self) -> dict:
|
|
479
|
+
"""Remove any recently-played hide state recorded for promos/bumpers.
|
|
480
|
+
|
|
481
|
+
Promos must never be subject to recently-played hiding. New completions
|
|
482
|
+
are already skipped in ``record_play_completion``; this purges rows
|
|
483
|
+
written before that exemption existed (run once at startup). Covers promo
|
|
484
|
+
pools plus hidden-category/tag promos (the usual station-promo case).
|
|
485
|
+
Returns the row counts removed.
|
|
486
|
+
"""
|
|
487
|
+
sub, sub_params = _promo_hidden_media_subquery()
|
|
488
|
+
pc = await self._fetch_one(
|
|
489
|
+
f"SELECT COUNT(*) AS c FROM play_completions WHERE media_id IN ({sub})",
|
|
490
|
+
sub_params,
|
|
491
|
+
)
|
|
492
|
+
pip = await self._fetch_one(
|
|
493
|
+
f"SELECT COUNT(*) AS c FROM playlist_item_played WHERE media_id IN ({sub})",
|
|
494
|
+
sub_params,
|
|
495
|
+
)
|
|
496
|
+
await self._execute(f"DELETE FROM play_completions WHERE media_id IN ({sub})", sub_params)
|
|
497
|
+
await self._execute(f"DELETE FROM playlist_item_played WHERE media_id IN ({sub})", sub_params)
|
|
498
|
+
return {"completions": pc["c"] if pc else 0, "playlist_pass": pip["c"] if pip else 0}
|
|
499
|
+
|
|
438
500
|
async def get_recently_played_debug(self, days: int) -> dict:
|
|
439
501
|
"""Snapshot of what the recently-played rules currently hide (admin test).
|
|
440
502
|
|
|
@@ -291,6 +291,24 @@ MIGRATIONS = [
|
|
|
291
291
|
);
|
|
292
292
|
CREATE INDEX IF NOT EXISTS idx_playlist_item_played_media ON playlist_item_played(media_id);
|
|
293
293
|
""",
|
|
294
|
+
# v14: One-time cleanup of recently-played hide state wrongly recorded for
|
|
295
|
+
# promo-pool clips. Promos are excluded from the public catalog and must
|
|
296
|
+
# never be treated like normal playlist items, so any hide state recorded for
|
|
297
|
+
# a promo clip (before the exemption in record_play_completion) is purged.
|
|
298
|
+
"""
|
|
299
|
+
DELETE FROM play_completions
|
|
300
|
+
WHERE media_id IN (
|
|
301
|
+
SELECT spi.media_id FROM saved_playlist_items spi
|
|
302
|
+
JOIN saved_playlists sp ON sp.id = spi.playlist_id
|
|
303
|
+
WHERE sp.promo_type IS NOT NULL AND spi.media_type = 'cm'
|
|
304
|
+
);
|
|
305
|
+
DELETE FROM playlist_item_played
|
|
306
|
+
WHERE media_id IN (
|
|
307
|
+
SELECT spi.media_id FROM saved_playlist_items spi
|
|
308
|
+
JOIN saved_playlists sp ON sp.id = spi.playlist_id
|
|
309
|
+
WHERE sp.promo_type IS NOT NULL AND spi.media_type = 'cm'
|
|
310
|
+
);
|
|
311
|
+
""",
|
|
294
312
|
]
|
|
295
313
|
|
|
296
314
|
|
|
@@ -349,4 +349,85 @@ async def test_recently_played_debug_reports_both_signals(db):
|
|
|
349
349
|
assert {r["media_id"] for r in debug["by_playlist_pass"]} == {"ep1"}
|
|
350
350
|
|
|
351
351
|
|
|
352
|
+
async def test_promo_clips_are_exempt_from_hiding(db):
|
|
353
|
+
# A clip in a promo pool (promo_type set) must never be recorded as played
|
|
354
|
+
# nor appear in the recently-played debug list.
|
|
355
|
+
await _add_catalog(db, "promo1", "Bumper", duration_sec=30)
|
|
356
|
+
pid = await db.create_saved_playlist(
|
|
357
|
+
name="Bumpers", description=None, is_immutable=False,
|
|
358
|
+
created_by="admin", promo_type="general",
|
|
359
|
+
)
|
|
360
|
+
await db.replace_playlist_items(pid, [
|
|
361
|
+
{"media_type": "cm", "media_id": "promo1", "title": "Bumper", "duration_sec": 30},
|
|
362
|
+
])
|
|
363
|
+
|
|
364
|
+
await db.record_play_completion(friendly_token="promo1", duration_sec=30)
|
|
365
|
+
|
|
366
|
+
rows = await db._fetch_all("SELECT media_id FROM play_completions")
|
|
367
|
+
assert rows == []
|
|
368
|
+
pip = await db._fetch_all("SELECT media_id FROM playlist_item_played")
|
|
369
|
+
assert pip == []
|
|
370
|
+
debug = await db.get_recently_played_debug(21)
|
|
371
|
+
assert debug["by_completion"] == []
|
|
372
|
+
assert debug["by_playlist_pass"] == []
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def test_purge_promo_hide_state_removes_stale_rows(db):
|
|
376
|
+
# Rows recorded before the promo exemption (e.g. in production) are cleaned up
|
|
377
|
+
# at the data layer by purge_promo_hide_state (also run by migration v14).
|
|
378
|
+
await _add_catalog(db, "promo1", "Bumper", duration_sec=30)
|
|
379
|
+
pid = await db.create_saved_playlist(
|
|
380
|
+
name="Bumpers", description=None, is_immutable=False,
|
|
381
|
+
created_by="admin", promo_type="general",
|
|
382
|
+
)
|
|
383
|
+
await db.replace_playlist_items(pid, [
|
|
384
|
+
{"media_type": "cm", "media_id": "promo1", "title": "Bumper", "duration_sec": 30},
|
|
385
|
+
])
|
|
386
|
+
# Simulate a stale row written before the exemption existed.
|
|
387
|
+
await db._execute("INSERT INTO play_completions (media_type, media_id) VALUES ('cm', 'promo1')")
|
|
388
|
+
# Before the purge it shows in the (unfiltered) debug view.
|
|
389
|
+
assert {r["media_id"] for r in (await db.get_recently_played_debug(21))["by_completion"]} == {"promo1"}
|
|
390
|
+
|
|
391
|
+
removed = await db.purge_promo_hide_state()
|
|
392
|
+
assert removed["completions"] == 1
|
|
393
|
+
|
|
394
|
+
assert await db._fetch_all("SELECT media_id FROM play_completions") == []
|
|
395
|
+
assert (await db.get_recently_played_debug(21))["by_completion"] == []
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
async def test_hidden_tag_promo_is_exempt_from_hiding(db):
|
|
399
|
+
# Station promos/bumpers are usually classified by hidden tag/category
|
|
400
|
+
# (e.g. `channelz`), not promo-pool membership. They must still be exempt.
|
|
401
|
+
await _add_catalog(db, "bumper", "CHANNEL Z - ITS 10PM", duration_sec=15)
|
|
402
|
+
tid = await db.upsert_tag("channelz")
|
|
403
|
+
await db.set_catalog_tags("bumper", [tid])
|
|
404
|
+
|
|
405
|
+
await db.record_play_completion(friendly_token="bumper", duration_sec=15)
|
|
406
|
+
assert await db._fetch_all("SELECT media_id FROM play_completions") == []
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
async def test_hidden_category_promo_is_exempt_from_hiding(db):
|
|
410
|
+
await _add_catalog(db, "bumper", "Z Promo", duration_sec=15)
|
|
411
|
+
cid = await db.upsert_category("Z Channel Promos")
|
|
412
|
+
await db.set_catalog_categories("bumper", [cid])
|
|
413
|
+
|
|
414
|
+
await db.record_play_completion(friendly_token="bumper", duration_sec=15)
|
|
415
|
+
assert await db._fetch_all("SELECT media_id FROM play_completions") == []
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
async def test_purge_removes_hidden_tag_promo_rows(db):
|
|
419
|
+
# A stale row written before the tag/category exemption is cleaned by purge.
|
|
420
|
+
await _add_catalog(db, "bumper", "CHANNEL Z - ITS 10PM", duration_sec=15)
|
|
421
|
+
tid = await db.upsert_tag("channelz")
|
|
422
|
+
await db.set_catalog_tags("bumper", [tid])
|
|
423
|
+
await db._execute("INSERT INTO play_completions (media_type, media_id) VALUES ('cm', 'bumper')")
|
|
424
|
+
|
|
425
|
+
removed = await db.purge_promo_hide_state()
|
|
426
|
+
assert removed["completions"] == 1
|
|
427
|
+
assert await db._fetch_all("SELECT media_id FROM play_completions") == []
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
|
|
352
433
|
|
|
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.32.1 → kryten_webqueue-0.32.3}/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
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/integrations/ytpipe/downloader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/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
|
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/static/js/admin-playlists.js
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/static/js/admin-queue-mgmt.js
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/static/js/admin-schedules.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/admin/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/admin/promos.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/feedback/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/kryten_webqueue/templates/queue/index.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.1 → kryten_webqueue-0.32.3}/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
|