kryten-webqueue 0.15.0__tar.gz → 0.15.1__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.15.0 → kryten_webqueue-0.15.1}/CHANGELOG.md +14 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/PKG-INFO +1 -1
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/config.example.json +3 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/__main__.py +3 -1
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/config.py +11 -0
- kryten_webqueue-0.15.1/kryten_webqueue/logging_config.py +81 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/promos/director.py +105 -4
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/pyproject.toml +1 -1
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/.gitignore +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/README.md +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/catalog/db.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/__init__.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/tests/test_save_results_to_playlist.py +0 -0
|
@@ -6,6 +6,20 @@ 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
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.15.1] — 2026-06-17
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Application logs were silently dropped.** The app never configured Python logging — `uvicorn.run(log_level="info")` only sets up uvicorn's own loggers, leaving the `kryten_webqueue` hierarchy with no handler, so Python's "last resort" path emitted only `WARNING`+. Every `logger.info(...)` (including all promo-insertion diagnostics) was discarded. A new `logging_config.build_log_config()` installs a `dictConfig` (passed to uvicorn via `log_config`) that attaches a console handler to the application loggers.
|
|
14
|
+
- **Silent failure in the promo poll loop.** `PromoDirector.on_poll()` swallowed every exception from the immutable-event-lock check with a bare `except: return` and no log line, hiding faults. It now logs at `WARNING` with a traceback before skipping the cycle.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Configurable log levels.** New `log_level` (default `INFO`) and `promo_log_level` (default falls back to `log_level`) config fields. Set `promo_log_level` to `DEBUG` for a full per-poll trace of promo decisions without flooding the rest of the app (the `kryten_webqueue.promos` logger is independently tunable).
|
|
19
|
+
- **Deep promo observability.** `PromoDirector` now emits detailed diagnostics across the whole insertion path: now-playing advance + cadence counter, clip selection (order, pool size, sequential index, random no-repeat avoidance), weighted type pick (candidates/weights/skipped), cadence-due reason, idempotency-guard skips, lead-in decisions, and the add/move/uid result. It warns on single-clip pools (which always repeat) and **errors** when an add succeeds but returns no uid (an untracked insertion that would otherwise repeat every poll).
|
|
20
|
+
|
|
21
|
+
[0.15.1]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.15.1
|
|
22
|
+
|
|
9
23
|
## [0.15.0] — 2026-06-14
|
|
10
24
|
|
|
11
25
|
### Added
|
|
@@ -3,13 +3,15 @@ import uvicorn
|
|
|
3
3
|
|
|
4
4
|
from .config import Config
|
|
5
5
|
from .app import create_app
|
|
6
|
+
from .logging_config import build_log_config
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def main():
|
|
9
10
|
config_path = os.environ.get("WQ_CONFIG", "/etc/kryten-webqueue/config.json")
|
|
10
11
|
config = Config.from_file(config_path)
|
|
11
12
|
app = create_app(config)
|
|
12
|
-
|
|
13
|
+
log_config = build_log_config(config.log_level, config.promo_log_level)
|
|
14
|
+
uvicorn.run(app, host=config.host, port=config.port, log_config=log_config)
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
if __name__ == "__main__":
|
|
@@ -104,6 +104,17 @@ class Config(BaseModel):
|
|
|
104
104
|
secret_key: str
|
|
105
105
|
session_ttl_hours: int = 24
|
|
106
106
|
|
|
107
|
+
# Logging
|
|
108
|
+
# Root application log level for the ``kryten_webqueue`` logger hierarchy.
|
|
109
|
+
# Without explicit configuration Python only emits WARNING+ via its
|
|
110
|
+
# "last resort" handler, so INFO diagnostics (e.g. promo insertions) are
|
|
111
|
+
# silently dropped. ``__main__`` installs a dictConfig using these values.
|
|
112
|
+
log_level: str = "INFO"
|
|
113
|
+
# Independent level for the promo subsystem (``kryten_webqueue.promos``).
|
|
114
|
+
# Set to "DEBUG" for a full per-poll trace of promo decisions without
|
|
115
|
+
# flooding the rest of the app. Falls back to ``log_level`` when None.
|
|
116
|
+
promo_log_level: str | None = None
|
|
117
|
+
|
|
107
118
|
# API Gate
|
|
108
119
|
api_gate_url: str = "http://127.0.0.1:24444"
|
|
109
120
|
api_gate_token: str
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Centralised logging configuration.
|
|
2
|
+
|
|
3
|
+
The app previously relied on ``uvicorn.run(log_level="info")`` which only
|
|
4
|
+
configures uvicorn's *own* loggers (``uvicorn``/``uvicorn.access``/
|
|
5
|
+
``uvicorn.error``). Application loggers in the ``kryten_webqueue`` hierarchy had
|
|
6
|
+
no handler, so Python's "last resort" handler emitted only ``WARNING`` and
|
|
7
|
+
above — silently dropping every ``logger.info(...)`` call (e.g. all promo
|
|
8
|
+
insertion diagnostics). This module builds a single ``dictConfig`` that installs
|
|
9
|
+
a console handler for both uvicorn and the application loggers, with an
|
|
10
|
+
independently tunable level for the promo subsystem.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalize_level(level: str | None, default: str) -> str:
|
|
17
|
+
if not level:
|
|
18
|
+
return default
|
|
19
|
+
candidate = str(level).strip().upper()
|
|
20
|
+
valid = {"CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"}
|
|
21
|
+
return candidate if candidate in valid else default
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_log_config(log_level: str = "INFO", promo_log_level: str | None = None) -> dict:
|
|
25
|
+
"""Return a ``logging.config.dictConfig`` dict for uvicorn + the app.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
log_level: Level for the root and ``kryten_webqueue`` loggers.
|
|
29
|
+
promo_log_level: Level for ``kryten_webqueue.promos`` (the promo
|
|
30
|
+
director). Falls back to ``log_level`` when not provided. Set to
|
|
31
|
+
``DEBUG`` for a full per-poll trace of promo decisions.
|
|
32
|
+
"""
|
|
33
|
+
app_level = _normalize_level(log_level, "INFO")
|
|
34
|
+
promo_level = _normalize_level(promo_log_level, app_level)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
"version": 1,
|
|
38
|
+
# Never tear down loggers created at import time (module-level
|
|
39
|
+
# ``getLogger`` calls); we only attach handlers/levels.
|
|
40
|
+
"disable_existing_loggers": False,
|
|
41
|
+
"formatters": {
|
|
42
|
+
"default": {
|
|
43
|
+
"format": "%(asctime)s %(levelname)-8s %(name)s: %(message)s",
|
|
44
|
+
"datefmt": "%Y-%m-%d %H:%M:%S",
|
|
45
|
+
},
|
|
46
|
+
"access": {
|
|
47
|
+
"format": "%(asctime)s %(levelname)-8s %(name)s: %(message)s",
|
|
48
|
+
"datefmt": "%Y-%m-%d %H:%M:%S",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
"handlers": {
|
|
52
|
+
"console": {
|
|
53
|
+
"class": "logging.StreamHandler",
|
|
54
|
+
"formatter": "default",
|
|
55
|
+
"stream": "ext://sys.stderr",
|
|
56
|
+
},
|
|
57
|
+
"access": {
|
|
58
|
+
"class": "logging.StreamHandler",
|
|
59
|
+
"formatter": "access",
|
|
60
|
+
"stream": "ext://sys.stdout",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
"root": {"handlers": ["console"], "level": app_level},
|
|
64
|
+
"loggers": {
|
|
65
|
+
"kryten_webqueue": {
|
|
66
|
+
"level": app_level,
|
|
67
|
+
"handlers": ["console"],
|
|
68
|
+
"propagate": False,
|
|
69
|
+
},
|
|
70
|
+
# Promo subsystem: independently tunable so operators can crank it to
|
|
71
|
+
# DEBUG for a deep dive without flooding the rest of the app.
|
|
72
|
+
"kryten_webqueue.promos": {
|
|
73
|
+
"level": promo_level,
|
|
74
|
+
"handlers": ["console"],
|
|
75
|
+
"propagate": False,
|
|
76
|
+
},
|
|
77
|
+
"uvicorn": {"level": "INFO", "handlers": ["console"], "propagate": False},
|
|
78
|
+
"uvicorn.error": {"level": "INFO", "handlers": ["console"], "propagate": False},
|
|
79
|
+
"uvicorn.access": {"level": "INFO", "handlers": ["access"], "propagate": False},
|
|
80
|
+
},
|
|
81
|
+
}
|
|
@@ -186,10 +186,18 @@ class PromoDirector:
|
|
|
186
186
|
def _general_due(self, now: datetime) -> bool:
|
|
187
187
|
g = self._config.general
|
|
188
188
|
if self._content_since_last_general >= g.every_n_items:
|
|
189
|
+
logger.debug(
|
|
190
|
+
"General promo due: content_since_last=%d >= every_n_items=%d",
|
|
191
|
+
self._content_since_last_general, g.every_n_items,
|
|
192
|
+
)
|
|
189
193
|
return True
|
|
190
194
|
if self._last_general_at is not None:
|
|
191
195
|
elapsed_min = (now - self._last_general_at).total_seconds() / 60.0
|
|
192
196
|
if elapsed_min >= g.every_m_minutes:
|
|
197
|
+
logger.debug(
|
|
198
|
+
"General promo due: elapsed=%.1fmin >= every_m_minutes=%.1f",
|
|
199
|
+
elapsed_min, g.every_m_minutes,
|
|
200
|
+
)
|
|
193
201
|
return True
|
|
194
202
|
return False
|
|
195
203
|
|
|
@@ -197,37 +205,70 @@ class PromoDirector:
|
|
|
197
205
|
"""Weighted choice among enabled general types with a non-empty pool."""
|
|
198
206
|
candidates: list[str] = []
|
|
199
207
|
weights: list[int] = []
|
|
208
|
+
skipped: list[str] = []
|
|
200
209
|
for t in GENERAL_PROMO_TYPES:
|
|
201
210
|
tc = self._config.types.get(t)
|
|
202
211
|
if not tc or not tc.enabled or tc.weight <= 0:
|
|
212
|
+
skipped.append(f"{t}(disabled/weight)")
|
|
203
213
|
continue
|
|
204
214
|
pool = await self._db.get_promo_pool_items(t)
|
|
205
215
|
if not pool:
|
|
216
|
+
skipped.append(f"{t}(empty-pool)")
|
|
206
217
|
continue
|
|
207
218
|
candidates.append(t)
|
|
208
219
|
weights.append(tc.weight)
|
|
209
220
|
if not candidates:
|
|
221
|
+
logger.warning(
|
|
222
|
+
"Promo general-type pick found no eligible types (skipped=%s)", skipped
|
|
223
|
+
)
|
|
210
224
|
return None
|
|
211
|
-
|
|
225
|
+
chosen = self._rng.choices(candidates, weights=weights, k=1)[0]
|
|
226
|
+
logger.debug(
|
|
227
|
+
"Promo general-type pick: chosen=%s candidates=%s weights=%s skipped=%s",
|
|
228
|
+
chosen, candidates, weights, skipped,
|
|
229
|
+
)
|
|
230
|
+
return chosen
|
|
212
231
|
|
|
213
232
|
def _select_clip(self, promo_type: str, pool: list[dict]) -> dict:
|
|
214
233
|
tc = self._config.types.get(promo_type)
|
|
215
234
|
order = tc.order if tc else "random"
|
|
235
|
+
pool_size = len(pool)
|
|
216
236
|
if order == "sequential":
|
|
217
|
-
|
|
237
|
+
raw_index = self._seq_index.get(promo_type, 0)
|
|
238
|
+
idx = raw_index % pool_size
|
|
218
239
|
self._seq_index[promo_type] = idx + 1
|
|
219
240
|
clip = pool[idx]
|
|
241
|
+
logger.debug(
|
|
242
|
+
"Promo clip select [%s] order=sequential pool_size=%d raw_index=%d "
|
|
243
|
+
"-> idx=%d media_id=%s title=%r next_index=%d",
|
|
244
|
+
promo_type, pool_size, raw_index, idx,
|
|
245
|
+
clip.get("media_id"), clip.get("title"), idx + 1,
|
|
246
|
+
)
|
|
220
247
|
else:
|
|
221
248
|
clip = self._rng.choice(pool)
|
|
222
|
-
|
|
249
|
+
repeated = False
|
|
250
|
+
if self._config.general.no_repeat and pool_size > 1:
|
|
223
251
|
last = self._last_clip_token.get(promo_type)
|
|
224
252
|
if clip.get("media_id") == last:
|
|
253
|
+
repeated = True
|
|
225
254
|
# Draw from the rest of the pool so the no-repeat guarantee
|
|
226
255
|
# always holds (bounded random retries could otherwise give
|
|
227
256
|
# up and return a repeat).
|
|
228
257
|
alternatives = [c for c in pool if c.get("media_id") != last]
|
|
229
258
|
if alternatives:
|
|
230
259
|
clip = self._rng.choice(alternatives)
|
|
260
|
+
logger.debug(
|
|
261
|
+
"Promo clip select [%s] order=random pool_size=%d no_repeat=%s "
|
|
262
|
+
"avoided_repeat=%s -> media_id=%s title=%r",
|
|
263
|
+
promo_type, pool_size, self._config.general.no_repeat,
|
|
264
|
+
repeated, clip.get("media_id"), clip.get("title"),
|
|
265
|
+
)
|
|
266
|
+
if pool_size == 1:
|
|
267
|
+
logger.warning(
|
|
268
|
+
"Promo pool for %r has a single clip; it will repeat every time "
|
|
269
|
+
"(media_id=%s). Add more clips to vary this promo type.",
|
|
270
|
+
promo_type, clip.get("media_id"),
|
|
271
|
+
)
|
|
231
272
|
self._last_clip_token[promo_type] = clip.get("media_id")
|
|
232
273
|
return clip
|
|
233
274
|
|
|
@@ -262,14 +303,30 @@ class PromoDirector:
|
|
|
262
303
|
logger.warning("Promo add failed (%s)", promo_type, exc_info=True)
|
|
263
304
|
return None
|
|
264
305
|
if not add_result or not add_result.get("success"):
|
|
306
|
+
logger.warning(
|
|
307
|
+
"Promo add rejected (%s media_id=%s): result=%r",
|
|
308
|
+
promo_type, clip.get("media_id"), add_result,
|
|
309
|
+
)
|
|
265
310
|
return None
|
|
266
311
|
uid = add_result.get("uid")
|
|
267
312
|
if uid is None:
|
|
313
|
+
# CyTube accepted the add but api-gate could not resolve a uid. The
|
|
314
|
+
# shadow can't track this promo, so the idempotency guard will never
|
|
315
|
+
# see it -> the cadence counter never resets and we'd re-add every
|
|
316
|
+
# poll. Bail loudly so this shows up in logs instead of silently
|
|
317
|
+
# spamming the queue.
|
|
318
|
+
logger.error(
|
|
319
|
+
"Promo add for %s (media_id=%s) returned success but NO uid; "
|
|
320
|
+
"cannot track in shadow (result=%r). Skipping shadow insert to "
|
|
321
|
+
"avoid an untracked, repeatable insertion.",
|
|
322
|
+
promo_type, clip.get("media_id"), add_result,
|
|
323
|
+
)
|
|
268
324
|
return None
|
|
269
325
|
|
|
270
326
|
if after_uid is not None:
|
|
271
327
|
try:
|
|
272
328
|
await self._api_gate.playlist_move(uid, after_uid)
|
|
329
|
+
logger.debug("Promo move ok: uid=%s after_uid=%s", uid, after_uid)
|
|
273
330
|
except Exception:
|
|
274
331
|
logger.warning("Promo move failed (uid=%s after=%s)", uid, after_uid, exc_info=True)
|
|
275
332
|
|
|
@@ -310,10 +367,21 @@ class PromoDirector:
|
|
|
310
367
|
return None
|
|
311
368
|
items = self._shadow.items
|
|
312
369
|
if self._has_lead_in(content_uid, items):
|
|
370
|
+
logger.debug(
|
|
371
|
+
"Viewer's-Choice lead-in already present for paid uid=%s; skipping",
|
|
372
|
+
content_uid,
|
|
373
|
+
)
|
|
313
374
|
return None
|
|
314
375
|
pred = self._direct_pred_uid(content_uid, items)
|
|
315
376
|
if pred is None:
|
|
377
|
+
logger.debug(
|
|
378
|
+
"Viewer's-Choice skipped: no predecessor for paid uid=%s", content_uid
|
|
379
|
+
)
|
|
316
380
|
return None
|
|
381
|
+
logger.info(
|
|
382
|
+
"Viewer's-Choice lead-in (pay path) for paid uid=%s after_uid=%s",
|
|
383
|
+
content_uid, pred,
|
|
384
|
+
)
|
|
317
385
|
return await self._insert_promo(
|
|
318
386
|
"viewers_choice", after_uid=pred, target_uid=content_uid, lead_in=True
|
|
319
387
|
)
|
|
@@ -331,6 +399,10 @@ class PromoDirector:
|
|
|
331
399
|
if np_uid != self._last_np_uid:
|
|
332
400
|
if self._last_np_uid is not None and not self._last_np_is_promo:
|
|
333
401
|
self._content_since_last_general += 1
|
|
402
|
+
logger.debug(
|
|
403
|
+
"Now-playing advanced: %s -> %s (is_promo=%s) content_since_last_general=%d",
|
|
404
|
+
self._last_np_uid, np_uid, np_is_promo, self._content_since_last_general,
|
|
405
|
+
)
|
|
334
406
|
self._last_np_uid = np_uid
|
|
335
407
|
self._last_np_is_promo = np_is_promo
|
|
336
408
|
|
|
@@ -341,8 +413,13 @@ class PromoDirector:
|
|
|
341
413
|
# No-op during an immutable scheduled event (curated content plays as built).
|
|
342
414
|
try:
|
|
343
415
|
if await self._db.is_event_lock_active():
|
|
416
|
+
logger.debug("Promo on_poll skipped: immutable event lock active")
|
|
344
417
|
return
|
|
345
418
|
except Exception:
|
|
419
|
+
logger.warning(
|
|
420
|
+
"Promo on_poll: is_event_lock_active() check failed; skipping this cycle",
|
|
421
|
+
exc_info=True,
|
|
422
|
+
)
|
|
346
423
|
return
|
|
347
424
|
|
|
348
425
|
items = self._shadow.items
|
|
@@ -350,6 +427,7 @@ class PromoDirector:
|
|
|
350
427
|
return
|
|
351
428
|
target = self._next_content(np_uid, items)
|
|
352
429
|
if target is None:
|
|
430
|
+
logger.debug("Promo on_poll: no upcoming content item found")
|
|
353
431
|
return
|
|
354
432
|
target_uid = target.get("uid")
|
|
355
433
|
direct_pred = self._direct_pred_uid(target_uid, items)
|
|
@@ -358,12 +436,24 @@ class PromoDirector:
|
|
|
358
436
|
# Lead-in first so a same-cycle general promo lands ahead of it.
|
|
359
437
|
leadin_type = self._leadin_type_for(target)
|
|
360
438
|
if leadin_type and not self._has_lead_in(target_uid, items):
|
|
439
|
+
logger.info(
|
|
440
|
+
"Promo lead-in due: type=%s target_uid=%s (is_pay=%s dur=%.0fs) after_uid=%s",
|
|
441
|
+
leadin_type, target_uid, target.get("is_pay"),
|
|
442
|
+
_duration_seconds(target), direct_pred,
|
|
443
|
+
)
|
|
361
444
|
await self._insert_promo(
|
|
362
445
|
leadin_type, after_uid=direct_pred, target_uid=target_uid, lead_in=True
|
|
363
446
|
)
|
|
447
|
+
elif leadin_type:
|
|
448
|
+
logger.debug(
|
|
449
|
+
"Promo lead-in already present for target_uid=%s (type=%s)",
|
|
450
|
+
target_uid, leadin_type,
|
|
451
|
+
)
|
|
364
452
|
|
|
365
453
|
# General cadence promo.
|
|
366
|
-
|
|
454
|
+
due = self._general_due(now)
|
|
455
|
+
has_general = self._has_general_before(target_uid, self._shadow.items)
|
|
456
|
+
if due and not has_general:
|
|
367
457
|
chosen = await self._pick_general_type()
|
|
368
458
|
if chosen:
|
|
369
459
|
pool = await self._db.get_promo_pool_items(chosen)
|
|
@@ -371,5 +461,16 @@ class PromoDirector:
|
|
|
371
461
|
chosen, after_uid=content_pred, target_uid=None, lead_in=False, pool=pool
|
|
372
462
|
)
|
|
373
463
|
if inserted is not None:
|
|
464
|
+
logger.info(
|
|
465
|
+
"Promo general inserted: type=%s uid=%s before content_uid=%s "
|
|
466
|
+
"(reset cadence counter from %d)",
|
|
467
|
+
chosen, inserted, target_uid, self._content_since_last_general,
|
|
468
|
+
)
|
|
374
469
|
self._content_since_last_general = 0
|
|
375
470
|
self._last_general_at = now
|
|
471
|
+
elif due and has_general:
|
|
472
|
+
logger.debug(
|
|
473
|
+
"Promo general due but one already precedes target_uid=%s; "
|
|
474
|
+
"skipping (idempotency guard)",
|
|
475
|
+
target_uid,
|
|
476
|
+
)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/admin/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/admin/promos.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/kryten_webqueue/templates/queue/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.15.0 → kryten_webqueue-0.15.1}/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
|